Design decisions¶
Accepted architecture and policy decisions for Bob, generated from the decisions memory web.
Answering A Walk Landing Advances The Walk¶
Status: Superseded in part by A Task Move Never Advances The Walk
A gesture that started on the row ]s just landed on, and whose committed write takes that row out of today's walk, advances the walk exactly once to the next remaining review item: Ctrl+Enter close, Ctrl+Alt+F, Alt+N, the Ctrl+Shift+Enter link, a resolving Task Card commit, and Ctrl+Shift+M. Alt+F is the one stay answer; Ctrl+Enter never crosses the PRE/POST boundary; a short gesture lock swallows double presses; every walk landing records a
Applies to. bob-plugins (bob-navigation-hotkeys, task-status-cycler, block-id-prompt, bob-ledger-tools), bob-cli docs, the Bob vault review ritual.
Claim. On the row ]s just landed on, a gesture whose committed write
takes that row out of today's walk advances once to the next remaining item.
That covers the Ctrl+Enter close, Ctrl+Alt+F, the Alt+N commit/release, the
Ctrl+Shift+Enter link, a resolving Task Card commit, and Ctrl+Shift+M (which
advances instead of focusing the destination). Alt+F is the one stay answer.
Ctrl+Enter never auto-advances across the PRE/POST checklist boundary.
Esc, refusals, no-op writes, unlink, reopen, Reword, status cycling, and
anything off a landing stay. A short gesture lock swallows double presses
(~350 ms settle window after each advance). Every walk landing records a
<C-o> jump back to the answered row. Each advance shows one toast (what
was done, then where the walk landed); Task Card commits keep their rich
notice cards with the landing following on its own.
Rejected alternatives:
- Binding each key to "action, then
]s". A blind second step jumps from the post-write cursor, skipping rows after line-shifting writes; anchor-only planning never does. - A passive queue watcher. Reacting to any write that drops a row cannot tell an answer from a daytime edit and would advance off-landing work.
- A second advance chord per gesture. One more key to learn per answer instead of zero; the stay answer (Alt+F) already covers the opt-out.
- A global review-mode toggle. A mode to enter, forget, and leave in the wrong state; the landing scope already gates every key.
- A dedicated triage card. A new surface for decisions the existing gestures already make.
- Nav performing every write. Every writer keeps its own write path; nav only judges the outcome through capture/continue.
- Ctrl+Enter crossing the checklist boundary. One habitual extra press would close a real commitment and move away from it; Ctrl+Alt+F still crosses.
- A config kill switch. Alt+F,
<C-o>, and a plugin rollback are enough.
Evidence:
research:202610/review_walk_answer_auto_advance/review_walk_answer_auto_advance.md;
plan:202610/review_walk_answer_auto_advance.md;
nav api v3 reviewWalk (capture/continue) in bob-navigation-hotkeys 2.6.1;
task-status-cycler 1.26.0; block-id-prompt 1.22.0; bob-ledger-tools 1.29.2;
docs/freshness.md §§6/13 in bob-cli.
Cost. A fast habitual ]s after an answer can still skip one row once
the 350 ms window passes ([s or <C-o> recovers it); Task Card commits
show two toasts; keys are swallowed for about 350 ms after each advance.
Reopens when. Bryan asks for Ctrl+Enter to cross the boundary; <C-o>
after an answer becomes routine; the lane keep rate stays above 90%, which
questions the Alt+F / Ctrl+Alt+F pair.
Links Review Walk Is Tiered With Daily Lane Review (the tier order and walk this advance moves through).
Superseded in part: Ctrl+Shift+M only — a move never advances the walk; see A Task Move Never Advances The Walk.
bob-cli Releases Ride a Daily Train¶
Status: Accepted
bob-cli ships only through the scheduled release train, at most one version per New York day after full CI on the exact SHA, with no release PR; agents never edit the Cargo.toml version or CHANGELOG.md and mark CLI or JSON contract breaks with !.
Applies to. bob-cli.
Claim. bob-cli ships only through the scheduled release train: at most one version per New York day, and only after full CI passes on the exact SHA. There is no release PR. Agents never edit the Cargo.toml version or CHANGELOG.md; release-plz owns both. Agents mark CLI or JSON contract breaks with ! in the Conventional Commit subject.
Why. The research report research:202610/crates_io_daily_release_train/crates_io_daily_release_train.md compared the release-plz quickstart, a release PR with a cron merge, and a scheduled release train. The quickstart publishes on every push; a release PR adds a merge model and persistent review queue that this direct-to-master repository does not use; and an hourly guarded publisher adds runs without improving the daily decision. The train checks the exact SHA, uses the registry publication time as the live source of truth, and rehearses in git_only mode before publishing.
The repository uses release-plz rather than cargo-semver-checks because src/lib.rs is an internal seam rather than the product contract. cargo-dist is a binary distribution tool, not the version and changelog authority.
Cost. A push can race the full CI gate; the fast-forward-only version bump then fails and waits for a retry slot. A release may lag master by up to one day, while Bryan's hosts and companion repositories track master. Once live, crates.io versions are permanent, including mistakes.
Reopens when. The repository moves to pull requests; crates.io has no outside users after one month of live releases; or the package size limit forces embedded assets into a separate crate.
Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites¶
Status: Accepted
Typing an existing-task capture link on a task Blocked only by its own open prerequisites drops those links and then performs the gesture; plugins keep today's consumption.
Applies to. bob-cli, Bob Mac Capture.
Claim. When a capture existing-task link gesture (@route:id /
^route:id, @route+id!, Ensure Next @route+id[#name], or
@route:id=x…) targets a task that is Blocked only by its own open
prerequisites (optionally plus its own single future scheduled),
capture removes those open prerequisite links from the managed
Depends-On line and [dependsOn::] field, then performs the gesture.
The drop runs only when every gate holds: a local_dependency reason;
no project or inherited project reason; a target other than a
recognised project's ^prj; at most the single-field future schedule
that pull-forward already retires; exactly one accepted managed
Depends-On child; an unchanged task line between assessment and write;
and the drop staged in the same batch as the rest of the gesture. Bob
Mac Capture only renders bob's dropped_dependencies and link_guard
fields. Plugin gestures (Ctrl+Shift+Enter, ^^) keep PDG1
consumption. docs/task-dependencies.md §13.5.1 owns the contract.
See also Projects Block Their Tasks From The ^prj Line and
Task Dependencies Are Links On One Depends-On Line.
Why. Bryan asked on 2026-10-13 that when an @file:id link
gesture in bob capture or Bob Mac Capture targets a Blocked task
with open dependencies, capture "remove the dependency and complete
the operation when it is safe to do so. I think we always fail to do
anything currently." Consumption left the target unlinked in both
clients and read as a failure. Rejected alternatives:
- Keep consumption. The gesture still looks like it did nothing.
- Drop for every reason, including project and
^prjreasons. Project prerequisites belong to the Task Card's subtree pass; dropping a resident's own links would not unblock it. - An opt-in sigil. The live preview already shows the drop before submit.
- Strike dropped links as history. The writer form forbids strikes; vault git keeps the history.
- Plugin parity in the same change. Ctrl+Shift+Enter and
^^stay on PDG1.
Evidence: plan:202610/capture_link_drops_blocking_deps.md; bob-cli
92515f1 (cli-core), bdfc8c3 (cli-gestures); bob-mac-capture
8f89423 (mac).
Cost. Dropped edges leave no trace on the task beyond capture
output and vault git. Capture and plugins diverge. Project-blocked and
^prj targets still refuse.
Reopens when. Bryan wants plugin gestures to drop too; he wants a
^prj drop through the subtree pass; or dropped edges need a record
on the task.
Capture * Blocks Existing Tasks¶
Status: Accepted
Capture * makes the new task a prerequisite of an existing task.
Applies to. bob-cli and Bob Mac Capture.
Claim. A whitespace-delimited *note:block-id capture modifier means
that the new or explicitly selected task blocks the named existing task.
The edge is stored only on the target task's managed Depends-On line;
the new task's BLOCKS line is its generated mirror. * is recognized only
in the documented leading and trailing modifier runs, with guards for
Markdown emphasis, list markers, multiplication, park operators, and
escaped literals. A bare * opens completion only as the final token after
other text on the same line. Capture consumes the target's live Task Links
by resolved path and block ID, preserves authored child notes as plain text,
and deletes only Pomodoro entries this capture empties. It reports consumed
links and removed entries. A newly captured blocker inherits the target's
commitment up to Next; it is linked before the target's Task Links are
consumed. Existing owners and & plus * on one item are supported.
Capture never auto-links the new task into the target's Pomodoro.
Why. The prerequisite is often discovered while its blocked task is
already in view, so a two-item draft and hand-invented block ID add avoidable
steps and cannot reach nested notes. * is the direct visual dual of &,
and the existing Depends-On writer can retain the single-line source of
truth. Rejected alternatives were a Task Card follow-up, directional &, a
blocks: keyword, a Mac-only toggle, authoring BLOCKS as source data,
automatic linking into the target's vacated slot, and other sigils. The
picker and preview remain renderers of bob's parse, completion, and capture
contracts.
Cost. Capture can edit a task note and today's ledger that the user did not name directly, including deleting a running Pomodoro when its last link is consumed. Capture's preservation behavior intentionally differs from reconcile and plugin PDL7 behavior. The block is soft: a later capture link gesture can drop it. Bob and Bob Mac Capture ship on separate release trains.
Reopens when. Bryan changes the * grammar or direction, asks for
automatic linking into the target's session, wants reconcile or plugin
consumption to preserve notes the same way, or replaces the Depends-On line
as the source of truth.
Evidence. research:202610/capture_star_blocks_modifier/capture_star_blocks_modifier.md;
plan:202610/capture_star_blocks.md; bob-cli commits 04a6e84 (contract),
1a074c6 (consumption), 8774e89 (parser), 158d6de (writer),
78f1a69 (completion), and 1b9d35f (execution); Bob Mac Capture commit
f2a5cb5 (direction-aware picker and preview); publish phase bob-cli-7n.8.
See also Task Dependencies Are Links On One Depends-On Line and
Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites.
Decay Decisions Are Available Immediately¶
Status: Accepted
Gesture-triggered approved-decay cards are available as soon as compatible plugins are installed; there is no calendar gate, replacement date, or counting-only period.
Applies to. bob-cli, bob-plugins, vault.
Claim. Approved-decay decisions fire on an explicit Alt+F/Alt+Shift+F
gesture as soon as decay is enabled and compatible plugins are loaded
(bob-ledger-tools freshness namespace v6 with freshnessDecayCard
version ≥ 2). There is no October 19 switch, replacement date, opt-in,
or calendar-based counting-only period. On an exact, due Ready
ROTTEN/TICKLER task at the keep limit, a single press opens the existing
consent card and writes nothing; counted and Task Link sessions skip
those targets. A card still requires an explicit gesture, never a timer
write. freshness.decay: false remains the off-switch. Counting,
eligibility, explicit choices, priority/log writers, and exclusions stay
as Rotten Keeps Decay Through The Priority Ladder states them.
Rejected alternatives:
- Keep or slide the 2026-10-19 gate. A moved constant still encodes obsolete policy and lets CLI, marks, and handlers disagree.
- A replacement date, opt-in, or counting-only period. Availability is immediate on a compatible install.
- Timer-fired cards. The card still needs a gesture.
Evidence:
plan:202610/freshness_decay_without_trial_date_1.md;
bob-cli fc438bc (docs/freshness.md §2a Availability, JSON schema 8
decay {enabled, keeps, enter},
tests/cli/freshness.rs::list_decides_on_early_dates);
bob-plugins f4b3562 (bob-ledger-tools 1.28.0, bob-navigation-hotkeys
2.2.0).
Cost. Mixed-version sessions stay truthful: new navigation with an older ledger falls back to counted keeps; new ledger with older or missing navigation shows pips without a card promise. Both languages stay in sync under schema 8 / namespace v6.
Reopens when. Mixed-version fallbacks hide a needed card from a capable install, or a future experiment needs a calendar gate.
Supersedes in part Rotten Keeps Decay Through The Priority Ladder for the activation boundary and trial-extension reopening condition only.
Amended in place 2026-10-07 at Bryan's request: the RETURNED walk tier is renamed TICKLER (machine tier tickler, footer label TICKS); the Ready state stays resurfaced.
An Empty Capture Draft Shows Today's Ledger Agenda¶
Status: Accepted
An empty capture draft shows bob's capture-pomodoros --tasks agenda (Now, Next, Later), cached in app memory and revalidated on filtered vault events and show; folds go farthest-first (logs, one-line, one-row, name strip) below a fixed eye line; the app never reads the vault.
Applies to. Bob Mac Capture (agenda cache, fit planner, panel layout) and bob-cli
(bob capture-pomodoros --tasks as the agenda's only fact source).
Claim. An empty capture draft shows today's ledger agenda, painted from memory in the first frame:
- bob owns every fact. Roles, Task Link recognition,
=x/=numbering, link resolution, task blocks, line kinds and depths, log tagging, clean titles, statuses, the date, and the completed summary all come frombob capture-pomodoros --tasks. See Bob Mac Capture Is A Thin Client Of bob and Today Is Read From The Ledger, Never Written To Tasks. - The app owns caching, fitting, and pixels — and never reads the vault. The last
good snapshot lives in app memory and is revalidated on launch, on filtered vault
events (visible
.mdnotes only), on every panel show (stale-while-revalidate), after each submit, and on wake, unlock, and day change. Byte-identical output publishes nothing. A snapshot whose date is not today is never shown; a failed refresh keeps the last good agenda and marks it stale. - Folds go farthest-first below a fixed eye line. When the agenda does not fit, the farthest Pomodoros lose detail first: logs, then one-line tasks, then one row per Pomodoro, then the Later name strip. Next and Now fold last. Every fold leaves a chip naming what is hidden, and expanding a chip never moves the editor: the agenda is capped at the below-eye-line budget and never scrolls.
- Numbers match the session operators. The current entry shows its
=xnumbers; every other open entry shows its=lineup number, because=#NAME~Kdrops by those same numbers. The Now countdown ticks at minute granularity while visible.
Why. The question Bryan most often has when opening the panel — "what am I doing, and what's next?" — is already answered by today's ledger, so the empty draft shows it instead of dead space, with no new concept. Caching in memory (not on disk, not in a daemon) keeps the first frame instant on a resident app that prefetches at launch. Farthest-first folding keeps Now fully detailed on heavy days, when recency is what matters. The fixed eye line keeps the caret still however the agenda folds, types, or refreshes. Rejected alternatives:
- A daily-file cache key. Stale-while-revalidate on filtered events is fresher than keying on the day file, which misses task-note edits entirely.
- A global fold ladder. Per-Pomodoro units with distance-ordered folding preserve Now; a global ladder would thin the running Pomodoro first on heavy days.
- A horizon cap. Dropping far Pomodoros hides actionable
=#NAME~Knumbers; the name strip keeps every entry reachable in one row. - Swift-side vault reads. The app never parses the daily note or task notes and never infers current/next from the clock; every ledger judgment stays in bob.
- Building on
bob plan.plananswers a different question (backlog order, not today's ledger lineup with operator numbers). - A fixed-height scroll list. Scrolling moves content under a fixed caret; the fold ladder guarantees the agenda always fits without scrolling.
Evidence:
research:202610/idle_capture_pomodoro_agenda/idle_capture_pomodoro_agenda.md;
epic bob-cli-66 (plan:202610/idle_capture_pomodoro_agenda.md); phases
bob-cli-66.1 (bob capture-pomodoros --tasks, docs/capture.md, goldens),
bob-cli-66.2 through bob-cli-66.6 (models, store, planner, view, polish in
bob-mac-capture, CI green at f8c9c28).
Cost. One in-memory snapshot plus a background refresh per trigger; chips and
expansions add per-unit view state that resets on hide and snapshot change. The
countdown ticks only while the panel is visible. Stale marking is the only promise
when a refresh fails — yesterday's plan is never shown, but a broken link or a dead
bob still needs the warning rows.
Reopens when. The agenda needs to write the vault (row clicks, ⌘1…9 insertion),
navigate by keyboard, or show completed Pomodoros — any of which changes what the
cache must hold and what a row may do.
Bob Mac Capture Is A Thin Client Of bob¶
Status: Accepted
Bob Mac Capture never parses capture grammar, computes previews, or writes the vault; it runs bob, renders the spans, candidates, and previews bob returns, and submits each draft as one bob capture call.
Applies to. Bob Mac Capture, bob-cli.
Claim. bob-cli is the only implementation of capture grammar, completion data,
preview, and vault mutation. Bob Mac Capture owns presentation, process orchestration,
the global hotkey, settings, launch at login, and packaging. It spawns bob directly —
capture-parse for semantic spans, capture-complete for candidates and replacement
ranges, capture --dry-run for previews — and renders what comes back. It may filter or
rank what bob returned, but it never decides syntax itself; BlockIDRules.swift puts
it as "Bob is the only authority for the grammar". A draft is submitted as one aggregate
bob capture, and the panel hides only after bob reports success for the whole draft.
New capture behavior lands in bob-cli and docs/capture.md first; the app then decodes
and presents it.
Why. The app replaced a Hammerspoon pop-up whose task_capture.lua (353 lines) was
a second, independent implementation of the capture grammar in Lua. The replacement
research named that duplication — not the widget — as the root cause of the missing
completion and highlighting, and noted that any client wanting highlighting would need a
third copy. Rejected alternatives:
- Re-implementing the grammar in Swift — "the current pain amplified".
- Linking bob-cli as a
staticlibover a C ABI — zero spawn cost, but it needs a stable C header andaarch64-apple-darwincross-compilation. - A resident daemon or FFI — judged premature: a
bob capture --dry-run --format jsonspawn measured about 5 ms, while the old pop-up paid about 95 ms per stage for a login shell (both measured on athena, not the Mac).
JSON endpoints also serve any later client (a Raycast extension, an iOS Shortcut, zsh
completion) for free. Epic bob-cli-2o followed the rule: its bob-cli phases shipped
plan_budget, destination roles, the =x drop outcome, and now_tag spans, and its
two Mac phases decoded and presented them. Evidence:
research:202608/bob_mac_capture_replacement/bob_mac_capture_replacement.md §§2.2, 2.3,
4.2, 4.3; the Mac README.md opening paragraph (since 9030832, 2026-08-13).
Cost. Every capture feature lands twice, bob-cli first, and the Mac half is gated by
macOS CI because agent hosts have no Swift toolchain. Every preview is a subprocess,
bounded by a 20-second timeout (BobProcessClient.defaultTimeout). bob and the app
are installed independently, so the JSON contract must grow additively: the app decodes
new fields with decodeIfPresent so an older bob still works, yet it rejects a
schema_version other than the one it expects.
Reopens when. Spawn latency measured on the Mac breaks live preview, or a second frontend needs an in-process library. Even then exactly one semantic implementation must remain: reopening can change the transport (a daemon, FFI), never add a second grammar.
Note Ready Cap Counts The Lane¶
Status: Accepted
The per-note soft cap (plan.max_ready_per_note, default 5; ready_cap: N|off) counts each area/project note's whole Ready lane by residence, whatever its freshness.
Applies to. bob-cli, bob-plugins, vault, dash.
Claim. Every area/project note has a soft cap on its Ready lane
(plan.max_ready_per_note, default 5; per-note ready_cap: N|off). The
cap counts each note's whole Ready lane by residence, whatever its
freshness. Recurring and ^prj rows are excluded. Crowded means strictly
over; full is neutral. Nothing is refused, written, or stored. The dash
chip order becomes NEW, PENDING, NEXT, READY, CROWDED, BLOCKED, ROTTEN,
TODAY.
Rejected alternatives:
- Gated per-note count. Rot cliff and review inversion: skipping review would lower the count without lowering pressure.
- OPEN/SHOWN vocabulary. Crowded, full, and room stay the words.
- Hard refusal of moves. Crowded notes never fail the report.
- Auto-defer or auto-split. The fix stays split, sequence, defer, or drop by hand.
- Stored counts. Counts stay a read-time evaluation; frontmatter
holds only the
ready_capoverride. - Hooks badge row. Lane semantics stay unchanged.
- Parent rollups. Residence only; never parent, heading, embed, or backlink.
- Separate area default. One global key plus the per-note override.
- Amber at-limit color. Full is neutral; only crowded is red.
- Folding into the READY chip. CROWDED is its own chip.
Evidence:
research:202610/per_note_ready_cap/per_note_ready_cap.md;
plan:202610/per_note_ready_cap.md;
docs/plan.md Ready cap per note in bob-cli.
Cost. Per-note counts don't sum to READY; two implementations
(Rust plus JavaScript) stay in sync under vectors R1–R14; the mobile
default cap applies where config.yml is unreadable.
Reopens when. Bryan finds the cap unhelpful after tuning N and the notice scope.
Amended in place 2026-10-04 at Bryan's request: the freshness trial was removed; nothing waits on it.
#now Is A User-Owned Weekly Bet, Never A Status¶
Status: Superseded by Next And Pending Are Sticky Lanes; Only Blocked Is Derived, Today Is Read From The Ledger, Never Written To Tasks
Only Bryan's explicit gestures add or remove #now; no automation infers, adds, or strips it. It never changes task status or feeds Next, and it stays a tag, never an inline field.
Applies to. bob-cli, bob-plugins, Bob Mac Capture, vault.
Claim. #now is a plain Obsidian tag on a #task line that marks one of this
week's bets. It is the Now tier of the roadmap: Now is #now, Next is Ready work, and
Later is a P1–P4 deferral (p:<N> in capture, Ctrl+Shift+P in Obsidian). Only an
explicit gesture writes it: typing it, a trailing #now on new capture text, or Bob
Navigation Hotkeys' Alt+N toggle and Ctrl+Shift+P #now row. Nothing adds it implicitly
— removing a Task Link with Ctrl+Shift+Enter or dropping one with =x~<K> leaves the
tag as it was — and no hook, cron job, or sync strips it. It is independent of the
checkbox: bob task-status-hooks only counts it, it never makes a task Next, and a task
can be both #now and [/]. The NOW cap (plan.max_now, default 15) is visibility,
not enforcement: over the cap, chips and bob plan turn red and now_cap_exceeded is
reported, but nothing refuses. docs/plan.md owns the NOW query and its Rust and
JavaScript implementations; docs/capture.md owns the grammar.
Why. The tag answers the status-lock trap measured on 2026-09-29: removing a task's
Task Link from today's Pomodoro ledger demoted it, and nothing else
remembered that it mattered this week, so everything stayed linked — about 50 [/] and
25 [*] tasks, and 80 queued links, 58% of them a week or more without a 🍅. A tag only
Bryan owns separates commitment from activity, so the derived statuses in
Active Task Statuses Are Derived, Not Authored can decay honestly: [/] is a footprint, #now a
promise. Rejected alternatives:
- An inline
[roadmap:: now]or[horizon:: …]field. Bob's property writer appends fields at the far right, and Tasks parses trailing Dataview fields right to left; a scratch-vault test showed such a field silently erasingpriorityandcreated. A tag is safe anywhere on the line, as#hidealready is. #nowas a Next source, or tools that backfill it. Keeping#nowtasks Next re-inflates NEXT; hooks or cron that sweep leftovers into a horizon edit history behind open editors and multi-machine sync; and silent diversion hides where a task went.- A hand-kept
roadmap.md, explicit Next/Later values, or aroadmap.base. Hand-kept horizon lists had already died twice in the vault (the zorg-eranow_*/soon_*/maybe_*notes andsase_blog_blockers.md); P-levels already are the Later horizon and resurface on their own; and Bases rows are files, not tasks. - New capture tokens (
h:,r:,~id,!id).#nowin body text already parsed.
Evidence:
research:202609/pomodoro_closed_day_now_tag_automation/pomodoro_closed_day_now_tag_automation.md
§§1, 2, 4, 6.4; research:202609/now_tag_vs_in_progress_status.md; epic bob-cli-2o
(plan:202609/pomodoro_plan_budget_now_tag.md); bob-cli d28f8cd; bob-plugins
b68618f.
Cost. A second marker with a manual lifecycle: only the Monday review prunes NOW, and if that lapses NOW becomes the next pile with nothing but a red chip to say so. Dropping a link never tags the task, so Bryan must tag before dropping or lose it from view. The tag survives deferral — a P-level makes the task Blocked and hides it from NOW, and it reappears when the date arrives unless untagged. Capture can tag only new task text; tagging an existing task needs Obsidian. The NOW predicate is implemented in Rust and mirrored in JavaScript, and both must keep matching the dash's query defaults.
Reopens when. The two-week trial (2026-09-30 → 2026-10-13) or a later weekly review
shows NOW ignored — the research's rule is then to delete the tag and keep only the
capped ledger; promotion from Ready proves too slow and a second horizon tag such as
#next is needed; or Obsidian Tasks or Bases gains task-level properties that are as
parser-safe as a tag.
Superseded: #now is retired — see Next And Pending Are Sticky Lanes; Only Blocked Is Derived and
Today Is Read From The Ledger, Never Written To Tasks. The Next lane now holds this week's
commitments.
Open PDF Writes Are Coordinated¶
Status: Accepted
bob writes Highlights PDFs only through bounded .forMerging file coordination around the whole per-PDF write, holding when uncertain or while you read; never UI automation, never .forReplacing.
Applies to. bob-cli (ref scan, ref sync, create-time ingest),
Bob Mac Capture (Refs scan presentation), the vault.
Claim. When highlights.coordinate_pdf_writes is on, every Highlights
PDF marker write goes through one bounded macOS file-coordination write
(NSFileCoordinator, .forMerging, 10 s per PDF from a 30 s scan cap)
around the whole per-PDF write sequence. macOS has Highlights save your
unsaved annotations first, bob re-checks the marker in the fresh bytes and
writes on top of them, and Highlights reloads the same tab in place. Any
uncertain branch becomes a hold — editor_active, coordination_timeout,
or marker_changed — with nothing written for that PDF, exit 0, and a
retry on the next scan. bob never presses keys, closes or reopens tabs,
grants itself Accessibility, or falls back to an uncoordinated write;
.forReplacing and option-less writes are never used. Bob Mac Capture
presents refreshes and holds from bob's scan receipt, never by reading
the vault. The key ships off until the guided acceptance test
(scripts/dev/highlights_coordination_acceptance) passes on the Mac.
Rejected alternatives:
- Scripted ⌘S / close / reopen of Highlights. Keystrokes steal focus, need an Accessibility grant for bob, and churn tabs.
.forReplacingor option-less writes. Both silently drop unsaved annotations; only.forMergingasks the open document to save first.- Defer-while-open as the primary strategy. Waiting for Highlights to go idle leaves markers stale for whole reading sessions; holds stay the exception (reading, blocked save, changed marker), not the rule.
- Dropping
-wfrom the cron. The cron keeps writing through coordination instead of going note-only.
Evidence:
research:202610/highlights_open_pdf_coordinated_marker_writes/highlights_open_pdf_coordinated_marker_writes.md;
plan:202610/highlights_coordinated_marker_writes.md;
epic bob-cli-at (coordinate, policy, executor, report,
mac-core, mac-app, accept phases).
Cost. Whole-PDF holds delay annotation import by at most one scan; a coordinated reload resets the document's undo history; the shipped default stays off until Bryan's acceptance run passes.
Reopens when. The real-Highlights acceptance test shows lost annotations or a conflict dialog (then Plan B, an app-driven "Apply now"), or a macOS upgrade breaks file coordination.
A Pomodoro Theme Is One Name Component¶
Status: Accepted
A theme is one + component of a Pomodoro's — NAME tail, keyed by normalize_component, with the catalog derived from canonical daily notes.
Applies to. bob-cli, bob-plugins, Bob Mac Capture, the Bob vault.
Claim. A Pomodoro theme is one +-separated component of a named ledger
entry's — NAME tail. Split with split_components. The key is
normalize_component (whitespace collapsed, lowercased). The display name is the
most recent authored spelling for that key. — BOB + DECKS is one Pomodoro name
with two themes. An unnamed entry, the budget's (unnamed) placeholder, a
Pomodoro home, and an Obsidian appearance theme are not themes. The catalog is
derived from canonical daily notes YYYY/YYYYMMDD.md through
capture_pomodoros::scan, plus the hide/color journal. bob themes lists it.
Why. Treating the whole — NAME as one theme would hide composites such as
BOB + DECKS from per-component recency, color, and hide. Scanning task text or
homes would invent history the ledger never recorded. Rejected alternatives:
- Whole-name identity —
BOB + DECKSwould be a third theme beside BOB and DECKS, and hide/color could not target one component. - Slug-only keys —
SASE V18andSASE-V18would collide or split against the plan budget's existingnormalize_component. - Inferring themes from tasks or homes — the ledger would stop being the source of truth.
Evidence: plan:202610/pomodoro_themes.md PT1–PT2; docs/pomodoro-themes.md.
Cost. Every consumer splits names the same way. A rename rewrites one component across tails, not a whole name. Clients must not scan the vault for identity.
Reopens when. The plan budget and the catalog disagree on what a component is, or unnamed entries need a remembered identity.
Projects Block Their Tasks From The ^prj Line¶
Status: Superseded in part by Sub-projects Inherit Project Blocking, Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites
A project's prerequisites and date live on its ^prj task; while that line is Blocked, every open resident is Blocked and tagged #prj_is_blocked, and Blocked tasks keep no Task Links under today's open Pomodoros.
Applies to. bob-cli, bob-plugins, vault.
Claim. A project's prerequisites live on its ^prj Depends-On line
and its date as inline [scheduled::], with no project frontmatter
depends_on or scheduled. While ^prj has its own reason to be
Blocked — an open projected prerequisite, or a future inline date —
every open task in its note is Blocked [?] and carries the
machine-owned #prj_is_blocked tag. ^prj itself is never tagged; a
hand-set [?] on ^prj blocks nobody. There is no sub-project
cascade: blocking is by residence only. Blocked tasks keep no Task
Links under today's open Pomodoros: a gesture that would link one
raises its open prerequisites instead (or reports a project date), and
bob task reconcile removes stale live links and emptied Pomodoros.
docs/task-dependencies.md §13 owns the contract; see also
Task Dependencies Are Links On One Depends-On Line,
Active Task Statuses Are Derived, Not Authored, and
Next And Pending Are Sticky Lanes; Only Blocked Is Derived.
Why. Frontmatter is not needed: no vault query reads project
scheduled or depends_on, and the ^prj line already drives
reconcile, R1–R10, successor recovery, the Depends on stage, and
ledger chips. The project design already treats the task line as the
interaction point. Inheritance without consumption would leave Blocked
work planned today. A tag on every open resident states a fact about
the project, so writers do not recompute it when a resident's own
reasons change. Rejected alternatives:
- Frontmatter
depends_on/scheduled(canceled bob-cli-6s). YAML storage was the odd one out and racedbob-project-tasks. - Inheritance without consumption. Blocked tasks would keep live Task Links.
- Copying project dependencies onto residents. Duplicate edges and a second writer surface.
- Residents depending on their own
^prj. A guaranteed deadlock. status: waitingas a reason. Status stays derived from the line.- A Blocked overlay. Lanes do not survive Blocked.
- A sub-project cascade. Blocking is by residence only.
- A render-only reason chip instead of a tag.
blocked.mdand JS paths need a stored token. - Tag only sole-reason residents. Every immediate writer would
recompute the tag;
every_residentstates a project fact.
Evidence:
research:202610/prj_task_inline_schedule_and_deps.md;
research:202610/project_dependency_blocking/project_dependency_blocking.md;
epic bob-cli-6u (plan:202610/prj_line_project_blocking.md);
bob-cli 37c100f (contract), f6fbd7d (cli-model), 23f2fd9 (hoist),
34d4ab6 (reconcile block/tag), 2340689 (consume links),
13d183f (close), 66c4ddb / 36f997b (capture);
bob-plugins c9938d8 (nav-model), a42f45d (schedule),
046028c (Depends on), 1abde2f (convert/move), 744247c
(cycler/block-id-prompt).
Cost. Lane loss is project-wide. The ^prj line is longer. Tasks
and Dataview tags include #prj is a substring match for
#prj_is_blocked. A missed immediate tag write is repaired only on
the next reconcile. Project dates still propagate onto residents
(raise-only; a hand-edited earlier ^prj date keeps old copies).
Consumption may delete a running timed Pomodoro. Daily-note tasks keep
the R9 carve-out.
Reopens when. Lanes must survive Blocked; Bryan wants a sub-project cascade or re-linking on unblock; a projects-by-date Properties/Bases view is needed.
Superseded in part: residence-only blocking remains for each project’s own residents; sub-project descendants now inherit blocked ancestor reasons — see Sub-projects Inherit Project Blocking. Capture link gestures on a task blocked only by its own prerequisites now drop them and link (Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites).
READY Is Freshness-Gated With NEW and ROTTEN Review¶
Status: Superseded in part by Note Ready Cap Counts The Lane, Review Walk Is Tiered With Daily Lane Review, Rotten Keeps Decay Through The Priority Ladder, Unblocked Tasks Resurface As Ticklers
READY is the freshness-gated confirmed/exempt backlog (visible TODO pool minus NEW and ROTTEN buckets) with TODAY → NEW → PENDING → NEXT → READY sections and NEW/PENDING/NEXT/READY/BLOCKED/ROTTEN/TODAY chips; review clears NEW then ROTTEN; no tags, fields, or status changes store review state.
Applies to. bob-cli, bob-plugins, vault, dash.
Claim. READY means recently human-confirmed work plus the preexisting
freshness-exempt Ready tasks, computed at read time from the stable bucket
contract in docs/freshness.md §4 (bucket(new), bucket(rotten) for
resurfaced and age-expired, null otherwise). The visible pool partitions as
B = NEW ∪ TICKLER ∪ ROTTEN ∪ READY, pairwise disjoint. Dash section order
is TODAY → NEW → PENDING → NEXT → READY; chip order is NEW, PENDING, NEXT,
READY, BLOCKED, ROTTEN, TODAY. NEW (unconfirmed, always visible, never
limited) clears to 0 first, then rotten.md (TICKLER plus expired ROTTEN)
until 0 or budget, then PENDING → NEXT. Tasks stay in their source notes;
rotten.md rows are click-through views and ]s / Alt+Shift+F reviews
source lines. Rejected alternatives:
- Stored
#rotten/#newtags, persisted classification, new statuses, task relocation, or automatic confirmation. Classification stays a read-time evaluation; stamps remain the only write. - Duplicate inline evaluators in dash queries. Dash and rotten share one
snapshot-backed bucket predicate and
reviewModel(). - Dim-not-hide as the initial policy. Review buckets stay out of READY; dimming is reconsidered only if gating hides needed work after interval/budget tuning.
- Filtering READY with
state === "fresh". That would lose exempt tasks; READY usesbucket !== "new" && bucket !== "rotten"on the visible pool.
Evidence:
plan:202610/freshness_gated_ready.md;
research:202610/freshness_gated_ready_dash/freshness_gated_ready_dash.md;
docs/freshness.md §§4/6/8/13 and docs/plan.md READY backlog in bob-cli;
api.freshness.bucket plus reviewModel() and gated readyBudget in
bob-ledger-tools 1.11.0; dash.md NEW/gated-READY plus rotten.md
TICKLER/ROTTEN groups in the vault.
Cost. READY visibility depends on review: skipping review lowers READY
without lowering total lane pressure. Headless bob query without the plugin
stays ungated; bob freshness list is the headless review interface. Two
implementations (Rust bucket plus JavaScript bucket/model) stay in sync under
schema 1 until the vocab-rotten migration.
Reopens when. A required plugin-free surface needs gating, or gating hides needed work (a lost-needed-task case) after interval/budget tuning.
Supersedes in part Today Is Read From The Ledger, Never Written To Tasks for the dash section list only.
Superseded in part: chip list only — see Note Ready Cap Counts The Lane.
Superseded in part: review ritual order only — see Review Walk Is Tiered With Daily Lane Review.
Amended in place 2026-10-04 at Bryan's request: the freshness trial was removed; nothing waits on it.
Superseded in part: 'stamps remain the only write' only — the approved decision card invokes separate existing writers on explicit approval, see Rotten Keeps Decay Through The Priority Ladder.
Amended in place 2026-10-07 at Bryan's request: the RETURNED walk tier is renamed TICKLER (machine tier tickler, footer label TICKS); the Ready state stays resurfaced.
Superseded in part: [unblocked::] is a stored input that makes a task
due, while classification stays read-time — see
Unblocked Tasks Resurface As Ticklers.
Ref Rename Keeps Reference Identity¶
Status: Superseded by Reference Renames Keep Identity
Ref renames change bundle paths while preserving reference and task identity.
Accepted · project
Applies to. bob-cli, bob-mac-capture, bob-plugins.
Claim. bob ref rename changes the file stem for a modern reference bundle:
the note, PDF, sidecar, audio, and vault links to those files. It keeps the
frontmatter id, the reading task's ^ref block ID, title, source_url, the
PDF marker, and the PDF bytes. V1 refuses annotated references. If annotated
renames are needed later, the pipeline will own an identity-origin frontmatter
field so it can preserve highlight IDs instead of re-keying ^h- anchors.
Why. Changing id or ^ref requires writing the PDF marker and churns the
Today, Task Links, and Depends-On joins. Re-keying highlights would require
rewriting anchors, assets, inbound fragments, and archived [h::] digests.
Matching by content would add fuzzy matching to every scan. The file stem is
therefore a locator; it is not the reference's identity.
Cost. The stable id may differ from the filename stem; six existing
references already have that shape.
Reopens when. An annotated reference rename is needed.
Evidence: research report
research:202610/bob_ref_rename_command/bob_ref_rename_command.md; epic
bob-cli-7x (plan:202610/bob_ref_rename.md); live writer and verification
phase bob-cli-7x.4 (docs/ref.md, tests/cli/ref_library/rename.rs).
Superseded: the earlier bundle-path and task-identity claim is consolidated in Reference Renames Keep Identity, which records the supported scope and identity invariants.
Reference Ids Follow File Stems¶
Status: Accepted
A reference id follows its note stem, with a numbered suffix only to avoid duplicate ids across ref/.
Applies to. bob-cli, Bob Mac Capture's rename card, and the Bob vault.
Claim. A reference's recorded id equals its note stem. The only exception is a
_N suffix that avoids a duplicate across ref/. bob ref rename re-keys the id in
frontmatter and the PDF marker with the stem; a same-stem rename repairs a mismatch.
The reading task's ^ref-… block ID, title, source_url, and dates stay unchanged.
References without an id stay id-less.
Why. Bryan's rule is that “a reference's id, which is recorded in the PDF's
page-1 marker and in the note's frontmatter id, must equal the reference note's
basename. The only allowed exception is avoiding a duplicate id. Ids must be distinct
across the whole ref/ directory.” The id is a lookup and display key; URL deduplication
uses source_url, so re-keying it breaks no identity join. The sync engine already
propagates a note-side id edit to the PDF marker and refreshes sync bookkeeping.
Rejected alternatives:
- Keep the stale id. This preserves the previous claim that rename identity lives in the id even though the file stem is the required id.
- Re-key the
^refblock ID too. That would churn Task Links,[id::], and Depends-On links that join on the block ID. - Refuse when another reference uses the stem id. A numbered suffix preserves the rule and leaves every id unambiguous.
Cost. An id change rewrites the PDF bytes and makes bob ref find <old id> stop
resolving. A PDF left open in Highlights could later write back its stale marker.
Reopens when. Reference ids gain another consumer, or the reading-task block ID must follow the file stem.
Evidence: plan:202610/ref_rename_id_follows_stem.md, docs/ref.md, and
tests/cli/ref_library/rename.rs.
Reference Renames Keep Identity¶
Status: Superseded in part by Reference Ids Follow File Stems
Renaming eligible references moves their bundle paths and inbound links while preserving reference and reading-task identity.
aka bob ref rename, reference rename, preserve ref identity
Applies to. bob-cli, Bob Mac Capture's Bob Refs rename flow, bob-plugins, and the Bob vault's reference links.
Claim. Renaming an eligible modern, PDF-backed, unannotated reference changes
its file stem and the corresponding bundle paths in the same reference-type
directory. The bundle includes the note, PDF, and any same-stem sidecar or audio
companions; the rename also rewrites inbound vault links and the note's companion
paths. It keeps the reference's id, ^ref-… block ID, title, source_url,
dates, PDF marker, and PDF bytes. The reading task therefore retains its identity
and links. V1 refuses legacy and intake references, annotated references, and
notes whose PDF sync plan is not clean. Refiling, retitling, bulk renames, and
intake renames are outside this contract.
Why. The id is the URL deduplication key and the ^ref-… block ID joins the
reading task to the reference. Changing either would break identity joins and
churn Today, Task Links, and Depends-On links. The file stem is a locator, not the
reference's identity; six existing references already have an ID that differs
from the filename stem. A scratch-vault experiment showed that moving the PDF
and note, rewriting inbound links, and updating only source_pdf lets
bob ref sync -d <new pdf> plan no changes while bob ref find <old id> still
resolves. Note-only renames can make the next scan recreate the old note and
duplicate the reading task. Re-keying highlights would require rewriting
anchors, assets, inbound fragments, and archived [h::] digests.
Rejected alternatives:
- Mint a new reference ID. That breaks URL deduplication and turns the same source into a second reference.
- Change the
^refblock ID. That disconnects existing task links from the reading task. - Rename annotated references in this version. Highlight IDs and follow-up digests depend on paths, so changing those paths would require re-keying annotations.
- Retitle or refile as part of rename. A rename changes the stem only; metadata and the reference-type directory remain stable.
Evidence: research report
research:202610/bob_ref_rename_command/bob_ref_rename_command.md; plan
plan:202610/bob_ref_rename.md (identity invariants and scratch-vault experiment A);
epic bob-cli-7x, phases bob-cli-7x.3, bob-cli-7x.4, and bob-cli-7x.7
(docs/ref.md and tests/cli/ref_library/rename.rs).
Cost. File paths and inbound links change while identity metadata remains
stable, so clients must follow the report's new path without treating the new
filename as a new reference. Annotated references and directory moves remain
unsupported. Browser bookmarks and copied obsidian:// or highlights:// URIs
cannot be updated by the vault rename.
Reopens when. The reference or reading-task identity contract changes, or annotated renames gain a safe method that preserves highlight IDs and follow-up links.
Superseded in part: the claim that a rename keeps the reference id, PDF marker, and PDF bytes is replaced by Reference Ids Follow File Stems; the ^ref-… block ID, title, source_url, and dates still stay.
References Are Ingested At Birth¶
Status: Accepted
The creating host files each reference under the maintenance and scan locks, queues locally when unsafe, and leaves annotation sync to the Mac.
Applies to. bob-cli and the vault hosts.
Claim. The host that renders a reference files it under
bob_sync.lock → scan.lock, with a pre-sync and one scoped commit. If it cannot
ingest safely, the PDF stays in that host's xlib/ for its local intake drain.
Annotation sync remains the Mac's full scan.
Why. The research measured a median 4.6-minute delay, an 11.6-hour maximum,
and a 24% rate of commits that contained a new library PDF without its note.
Option G closes that split-commit window and makes a reference available from
the creating host without waiting for the Mac. It rejects A's tighter Mac cron
and B's SSH nudge because neither works while the Mac sleeps; C's filesystem
watchers are unreliable or Linux-only; D's full scan on each host breaks the
single annotation writer and widens the partial-commit window; E's tracked
xlib/ duplicates binary Git churn and makes athena a single point of failure;
F's athena outbox is sound but more complex than needed unless real
cross-host collisions appear.
Cost. Agents' creates commit to the vault, and parent-note merges rely on Git.
Reopens when. _conflicts/sync_conflicts.md shows same-stem collisions;
revisit the single-publisher design then.
Evidence: research:202610/ref_create_auto_ingest_across_tailnet/ref_create_auto_ingest_across_tailnet.md;
plan:202610/ref_ingest_at_birth.md; bob-cli 85eac25 (writing-scan lock),
c4ac06f (create ingest), fa7745f (intake-only drain), 5848c39 (producer
ingest), and 4703619 (intake health).
Review Walk Is Tiered With Daily Lane Review¶
Status: Accepted
The ]s walk visits one shared queue in explicit tiers PRE → NEW → PROJECTS → PENDING → NEXT → TICKLER → REFERENCES → ROTTEN → POST; #gtd #pre/#post checklist rows resolve only by completion; Pending and Next come due daily under pending_interval / next_interval (default 1, false walks that lane off); tiers never feed buckets or chips; upkeep outside the lanes counts the budget; stamps stay and the seed never re-runs.
Applies to. bob-cli, bob-plugins, vault, dash.
Claim. The morning walk is a structural tier order in the shared
evaluator (Rust plus JavaScript, kept in sync under the walk vectors):
PRE → NEW → PROJECTS → PENDING → NEXT → TICKLER → REFERENCES → ROTTEN →
POST, with per-tier comparators in docs/freshness.md §4. Checklist
membership is exact whole-token tags, case-insensitive: #gtd plus
#pre is PRE, #gtd plus #post without #pre is POST. Checklist
scope admits open status symbols , *, /, and ?; recurring,
canonical daily-note, and Today-linked rows are admitted; hidden,
dependency-blocked, future-scheduled, and template/conflict rows are
not. Checklist rows resolve only by completion through Tasks, never by
a stamp. PRE is a commitment tier; POST is the closing tier after
ROTTEN that ]S reaches. Pending ([/]) and Next ([*]) tasks come due
for a daily review set by pending_interval / next_interval (integer
1–365 or false; absent or null means the default 1). A walked lane's
interval overrides the whole Ready chain for that task ([refresh::],
note task_refresh, freshness.interval); false walks that lane off
and its tasks fall back to the Ready chain. Tiers are due-only: a task
stamped today drops out of the walk, and recurring, daily-note, hidden,
dependency-blocked, future-scheduled, and Today-linked tasks are in no
tier except as PRE/POST checklist rows. The walk stays out of buckets
and chips: state(), bucket(), NEW and READY gating, the
B = NEW ∪ TICKLER ∪ ROTTEN ∪ READY partition, and the dash chips are
unchanged, and lane rows carry null state/bucket. The upkeep budget
counts today's stamps on tasks outside the lanes (upkeep_today), and
every ✓ meter shows that number. No stamps are stripped or flattened
and bob freshness seed never re-runs: the seeded [fresh::] dates
come due by 2026-10-08 and are truthful after that.
Rejected alternatives:
- One interval-sorted queue. A single
[refresh:: 1]Ready task would outrank a Next task; the tier order is structural, not emergent. - A
scheduledinterval key. A 1-day leash on every past-scheduledtask would bring 59+ tasks back each morning. "Reviewed on the day it is due" is the TICKLER tier instead. Reopen as an opt-in Ready-only key if Bryan wants the daily nag. - Lanes always in the walk. The
falseoff-switch returns to Ready-only tiers (NEW → TICKLER → ROTTEN) with no code change. - A nav-only walk. Both evaluators implement the contract; nav renders what the shared queue says.
- Lanes in
bucket = rotten. Buckets and chips keep their partition; tiers are a separate walk dimension. - Stripping or flattening stamps. That would turn about 150–200 tasks NEW at once; the census stays read-only.
- An escalation sub-tier. Deferred tickler tasks roll their P-level; no extra tier tracks them.
- A Ready default of 1. The Ready backlog keeps its 7-day cadence; only the lanes default to daily.
- A second keymap or gtd-only walk. PRE/POST join the one shared
queue;
]sis still the walk. - Membership by file path, block ID, inline field, or nested tag.
Exact
#gtdplus#pre/#posttokens are the only members. - Lifting the recurring exclusion for every task. Only tagged
checklist rows admit recurrence, daily notes, Today, and
[?]. - Dropping
repeat. Recurrence still writes the next occurrence; completion is the resolution. - POST before ROTTEN. Reopen if
]Sis pressed at the boundary with zero ROTTEN done on most mornings. - Auto-completing Morning review. Checking it is the closeout.
Evidence:
research:202610/tiered_morning_review_walk/tiered_morning_review_walk.md;
plan:202610/tiered_morning_review_walk.md;
research:202610/gtd_pre_post_review_tiers (artifact
file:explicit:715e310d22ca404995bfa4a6);
plan:202610/gtd_pre_post_review_tiers.md;
docs/freshness.md §§2/4/6/7/10–13 and schema 9 in bob-cli;
api.freshness namespace v4 in bob-ledger-tools;
tier notices, walk anchor, and lane-aware refresh row in
bob-navigation-hotkeys 1.50.0.
Cost. About 25 lane decisions a day at the caps (≤ 10 pending, ≤ 15 next); the rubber-stamp risk of a daily lane queue; two evaluators kept in sync under the Q/L/R/S/B vectors.
Reopens when. The lane keep rate stays above 90% after lengthening
next_interval, or long-interval tasks starve past the weekly prune,
or PRE rows are skipped with ]s on most mornings (then the habits
leave #task).
Amended in place 2026-10-04 at Bryan's request: the freshness trial was removed; nothing waits on it.
Amended in place 2026-10-04 at Bryan's request: PRE/POST checklist tiers.
Links Next And Pending Are Sticky Lanes; Only Blocked Is Derived (its "daily review with release" cost) and READY Is Freshness-Gated With NEW and ROTTEN Review (its partition, chips, and gating claims stand; only the review ritual order is superseded in part).
Amended in place 2026-10-07 at Bryan's request: the RETURNED walk tier is renamed TICKLER (machine tier tickler, footer label TICKS); the Ready state stays resurfaced.
Rotten Keeps Are Logged¶
Status: Accepted
Every counted keep — Alt+F/Ctrl+Alt+F on an exact due Ready ROTTEN or scheduled TICKLER row, or the decay card's Keep — asks an optional reason and writes one dated 🥀 ROTTEN LOG entry with the stamp; the entry count is display-only history, never a field or a decay input.
Applies to. bob-plugins (bob-navigation-hotkeys, bob-ledger-tools), bob-cli (managed-log registry, agenda JSON, note snapshots, docs), Bob Mac Capture (agenda chip), and the vault.
Claim. A counted keep is an exact, due, Ready rotten/tickler target that the
single keep helper would increment: an exact pre-write queue row in the Ready lane
with tier rotten or scheduled tickler, not returned by an unblock, not already
stamped today, and confirmed by a dry run of the keep helper. Every counted keep —
a single Alt+F/Ctrl+Alt+F press below the decay limit, the decay card's Keep,
counted N<Alt+F> targets, and Task Link counted targets — first asks "Why keep
it as written?" and then writes exactly one dated entry under a 🥀 **ROTTEN LOG**
direct child in the same transaction as the stamp, with one undo. The reason is
optional: a blank answer logs 🤷 no reason given. Esc writes nothing: no stamp,
no entry, no advance. The entry reads *YYYY-MM-DD* — <reason> · kept N× with
the local review date, newest first directly under the marker; N is the
[[keep-streak]] after that keep, 1–999. The log is audit and display only: its
count ("N keeps logged") never resets, is never stored as a field, and nothing
reads it to change decay, buckets, tiers, lanes, Today, schedule, priority, or
Task Freshness. NEW, early/FRESH, Pending, Next, PROJECTS, recurring,
checklist, closed, and unblock-tickler keeps never prompt and never write it;
Pending summaries stay in the Work Log and deferrals in the
Schedule Log. bob capture, Rust, hooks, randomize, and seed never write it.
There is no backfill: keeps before rollout leave no entries.
Rejected alternatives:
- Nonblank-only entries. A blank reason logs
🤷 no reason given; the entry is never optional. - Logging every Alt+F. Only counted keeps log; uncounted stamps stay silent.
- ROTTEN tier only. Scheduled ticklers log; unblock ticklers do not.
- A lifetime field or numeral by the mark. The entry count is display-only and never stored on the task line.
- Schedule- or Work-Log storage. Keeps get their own
🥀 **ROTTEN LOG**child. - The journal as the store. The log lives with the task and travels with it.
- A second-keep gate. Every counted keep prompts, from the first.
Evidence:
research:202610/rotten_log_keep_history/rotten_log_keep_history.md;
plan:202610/rotten_log.md;
docs/freshness.md §2a and docs/task-dependencies.md §2.4 in bob-cli.
Cost. One more kind in the closed registry across Rust and every plugin parser; a two-key keep (Alt+F, then Enter); log growth of about four lines a month on a weekly-kept reading task; two numbers to keep apart (resetting streak vs. never-reset logged count); editable, not tamper-proof Markdown.
Reopens when. A month of entries is more than 80% 🤷, or counted keeps exceed
about five per morning (consider a from-the-second-keep gate), or the log goes
unread.
Supersedes in part Rotten Keeps Decay Through The Priority Ladder for the rejection of a new REVIEW LOG structure and "Keep anyway leaves only the line increment" only; counting, explicit consent, the priority ladder, and the other card outcomes stand.
Rotten Keeps Decay Through The Priority Ladder¶
Status: Superseded in part by Decay Decisions Are Available Immediately, Unblocked Tasks Resurface As Ticklers, Rotten Keeps Are Logged
Repeated due-Ready keeps earn an explicit approved decision that enters the existing priority ladder; nothing decays silently and freshness itself still never changes priority or schedule.
Applies to. bob-cli, bob-plugins, vault.
Claim. The stored [keeps:: N] field is a streak of due-Ready bare keeps
(how many consecutive due reviews confirmed the task as written), not a
lifetime refresh counter; absence means 0 and writers omit zero. Only an
exact, due, Ready rotten/tickler target increments, once per review, through
the single JavaScript helper api.freshness.keepLine; every other human stamp
clears the streak, and automation, hooks, randomize, and seed neither
increment nor reset. At the configured limit (default 3) the next due
Alt+F/Alt+Shift+F press opens an explicit consent card — Not now, Less often,
Reword, Drop, Keep — and writes nothing until Bryan chooses; Enter can defer
but never cancels from that card. Approved schedule-changing decisions reuse
the existing priority roll/decay ladder and log writers with · kept N×
reason tails; Keep anyway just counts again. Freshness itself still never
changes a lane, Today, schedule, or priority. Repeated keeps earn a question
about urgency, never an automatic change.
Rejected alternatives:
- Lifetime
refresh_countor a second alias counter. One streak field; no backfill: seeded, captured, and oldfreshstamps are not evidence of keeps. - Silent auto-decay at the limit. The card asks; nothing changes without an explicit choice.
- Counting non-Ready lanes or NEW tasks. Only due Ready rotten/tickler targets count; other targets stamp and preserve.
- Timer-fired cards. A card still requires an explicit gesture after the 2026-10-19 activation date, never a background write.
- A new REVIEW LOG structure. Decisions reuse the existing Schedule, Cancel, and Work logs; Keep anyway leaves only the line increment.
Evidence:
research:202610/rotten_keep_streak_and_approved_decay/rotten_keep_streak_and_approved_decay.md;
plan:202610/rotten_keep_streak.md;
docs/freshness.md §§2a/7/10a/11 and docs/projects.md ("Approved-decay
decision planner") in bob-cli; api.freshness.keepLine (namespace v5) plus
the FreshnessDecayCardModal in bob-ledger-tools 1.24.0 /
bob-navigation-hotkeys 1.69.0; shared vectors
tests/fixtures/freshness_keeps/vectors.json.
Cost. Two implementations (Rust read/clear/report plus JavaScript count/display/decide) stay in sync under the shared vectors; the scalar is not an exact global event counter (git sync may lose an increment, caches under-count, hand edits stay unobserved). Calibration is a lightweight human tally, never telemetry or automatic tuning.
Reopens when. The post-activation calibration tally shows the limit misfires (mostly Keep suggests 4, mostly Drop suggests 2), or the recorded trial extends the 2026-10-19 boundary.
Superseded in part: 2026-10-19 activation boundary and the trial-extension reopening condition only — see Decay Decisions Are Available Immediately. Counting, explicit consent, priority/log writers, and exclusion rules stand.
Amended in place 2026-10-07 at Bryan's request: the RETURNED walk tier is renamed TICKLER (machine tier tickler, footer label TICKS); the Ready state stays resurfaced.
Superseded in part: a keep on an unblock tickler leaves the streak unchanged — see Unblocked Tasks Resurface As Ticklers.
Superseded in part: the rejection of a new REVIEW LOG structure and 'Keep anyway leaves only the line increment' only — Keep now asks why and writes one Rotten Log entry; see Rotten Keeps Are Logged. Counting, explicit consent, the priority ladder, and the other card outcomes stand.
Similar Tasks Are A Settled Heads-Up¶
Status: Accepted
Capture shows a "Similar tasks" band only after typing settles and only when a match clears bob's calibrated bars; every row action is an explicit draft edit and Return never changes.
Similar Tasks Are A Settled Heads-Up¶
Accepted · project
aka similar tasks band, duplicate heads-up
Applies to. Bob Mac Capture, bob-cli.
Claim. When a capture draft will create new tasks, the preview shows a "Similar tasks" band at the foot of the new task's own preview card. It is a heads-up, never a gate:
- A row carries a Same title or Possible duplicate mark only when the evidence supports it. No percentages, and no score is ever shown as a probability.
- Zero pixels unless a match clears the bar. No header, no placeholder, never a "No duplicates" line.
- Settle-gated. bob is asked after about 350 ms without edits, on its own cancellable lane. Rows on screen hold while the user types and swap only if the settled result changed.
- Scope. Every open task in routable notes, plus tasks closed in the last 30 days at the duplicate bar only (at most 1 row).
- Gate. Only items that create a task:
task,pomodoro_task, and the^prjtitle ofproject_note/pomodoro_project_note. Require at least 2 meaningful terms, with a stricter bar under 3 terms except for an exact title match.@@template:drafts are skipped. - Cap. 2 rows for a single-item draft plus a "+N" capsule, 1 row per item in bulk drafts, at most 5 per draft.
- Actions are explicit draft edits only. The strip's keys are the capture
sigils (
~relate,+add as note,&needs it,*blocks it,/open,⌫discard mine). Return is never intercepted or re-confirmed, nothing is auto-linked, and@and!are not v1 actions. The ↩ default is split: ⌫ Discard mine for Same title, + Add as note for Possible duplicate. - Numbers. Zero extra spawns per keystroke. Preview p95 is unchanged with the feature on versus off. The related lane's p95 on the Mac is at most 50 ms.
- Settings toggle ("Show similar tasks while capturing", on by default). Thresholds live in bob, not in Settings.
- Calibrated bars (
bob capture-related, schema_version 1):POSSIBLE_DUPLICATE0.70,SIMILAR0.48,CLOSED_DUPLICATE0.70,HOLD_MARGIN0.10,SHORT_QUERY_BOOST0.10,MIN_TERMS2 — with closed-class function-word stopwords, single-digit numbers kept as terms, and a number guard (titles whose number sets differ are never duplicates). Measured on the 496-query leave-one-out census: possible_duplicate precision 6/6, similar usefulness 39/54 (72.2%), firing 55/496 (11.1%, about 1 capture in 9). Mac release p95 is 43.494 ms against the 50 ms gate. - bob owns meaning; the app owns timing and pixels. Eligibility, tokens,
weights, bands, hysteresis, exclusions, action availability, defaults,
splice anchors, and replacement strings come from
bob capture-related. The app schedules the call, holds and fits rows, and applies the edits bob describes.
Why. The research showed wording reliably finds the same work captured twice but does not find workflow relations: on 95 real dependency pairs the true prerequisite ranked top-5 only about 20% of the time — so the UI promises a duplicate heads-up, not relations. The Mac turned out fast (4.7 ms spawn, 21 ms open-task scan), so one per-settle subprocess call meets the 50 ms gate with no in-app index. TF-IDF over Bob-aware tokens beat the picker's all-terms matcher (3% fire rate, missed paraphrases) and plain Dice (missed stemmed duplicates). Calibrated from 80 reviewed leave-one-out pairs labeled under Bryan's delegation (10 D / 47 U / 23 N): scalar thresholds alone capped possible_duplicate precision at 60% until the number guard and stopword fixes reached 6/6. These are agent judgments, not Bryan-authored labels, and the figures are in-sample, not held-out.
Rejected alternatives:
- A "related tasks" list. Matching cannot deliver workflow relations, so the band must not promise them.
- An in-app index for v1. Measured latency leaves headroom; the index stays a measured escalation path if the gate ever breaks.
- Embeddings, Dice, or the picker's all-terms matcher. Unjustified cost, missed stemmed duplicates, or a 3% fire rate respectively.
- ↩ adds as note for both (
duplicate_default = note_both). An exact recapture is almost always a forgotten task; a lexical near-match may not be. - Shipping the research's starting bars (
calibration = starting_bars). The bars come from labels, and the starting 0.55 similar bar fired at only 6.7% with 60% duplicate precision. - A closed bar above 0.82. It would read 2/2 but rests on one pair; re-check when more closed pairs accumulate.
- An abbreviation table (appt vs appointment). Vocabulary-specific; add only if future labels show recurring misses.
Cost. Every capture feature lands twice, bob-cli first, and the Mac half is gated by macOS CI. The JSON contract grows additively at schema_version 1 with old-bob fallback. Thresholds retune in bob, never in Settings. The band folds to one line before it can cause scrolling, and held rows render only to keep an already-visible row steady.
Reopens when. The 50 ms gate breaks on measured Mac hardware (escalate to the in-app index); closed-pair evidence accumulates enough to revisit the closed bar; abbreviation misses recur in labels; or firing drifts far from about 1 capture in 9 on real captures rather than the title-census proxy.
Evidence: research:202610/capture_similar_tasks_band/capture_similar_tasks_band.md;
plan:202610/capture_similar_tasks_band.md; epic bob-cli-ab phases
bob-cli-ab.1–bob-cli-ab.7; bob-cli 787d62cb (engine), 062fc3da
(contract), 453dc4a7 (calibration); calibration report and Mac timing in the
research sidecar (202610/capture_similar_tasks_band/calibration.md,
calibration_mac_timing_final.json).
Bob Mac Capture Is A Thin Client Of bob
Unblocked Tasks Are Offered, Never Auto-Linked
Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites
An Empty Capture Draft Shows Today's Ledger Agenda
Sub-projects Inherit Project Blocking¶
Status: Superseded in part by Unblocked Tasks Are Offered, Never Auto-Linked
A blocked project passes its dependency or schedule reason through its project descendants, with reconcile as the authority and the ^prj tag as the proxy for other engines.
Applies to. bob-cli, bob-plugins, vault.
Claim. A project note has at most one parent, stored as a scalar frontmatter
parent: wikilink. The exact vault-relative path wins; otherwise only a unique,
case-insensitive basename resolves. List-form, ambiguous, unresolved, self, terminal,
and non-project parents do not create inheritance. Every active project inherits each
ancestor's own ^prj Blocked reason, nearest ancestor first. An inherited ^prj is
Blocked [?] and tagged #prj_is_blocked; every open resident is Blocked and tagged
when its project is blocked by its own or an inherited reason. A project's own ^prj
reason does not tag that same ^prj line. Reconcile derives and reverses the cascade;
the tag is the stored proxy read by engines that do not walk the tree.
The Task Card applies and reverses the cascade in the same write for Depends on changes
and, under schedule_cascade = yes, Schedule changes to ^prj. Other writers respect
the tag and leave descendant recovery to the next reconcile; close_recovers_subprojects
= no. When closing a child project's ^prj makes its parent project empty, the parent
^prj is pulled in as Next (docs/task-dependencies.md §12.10 trigger (c)). A project's
prerequisite may not target a task in its own descendant.
Why. A blocked project's work must not reappear as Ready merely because it lives in a
child note. Reconcile already knows the project tree and all project reasons. Storing
the inherited fact on the child's ^prj lets close paths and plugin link guards enforce
the same rule without copying dependency edges or walking the tree on every gesture.
Trigger (c) keeps a parent project moving when its last open child project closes.
Rejected alternatives:
- Residence-only blocking. A child's Ready lane would surface work that remains blocked by an ancestor.
- Copying ancestor prerequisites or dates into descendants. Duplicate edges and schedule fields create new writers and drift; the tag records the inherited fact.
- Immediate descendant recovery on close or status cycling. That expands each close into a tree write; reconcile repairs the tags on its next pass.
- Stem matching ambiguous parents. It can select the wrong note; only a unique basename may resolve when the exact path is absent.
Evidence: plan:202610/subproject_cascade.md; docs/task-dependencies.md §§12.10–13;
epic bob-cli-78 and its contract, reconcile, close, and cycler phases; task-status-cycler
1.37.0 and block-id-prompt 1.30.0.
Cost. Non-Task-Card writers can leave descendants tagged until reconcile runs. The
project tree and tag proxy must stay consistent across Rust and plugin readers. A parent
^prj with a descendant target is refused to avoid a dependency cycle.
Reopens when. Projects need multiple parents, descendant recovery must happen immediately outside the Task Card, or the parent relationship stops being a scalar frontmatter wikilink.
Supersedes in part Projects Block Their Tasks From The ^prj Line: blocking by residence
alone is retired; a project's own prerequisites and date still originate on its ^prj.
Superseded in part: the parent project's trigger (c) pull-in is now offered and linked only when picked. See Unblocked Tasks Are Offered, Never Auto-Linked.
Task Dependencies Are Links On One Depends-On Line¶
Status: Superseded in part by Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites
A task's prerequisites live as plain task dependency links on one managed Depends-On first-child line; that line is the source of truth and the [dependsOn::] / [id::] fields are derived from it.
Applies to. bob-cli, bob-plugins, vault.
Claim. A task's prerequisites live as plain, never-transcluded Task Links
on one managed ⛓️ **DEPENDS ON:** first-child line. That line is the source of
truth: bob task-status-hooks derives the task's [dependsOn::] field and each
target's [id::] from it through R1–R10 reconciliation, and the task stays Blocked
[?] while any prerequisite is open. Linking the dependent under today's Pomodoros
raises its open prerequisites to Next; closing the dependent never closes its
targets. docs/task-dependencies.md owns the grammar, identity, link form,
reconciliation, and chip rules.
Why. Dependencies used to be inferred from any sole transcluded child, so the
Ctrl+Shift+P picker only saw the current note (44 of 45 edges were same-note),
#^ref reading embeds promoted tasks they merely quoted, closing an embedded
ledger link recursively closed its prerequisites, Ctrl+D on dependsOn orphaned
the embeds, and picker writes took several undo steps. One managed line with
plain links removes the inference: the picker fuzzy-searches every open task in
the vault, each write commits in one transaction, and live chips render each
link's status. Rejected alternatives:
- Links only. No derived field, so Tasks queries and Blocked derivation lose their whole-vault index.
- Fields only with a virtual line. Nothing readable or hand-editable in the note; hand edits have nowhere to land.
- A field-authoritative generated line. Writers would have to update the field and regenerate the line in lockstep; the line could never be the edit surface.
- One bullet per dependency. Unbounded children bloat the task block and collide with logs and notes.
- Links on the task line. No room for several prerequisites; the task line is already crowded with fields.
- The Tasks modal as the editor. It cannot be made a real editing surface, so its dependency edits to a task that has a line are dropped with a warning.
!as the gesture. The transclusion toggle stays a pure toggle; dependency edits go through Ctrl+Shift+P → Depends on, so the gesture never rewrites the parent task line.- Stored aliases and strikes. They break the canonical writer form and the chip model; reader tolerance plus canonicalisation covers hand variants.
Cost. Four parsers kept in sync by shared vectors (Rust, nav, ledger-tools,
cycler/block-id-prompt recognisers); the projection exists at all; deleting the
whole line outside Obsidian is re-adopted rather than honoured; Tasks-modal
dependency edits to a task that has a line are dropped; #^ref embeds no longer
promote.
Reopens when. R1/R2 warnings appear on most hooks runs for two weeks; the
Tasks modal becomes a real editing surface; Tasks' is blocked stops being
load-bearing.
Evidence: research:202610/task_dep_link_depends_on_line/task_dep_link_depends_on_line.md;
epic bob-cli-3n (plan:202610/task_dep_links.md); docs/task-dependencies.md
(f4b0d12); hooks parser and edges (2d4d508); R1–R10 reconciliation (043d9c5);
bob-plugins 1831db4 (chips), e7baeb5 (compat), 5194bc8 (nav-model),
08d1560 (nav-stage), 82aec34 (nav-gestures), 46ddd1e (migration script);
vault migration 66c6d47c; fix epic bob-cli-3n.12
(plan:202610/task_dep_links_fixes.md): bob-cli 8d54b0e (hooks-correctness),
b61727a (api v1 line refusal), a069239 (DP24–DP29), 79e39af
(hooks-docs-cleanup); bob-plugins 3ffa187 (nav-writer-fix), 330fc58
(chips-compat-fix), f78ffad (nav-mirror-gestures), 40e2e3a
(nav-stage-polish).
Linking a dependency-blocked dependent from capture drops its open prerequisites instead of raising them (Capture Links A Dependency-Blocked Task By Dropping Its Prerequisites).
Next And Pending Are Sticky Lanes; Only Blocked Is Derived¶
Status: Superseded in part by Projects Block Their Tasks From The ^prj Line
Linking raises Ready to Next and an =x close sets In Progress (PENDING); no unlink, hooks run, or capture drop lowers them; only Alt+N release returns a task to Ready; Blocked stays derived.
Applies to. bob-cli, bob-plugins, Bob Mac Capture, vault.
Claim. Lanes are sticky:
- Adding a Task Link under today's open Pomodoros raises Ready
[ ]or Blocked[?]to Next[*]; Next and Pending are unchanged. - An
=xin-progress outcome sets[/](unchanged). - Removing a link never changes the lane: the hooks, Ctrl+Shift+Enter,
@route+id!,=x~K,=~K, and hand deletion. - Only release (Alt+N, or the Ctrl+Shift+P lane row) lowers Next or Pending to Ready. Releasing also removes the task's live links from today's open Pomodoros; releasing a Pending task offers the optional Work Log prompt.
- Alt+N on a Ready task commits it to Next without linking it.
- Blocked stays derived, overrides every lane, and recovers to the existing derived rank (Ready unless linked today or reached by recent activity).
- Carve-out (R9): tasks that live in canonical daily notes (such as
^gtd) keep today's derived clearing of[*], including the recent-activityKeptNextgrace. - R12: the
[/]symbol, its "In Progress" name and IN_PROGRESS type stay. Only dash and chip labels say PENDING. Picker group names (in_progress) stay.
Why. Sticky lanes protect dropped work by default, where #now did only
if tagged before dropping. The one bulk #now tagging covered exactly the 73
[/] + [*] tasks and was stripped the same day. Bryan cancelled a separate
Submitted status ("WIP status should fill this role"). Swarm work needs a
quiet place to wait. Pending → Next → Ready is Kanban's pull order and matches
capture's queued > in_progress > next. Rejected alternatives:
- Keep
#nowwith a derived[*]. It repeats Today. - A Submitted/Waiting status. Bryan cancelled it; WIP fills the role.
- No promotion on link. That is the status-lock trap.
- Age-based Next decay. Not adopted, and never for Pending.
- A hidden pre-block status. No hidden previous-status field is stored.
Evidence:
research:202609/retire_now_sticky_lanes_ledger_today/retire_now_sticky_lanes_ledger_today.md;
epic bob-cli-2y (plan:202609/retire_now_sticky_lanes.md); phase
bob-cli-2y.2 (hooks-sticky implementation and verification).
Cost. No automatic forgetting: about 2–3 tasks a day of lane growth at
September's rates, so caps, chips, lints, a daily review with release and a
weekly prune are required. [/] no longer means "worked recently". Lanes
don't survive Blocked. Dependency-promoted Next is sticky too. About 80 legacy
statuses need one-time triage.
Reopens when. Lanes must survive Blocked (then Blocked becomes an overlay).
Amended in place 2026-10-04 at Bryan's request: the freshness trial was removed; nothing waits on it.
Superseded in part: claim 1 no longer raises a Blocked task (its link is consumed and its prerequisites rise), and "lanes don't survive Blocked" is now project-wide — see Projects Block Their Tasks From The ^prj Line.
A Task Move Never Advances The Walk¶
Status: Accepted
Ctrl+Shift+M never advances the ]s walk, even from a landed row: it follows the moved task to its destination note and parks the walk, so the next ]s / [s resumes at the moved row's walk neighbour.
Applies to. bob-plugins (bob-navigation-hotkeys), bob-cli docs, and the Bob vault review ritual.
Claim. Ctrl+Shift+M never advances the ]s walk, even from a landed row:
it follows the moved task to its destination note and parks the walk, so the
next ]s / [s resumes at the moved row's walk neighbour.
- The resume identifies rows by note path and line text, so the line shift a move causes never skips a row.
- The gesture lock still swallows Ctrl+Shift+M while another answer settles.
- Pomodoro bullet and entry moves were never part of the walk.
- Every other answer in Answering A Walk Landing Advances The Walk keeps advancing.
Why. Bryan keeps working on the task where it now lives; after moving it he often adds context, dependencies, or a link in the destination note (his answer in this plan's questions round).
Rejected alternatives:
- Advancing instead of focusing (the epic
bob-cli-4lrule, 2026-10-06). It took Bryan away from the note where the follow-up work happens. - Staying in the source note without advancing. The follow-up happens in
the destination, and the seam is usually the next due row, which a bare
]sfrom a due row steps past. - Restoring the pre-epic move unchanged. Line-keyed anchors make the next
]sskip the row below the moved task once the Tasks cache refreshes. - A per-gesture toggle or config switch. That is one more setting for one key.
Evidence:
plan:202610/ctrl_shift_m_never_advances_walk.md;
bob-navigation-hotkeys 2.7.0 (this change);
docs/freshness.md §§6/13 in bob-cli.
Cost. Reaching the next review item after a move takes one ]s. The
resume lives only until the next landing or stamp, so an Alt+F or Ctrl+Alt+F
elsewhere in between re-anchors the walk there.
Reopens when. Bryan finds himself pressing ]s right after nearly every
move, or asks for the move to advance again.
Links Answering A Walk Landing Advances The Walk (the walk this move stays out of) and Review Walk Is Tiered With Daily Lane Review (the tier order the resume walks).
Active Task Statuses Are Derived, Not Authored¶
Status: Superseded in part by Next And Pending Are Sticky Lanes; Only Blocked Is Derived, Task Dependencies Are Links On One Depends-On Line, Projects Block Their Tasks From The ^prj Line
Today's Pomodoro ledger drives Next and In Progress; open dependencies and future scheduled dates drive Blocked. bob task-status-hooks reconciles them, so writers change those inputs, never just the checkbox.
Applies to. bob-cli, bob-plugins, vault.
Claim. Next [*], In Progress [/], and Blocked [?] are derived, not authored. A
task is Next while a Task Link to it, or a transcluded dependency path, sits under
today's open Pomodoros. It becomes [/] when an =x close records work on it, and in
area and project notes [/] resets once neither today's ledger nor the previous daily
links it. It is [?] while it has an open dependsOn target or a scheduled date
after today, which overrides the other two. bob task-status-hooks is the vault-wide
reconciler and runs unattended (every 15 minutes on the MacBook). Capture and the
plugins' keymaps may apply the same rules at once for feedback, but they change the
inputs — a Task Link, a dependency, a schedule — and the hooks have the final word.
docs/task-status-hooks.md owns the full rules and precedence table; see also
Task Link.
Why. The authored status failed first: [B] Blocked was retired unused — no #task
line in the vault carried it — when Next replaced it (bob-plugins 890ed13, feat!,
2026-07-08). Next was derived from open Pomodoros two days later (bc829fa), and
Blocked came back on 2026-07-16 as derived state (fc39562). Its plan,
plan:202607/blocked_task_status.md, calls Blocked "derived state, not a second
dependency model" and keeps the command "the vault-wide source of truth", because only a
whole-vault scan knows every remaining dependency: the ! keymap sets [?] when it
adds a dependency but stays status-neutral when one is removed, for exactly that reason.
In Progress rollback followed on 2026-07-21 (084bb62) and future schedules on
2026-07-24 (9bb625a). Rejected alternatives: an authored Blocked status; letting each
command or keymap own the final status (an =x close deliberately leaves Blocked
recovery to the hooks); and keeping a hidden pre-block status to restore later.
Cost. A hand edit to a derived checkbox is silently undone on the next run, and
between runs the vault can look inconsistent. Every writer must keep the inputs
consistent: a hand unblock must also retire the future scheduled date, or the hooks
re-derive [?] (bob-plugins 786fc1d). A recovered task returns to its derived rank,
not its pre-block status. Because statuses decay, "this matters this week" cannot live
in a checkbox — that is what #now Is A User-Owned Weekly Bet, Never A Status is for.
Reopens when. A status is needed that no ledger, dependency, or schedule input can express, or reconciliation moves off a periodic whole-vault scan (for example, behind a transactional vault service).
Superseded in part: Next and In Progress are no longer derived — see
Next And Pending Are Sticky Lanes; Only Blocked Is Derived. The transcluded-dependency path and the !
removal rationale are retired by Task Dependencies Are Links On One Depends-On Line;
the Blocked rule stands. The Blocked rule also inherits from the project —
see Projects Block Their Tasks From The ^prj Line.
Hiding A Theme Removes It From Recent Only¶
Status: Accepted
Hiding a theme removes it from Recent suggestion rows only; today, Stats, colors, and typing the name stay unchanged, and using it never unhides it.
Applies to. bob-cli bob themes, capture-complete Recent rows, Obsidian
and Mac Pomodoro-name menus.
Claim. Hiding a theme removes it from Recent rows only. Today's ledger
rows, typing the name, history, Stats, and colors are unaffected. Using a hidden
name never unhides it. bob themes list omits hidden themes by default; -H
lists only them; -a lists every known theme and flags hidden ones.
Why. Deleting a theme from the catalog would rewrite history. Auto-unhiding on use would fight the hide gesture the next morning. Muting colors on hide would mix a suggestion preference with the hue rule. Rejected alternatives:
- Delete from the catalog — Stats and old daily notes would lose the name.
- Unhide on next use — a hidden name typed to start a Pomodoro would bounce back into Recent.
- Hide also mutes color — color would stop meaning identity.
Evidence: plan:202610/pomodoro_themes.md PT9; docs/pomodoro-themes.md.
Cost. Hidden themes still occupy Stats and still wear a hue. Menus must filter Recent after ranking, not drop the catalog row.
Reopens when. Hide should also suppress Stats or color, or using a hidden name should be an unhide gesture.
Theme Hue Is Pin Then Exempt Then Area Then Hash¶
Status: Accepted
A theme's hue is journal pin, then plan.exempt mute, then top-level area stem, then name hash; clients render the hue Bob reports.
Applies to. bob-cli catalog and JSON, Obsidian theme pills, Bob Mac Capture
swatches, terminal bob themes output.
Claim. Hue is resolved in order: a journal pin (hue_source: pin); else a
normalized plan.exempt key is muted (hue: null, hue_source: exempt); else
a key equal to a top-level area note's stem (parent [[area]]) wears that
area's resolved hue (hue_source: area); else
Hue::ALL[fnv1a32(key) % 8] (hue_source: hash). Rust computes the hue.
Clients render the hue Bob reports and never hash or scan areas themselves.
Colors are stable, not unique; pins fix recurring collisions.
Why. Hash-only identity made JOB and BOB the same magenta, and GTD would wear an area orange it does not mean. Unique colors cannot fit an unbounded catalog in an eight-slot palette. Rejected alternatives:
- Hash only, even for area names — JOB would not wear aqua.
- A unique color per theme — the palette is eight slots.
- Clients hash independently — JS, Swift, and Lua would drift.
Evidence: plan:202610/pomodoro_themes.md PT5; epic decision area_hues=area;
docs/pomodoro-themes.md.
Cost. Recurring collisions need an explicit pin. Exempt keys such as GTD
are muted even when an area note shares the stem. Every client must wait for
Bob's hue field.
Reopens when. The palette grows, area notes stop being the hue source for matching stems, or a client needs a hue before Bob can answer.
Theme Preferences Live In A Per-Host Journal¶
Status: Accepted
Hide, unhide, color, and rename events live in vault-synced .bob/themes/<host>.jsonl; fold is last-writer-wins and bob is the only writer.
Applies to. bob-cli, Bob Mac Capture, Obsidian plugins that hide, pin, or rename themes.
Claim. Preferences live in .bob/themes/<host>.jsonl, one append-only file
per host inside the vault so Git syncs them. host is the same label
vault-sync uses in commit subjects. Events are v: 1 theme.hide,
theme.unhide, theme.color (hue is a slot name or null to unpin), and
theme.rename. Fold reads every *.jsonl file, skips malformed lines with a
bounded warning, sorts by (ts, host, path, line), and lets the last writer
win per key. A rename moves the source key's explicit state unless the target
already has state. bob is the only writer; other clients call bob.
Why. Frontmatter on daily notes would mix derived history with preferences. A machine-local SQLite file would not sync hide/color across apollo and the Mac. A single shared JSON file would lose concurrent host appends. Rejected alternatives:
- Daily-note frontmatter — every hide would dirty today's ledger.
- Machine-local SQLite — hide on apollo would not reach the Mac.
- One shared JSON object — two hosts writing at once would clobber.
Evidence: plan:202610/pomodoro_themes.md PT4; docs/pomodoro-themes.md.
Cost. Fold must skip malformed lines. Rename is a logged event plus a ledger rewrite, not a journal-only alias. Writers serialize through bob.
Reopens when. Concurrent host folds mis-order clocks often enough to need a vector clock, or preferences must live outside the vault.
Today Is Read From The Ledger, Never Written To Tasks¶
Status: Superseded in part by READY Is Freshness-Gated With NEW and ROTTEN Review
Today is the open tasks with a dedicated Task Link under today's open Pomodoros, computed at read time by bob plan and bob-ledger-tools; never a tag, task-line field, or file-path filter; #now is retired.
Applies to. bob-cli, bob-plugins, vault, dash.
Claim. Today means dedicated Task Links under today's open Pomodoros
only; closed 🍅 lines are history. The definition lives in docs/plan.md,
with Rust today_tasks and ledger-tools isToday sharing conformance
vectors. The dash sections are exclusive (TODAY / PENDING / NEXT / READY).
#now is removed everywhere, and "committed but not today" is the Next lane.
Why. A file-path filter selects tasks that live in the daily note (1
result against 7 live links), and a hooks-managed #today tag is a second
copy of the ledger rewritten on task lines every 15 minutes, with churn,
races, and staleness. filter by function can call the plugin api, and the
Tasks reload event exists. Rejected alternatives:
- A path filter. It measures residence, not work.
- A
#todaytag. A stopgap only, with write churn on every pass. - Keeping
#nowthrough its trial. The trial already failed; the tag is retired now. - One
bob-dashboardblock. The fallback if the Tasks refresh proves fragile.
Evidence:
research:202609/retire_now_sticky_lanes_ledger_today/retire_now_sticky_lanes_ledger_today.md;
epic bob-cli-2y (plan:202609/retire_now_sticky_lanes.md); phase
bob-cli-2y.2 (superseding record).
Cost. It relies on an internal Tasks event. Headless bob query sees an
empty Today. There are two implementations (Rust and JavaScript) to keep in
sync. Resolver divergence on ambiguous basenames is documented. Capture can
no longer mark "this week" on new text.
Reopens when. Tasks removes the event or app access, or the refresh
proves unreliable.
Superseded in part by READY Is Freshness-Gated With NEW and ROTTEN Review for the dash section list only (TODAY → NEW → PENDING → NEXT → READY); the ledger-only Today claim above is unchanged.
Unblocked Tasks Are Offered, Never Auto-Linked¶
Status: Accepted
Every would-be successor link from a managed Done gesture is offered for explicit selection; recovery remains automatic.
Unblocked Tasks Are Offered, Never Auto-Linked¶
Requested · project
aka successor offers, pull-in offers
Applies to. bob-cli, bob-plugins, Bob Mac Capture, vault.
Claim. Managed Done gestures keep recovery automatic: tasks still unblock, lose
#prj_is_blocked, and receive the [unblocked::] WRITE stamp when applicable, whether
or not anything is picked. Every would-be successor link is instead a
Successor Offer: direct dependents (hand_off), open residents of a
recovered project (project), and project-finished pull-ins (project_finished). One
picker appears per gesture when offers exist; nothing is preselected and there is no
offer cap. Only picked rows are linked and raised to Next, receive a missing block ID,
and get [fresh:: today] (clearing [unblocked::] and [keeps::]). Unpicked rows
recover without a link and resurface through the existing freshness rules. bob capture
never prompts or auto-links: callers make an explicit selection, while the Mac presents
the choice in-panel and submits it to bob.
Why. A close once encountered eleven direct dependents and the all-or-nothing limit
linked none, leaving Bryan to hand-link selected tasks, including one in another
Pomodoro. A picker lets the person choose among all useful successors while retaining
automatic recovery and making project residents visible even when they are not direct
dependents. Project groups enumerate residents of the recovered project's note;
project-finished pull-ins retain their existing recovery and #hide removal, with the
link offered separately. The detailed contract is docs/task-dependencies.md §12 and
the accepted design is plan:202610/successor_offers.md.
Rejected alternatives:
- Raise the cap. Any finite cap still drops a valid fan-out, while an unbounded automatic pass can plan too much work.
- Auto-link direct dependents only. It leaves project residents invisible and keeps different successor classes inconsistent.
- Skip the picker for one offer. Even one candidate needs an explicit choice.
- Choose after commit. A later picker cannot reliably revalidate and atomically apply the same gesture.
- Use
>1,3as the selection grammar. Number and key selection is explicit in capture; the UI owns its own multi-select interaction. - Put checkboxes in the Mac preview. Choice is a distinct in-panel step, and bob remains the sole capture and write authority.
- Never prompt during the review walk. A managed Done gesture has one consistent offer choice, including when it began in the walk.
- Use a tray of held offers. Offers are tied to the close and are immediately applied or left to recover; no separate queue is needed.
Cost. The Rust, cycler, navigation, and Mac surfaces must preserve one offer/key contract. The picker pauses a managed Done gesture and each selected write must be revalidated against current note text. Capture clients must understand additive offer and pull-in output, and older Bob binaries need capability fallback in the Mac client. Unpicked recovery continues to rely on the existing review tiers; no later rescue command is included.
Reopens when. People need to retrieve offers after the gesture; offer sets become too large for grouping and filtering; or a supported client cannot preserve the shared selection and write contract.
Evidence: plan:202610/successor_offers.md; epic bob-cli-7c and phases
bob-cli-7c.3–bob-cli-7c.7; bob-cli 2d1adfa (contract), f5c82d6 (planner),
e7a0c7b (capture); Bob Mac Capture 6c4209b (in-panel choice).
Unblocked Tasks Resurface As Ticklers
Projects Block Their Tasks From The ^prj Line
Sub-projects Inherit Project Blocking
Bob Mac Capture Is A Thin Client Of bob
Unblocked Tasks Resurface As Ticklers¶
Status: Superseded in part by Unblocked Tasks Are Offered, Never Auto-Linked
Every automatic Blocked-to-Ready recovery records [unblocked:: today] unless a WRITE skip applies, so the task is TICKLER in the next morning review; a keep on that row leaves the streak unchanged.
Applies to. bob-cli, bob-plugins, vault.
Claim. Every automatic transition of an open task from Blocked
[?] to Ready [ ] records [unblocked:: today] on the recovered
line (replacing any older value), unless a WRITE skip applies: the row
is outside freshness scope, has no [fresh::], is already a scheduled
tickler (fresh(t) < scheduled(t) ≤ today), or lands in Next or
Pending. Writers are reconcile Unblock; capture/cycler close recovery
(including project_member); nav/cycler project recovery of
residents; and nav recoveries of lines other than the gesture's own
line. Every stamp clears the field; reconcile prunes [unblocked:: U]
when fresh > U. At read time,
returned_on is the latest of an arrived scheduled and a valid
unblocked (fresh ≤ unblocked ≤ today); returned_by is
scheduled or unblocked, with scheduled on a tie; RESURFACED plus
tier TICKLER follow from returned_on. A keep on a
returned_by: unblocked row neither increments nor resets
[keeps::], and decide_for never opens the decision card for it.
docs/freshness.md §4 and §10b own the contract; see also
Review Walk Is Tiered With Daily Lane Review and
READY Is Freshness-Gated With NEW and ROTTEN Review.
Why. Recover-only, never-linked project members kept their old
[fresh::] and could sit FRESH, outside the walk, for up to six
mornings. Read-time returned_on from prerequisite completion dates
(option A) misses the project unblocks Bryan performs himself —
removing a ^prj prerequisite or Ctrl+D of the project date — plus
deleted prerequisites and closed projects. Rejected alternatives:
- Read-time
returned_on(option A). Misses those unblocks. - Clearing
[fresh::]. A stamp is a human confirmation; automation never stamps. - Recovering to Next. That would invent a lane.
- Successor-linking residents. Residents recover recover-only.
Evidence:
research:202610/unblocked_task_review_guarantee.md;
research:202610/prj_task_inline_schedule_and_deps.md §4;
epic bob-cli-6u (plan:202610/prj_line_project_blocking.md);
bob-cli f51ba28 (Rust freshness), 34d4ab6 (reconcile Unblock),
13d183f (close recovery);
bob-plugins f47be84 (ledger-tools evaluator, api.freshness v12).
Cost. Automation writes a non-stamp freshness input. Recovery
writers live in four engines. The guarantee relies on reconcile having
run since the unblock. Lane walks must stay on. Old plugin versions do
not write the field during the mixed-version window; reconcile prunes
fresh > U.
Reopens when. A required surface cannot wait for reconcile, or stamps must remain the only freshness write.
Superseded in part: project residents are now offered as successors and linked only when picked; the former rejected "successor-linking residents" behavior is an explicit offer. See Unblocked Tasks Are Offered, Never Auto-Linked.