Repository navigation
Conversation
Product contract for Workflowy-style [[id]] links in the TUI, built on a jj-style stable item ID beside the existing numeric ID. Covers identity, link syntax and rewrite-on-save, side-pane backlinks, archive behaviour, and sync-safe deterministic backfill. CLI/MCP/server unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Older taskbook-rs binaries (including a long-running tb --mcp) drop the unknown stable-ID field on write. Random IDs for new items could never be recovered, silently breaking links in a mixed-version window. Making every ID a pure function of immutable creation fields lets the next load re-derive them identically. Review: feasibility + adversarial (independent agreement). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The Goal Capsule promised links keep working through archive, contradicting R16. Review: coherence. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Otherwise a later unrelated TUI edit would turn stored plain-text [[12]] into a permanent link to whatever holds numeric ID 12, or block the save, contradicting R7. Review: adversarial. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
One mistyped [[99]] would otherwise discard a whole \$EDITOR session. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
No existing TUI pattern covers an item picker. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Enrich the requirements-only plan in place with a Planning Contract (18 KTDs, design diagrams), ten implementation units, a verification contract and definition of done. Product Contract changes, confirmed in plan scoping: stable IDs are a pure function of _timestamp (HUID-style), computed on read and never stored; uniqueness enforced at creation (R19); same-timestamp legacy items are divergent (R20); R8/R10 clarified. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
KTD7, U6 and the save-path diagram disagreed on when the baseline is read; a save-time re-read could reclassify spans synced mid-edit. Review: coherence. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
R15 promised every listed entry was jumpable, contradicting R16/R20 and KTD15: broken links have no target and divergent IDs are ambiguous. Review: coherence. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Config::save always writes the real config path, so App tests calling set_view would overwrite it and race each other. Review: feasibility. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Clearing the command line only after success would wipe pending_confirm set by /delete and /clear. Re-activate on rejection instead. Review: feasibility. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
…can list inbound links U4/U5 tests forbade recording links to non-active IDs, which made the KTD17 Archive-view backlinks impossible. Review: feasibility. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Validating title and body together before either existing write already prevents a half-saved note on rejection; the new method added work for no requirement. Review: scope-guardian. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Pre-existing error path unrelated to backlinks; the explicit post-restore reload (kept, since set_view is a no-op inside Archive view) already removes the stale row. Review: scope-guardian. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The l flow opened on an empty query that suggests nothing, Enter could take an unhighlighted row, and Enter with no match saved a stray '[[query'. Review: design-lens, adversarial. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The broken marker, link style and ID field width were unspecified, so U7 tests could not assert anything concrete. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Status messages expire after 3 s and clip at terminal width, so R9's message naming the reference could vanish. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
In a default 80-column terminal the keys otherwise appear to do nothing. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
…states Unspecified states would be invented per implementer, and backlinks of a divergent item cannot be attributed. Review: design-lens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
R19 promised no shared IDs at all, but cross-device same-millisecond creates can collide; the plan already treats those as divergent (R20). Review: coherence; approved by user. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
…rewrites A bracketed word of k-z letters silently became a link whenever exactly one ID prefix matched. Lowercase-only matching keeps capitalized wiki text plain, and naming each rewrite makes accidental matches visible. Review: adversarial (FYI); adopted by user. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Add stable_id: an invertible 64-bit mix (splitmix64 finalizer) of _timestamp written as 16 letters k-z, jj change-ID style. Computed on read via Item::stable_id() / StorageItem::stable_id(); nothing is serialized, so the storage format is unchanged. Golden vectors, computed independently, freeze the mapping. Plan: U1 (R1-R3, R5, KTD1-KTD2). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Stable IDs derive from _timestamp, so two items created in the same millisecond (MCP loops, scripts) would share one. Every Taskbook create path now bumps the timestamp past any active or archived item before inserting; the shift is a few ms at most and only affects ordering. Plan: U2 (R19, R1; AE7 creation half; KTD3). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
links::scan classifies closed, single-line spans as full stable-ID tokens, numeric refs, lowercase k-z prefix refs, or plain text. rewrite_on_save diffs the draft against the edit's baseline as a multiset: only references added in this edit are resolved to full tokens; unresolvable, ambiguous or divergent ones are returned as rejections, and full tokens and plain text are never touched. join_note/split_note let a note's title and body diff as one text. Plan: U3 (R7-R10, R20; AE1-AE3; KTD6-KTD8). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
LinkIndex is built from the active items and answers every display question: outgoing links per item, backlinks keyed by target stable ID (so an archived item can still list who links to it), divergent IDs, shortest unique display prefixes (also for IDs outside the active set), and rendering text into plain, link, broken and divergent segments with one level of title substitution so link cycles cannot recurse. Plan: U4 (R6, R13, R14, R16, R20; KTD4, KTD16, KTD17). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
App now loads active items and rebuilds a LinkIndex on every refresh, view switch, SSE data change (including while in Archive view) and restore. refresh_items is view-aware, so refreshing in Archive view keeps showing the archive, and restore reloads explicitly because set_view(Archive) is a no-op there. Config writes go through one helper so a test constructor can point them at a temp file. Plan: U5 (R14, R16; AE5; KTD9). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
… drafts /task, /note and /edit run the description through rewrite_on_save against the edit-session baseline (the e prefill, or storage on first submit), held across retries. A rejection re-opens the command line with the draft and cursor and shows a sticky "Not saved: ..." status that collapses later references into "+N more" to fit; submit order is unchanged so /delete and /clear confirmations still work. The note editor validates title and body together before either write and, on rejection, reopens on the edited content with the reason as a leading // comment. Successful saves name each rewritten reference and the title it now links to. Plan: U6 (R8-R10; AE1-AE3; KTD7, KTD8, KTD10, KTD11). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Rows show a 6-character stable-ID field after the numeric ID (unique prefix highlighted, rest dimmed, ?? when divergent) and render [[id]] tokens as the underlined target title, broken links as ✗ plus the short ID, and divergent ones as the short ID plus ??. The journal preview and existing item suggestions use the rendered text; the preview now cuts by characters instead of bytes. Plan: U7 (R6, R13, R16, R20; KTD16). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
An unclosed [[ in the description of /task, /note or /edit lists active, non-divergent items (newest first, current board ahead, excluding the item being edited), matched by rendered title, numeric ID or stable-ID prefix; the first row is pre-selected and accepting inserts [[<full id>]]. Inside an unclosed [[, Enter accepts the selection or, with nothing to accept, shows a hint instead of saving a stray "[[query"; Esc closes only the dropdown until the cursor leaves that [[. l opens /edit on the selected item with the dropdown showing. Plan: U8 (R11, R12; F1, F2; KTD12-KTD14). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
On terminals at least 100 columns wide the content area splits to show
the selected item's outgoing links ("Links to") and backlinks ("Linked
from"); each entry keeps its short stable ID visible, broken and
divergent entries are marked and not jumpable, empty sections say
(none), and a divergent selection shows a notice instead of backlinks.
An archived item in Archive view lists the active items still linking
to it. v toggles the pane and o focuses it (j/k, Enter jumps, Esc or o
returns); both explain themselves when the terminal is too narrow.
Jumping switches Archive to Board view, clears search and board
filters and reveals a hidden completed target, without changing saved
settings, and names what it changed.
Plan: U9 (R14-R17; F3, AE6; KTD14, KTD15, KTD17, KTD18).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
README gains a Links and Backlinks section (the [[ picker, numeric and prefix references, the l key, the side pane, broken and divergent markers, and that other surfaces show raw tokens); docs/README lists the feature; CLAUDE.md records the new modules and the stable-ID design constraint. Plan: U10. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Behavior-preserving cleanup from a simplification pass: - share item_link_targets, the unique-prefix computation, the broken and divergent markers and a truncate_chars helper instead of copies; - journal view reuses should_show_item; EditBaseline, OpenLink and LinkCandidate replace anonymous tuples; the pane key handler matches on the target directly; /task and /note share rewrite_new; - refresh derives boards from the items it just loaded (one storage read instead of two), the backlink sort caches positions, the unique timestamp check only collects candidates at or after the new one, plain_text skips rendering text without [[, and the link query is only computed for Enter and Esc; - drop a plan-ID reference from a code comment. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
refresh_items now loads the archive into app.items in Archive view, so `/note` there picked the highest archived ID as "the new note" and edit_note_external read archived notes. Saving then wrote an archived note's text into the active item with the same numeric ID. Use the ID create_note_direct_with_tags returns, read the note to edit from active_items, and answer Enter on an archived note with a status instead of opening the editor. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The existing retry test left storage unchanged between submits, so it passed even with the retained baseline or the `b.id == id` guard removed. Change storage under an open draft, and retarget an `e` prefill to another item; each new test fails under the matching mutation. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The pane's j/k cursor, Esc, the divergent "cannot jump" status and the fallback that hands keys back to the list once the pane is hidden had no test that could fail. Each new assertion fails under the matching mutation of handle_pane_key or handle_key_event. The broken-link test now also checks its status. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The pane gets an item's outgoing links from item_link_targets, so the per-item map the index built on every refresh was a second, unused source of the same answer. The dedupe test now asserts on item_link_targets directly. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Notes are joined with NOTE_FIELD_SEPARATOR, not `\n`; that constant's own comment explains why no reference straddles title and body. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Found in code review: after `e`, KTD13's Enter rule blocks saving text that already holds an unclosed `[[`, and the hint's "press Esc" cancels the edit. Recorded with a candidate fix rather than changing KTD13 now. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Add taskbook-common::limits: 1,024 active and 4,096 archived items supported, 512 active recommended. Features are designed for that size, which is what makes same-millisecond stable-ID collisions acceptable. Envelope tests: - creates stay unique against a full store whose timestamps form one dense run starting now (fails if creates stop reading the archive); - unique stable-ID prefixes fit the 6-character TUI column at 1,024 active items; - an #[ignore]d create latency budget (500 ms) for `--ignored` runs. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
CLAUDE.md gains a Design Envelope decision (whole-file JSON, O(n) per operation is fine, larger stores are unsupported) and ties the stable-ID collision tradeoff to it. README gains a Limits section with measured numbers, reproducible with scripts/bench-envelope.py. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
taskbook is a board a person reads and keeps up with; that sets the bound. 512 active is the recommendation, 1,024/4,096 is headroom, and agent use via MCP stays within it. Features handle what a person can produce; machine-scale scenarios are unsupported rather than edge cases. Performance numbers stay, as evidence the JSON storage fits this size. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
SizeNotice::check(active, archived) returns a notice once active items pass the 512 recommendation or the archive passes 4,096, and summary() names each limit crossed. Informational only; callers decide wording. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
create_task, create_note and restore_items add a second text block when the store is past the envelope. It asks the agent to tell the user and not to archive or delete items itself, and mentions `tb --clear` only when there are completed tasks to archive. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
The header gains a warning-styled indicator beside the existing ones, suggesting /clear only when completed tasks exist. The archive count is read once at startup and whenever Archive view loads the archive, so it can lag deletes made in the same session. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ATtYfxsHkkaRLf94U8Mzmw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
[[stable-id]]links between tasks and notes in the TUI, a backlinks side pane with jump-to, and documentation and tests for the human-scale size limits taskbook is designed for. The plan is indocs/plans/2026-10-21-2154-feat-backlinks-plan.md. Each commit is one unit or one review fix, so it reads best commit by commit.Stable IDs
Numeric
_ids are renumbered on archive and restore, so links use a stable ID instead._timestamp, written as 16 lettersk–z, jj change-ID style._timestampunique across active and archived items.??) and can't be linked to.Links (TUI only)
[[N]]and[[prefix]]references added in that edit are rewritten to full tokens.$EDITOR, the editor reopens with a//notice.[[opens autocomplete, andllinks the selected item to another one.✗).vtoggles it,ofocuses it, Enter jumps) lists outgoing links and backlinks. It's hidden below 100 columns.Size envelope
taskbook is a board a person reads and keeps up with, and that sets the bound.
taskbook-common/src/limits.rs). Anything larger is unsupported, not an edge case to design for. This is also why the stable-ID collision tradeoff is acceptable.scripts/bench-envelope.pyreproduces them).create_task,create_noteandrestore_itemstools show a notice. It blocks nothing, and it suggests/clear(tb --clear) only when there are completed tasks to archive.#[ignore]d; run it withcargo test -- --ignored.Review
ce-code-reviewround confirmed 6 findings. Five are fixed in separate commits./notein Archive view could overwrite an active item.Testing
cargo fmt --checkandcargo clippy --workspace --all-targets -D warningsare clean.cargo test: client 100 passed (1 ignored), common 92, server 3.Not in this PR
/searchmatches raw link tokens, not rendered titles (deferred in the plan).[[: text that already contains an unclosed[[can't be saved aftere(deferred in the plan, with a candidate fix).fix/render-id-padding.Generated by Claude Code