Skip to content

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 x close, 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

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.

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:

  1. The single open top-level Pomodoro with a valid time range is the current Pomodoro.
  2. If there is no current Pomodoro, the last completed ([x] or [X]) top-level Pomodoro in document order is used.
  3. 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.

Tested rolling snapshot bob-cli 41afa14 bob-mac-capture 556bb67 bob-plugins af1e691 Built 2026-10-24T22:34:13Z (zensical) build.json