Task Status Hooks¶
bob task reconcile (formerly bob task-status-hooks, still accepted)
makes the current Pomodoro ledger the source of truth for active Obsidian
Tasks promotions and structural cleanup. Run it after capturing or closing
Pomodoro-linked work, after bob projects sync writes schedules, and as
part of any vault maintenance that should leave Next, In Progress, and
Blocked markers consistent with today's ledger.
In practice it:
- Owns vault-wide Blocked recovery and completed-reference retirement,
including after a
bob capture xclose, which deliberately leaves both to this command - Promotes tasks linked under today's open Pomodoros to Next (
[*]), leaving In Progress ([/]) alone - Never lowers Next (
[*]) or In Progress ([/]) when links disappear: lanes are sticky and only an explicit release returns them to Ready. (In Obsidian, Alt+[ / Alt+] move a linked task between Next and In Progress, and reconcile keeps both.) The single exception is a Next task that lives in a canonical daily note or the selected current ledger, which still clears once unlinked and stale - Follows task dependency links on each task's managed Depends-On line
(
⛓️ **DEPENDS ON:**, see the task dependency contract) plus R8 legacy dependency children, and promotes those too - Marks a task Blocked (
[?]) when it has an open Dataview dependency or a future[scheduled:: YYYY-MM-DD]date - Cleans the ledger: de-duplicates links across open Pomodoros, retires
completed references, removes canceled-task bullets, consumes live Task
Links to effectively Blocked tasks under today's open Pomodoros, deletes
childless Pomodoros, and repairs
🍅markers
The latest existing earlier daily note is a read-only recent-activity source:
it supplies the rank an otherwise-unblocked Blocked task recovers to and the
grace that keeps a directly referenced daily-note Next task, but it never
promotes a Ready task and is never written. Completed-task references are retired as struck,
non-embedded links. Live non-transcluded links beneath completed Pomodoros
carry the machine-owned 🍅 marker; embedded and provenance-unknown retired
links do not. Links beneath open Pomodoros are unmarked. A link to an
unambiguously canceled Tasks task removes its complete Markdown list-item
subtree from an open Pomodoro without changing the canceled task itself.
Contents¶
- Usage
- Sync rules
- Dependency lines
- Relation lines
- Derived Blocked status
- Blocked task links are consumed
- Status grouping
- Pomodoro marker
- Guard rails
- Retries
- Output
Usage¶
bob task reconcile [-b|--bob-dir DIR] [-d|--dry-run] [-f|--format human|json] [-r|--retry-timeout SECONDS]
The vault root comes from --bob-dir, then BOB_DIR, then ~/bob. The current
daily ledger comes from BOB_DAY_FILE when set; otherwise it is
<vault>/YYYY/YYYYMMDD.md for the local date or BOB_NOW override. When the
selected filename has a valid daily date, that date anchors the historical
lookup and future-schedule boundary even for a fixture or manual override.
Otherwise the effective current date is the anchor. A task scheduled after
the anchor is future; the anchor date itself and earlier dates are not.
Starting strictly before the anchor, the command selects the newest existing
canonical <vault>/YYYY/YYYYMMDD.md file. Missing intervening days or weeks
and year boundaries need no special handling. Malformed names, mismatched year
directories, the anchor date, and future dates are ignored. No earlier daily
note is a valid result.
Use --dry-run to compute and print the complete sync without writing files.
Repeated successful runs are idempotent. Live runs retry certain transient
failures with jittered backoff, controlled by -r, --retry-timeout SECONDS
(default 120); see Retries.
The hidden task-status-hooks, task-status-setter, and mark-next-tasks
spellings remain compatibility-only dispatch aliases and show canonical
usage when asked for help, including the same retry behavior.
Sync Rules¶
Within the current daily note's ## Pomodoros section, the command reads block
links from indented bullets beneath open top-level Pomodoro entries:
- [ ] Write and review (0900-0930)
- [[dev#^write-design]]
- Review [[Projects/Alpha#^parser-review]]
- [x] Earlier session (0830-0900)
- 🍅 [[dev#^closed-session-link-is-not-a-next-source]]
Only links with a block fragment ([[note#^block-id]]) count. Ordinary note
links and heading links do not. Targets resolve by an exact vault-relative
path first and then by a unique, case-insensitive note basename. Ambiguous or
missing targets produce warnings and are not guessed.
The normal resolver is backed by the active vault scan described in
Guard rails. As a narrow archive exception, a current or
previous Pomodoro ledger link that names an exact normalized vault-relative
Markdown target beneath the top-level done/ directory, such as
[[done/dev_done#^finished]], may load that one archive note read-only. This
lets collection-rewritten terminal task links be struck, moved, removed when
canceled, or normalized for recent-activity identity without adding the
archive to active synchronization. Basename links do not search done/, and
parent traversal, missing archive files, missing task blocks, and conflicting
block IDs still warn.
The Bob Navigation Hotkeys Ctrl+Shift+P picker already removes a task's live
links from today's open Pomodoros the moment that task is given a future
scheduled date (see
projects.md),
so by the time this command runs it typically sees a smaller root set — a
deferred task's dependency chain never got the chance to seed a promotion
here in the first place.
Before task statuses, completed-reference retirement, or relocation are planned, the command removes cross-Pomodoro duplicates. For each link that resolves to a scanned Tasks task, identity is the resolved vault-relative Markdown path plus block ID. Explicit paths, unique basenames, same-note targets, aliases, and embeds therefore compare equal when they name the same task; the same block ID in different notes remains distinct.
Ownership follows file order. The first surviving line beneath an open Pomodoro owns each resolved task on that line. A line beneath a later open Pomodoro is removed when any task on it is already owned by a different open Pomodoro. Repeats within the same owning Pomodoro do not cause removal. Every later conflicting line is removed in one run, and a line selected for removal does not become the owner of its other links.
Removal is deliberately physical-line based: authored text and unrelated links on a conflicting line are removed with it, while following nested lines remain unless independently selected. If several duplicate tasks identify one line, the line is removed and reported once with every canonical task identity. Links beneath completed or cancelled Pomodoros, top-level entry lines, fenced examples, and unresolved or non-task targets are not destructive cleanup candidates.
Canceled Task References¶
After duplicate-line ownership is decided, every surviving block-link
occurrence beneath an open Pomodoro is checked against the Tasks status
registry. When an occurrence resolves only to tasks with a recognized
CANCELLED status, its complete Markdown list item is removed: deletion starts
on the containing bullet's line and continues through the end of its nested
list-item subtree. This includes conventional [-] tasks and any configured
single-character symbol whose statusSettings.coreStatuses or
statusSettings.customStatuses entry has type CANCELLED. The task itself
remains canceled.
Plain, embedded, aliased, Pomodoro-marked, and exactly struck forms all qualify. The list item is the mutation unit, so authored prose, live or completed sibling links, unresolved links, embeds, aliases, markers, strikethrough, and nested content on that item are removed with it. A canceled link in a nested bullet removes that nested item without removing its parent; a canceled link on the parent removes the parent and all descendants. Multiple qualifying occurrences on one bullet still produce separate compatibility records in occurrence order even though the subtree is deleted once. Covered descendant bullets do not produce redundant cancellation, token-edit, move, or marker reports.
Cleanup is limited to indented bullets owned by open Pomodoros in the selected
daily note. Links beneath completed or canceled Pomodoros, on top-level
Pomodoro lines, outside ## Pomodoros, or inside fenced examples remain
untouched by this rule. Unresolved links and links that do not resolve to a
scanned Tasks task also remain and keep their warning behavior.
If one block ID matches several Tasks task lines, every match must have a
recognized CANCELLED type before the containing item qualifies. All-canceled
duplicates qualify; canceled/open, canceled/done, and canceled/unknown mixes
remain in place and produce an ambiguity warning. A physical line already
selected by cross-Pomodoro duplicate cleanup is deleted once and does not also
produce a canceled-reference report. Duplicate cleanup remains physical-line
only; canceled-reference cleanup uses the full list-item subtree. When a
completed parent bullet is relocated, any independently canceled descendant
subtree is omitted from the moved content.
The Ctrl+Shift+P Cancel row in Bob Navigation Hotkeys applies this same
removal at once, using the deferral prune's dedicated-bullet/token semantics
(a dedicated link bullet is removed with its subtree; otherwise only the link
token is removed). Struck links, closed Pomodoros, and other days' notes are
untouched either way. This command stays authoritative: it re-derives the
same cleanup on its next run.
Empty Pomodoros¶
After duplicate-line removal, canceled-reference subtree removal, blocked-link
consumption, completed-reference retirement or relocation, and marker repair
have been composed, the command removes childless Pomodoro entries from the
selected current daily note. The cleanup is limited to recognized column-zero open or
completed ledger entries in the real ## Pomodoros section; nested checkbox
lookalikes, fenced examples, entries outside the section, and statuses that
the Pomodoro parser does not recognize are left alone.
A Pomodoro is kept when it has at least one real direct Markdown list-item child, even when that child is plain prose and contains no resolvable task block link. Fenced-example bullets do not count. If an empty entry owns indented continuation text but no list-item child, its whole entry block is removed so no orphaned text remains. Line endings and the original final newline state are preserved.
Empty entries do not count toward the multiple-open-timed-Pomodoro guard and are not selected as current or completed fallback destinations for completed reference relocation. Multiple non-empty open timed entries still fail before any write. Because empty cleanup runs after the other structural rewrites, it also removes entries whose last child was removed or moved away during the same invocation, while retaining an entry that receives a moved child.
Dry-run reports the same deterministic removals without changing the vault. Live runs write through the normal guarded snapshot, recovery, and retry path. A second run after a successful cleanup is a no-op.
Plugin keymap cleanup¶
The Bob Navigation Hotkeys, Block ID Prompt, and Task Status Cycler plugins apply the same childless-entry rule when a keymap removes Task Links. A gesture deletes an entry only when it removes at least one link or line from that entry and, after all of the gesture's edits to the note, no direct list-item child remains. Plain prose children keep the entry; fenced example bullets do not count. The whole entry block is removed, including continuation text, in the same editor transaction or vault write as the link removal, so undo also restores it. Cleanup of today's open Pomodoros is limited to open entries; selected-link deletion and deferred-only completion can also remove an emptied closed entry. Placeholders that were already childless are left alone.
The rewritten Pomodoro section is scanned again after duplicate removal, canceled/completed-reference structural rewrites, marker repair, and empty Pomodoro cleanup. Only references that will actually be written contribute to the direct desired-status map and dependency graph, except that live links to effectively Blocked tasks still seed promotion in the same run and are consumed afterwards. Thus an otherwise-live task mentioned only as sibling or nested content in a removed item, a removed canceled root, a removed empty Pomodoro, and any otherwise-unreachable dependency chain stop contributing desired Next or In-Progress state in that same run.
After resolving the surviving direct Pomodoro links, the command reads dependency edges from the linked tasks' Depends-On lines plus their R8 legacy children, per the task dependency contract. A Depends-On line is the single managed first-child line holding a task's plain dependency links:
- [ ] #task Ship the feature ^ship
- ⛓️ **DEPENDS ON:** [[#^write-tests]] • [[Quality/Review#^review]]
- [ ] #task Write tests ^write-tests
Same-note and cross-note targets use the same resolver as Pomodoro links.
Every resolved line link to a scanned Tasks task is an edge. A legacy
child — a direct child bullet whose only content is one block link
(plain, ![[…]], ~~[[…]]~~, or ~~![[…]]~~) — is an edge only when
its resolved target's id (its [id::], canonical id, or same-note bare
block id) is in the dependent's [dependsOn::] field. Sole embeds that
are not field-managed (for example #^ref reading embeds), links into
done/, fenced examples, and targets without a matching task block are
never edges; archive targets never warn. Other unresolvable candidates
emit a warning naming the referencing task's file and line.
Block fragments are file-scoped: Alpha.md#^review and Beta.md#^review are
distinct graph nodes, and an explicit note path selects only its named
note. The active-rank graph deliberately traverses these resolved
path-plus-fragment links. Tasks metadata separately determines whether the
displayed status is Blocked, using vault-wide IDs such as Alpha__review.
Each surviving direct Pomodoro task has a minimum desired status of Next. A
direct task already In Progress instead seeds In Progress. Every dependency
inherits its source task's effective ranked status, using the order
[ ] < [*] < [/]. The effective status is the stronger of the incoming request
and the target's current supported status, so an already-In-Progress
intermediate task promotes lower-status descendants to In Progress. Multiple
parents merge by taking the strongest request.
Traversal uses a monotonic work queue and is cycle-safe: a task is revisited only when its effective rank increases. Dependencies of dependencies are included, while a task reached both directly and through a dependency is counted as direct. Removing a Pomodoro link removes that task's otherwise unreachable dependency chain from the desired map on the next run.
Rolling Recent Activity¶
Status promotion still uses only the structurally normalized current ledger.
Separately, the command builds a recent-activity identity set from every
non-retired block link beneath a recognized Pomodoro in the normalized current
ledger and the selected previous daily note. A struck link is retired and does
not count; aliases, embeds, markers, alternate note spellings, and same-note
links normalize through the usual resolved vault-relative path plus block ID.
Each daily retains its own resolution context, so [[#^local]] resolves
against the daily where it appears even when both notes contain identical raw
link text.
The previous daily note is read-only. It never receives duplicate cleanup,
link retirement, marker repair, canceled-reference removal, Pomodoro
relocation, or task-status writes. If it has no ## Pomodoros section, it is
the selected source with zero references; the command does not fail or fall
through to an older note. Non-retired historical links never promote a Ready
task to Next or In Progress. They feed two read-only consumers: the
recovery-only rank below, and the grace that keeps a directly referenced
daily-note Next task (KeptNext) while it remains recent.
Recent roots traverse the same eligible dependency graph as current promotion roots with the same strongest-rank merge and cycle rules. This produces a recovery-only rank for unblocking. A Blocked task reached directly defaults to Next because Blocked has no active rank. An already-In-Progress task on the dependency path can strengthen downstream recovery to In Progress. A task absent from this graph recovers to Ready. The recovery rank does not promote an ordinary Ready task; a directly recent task can retain an existing Next so a recovered task remains stable on repeated runs. Dependency-only recovery still follows the daily-note Next clearing policy after its Blocked status is gone: a recovered Next outside a daily note stays Next, while one inside a daily note clears unless directly recent.
Recent activity no longer clears anything. An In Progress task stays In Progress wherever it lives, however long its links have been gone, and a Next task outside canonical daily notes (or the selected current ledger) stays Next. Note kind, directory names, and tags play no role in lane decisions.
Dependency lines¶
Task dependency links live on one managed Depends-On line per the
task dependency contract §2–§4; this section
summarises the R1–R10 reconciliation that section specifies. Only open
dependents (TODO, IN_PROGRESS, ON_HOLD, including [?]) are
reconciled; closed tasks are never touched, and the previous daily
note snapshot is never written.
Reconciliation (contract §4.2) projects each dependent's line links
plus its R8 legacy children into [dependsOn::] in set order (R1),
adopting field ids into a new canonical line when no line exists (R2),
healing a moved link when exactly one scanned task carries its block
id and an unaccounted field id (R3), and rewriting non-canonical
lines to the writer form. Targets without [id::] gain one; archive
(done/) targets keep their id without a warning and never block, and
their links keep the explicit done/ path form (the only form the
hooks re-resolve, since done/ notes sit outside the note index).
Unresolvable links stay verbatim with an unresolved_dependency_link
warning and never block, while their unaccounted field ids stay on as
heal breadcrumbs (R4). Non-task targets (R5), self links (R6), and
cycles (R7, every member stays Blocked) warn and are never projected.
Label-only lines with no legacy children are deleted with the field
(R9); malformed lines warn and are left alone (R10). A removal is
never inferred from a missing line — only a present line drops ids —
and lines are never moved or reordered. Projection-only notes flow
through the same guarded snapshot, recovery, and retry path as status
writes and count as structural, so the quiet interval still defers
notes modified less than 2 s ago.
Every run reports dependency_projection_updates (kinds
dependent_field, target_id, line_removed),
adopted_dependency_lines, healed_dependency_links,
canonicalized_dependency_lines, legacy_dependency_children (a
count for the legacy-window retirement decision), and
dependency_warnings ({kind, path, line, detail}) in JSON, with
matching human sections, a Dependencies: summary line, dependency
counts appended to the Summary: line, and stderr warnings. A second
run over a reconciled vault reports no dependency changes.
After that projection, a second pass writes the generated 🧱 **BLOCKS:**
mirror on every open prerequisite (docs/task-dependencies.md §14).
It uses the accepted explicit edges from R1–R10 (including id-less
dependents, excluding implicit resident → ^prj edges and R4/R5/R6,
archive, previous-daily, unencodable, and ^prj internal targets).
Open owners gain, rewrite, canonicalise, deduplicate, or lose the
line; closed owners freeze; malformed BLOCKS children warn
malformed_blocks_line and are left alone; dependents that still
need a mint in a note the quiet interval would refuse are skipped
this run. dependencies.blocks_lines: false removes every BLOCKS
line on an open task and mints nothing. JSON reports the additive
blocks_line_updates ({path, line, task, kind, added[], removed[]})
and minted_dependent_block_ids ({path, line, block_id}) arrays;
human output adds a 🧱 BLOCKS lines group.
Relation lines¶
A third pass maintains the generated 🪢 **LINKED FROM:** mirror from
the authored 🪢 **LINKS:** lines, as Task relations
§6–§7 specifies. It collects relation edges from every active note
plus the previous daily note and every done/**/*.md (read-only),
byte-prefiltered for LINKS. Sources resolve through the note index;
explicit done/ targets resolve through the loaded archives — the
same narrow done/ read shape as the exact-done/-target exception
above, documented here because the contract requires it. Open or
closed writable targets gain, rewrite, canonicalise, deduplicate, or
lose the line; targets in done/ or the previous daily note are never
written. Sources without a ^block-id are minted silently in the same
pass (no [id::], no [fresh::]), except when the owning note is
quiet-deferred — that source is skipped until the next run and never
listed on uncertainty (malformed LINKS, duplicate block ids).
Reconcile never edits a LINKS line: it only reads, warns, and mints.
relations.linked_from_lines: false removes every LINKED FROM line on
a writable task, mints nothing, and leaves LINKS alone.
JSON reports the additive linked_from_line_updates,
minted_relation_source_block_ids, and relation_warnings[]
({kind, path, line, message}) arrays. Warning kinds are
malformed_links_line, malformed_linked_from_line,
unresolved_relation_link, relation_target_not_task,
self_relation, and duplicate_block_id. Human output adds a 🪢
group in the dependency style, and --dry-run previews all of it. A
second run over a reconciled vault reports no relation changes.
Derived Blocked Status¶
Dependency lines are reconciled as described in Dependency lines before Blocked is derived.
After the full vault scan and the final post-rewrite Pomodoro graph are known, the command indexes Dataview task identities from both square-bracket and parenthesized fields:
- [ ] #task Parent [dependsOn:: Tasks__child]
- [ ] #task Child [id:: Tasks__child]
- [ ] #task Equivalent parenthesized metadata (dependsOn:: Tasks__child)
A recognized non-terminal parent is blocked when any dependsOn value matches
at least one open task with the same vault-wide id. TODO, IN_PROGRESS,
and ON_HOLD targets are open. DONE, CANCELLED, and NON_TASK targets do
not block, and neither do unrecognized target statuses. Missing IDs are
ignored; if an ID is duplicated, any recognized open instance is sufficient.
Self-dependencies, chains, and cycles therefore remain blocked under the same
direct Tasks 8 semantics. Only direct metadata decides a parent's marker;
transitive blocking follows because Blocked is itself an open ON_HOLD
status.
The same scan retains a calendar-valid scheduled value from trailing
square-bracket or parenthesized task metadata:
- [ ] #task Tomorrow [scheduled:: 2026-07-17]
- [/] #task Later (scheduled:: 2026-07-20)
Whitespace and other supported trailing Tasks metadata may appear in either
order. The value must have the exact YYYY-MM-DD shape and name a real
calendar date. It blocks only when it is strictly later than the effective
daily anchor, so yesterday and today do not block while tomorrow and later do.
The project date lives as inline [scheduled::] on ^prj;
bob projects sync and Bob Navigation Hotkeys propagate it into ordinary
task-level scheduled fields. From that point this command treats those
fields, and the ^prj date itself, exactly like any other inline task
schedule. The ^prj lifecycle task keeps its separate #hide surfacing
contract.
Blocked is derived state: a recognized open task is blocked when it has at
least one open dependency, a valid future schedule, or both. A task that
lives in a blocked project is Blocked too, and so is every open task in
that project's sub-projects (recursively through parent:): a sub-project
inherits each ancestor's ^prj reasons, and its ^prj and residents are
Blocked with a project_dependency or project_schedule reason naming the
ancestor. Project blocking comes from the
project's open ^prj task alone: its projected Depends-On prerequisites and
its inline [scheduled::] date (^prj links to its own note or any
descendant never project and
warn project_internal_dependency; a parent cycle warns
project_parent_cycle). While ^prj carries any such reason,
every open resident of the note — nested and #ref tasks included, fenced
lines excluded — is Blocked with a project_dependency or project_schedule
reason, and carries exactly one machine-owned #prj_is_blocked tag; ^prj
itself carries the tag exactly when it inherits a reason from an ancestor.
A hand-set [?] on ^prj is not a reason and blocks nobody. Residents inherit ^prj
links as implicit promotion edges, and dependency promotion never raises a
^prj target into a lane — the walk continues through it to that project's
prerequisites. A direct Task Link raises ^prj to Next like any other task.
The full ^prj line-model rule, tag placement, and
consumption of links to Blocked tasks are in
task-dependencies.md §13. This combined
state overrides Ready ([ ]), Next ([*]), and In Progress ([/]), including
Pomodoro promotion for a task inherited from a scheduled project. A task
already [?] remains Blocked until every reason is gone. Only then does it
return to the stronger of the active and recovery-only ranks: Next when
directly recent, In Progress only when a stronger eligible dependency path
requests it, or Ready when unreachable. Every automatic Blocked-to-Ready
recovery writes [unblocked:: today] on the recovered line unless the row is
out of freshness scope, unstamped, already a scheduled tickler, or landing in
Next/Pending; stale [unblocked::] fields (fresh newer than the field) are
pruned from open lines. A directly referenced Blocked task
does not recover to its status from before blocking because no hidden
previous-status field is stored. Terminal parents and unknown/custom parent
statuses remain untouched even when they retain dependency or scheduled
metadata.
Ctrl+Enter recovery in the Task Status Cycler plugin is intentionally narrower
and immediate. After that keypress actually changes one or more tasks to Done,
the plugin reopens only Blocked dependents that directly name one of those
tasks and have no other recognized open dependency in the post-close vault
snapshot. The immediate target is the derived rank: [?] becomes [ ],
or [*] when the dependent has a live link in the post-gesture day text —
and a dependent this close fully unblocked is offered as a successor
(Next when picked) in the same pass, per
task-dependencies.md §12.
It reads unsaved open Markdown buffers, preserves
the active cursor, skips stale or failed notes without rolling back completed
tasks, and serializes recovery with closed-reference retirement. A later
bob task reconcile run remains authoritative across the whole vault and
may promote the recovered task to Next or In Progress. Closing a dependency
never reopens a Done, canceled, non-task, unknown, unrelated, or already-active
dependent, and Ctrl+Enter does not clean unrelated Blocked tasks with no
dependencies. Two guards keep this recovery truthful: a Blocked dependent
with a strictly future scheduled date stays Blocked (recovered only once
the date is reached or removed), and Ctrl+Enter on a Task Link that
resolves to a Cancelled task is consumed with a "reopen it first" notice
instead of completing the owning Pomodoro. The picker's Cancel gesture reuses
this same recovery through Task Status Cycler's versioned
api.recoverBlockedDependents entry point; when that API is missing or fails,
the cancel still lands and this command recovers the dependent later.
Hand-unblocking with the Task Status Cycler plugin's option-bracket cycle
(<option+]>/<option+[> on a [?] task, to Ready or Cancelled) retires the
future schedule that caused the Blocked marker, which is what keeps this
command from immediately re-deriving [?] on its next run. A remaining open
dependency, or a project-level date on ^prj that
bob projects sync re-propagates onto residents, still re-blocks the task
on the next run. Alt+] / Alt+[ on a [?] ^prj that leaves the project with no
remaining reason (its date retired, no Depends-On target that resolves to
an open task; a missing id never blocks) strips #prj_is_blocked from
every open resident and recovers those whose only reason was the project,
writing [unblocked::] when freshness.md §5's WRITE rule applies. Cycling
^prj to Cancelled recovers the same way even though the note is then no
longer project(T), and a transcluded ^prj recovers residents in its
source note. Closed lines stay untouched.
Cycling any task into Done through the same keys runs the same immediate
recover-and-offer pass as Ctrl+Enter above (offering what the close
unblocked, linking only the picked rows);
cycling into Cancelled runs recovery only and never offers. Reopening the
predecessor the same day through Ctrl+Enter takes back untouched picked
successor links from that pass and restores their statuses and freshness
fields.
The installed Tasks registry must contain exactly one compatible status:
{
"symbol": "?",
"name": "Blocked",
"nextStatusSymbol": " ",
"availableAsCommand": true,
"type": "ON_HOLD"
}
If a planned Blocked or unblocked write needs this contract and the definition is missing, duplicated by symbol/name, or incompatible, the command exits with an actionable error before any note write.
The command scans Markdown task lines allowed by the Obsidian Tasks
globalFilter setting. If that setting cannot be read, the filter defaults to
#task. The combined transition precedence is:
| Existing status | Desired/reachability state | Result |
|---|---|---|
| done, canceled, non-task, or unknown | any derived blocking reason | unchanged |
[ ], [*], or [/] |
open dependency and/or future schedule | [?] |
[?] |
open dependency and/or future schedule | unchanged |
[?] |
no blocking reason; desired In Progress | [/] |
[?] |
no blocking reason; desired Next | [*] |
[?] |
no blocking reason; unreachable | [ ] |
[ ] |
desired Next | [*] |
[ ] or [*] |
desired In Progress | [/] |
[*] |
desired Next | unchanged |
[/] |
desired Next or In Progress | unchanged |
[*] in a canonical daily note or the current ledger |
unreachable, not directly recent | [ ] |
[*] in a canonical daily note or the current ledger |
unreachable, directly recent | unchanged (kept_next) |
[*] outside daily notes |
unreachable | unchanged (sticky lane) |
[/] |
unreachable | unchanged (sticky lane) |
[ ] |
unreachable | unchanged |
| done, canceled, non-task, or unknown/custom | any | unchanged |
Ranked propagation itself is monotonic and never lowers a dependency target. Removing a dependency link therefore does not perform a matching rollback. The daily-note cleanup rule resets only a Next task that lives in a canonical daily note or the selected current ledger and is no longer reachable from the final open-Pomodoro graph (unless directly recent). Every other unlinked Next or In Progress task keeps its lane. Open Dataview dependency derivation takes precedence over the cleanup rule, producing one stable final state.
The machine-managed #task #ref ... ^ref reading task in a generated reference
note is an ordinary scanned task. Promoting it to [*] or [/] therefore
flows through the next highlights sync as the corresponding reference status;
clearing an unreachable daily-note [*] back to [ ] flows through as
status: ready. Blocking it to [?] is status-neutral for highlights sync:
the reference keeps its stored lane and needs no PDF marker write; unblocking
flows through like any other checkbox change. Generated reference notes are ordinary notes for lane
purposes: their [/] tasks are sticky like any other. Because the highlights lifecycle is also
stored in the PDF marker, preview with bob ref scan --dry-run and use a
reviewed bob ref scan --write-pdfs when marker write-back is needed.
Completion is classified separately from Next synchronization. Conventional
[x] and [X] tasks are complete. A custom checkbox symbol is also complete
when its entry in statusSettings.coreStatuses or
statusSettings.customStatuses has type DONE. CANCELLED, IN_PROGRESS,
ON_HOLD, NON_TASK, unknown, and unchecked statuses are not complete.
Blocked task links are consumed¶
After transitions compute the final Blocked set (newly blocked tasks plus
unchanged [?] rows that still have a reason), the command removes those
tasks' live, unstruck Task Links under today's open Pomodoros. Closed
entries, 🍅 history, other days, and Depends-On links are never touched.
A link-only bullet is removed with its subtree; otherwise only the link
token is removed. The empty-Pomodoro rule then runs and may delete a running
timed entry (accepted; matches cancel). Before removal, the link seeds
promotion along edges(T) exactly as a surviving link would, so open
prerequisites (own plus inherited) rise to at least Next. A link whose
target's last reason clears in the same run is kept.
Human and JSON output report each occurrence as
consumed_blocked_references
({path, line, pomodoro, task, reasons, promoted[]}). Emptied entries fold
into removed_empty_pomodoros. Dry-run output equals the live write, and a
second run changes nothing except the documented daily-note exception: a
prerequisite in a canonical daily note promoted only by consumption follows
the sticky-lanes R9 carve-out and may clear on the next run.
See task-dependencies.md §13.5 and the PDL vectors.
Status Grouping¶
After all final checkbox changes and daily-note structural cleanup are
composed, the command groups eligible [[area]] and [[project]] note
Tasks sections. Grouping is applied to the composed bytes, so a task promoted
to Next, blocked by a future schedule, completed, canceled, or reopened in the
same run lands in the group matching its final status. Area/project notes with
no checkbox edits are still grouped when their current layout needs it.
Ready [ ] tasks remain in the unheaded intake directly under Tasks, along
with introductory prose and new ordinary captures. Decorated containers also
get a generated status-count badge row before the intake. The generated groups
are plain child headings owned by title and shape: a heading directly under a
Tasks container (or under an authored topic inside one) is managed if and
only if its title is exactly Next & In Progress, Blocked, or
Done & Canceled and its body holds only movable task blocks and blank
lines. bob capture section completion, bob capture-sections, and @route#
placement skip these headings:
# Project
## Tasks
[`⚪ 1 open`](#Project#Tasks) · [`🔵 2 next/wip`](#Project#Tasks#Next%20&%20In%20Progress) · [`🔴 1 blocked`](#Project#Tasks#Blocked) · [`🟢 2 done/canceled`](#Project#Tasks#Done%20&%20Canceled)
Project context.
- [ ] #task Later idea ^idea
### Next & In Progress
- [/] #task Finish implementation ^finish
- [*] #task Review ^review
### Blocked
- [?] #task Wait for dependency ^wait
### Done & Canceled
- [x] #task Agreed scope ^scope
- [-] #task Superseded option
A group exists only while it holds a task: empty groups are never emitted,
and group order stays fixed (Next & In Progress, Blocked,
Done & Canceled), so a returning group always lands in the same place. A
container is decorated (badge row plus groups) if and only if it holds at
least one groupable task (Next, In Progress, Blocked, Done, or Canceled).
When it holds none, the badge row and every group heading go, and any Ready
task found inside an old group returns to the intake. Nothing is remembered
between runs: the layout is a pure function of note content.
The badge row has four fixed-order chips: open, next/wip, blocked, and
done/canceled. Counts are per-container and non-recursive: the open chip counts
root task blocks that remain in that container's intake, while the other chips
count that container's generated status groups. A chip whose group does not
exist renders as a plain unlinked code span (`🔴 0 blocked`); the open
chip always links to the container heading. Each linked chip points at the
counted heading using Obsidian's nested heading path form, such as
#Project#Tasks#Blocked; if any heading segment in the container ancestry
contains #, the whole row is rendered as unlinked code-span chips because
that path cannot be represented safely.
The row is recognized by its exact generated grammar: four fixed-order
code-span chips (open, next/wip, blocked, done/canceled), each
optionally wrapped in a heading link, joined by ·. On every grouping
rewrite, duplicate or relocated rows in the intake are regenerated into the
slot directly beneath the Tasks or authored-topic heading, so a capture
that temporarily lands above the row is self-healed on the next run. An
orphaned badge row in a container with no generated groups and no groupable
tasks is removed. A row inside a managed group fails closed with
misplaced_badge_row; the affected container's bytes are left unchanged.
Legacy <!-- bob:task-status-badges:v1 --> comments are removed on the next
rewrite.
Badge counts are a written snapshot, not a live Obsidian view. bob capture,
manual checkbox edits, and bob task archive can make them stale; the next
bob task reconcile run reconciles the row idempotently.
The command preserves authored topic headings under a Tasks section. Direct
task blocks are grouped locally inside the container where they already live,
so a custom ### Backend topic receives its own generated child groups rather
than losing its context. Re-running on unchanged semantic input is
byte-identical.
Only root task blocks accepted by the configured Tasks globalFilter move.
The parent root's final status chooses the destination; nested children, notes,
links, block IDs, metadata, continuations, and internal fenced snippets travel
with that root. Ordered-list task roots and tasks nested under ordinary list
items are reported as unsupported for grouping and left intact. H6 Tasks
containers, ambiguous Markdown boundaries, group-title headings holding
non-task content (ownership_collision), legacy comments with an unknown kind
or in the intake (malformed_ownership), legacy comments under the wrong
title (renamed_marked_heading), duplicate group headings, badge rows inside
groups (misplaced_badge_row), and authored child headings inside a managed
group are warnings; the affected container is left unchanged, but other safe
containers may still be written. Legacy
<!-- bob:task-status-group:v1:* --> comments are removed on the next
rewrite. An older bob re-adds the comments and empty groups (losslessly), so
upgrade every host that runs reconcile or reroll together.
Grouping never creates a missing Tasks section. It excludes canonical daily
notes, the selected current daily ledger even when BOB_DAY_FILE points
outside the vault, the selected previous daily, ordinary notes, read-only
archive references, hidden directories, done/, _generated/, and
_templates/.
bob task reroll composes derived Blocked status and this same grouping into
its own write (see randomize.md), so
after a randomize run this command has nothing left to change in those
notes. bob capture keeps adding Ready tasks to the intake before
generated group headings; a later bob task reconcile run moves that task
only if its final status becomes Next, In Progress, Blocked, Done, or Canceled.
For each bullet beneath an open or completed Pomodoro that contains a block
link resolving unambiguously to a completed Tasks task, the command retires
that link as ~~[[...]]~~. Aliases and neighboring text are preserved, so
[[dev#^done|result]], ![[dev#^done|result]], and
~~![[dev#^done|result]]~~ all become
~~[[dev#^done|result]]~~. On mixed-content bullets, only links proven
complete are changed. Canonical struck links are unchanged.
Pomodoro Marker¶
The marker records that a non-transcluded link participated in a completed Pomodoro. It belongs to the individual link, not the bullet, and is normalized from each occurrence's syntax before retirement:
| Link state | Canonical grammar |
|---|---|
| completed, live non-transcluded | 🍅 [[dev#^write-tests]] |
| completed, embedded | ![[dev#^reference]] |
| retired with recorded participation | 🍅 ~~[[dev#^write-tests|alias]]~~ |
| retired with unknown/embed provenance | ~~[[dev#^reference]]~~ |
| mixed content | Work on 🍅 [[a#^x]] and ~~[[b#^y]]~~ |
The sync adds or canonicalizes markers on live non-transcluded links beneath
completed Pomodoros and removes markers from embedded links. For an already
struck non-embedded link, it preserves whether a marker exists: an existing
marker is canonicalized, but a missing marker is not backfilled because the
strike may be the only surviving evidence that the link was retired from a
transclusion. Open Pomodoros are unmarked. Cancelled ([-]) Pomodoros, fenced
code, the top-level Pomodoro line, and links outside ## Pomodoros are
untouched. Marker repair is decoration-only and never changes Next/dependency
selection.
The containing bullet is relocated according to this order:
- The single open top-level Pomodoro with a valid time range is the current Pomodoro.
- If there is no current Pomodoro, the last completed (
[x]or[X]) top-level Pomodoro in document order is used. - If neither exists, the bullet stays where it is and only the link is struck.
Relocation happens at bullet granularity. Nested descendants move with their
parent, multiple moved bullets retain their document order, and the root
indentation is normalized to the destination's child indentation. When the
current Pomodoro is already the owner, only retirement is needed. A repair
found beneath a completed Pomodoro is always normalized in place and is never
moved into a newer session. On a bullet moved to the completed fallback, a
completed embedded link becomes unmarked ~~[[...]]~~, while a completed live
non-transcluded link becomes 🍅 ~~[[...]]~~. A bullet moved to the current
open Pomodoro remains unmarked.
For example, a completed task under a future Pomodoro is moved to the current timed entry:
- [ ] Current work (0900-0930)
- ~~[[dev#^finished]]~~
- [ ] Future work
With no timed open entry, the last completed entry is the fallback:
- [x] Earlier work
- 🍅 ~~[[dev#^finished]]~~
- [ ] Future work
With only untimed open entries, the link is struck in place because no relocation target exists. Both reconcile and the scoped capture-side retirement run this same structural planner with its dedupe flag on, so a carried copy is dropped when the destination entry already links the task. A live non-transcluded link moved to the completed fallback still gets the tomato marker when it is the first copy; a duplicate is never inserted.
A task must have a trailing ^block-id to be linked. The edit changes only
the status character, preserving indentation, list markers, descriptions,
block IDs, and line endings.
The active vault scan skips dot-prefixed directories, done/, _generated/,
and _templates/, so archived tasks and templates are never synchronized.
Exact done/... Pomodoro-reference targets from the current or previous
ledger may be loaded as a read-only supplemental catalog, but they are not
counted as scanned files, never receive status writes, and never participate
in dependency propagation or derived Blocked state. Consequently, a dependency
link into done/ is still reported as unresolved; the archived task itself
remains untouched.
Guard Rails¶
The command exits with status 1 and writes nothing when the current daily note
is missing, has no ## Pomodoros section, or contains multiple non-empty open
timed Pomodoros. The multiple-open-timed error names each conflicting entry
with its one-based line and time range, for example
Bob daily note has multiple open timed Pomodoros: First (line 3, 0900-0930),
Second (line 5, 0930-1000); close all but one with `bob capture x`, or
mark it `[x]`. Empty timed entries are pruned instead of making the current
Pomodoro ambiguous. A valid but empty current section is a valid source of
truth: it clears every daily-note [*] task without recent-activity grace
and leaves every other lane untouched. This distinction prevents a
missing or malformed current ledger from causing a mass clear.
The previous daily is optional and never weakens those current-ledger guards. No previous note is valid, while a selected previous note with no Pomodoros section is a successful empty historical source. The selected file remains byte-for-byte read-only even when the current ledger and task notes change.
A planned Blocked/unblocked status edit also fails atomically when the Tasks
registry is unreadable or its Blocked definition is missing, duplicated, or
incompatible. Dry-run uses the same guard. This prevents both an unknown [?]
marker and partial composition with daily-note structural edits.
Live runs take the shared vault-maintenance lock (BOB_VAULT_SYNC_LOCK_FILE,
the same lock as bob vault-sync and bob nightly) before planning and hold
it through application. Before the first note replacement the command re-reads
every planning input — scanned notes, the current and previous daily, loaded
archive notes, Tasks settings (missing or present), and the scan manifest —
and compares complete bytes plus regular-file identity, not only mtime or
length. A concurrent save, a replacement inode, a symlink substitution, a
changed Tasks settings file, or a new or deleted scan candidate cancels the
run with no note writes. Replacements are staged as uniquely created
temporaries in the destination directories; originals and proposed bytes are
stored first under
${XDG_STATE_HOME:-$HOME/.local/state}/bob-cli/task-status-hooks/<vault-hash>/<run-id>/.
Recovery failure prevents note writes. A later failure after some notes have
already been replaced reports the applied and remaining paths and does not
roll earlier notes back. Byte checks and atomic rename reduce lost-update
risk but cannot provide a true compare-and-swap against an editor that does
not coordinate with the command. When a note needs structural status grouping
or a dependency projection-only write, the live writer also requires a
two-second quiet interval after that note's observed modification time; if
the timestamp is uncertain, in the future, or changes during the bounded
wait, the run is deferred with no note writes.
BOB_NOW does not affect this real filesystem check. An unsaved Obsidian
buffer is not observable, and a save can still race the final check. Recovery
copies are the observed originals and intended outputs for compare/merge into
the current note, not a blind vault restore. --dry-run does not wait, lock,
stage, or write recovery records. A live no-op may create the ordinary lock
file, but no recovery or note staging artifacts. Lock contention, a changed
input, and a quiet-period deferral all exit with status 1; the retry
controller described in Retries reruns each of those
automatically within its budget. A partial apply also exits with status 1
but is never retried automatically — its reason can combine different
failure stages, so treat it as an instruction to inspect the recovery
directory and rerun by hand once the vault settles, not as something the
automatic policy replays for you.
Unresolved direct or dependency links are warnings, not failures. If duplicate
task block IDs occur in one resolved note, every matching task is synchronized
and the ambiguity is reported. Completed-link normalization proceeds only when
all duplicate matches are complete; conflicting completion states are warned
and left structurally unchanged. Canceled-reference list-item removal likewise
proceeds only when every match has a recognized CANCELLED status; mixed
cancellation states are warned and retained.
Retries¶
Live runs (anything without --dry-run) wrap one complete sync attempt in a
bounded, jittered-backoff retry controller. -r, --retry-timeout SECONDS sets
the budget (default 120); 0 performs exactly one attempt and preserves
the pre-retry fail-fast behavior. Each retried attempt is a wholly fresh
attempt: returning from one drops its maintenance lock and every snapshot
before any backoff, and the next attempt reacquires the same lock, rereads
every input, and recomputes the whole plan from scratch — never replaying a
stale plan or staged output from an earlier attempt. Backing off between
attempts therefore never holds the shared maintenance lock, and an
in-flight attempt is always allowed to finish; the budget only gates
whether another attempt starts, never a running one.
Only these existing failure reasons are ever retried automatically, using an explicit allowlist rather than error-message matching or a blanket retry on exit status 1:
| Failure | Automatic action |
|---|---|
lock_contention |
Retry within the budget. |
vault_changed, quiet_period, unstable_read |
Retry only when the attempt applied no files. |
partial_apply, or any error that lists applied files |
Stop and retain recovery details. |
recovery_failed, unsupported_file, io, validation errors, and unknown or missing reasons |
Stop immediately. |
Backoff delays follow ceilings of 2, 4, 8, 16, then 30 seconds and capped there, drawing a random subsecond delay from the upper half of each ceiling so retries do not synchronize with the 15-second vault-sync LaunchAgent timer. Each sleep is clamped to the remaining budget, and the elapsed budget is checked again before every new attempt, so the command never starts another attempt once the budget is exhausted.
Each retry decision prints one concise timestamped line with a run identifier,
attempt number, elapsed time, failure reason, error detail, next delay, and
the recovery directory when the failed attempt left one. Once the retry
sequence ends, one more timestamped summary line reports success, an exhausted
budget, or a terminal stop, with the attempt count and elapsed time — an
exhausted budget is never reported as a success. In default human mode, these
routine retry decision and summary lines use stdout so successful scheduled
runs can append retry progress with the normal final report. In --format json
mode, retry progress remains on stderr so stdout continues to contain exactly
one parseable final JSON object. An uncontended run, whose first attempt either
succeeds or fails terminally, prints none of these retry lines.
--dry-run always uses exactly one attempt and never enters the retry
controller's sleep or logging behavior, even when --retry-timeout is
supplied; it still creates no lock, recovery records, staged files, or
modified notes.
This command does not manage its own log file; the scheduler owns
redirection. For the default human cron invocation, append stdout with
>> logfile so routine retry progress and the final result are logged while
warnings and terminal failures still go to stderr and can trigger scheduler
mail. JSON schedulers that want a combined log must choose explicitly whether
to keep stderr separate for retry progress or merge it with 2>&1. See
Mac scheduled maintenance for
the exact staggered crontab this project runs.
Output¶
Human output lists every Next promotion, In-Progress promotion, daily-note
Next clear, Blocked transition, unblock, duplicate line removal,
empty Pomodoro removal, retired reference, move, and marker repair, plus every
canceled-reference list-item trigger, followed by a summary. The legacy
cleared in progress section is retained but always empty, and the kept line
(kept X already next · Y in progress) counts only daily-note Next grace
keeps plus In Progress tasks that already held a desired rank. The selected
previous daily path and its reference count appear in changed and no-op
reports.
Canceled-reference rows show the target, block ID, original one-based line
number, and owning Pomodoro that triggered complete list-item deletion. Blocked
rows identify the reason kinds (local_dependency, local_schedule,
project_dependency, project_schedule), the future scheduled date, open
dependency IDs, and — for inherited reasons — the project path with its open
prerequisite IDs or schedule date; unblocked rows include unresolved IDs when
present. Tag writes appear under added/stripped project-blocked tags and
[unblocked::] writes and prunes under wrote/pruned unblocked fields, each
with its own trailing Summary: counts. Duplicate removals show the
original daily-note line number, text, owning Pomodoro, and canonical task
identities. Marker additions and removals have their own
marked/unmarked sections and summary counts. Dependency-derived promotions
carry a (dependency) suffix. Next and In-Progress promotions have separate
sections and summary counts. Empty-Pomodoro removals appear under
removed empty Pomodoros or would remove empty Pomodoros, show the original
one-based Pomodoro line number and line text, and have their own summary
count. Grouping changes appear under
grouped task sections or would group task sections, with the note path,
heading ancestry, open intake count, per-group root-task counts, and moved-block count. A
successful live write that changed notes also prints the recovery-copy
directory. Dry-run uses the same planning path and reports what would happen
without changing any file. Warnings go to stderr. A no-op prints a single
already in sync summary line only when task statuses, daily-note links, grouping,
and grouping diagnostics are all unchanged.
When a plan budget is available, both changed and no-op reports print one
meter line, for example plan 3/3 themes · 7/10 links · TODAY 7 · PENDING 8/10 · NEXT 12/15.
It follows the sync stats line on changed runs and the already in sync
summary on no-op runs. Over-cap meters are red.
When the report carries plan warnings, the line appends the count and points
at bob plan, for example · 2 plan warnings (run bob plan); individual
plan lints are never printed here. The budget is read-only: it never changes
the exit code and never writes anything. An invalid plan config prints a
single stderr warning and reports plan_budget as null.
JSON mode prints one object on stdout with these stable fields:
{
"ok": true,
"dry_run": true,
"daily_file": "2026/20260710.md",
"previous_daily_file": "2026/20260703.md",
"open_pomodoros": 1,
"references": 2,
"previous_daily_references": 3,
"recent_activity_references": 4,
"dependency_references": 1,
"scanned_files": 128,
"marked_next": [
{
"path": "dev.md",
"line_number": 12,
"block_id": "write-design",
"description": "Write the design",
"dependency": false
},
{
"path": "dev.md",
"line_number": 18,
"block_id": "write-tests",
"description": "Write tests",
"dependency": true
}
],
"marked_in_progress": [
{
"path": "dev.md",
"line_number": 24,
"block_id": "review-tests",
"description": "Review tests",
"dependency": true
}
],
"cleared": [],
"cleared_in_progress": [],
"marked_blocked": [
{
"path": "dev.md",
"line_number": 30,
"block_id": "ship",
"description": "Ship the feature",
"from": "/",
"to": "?",
"open_dependency_ids": ["dev__review"],
"unresolved_dependency_ids": [],
"future_scheduled_date": "2026-07-12",
"reason_kinds": ["local_dependency", "local_schedule"],
"project_path": null,
"project_open_dependency_ids": [],
"project_scheduled_date": null
}
],
"unblocked": [
{
"path": "dev.md",
"line_number": 38,
"block_id": "released",
"description": "Release the feature",
"from": "?",
"to": "*",
"open_dependency_ids": [],
"unresolved_dependency_ids": ["deleted_task"],
"future_scheduled_date": null,
"reason_kinds": [],
"project_path": null,
"project_open_dependency_ids": [],
"project_scheduled_date": null
}
],
"prj_is_blocked_tags_added": [
{
"path": "Projects/Alpha.md",
"line_number": 12,
"block_id": "write-tests"
}
],
"prj_is_blocked_tags_stripped": [],
"unblocked_fields_written": [
{
"path": "dev.md",
"line_number": 44,
"block_id": "polish",
"date": "2026-07-16"
}
],
"unblocked_fields_pruned": [],
"struck_completed_references": [
{
"target": "dev",
"block_id": "finished",
"pomodoro": "- [ ] Future work",
"removed_embed": false
}
],
"embedded_completed_references": [],
"moved_completed_references": [
{
"target": "dev",
"block_id": "finished",
"source_pomodoro": "- [ ] Future work",
"destination_pomodoro": "- [ ] Current work (0900-0930)"
}
],
"marker_added_references": [
{
"target": "dev",
"block_id": "finished",
"pomodoro": "- [x] Earlier work"
}
],
"marker_removed_references": [],
"removed_canceled_references": [
{
"target": "dev",
"block_id": "canceled-work",
"line_number": 8,
"pomodoro": "- [ ] Current work (0900-0930)"
}
],
"consumed_blocked_references": [
{
"path": "2026/20260710.md",
"line": 12,
"pomodoro": "- [ ] Current work (0900-0930)",
"task": {
"path": "Projects/Alpha.md",
"block_id": "ship"
},
"reasons": ["project_dependency"],
"promoted": [
{
"path": "dev.md",
"block_id": "write-design"
}
]
}
],
"removed_duplicate_lines": [
{
"line_number": 9,
"pomodoro": "- [ ] Later work",
"line": " - [[Alpha#^ship|duplicate]] and [[Beta#^review]]",
"duplicate_tasks": [
{
"path": "Projects/Alpha.md",
"block_id": "ship"
}
]
}
],
"removed_empty_pomodoros": [
{
"line_number": 12,
"line": "- [ ] () — GTD"
}
],
"grouped_task_sections": [
{
"path": "Projects/Alpha.md",
"original_heading_line": 12,
"heading_ancestry": ["Alpha", "Tasks"],
"open": 1,
"next_and_in_progress": 2,
"blocked": 1,
"done_and_canceled": 1,
"moved_block_count": 4,
"moved_blocks": [
{
"original_line": 17,
"destination": "next_and_in_progress"
}
]
}
],
"grouping_warnings": [
{
"path": "Projects/Beta.md",
"original_heading_line": 20,
"heading_ancestry": ["Beta", "Tasks"],
"code": "ownership_collision",
"message": "status-group heading in \"Tasks\" at line 20 contains non-task content"
}
],
"dependency_projection_updates": [
{
"kind": "dependent_field",
"path": "dev.md",
"line": 30,
"detail": "dependsOn := dev__review"
}
],
"adopted_dependency_lines": [],
"healed_dependency_links": [],
"canonicalized_dependency_lines": [],
"legacy_dependency_children": 0,
"dependency_warnings": [
{
"kind": "unresolved_dependency_link",
"path": "dev.md",
"line": 30,
"detail": "link [[missing#^gone]] kept verbatim; it never blocks"
}
],
"blocks_line_updates": [
{
"path": "dev.md",
"line": 34,
"task": "Review the outline",
"kind": "inserted",
"added": ["[[#^ship]]"],
"removed": []
}
],
"minted_dependent_block_ids": [
{
"path": "inbox.md",
"line": 12,
"block_id": "call-back"
}
],
"linked_from_line_updates": [
{
"path": "dev.md",
"line": 36,
"task": "Design a retry policy",
"kind": "inserted",
"added": ["[[#^fix-retries]]"],
"removed": []
}
],
"minted_relation_source_block_ids": [
{
"path": "inbox.md",
"line": 4,
"block_id": "fix-retries"
}
],
"relation_warnings": [],
"applied_files": [],
"deferred_files": [],
"recovery_directory": null,
"kept_next": 0,
"kept_in_progress": 1,
"unresolved_references": [],
"plan_budget": {
"date": "2026-07-10",
"daily_file": "2026/20260710.md",
"caps": { "max_themes": 3, "max_links": 10, "max_next": 15, "max_pending": 10, "strict": false },
"status": "ok",
"themes": { "count": 1, "cap": 3, "over": false },
"links": { "count": 2, "cap": 10, "over": false },
"today": { "count": 0 },
"next": { "count": 0, "cap": 15, "over": false },
"pending": { "count": 0, "cap": 10, "over": false },
"today_tasks": [],
"theme_names": ["GOALS"],
"entries": [
{
"line": 5,
"name": "GOALS",
"components": ["GOALS"],
"exempt": false,
"running": false,
"highlight": true,
"links": 2
}
],
"warnings": []
}
}
scanned_files counts only Markdown notes included in the active vault scan;
it does not include read-only archive notes loaded only for exact done/...
Pomodoro references or for relation LINKS sources and explicit done/
targets. references retains its input-count contract and counts
unique raw direct Pomodoro block links before structural cleanup; consumers do
not need to reinterpret that older field. A canceled reference removed during
this run therefore remains part of references, while the post-rewrite graph
excludes it. dependency_references counts additional unique task blocks reached
through dependency edges in the final rewritten ledger.
previous_daily_file is the optional vault-relative selected historical path;
it is null when no earlier canonical daily exists.
previous_daily_references counts unique eligible non-retired raw links in
that note, or zero for no note or a note without a Pomodoros section.
recent_activity_references counts unique resolved path-plus-block roots from
the normalized current and previous sources before dependency traversal.
marked_next contains only [ ] -> [*] changes, while
marked_in_progress contains both [ ] -> [/] and [*] -> [/] changes. Each
change item's dependency boolean distinguishes direct references from
dependency-only graph reachability. marked_blocked and unblocked are
additive fields and do not duplicate changes into those older arrays.
cleared lists only daily-note [*] -> [ ] changes: a Next task that lives
in a canonical daily note or the selected current ledger and lost its links
without recent-activity grace. Every other unlinked Next or In Progress task
keeps its lane and never appears here. cleared_in_progress is an
always-empty compatibility list (the In Progress rollback is retired) with the
same path, line, block ID, description, and dependency shape. kept_next
counts only directly recent daily-note Next tasks held by grace; sticky
non-daily Next tasks stay without incrementing it. Their
from/to values are the actual checkbox symbols, and their dependency-ID
arrays and nullable future_scheduled_date explain the derived decision. The
date is present only when the task-level schedule is later than the effective
anchor. Each
removed_canceled_references item represents one removed occurrence and
contains its link target, block_id, one-based original line_number, and
owning pomodoro. This stable compatibility field reports the qualifying
references that triggered list-item deletion; multiple qualifying occurrences
on one deleted item remain separate entries. The array follows deterministic
file/occurrence order. Each
consumed_blocked_references item is one live Task Link removed because its
target is effectively Blocked: the daily-note path, one-based line, owning
pomodoro, the blocked task {path, block_id}, reasons (the §13.9
kinds), and promoted[] identities raised along edges(T) by that
consumption seed. Each
removed_duplicate_lines item represents one physical line and contains its
one-based original line_number, original line, owning pomodoro, and one or
more canonical path-plus-block duplicate_tasks.
removed_empty_pomodoros contains one item per deleted Pomodoro entry in source
document order. First-pass deletions (canceled links, completed-link
retirement) report the original daily-note line_number and line.
Consumption-pass deletions (emptied after blocked-link removal) report
the composed daily-note text after that first rewrite pass. Compatibility
counters such as
open_pomodoros and references keep their input-count meaning and are not
decremented by this cleanup. Each unresolved reference
contains target, block_id, and reason; marker-reference entries contain
target, block_id, and the owning pomodoro line.
grouped_task_sections contains only containers whose bytes changed for
grouping, with original source heading lines and moved-block destinations
reported from the pre-move source. It now includes containers that returned
to a plain layout, with all group counts at 0. Its open count is the number
of root task blocks emitted into that container's intake after the transform,
including recovered or structurally ineligible tasks that remain there.
grouping_warnings contains safe-to-report container diagnostics; warnings do
not by themselves make ok false.
dependency_projection_updates lists every reconciled field, target-id, and
line-removal edit with its kind, path, one-based line, and detail;
adopted_dependency_lines, healed_dependency_links, and
canonicalized_dependency_lines list the adopted, healed, and canonicalized
lines in the same shape, legacy_dependency_children counts legacy children
for the legacy-window retirement decision, and dependency_warnings lists
{kind, path, line, detail} rows (for example unresolved_dependency_link,
previous_daily_target, unencodable_dependency_target,
dependency_field_ids_dropped, malformed_blocks_line).
blocks_line_updates lists each BLOCKS-line insert, rewrite,
canonicalization, removal, or deduplication with path, one-based line,
owner task description, kind, and added/removed canonical links;
minted_dependent_block_ids lists each dependent ^block-id minted so a
BLOCKS entry can name it. linked_from_line_updates lists each LINKED FROM
insert, rewrite, canonicalization, removal, or deduplication in the same
shape; minted_relation_source_block_ids lists each source ^block-id
minted so a LINKED FROM entry can name it; relation_warnings lists
{kind, path, line, message} rows (malformed_links_line,
malformed_linked_from_line, unresolved_relation_link,
relation_target_not_task, self_relation, duplicate_block_id). The
human report mirrors these with projected-field, adopted, healed,
canonicalized, 🧱 BLOCKS, and 🪢 LINKED FROM sections plus
Dependencies: and Relations: lines, and the Summary: line appends the
same counts.
applied_files, deferred_files, and recovery_directory describe live
application. On dry-run and live no-op, applied_files and deferred_files
are empty and recovery_directory is null; on a successful live write,
applied_files lists the vault-relative notes replaced and
recovery_directory points to the recovery record. JSON failures also remain
machine-readable as { "ok": false, "error": "...", "reason": "..." } and
include applied/deferred paths plus recovery directory when available; they
carry "plan_budget": null.
embedded_completed_references is a deprecated, always-empty compatibility
field for one contract cycle.
plan_budget holds the shared plan report described in
plan.md: date, daily file, caps, status, themes/links
meters, Today count and rows, NEXT/PENDING lane meters, theme names,
entries, and warnings. It is computed read-only from
the original daily note, before this run's ledger cleanup, so a live sync
can leave the final ledger with different counts. It is null when there
is no daily note or Pomodoros section, or when the plan config is invalid.