Skip to content

Latest commit

Β 

History

History
690 lines (607 loc) Β· 32.5 KB

File metadata and controls

690 lines (607 loc) Β· 32.5 KB

Task Status Hooks

bob task-status-hooks 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:

  • Promotes tasks linked under today's open Pomodoros to Next ([*]), leaving In Progress ([/]) alone
  • Follows transcluded dependency bullets (![[note#^id]] as the entire child) 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, and repairs πŸ… markers

The latest existing earlier daily note is a read-only recent-activity source: it can keep an area/project In Progress task active and can supply the rank an otherwise-unblocked Blocked task recovers to, 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-status-hooks [-b|--bob-dir DIR] [-d|--dry-run] [-f|--format human|json]

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.

task-status-hooks is the canonical and documented command name. The hidden task-status-setter and mark-next-tasks spellings remain compatibility-only dispatch aliases and show canonical usage when asked for help.

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 rewritten Pomodoro section is scanned again after duplicate removal and canceled/completed-reference structural rewrites. Only references that will actually be written contribute to the direct desired-status map and dependency graph. Thus an otherwise-live task mentioned only as sibling or nested content in a removed item, a removed canceled root, 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' child blocks. An edge must be a child bullet whose entire content is one transcluded block link:

- [ ] #task Ship the feature ^ship
  - ![[#^write-tests]]
  - ![[Quality/Review#^review]]
- [ ] #task Write tests ^write-tests

Same-note and cross-note targets use the same resolver as Pomodoro links. The target block must belong to a scanned Tasks task. Plain [[#^id]] links, aliases, mixed-content bullets, fenced examples, non-task #^ref blocks, and unresolvable targets are not dependency edges. 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 transclusion 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 may preserve an already-In-Progress task or an existing directly referenced recovery-ranked Next task, but they never promote a Ready task to Next or In Progress.

Recent roots traverse the same eligible transcluded-task dependency graph as current promotion roots with the same strongest-rank merge and cycle rules. This produces a recovery-only rank in addition to protecting an area/project In-Progress dependency. A Blocked task reached directly defaults to Next because Blocked has no active rank. An already-In-Progress task on the transclusion 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 vault-wide Next clearing policy after its Blocked status is gone.

After this traversal, a recognized [/] Tasks task is reset to [ ] only when all of the following are true:

  • its note has frontmatter type: [[area]] or type: [[project]], including the shared parser's single- and double-quoted scalar forms;
  • the note is not a canonical daily note or the selected current ledger;
  • the task's canonical path-plus-block identity is absent from recent activity, or the task has no usable trailing block ID; and
  • it has no derived blocking reason from either an open Dataview dependency or a future task-level schedule.

Directory names and tags do not establish area/project scope. Ordinary notes, daily notes, generated references, terminal statuses, unknown/custom statuses, and other checkbox states are not subject to this rollback.

Derived Blocked Status

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. YAML scheduled: YYYY-MM-DD project frontmatter participates indirectly: bob projects sync and Bob Navigation Hotkeys' project property picker propagate it into ordinary task-level scheduled fields. From that point this command treats the fields exactly like any other inline task schedule. The ^prj lifecycle task remains excluded from propagation and 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. 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 transclusion path requests it, or Ready when unreachable. 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 always Ready ([ ]): the plugin does not guess the final Pomodoro rank. 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-status-hooks 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.

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 scheduled frontmatter date that bob projects sync re-propagates, still re-blocks the task on the next run.

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
[*] unreachable [ ]
[/] in an area/project note no rolling recent activity [ ]
[/] in an area/project note rolling recent activity from either daily or an eligible dependency path unchanged
[ ] or out-of-scope [/] unreachable unchanged
done, canceled, non-task, or unknown/custom any unchanged

Ranked propagation itself is monotonic and never lowers a dependency target. Removing a transclusion therefore does not perform a matching rollback. The separate vault-wide cleanup rule still resets any Next task that is no longer reachable from the final open-Pomodoro graph. The distinct scoped rollback resets stale In Progress only under the rolling-activity rules above and never resets terminal/custom statuses. Open Dataview dependency derivation takes precedence over both cleanup rules, 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 [*] back to [ ] flows through as status: ready. Existing [/], [x]/[X], and [-] statuses in generated reference notes are never lowered because those notes are outside the area/project rollback scope. Because the highlights lifecycle is also stored in the PDF marker, preview with bob highlights scan --dry-run and use a reviewed bob highlights 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.

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
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.

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 open timed Pomodoros. A valid but empty current section is a valid source of truth: it clears every scanned [*] task and applies scoped stale-In-Progress rollback using the optional previous source. 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.

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.

Output

Human output lists every Next promotion, In-Progress promotion, Next clear, scoped In-Progress clear, Blocked transition, unblock, duplicate line removal, retired reference, move, and marker repair, plus every canceled-reference list-item trigger, followed by a summary. 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 future scheduled date, open dependency IDs, or both; unblocked rows include unresolved IDs when present. 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. 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 line only when neither task statuses nor daily-note links need changes.

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": [
    {
      "path": "Projects/Alpha.md",
      "line_number": 29,
      "block_id": "stale-work",
      "description": "Stale project work",
      "dependency": false
    }
  ],
  "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"
    }
  ],
  "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
    }
  ],
  "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)"
    }
  ],
  "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"
        }
      ]
    }
  ],
  "kept_next": 0,
  "kept_in_progress": 1,
  "unresolved_references": []
}

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. 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 remains the compatibility list for [*] -> [ ], while cleared_in_progress separately reports scoped [/] -> [ ] changes with the same path, line, block ID, description, and dependency shape. 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 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. Each unresolved reference contains target, block_id, and reason; marker-reference entries contain target, block_id, and the owning pomodoro line. JSON failures also remain machine-readable as { "ok": false, "error": "..." }. embedded_completed_references is a deprecated, always-empty compatibility field for one contract cycle.