task-view — TECH
task-view — TECH
Section titled “task-view — TECH”Status: DRAFT (ID-20.4, kh-prod-readiness-S58). Authored by task-planner under
write-tech-specper S58 dispatch. Fresh-context Planner per Q-PLANNER-2 (NOT the 20.3 PRODUCT Planner). Cross-project workflow tool — NOT a Knowledge Hub product. Lives in its own repository; consumed by KH (and any other project carrying conformant Zod ledgers) as an external CLI + Claude plugin.
Companion specs
Section titled “Companion specs”- PRODUCT spec:
./PRODUCT.md— 55 numbered Behaviour invariants (inv 1throughinv 55). Note: PRODUCT working draft uses the placeholderledger-mirrorthroughout. The Liam-ratified final name istask-view. This TECH usestask-viewexclusively; the §0 “name resolution” note records the rename mapping. - Research:
docs/research/per-task-render-surface.md(ID-20.2 — §1 Plannotator capability inventory, §2 fork-delta cost ledger, §5 three-option comparison with Option A as the ratified pick). - Upstream:
https://github.com/backnotprop/plannotatorv0.19.18 — fork source. Dual-licensed MIT OR Apache-2.0, legally clear for fork + rename. Local fork already present at/Users/liamj/Documents/development/plannotator. - KH ledgers driving the schema set:
lib/validation/task-list-schema.ts(226 lines),lib/validation/roadmap-schema.ts(260 lines),lib/validation/backlog-schema.ts(144 lines),lib/validation/work-status.ts(104 lines — masterWorkStatusenum + per-surface.exclude(...)derivations +Prioritymaster enum). TECH refers to these as the schema set this build vendors at fork time.
§0 Name resolution
Section titled “§0 Name resolution”PRODUCT and RESEARCH use working placeholders. Liam ratified at S58 dispatch:
| Surface | Working placeholder (PRODUCT) | Ratified final (this TECH) |
|---|---|---|
| Tool name | ledger-mirror | task-view |
| CLI binary | ledger-mirror | task-view |
| Plugin slash command | /ledger-mirror | /task-view |
| Repository | TBD | task-view (new GitHub repo) |
| Plugin manifest dir | .claude-plugin/plugin.json | same (Claude convention) |
| Mirror persistence | (OQ-B working draft) | committed alongside canonical JSON |
| Record-id filename | (OQ-C working draft) | raw id, unsafe-char → - |
When mapping a PRODUCT invariant that names ledger-mirror, treat the
mapping as substituting task-view. PRODUCT inv 1 in particular requires
this substitution.
Context
Section titled “Context”What’s being built
Section titled “What’s being built”task-view is a cross-project developer tool that renders any of three
canonical workflow ledgers (task-list.json, product-roadmap.json,
product-backlog.json) as a per-record browser view + edit-back interface.
It ships as a forked-and-renamed copy of Plannotator v0.19.18 with three
strip-and-replace deltas:
- STRIP: annotation pipeline (DELETION / COMMENT / GLOBAL_COMMENT toolbar
- AnnotationPanel + AnnotationCard + share-URL annotation handlers + plan diff infra + 7 of 9 Plannotator modes — see §1.2 strip ledger).
- REPLACE: read-only
FrontmatterCardbecomes an editableRecordFrontmatterCardwith dropdowns (Zod-enum sourced) + textareas (free text) + per-entry forms (DocLinkSchema arrays). - ADD: per-record patch server (
PATCH /api/ledger/record), atomic write-to-temp + rename, mtime collision detection, mirror generator, three schema detectors (TaskListSchema / RoadmapSchema / BacklogSchema).
The tool’s runtime dependency on Knowledge Hub is zero. The three Zod
schema files are vendored into the tool’s packages/schemas/ directory at
fork time (frozen copies — see §1.5 vendoring strategy). Consuming projects
opt in by carrying a task-list.json / product-roadmap.json /
product-backlog.json that validates against the vendored schemas; the tool
does not reach into the consuming project’s lib/validation/ at any point.
How the current system works (Plannotator v0.19.18)
Section titled “How the current system works (Plannotator v0.19.18)”| File | Role | Fork action |
|---|---|---|
apps/hook/server/index.ts | Bun entrypoint — multiplexes 9 modes (annotate / annotate-last / annotate-folder / plan-review / code-review / portal / image-annotator / web-annotator / paste-receiver). | RENAME to apps/server/index.ts; collapse to 3 modes (ledger-task / ledger-roadmap / ledger-backlog) + 1 record-shortcut mode (per inv 6). |
packages/server/annotate.ts (lines 33-83) | AnnotateServerOptions + AnnotateServerResult — the type contract for spawning the Bun server. | KEEP shape; rename to startTaskViewServer in packages/server/ledger.ts. |
packages/server/annotate.ts:153 | Bun.serve({...}) with random-port retries (MAX_RETRIES = 5). | KEEP — already matches inv 49 retry policy. |
packages/server/draft.ts | ~/.plannotator/history/ per-content-hash auto-save. | KEEP — re-purposed as local-storage drafts for PRODUCT inv 51. Migrate from server-side to browser-side per inv 51 wording. |
packages/server/external-annotations.ts | Annotation handler — /api/feedback. | DELETE — annotation paths stripped per §1.2. |
packages/ui/components/Viewer.tsx (lines 110-138) | FrontmatterCard — read-only YAML frontmatter table. | REPLACE with editable RecordFrontmatterCard (per inv 30). |
packages/ui/components/AnnotationPanel.tsx | Annotation card list with inline editing. | DELETE — annotation surface stripped. The isEditing pattern from AnnotationCard is preserved as the inspiration for textarea-toggle in RecordTextareaField (per inv 26-27). |
packages/ui/utils/parser.ts (lines 7-86) | Frontmatter interface + extractFrontmatter + parseMarkdownToBlocks. | KEEP whole-cloth — markdown parsing capability floor per inv 10. |
packages/server/repo.ts | getRepoInfo for GitHub repo display in viewer header. | KEEP — drives the Commit refs GitHub linking per inv 7. |
packages/server/browser.ts | openBrowser cross-platform helper (macOS/Linux/Windows/WSL). | KEEP — matches inv 52 cross-platform requirement. |
apps/hook/package.json bin field | Already wires CLI → bin/plannotator.js. | RENAME to task-view binary; rewire bin/task-view.js to delegate to apps/server/index.ts. |
Knowledge Hub patch-surface integration points
Section titled “Knowledge Hub patch-surface integration points”These are the surfaces in this repo that the task-view tool touches
when KH is the consuming project. The tool itself contains zero KH code;
KH carries glue code (no glue beyond the conformant ledgers themselves
is strictly required).
| File | Role | Notes |
|---|---|---|
docs/reference/task-list.json (lines 1-736) | Canonical TaskList ledger. | Tool reads via filesystem; writes via atomic rename. Tool never reaches into KH lib/. |
docs/reference/product-roadmap.json | Canonical Roadmap ledger. | Same access pattern. |
docs/reference/product-backlog.json | Canonical Backlog ledger. | Same access pattern. |
lib/validation/task-list-schema.ts | KH Zod schema for TaskList. | Vendored into the tool at fork time. Tool consumes its own frozen copy of this file; updates to the KH schema require re-vendoring. See §1.5 vendoring strategy. |
lib/validation/roadmap-schema.ts | KH Zod schema for Roadmap. | Same vendoring rule. |
lib/validation/backlog-schema.ts | KH Zod schema for Backlog. | Same vendoring rule. |
lib/validation/work-status.ts | Master WorkStatus enum + .exclude(...) derivations + Priority master enum. | Same vendoring rule. The four files form one vendored bundle. |
.claude/settings.json PreToolUse Write|Edit|MultiEdit matcher | Tier 2.2 worktree-isolation hook (ID-19.3, S57). Blocks Write/Edit/MultiEdit to absolute /Users/liamj/Documents/development/knowledge-hub* paths from a CWD that is NOT a prefix of the file_path. | The tool’s patch server runs from the consuming project’s working directory (the CWD where the user launched it). When a sub-agent invokes the tool via /task-view, the hook will refuse to let the sub-agent emit a Write/Edit to a path outside its worktree. The tool’s patch server is therefore safe by construction: it writes via Bun.write from inside the user’s CWD, not via the Claude-Code Write tool — the hook fires only on Write/Edit/MultiEdit tool calls. See §5.6 hook interaction. |
docs/reference/tasks/ID-N.md (and roadmap/backlog equivalents) | Per-record mirror files. | NEW (created by tool first run). Tracked by git; visible in git diff; browsable on GitHub. Per OQ-B ratified default. |
Why the Plannotator fork (not a clean Option C build)
Section titled “Why the Plannotator fork (not a clean Option C build)”Liam ratified at S58 dispatch: fork+rename (Option A) over new minimal UI (Option C). Rationale captured in 20.3 PRODUCT journal at S57:
- Plannotator’s
Viewer+parseMarkdownToBlocks+BlockRenderer+MermaidBlock+GraphvizBlock+CodeBlock+TableBlockgive task-view 80% of its rendering capability for free, at the floor required by inv 10 (CommonMark + GFM + diagrams + tables). - Plannotator’s keyboard navigation (inv 53) ships intact.
- Plannotator’s
Bun.serverandom-port retry,openBrowsercross-platform helper, andgetRepoInfoGitHub linker are all directly reusable. - Plannotator’s
Frontmatterparser already handles the YAML frontmatter shape the mirror generator produces.
Cost: fork-maintenance overhead is bounded because the fork is permanently divergent (PRODUCT inv 1). No upstream-rebase commitment. Repo licence is MIT-OR-Apache-2.0 (verified via the LICENSE-MIT + LICENSE-APACHE files in the local fork) so the rename is legally clear.
The Option C ~10h-from-scratch estimate (research §5.3) becomes ~9-13h via fork (per research §2.5 summary table) but with substantially less risk on the rendering capability floor.
§1 Fork plan + Plannotator dependency strip
Section titled “§1 Fork plan + Plannotator dependency strip”§1.1 Repository creation
Section titled “§1.1 Repository creation”- Create new GitHub repo
task-view(account: Liam’s personal account; no ai-solution-hub org coupling since this is a cross-project tool). - Bootstrap from a clean clone of upstream
backnotprop/plannotatorat commit pinned for v0.19.18 (use existing fork at/Users/liamj/Documents/development/plannotatoras the source). - First commit: rename top-level identifiers (per §1.4 below).
- Second commit: apply strip ledger (per §1.2 below).
- Third commit: install vendored Zod schemas (per §1.5 below).
- Fourth commit: wire the new
RecordFrontmatterCard+ patch-server endpoint. - Tag
v0.1.0-task-viewas the first releasable cut.
License: retain dual MIT-OR-Apache-2.0 of upstream. Author attribution
line added to AUTHORS.md crediting upstream Plannotator (backnotprop) plus
Knowledge Hub team for the fork.
§1.2 Strip ledger — what gets deleted
Section titled “§1.2 Strip ledger — what gets deleted”The Plannotator monorepo carries ~14 apps and ~7 packages. Most are
out-of-scope for the task-view use case (the task-view server is the
only one task-view needs). Strip these to reduce maintenance surface:
Apps to delete entirely:
apps/codex/,apps/copilot/,apps/gemini/— alt-LLM SDK adapters not needed by task-view (it has no LLM dependency).apps/marketing/,apps/portal/— landing-page and SaaS portal, irrelevant for a developer-local tool.apps/paste-service/— short-URL paste service (cross-machine sharing not in scope per PRODUCT inv 44 “127.0.0.1-only”).apps/pi-extension/,apps/vscode-extension/,apps/opencode-plugin/— alternate-host integrations not needed for the Claude-plugin + CLI dual invocation per PRODUCT inv 2.apps/review/,apps/skills/— code-review and skill-runner apps; not in the task-view scope.
Apps to rename + collapse:
apps/hook/→apps/server/(single Bun server entrypoint).
Packages to delete:
packages/editor/,packages/review-editor/— annotation editor apps whose only consumer is the deletedapps/review/.
Packages to keep but trim:
packages/ui/— keep the markdown rendering surface (Viewer, parser, BlockRenderer, blocks/, InlineMarkdown, ListMarker, ListMarker, TableOfContents, MermaidBlock, GraphvizBlock, CodeBlock, TableBlock, ThemeProvider, Settings minus annotation tabs, ConfirmDialog, Popover, Tooltip). Delete: AnnotationPanel, AnnotationSidebar, AnnotationToolbar, AnnotationToolstrip, AnnotationCard, CommentPopover, FloatingQuickLabelPicker, QuickLabelDropdown, EditorAnnotationCard, PinpointOverlay, PlanHeaderMenu, PopoutDialog, ImageAnnotator, CompletionOverlay, plan-diff/, sidebar/, goal-setup/, settings/Goal*, settings/Agents*, settings/AI*, AISettingsTab, AISetupDialog, AgentsTab, ApproveDropdown, AttachmentsButton, DiffTypeSetupDialog, ExportModal, ImportModal, KeyboardShortcuts (replace with simpler version exposing only Tab/Enter/Esc/Cmd+Enter per inv 53), Landing, ModeToggle, PermissionModeSetup, ReviewAgentsIcon, SearchableSelect, StickyHeaderLane, TaterSprite*, ToolbarButtons, UpdateBanner, ImageThumbnail. Delete usePinpoint, useAnnotationHighlighter; keep useScrollViewport, useValidatedCodePaths.packages/server/— keep the Bun-serve scaffold (browser.ts,repo.ts,path-utils.ts,remote.ts— though loopback-only per inv 44 makes most ofremote.tsunused),code-nav.ts,config.ts,draft.ts(re-purposed), shared handlers. Deleteannotate.ts(replaced byledger.ts),external-annotations.ts,editor-annotations.ts,claude-review.ts,codex-review.ts,review.ts,agent-jobs.ts,agent-review-message.ts,goal-setup.ts,image.ts,share-url.ts,vcs.ts,jj.ts,p4.ts,pr.ts,git.ts(the patch server doesn’t need git operations).packages/shared/— keep storage helpers + runtime detection. Delete agent-typing helpers, annotation-only types.packages/ai/— DELETE entirely (no LLM dependency in task-view).packages/server/tour/— DELETE (annotation tutorial walkthrough).bin/— KEEP, rewire totask-view.js.scripts/,tests/— KEEP both directories; trim contents to task-view scope only (annotation-related fixture tests deleted; markdown parser tests kept; new mirror-generator and patch-server tests added in §3 + §5).
Estimated line count post-strip: roughly 40-50% of upstream LOC (Plannotator ~25 kLOC TS + tests; post-strip ~10-12 kLOC).
§1.3 Strip ledger — what gets kept
Section titled “§1.3 Strip ledger — what gets kept”Components and modules retained:
packages/ui/components/Viewer.tsx— primary rendering shell. Lines 140-630 retained minus annotation pipeline; specificallyFrontmatterCard(lines 110-138) replaced byRecordFrontmatterCard(see §4 below); the scroll viewport hook retained for inv 53 keyboard nav.packages/ui/components/BlockRenderer.tsx— dispatchesBlocktypes to block components (CodeBlock,TableBlock,MermaidBlock,GraphvizBlock,ListMarker,InlineMarkdown).packages/ui/utils/parser.ts— full file kept (CommonMark + GFM floor per inv 10).packages/ui/utils/slugify.ts— for in-page anchor generation per inv 13.packages/ui/components/ListMarker.tsx,InlineMarkdown.tsx,TableOfContents.tsx— render primitives.packages/ui/blocks/CodeBlock.tsx,TableBlock.tsx(+ TableToolbar, TablePopout) — inv 10 floor.packages/ui/components/MermaidBlock.tsx,GraphvizBlock.tsx— diagram primitives (inv 10).packages/ui/components/ThemeProvider.tsx— keep but disconnect from KH semantic tokens. The tool ships with its own neutral Tailwind theme per inv 54 (“base contract: plain text”). Future styling work is out of scope per PRODUCT inv 54.packages/server/browser.ts—openBrowser()cross-platform per inv 52.packages/server/repo.ts—getRepoInfo()for GitHub linking per inv 7.packages/server/draft.ts— content-hash draft logic; ported to browser- local-storage per inv 51.
§1.4 Rename map
Section titled “§1.4 Rename map”| Upstream identifier | Task-view identifier |
|---|---|
plannotator (npm package) | task-view |
plannotator (bin) | task-view |
/plannotator-annotate (slash command) | /task-view |
PLANNOTATOR_REMOTE (env) | TASK_VIEW_REMOTE (unused; loopback-only per inv 44) |
PLANNOTATOR_PORT (env) | TASK_VIEW_PORT |
~/.plannotator/ (config dir) | ~/.task-view/ |
startAnnotateServer | startTaskViewServer |
AnnotateServerOptions | TaskViewServerOptions |
apps/hook/ | apps/server/ |
FrontmatterCard (Viewer.tsx) | RecordFrontmatterCard (Viewer.tsx) |
@plannotator/ui, @plannotator/server, @plannotator/shared | @task-view/ui, @task-view/server, @task-view/shared |
.plannotator/plugin.json | .claude-plugin/plugin.json (Claude-plugin convention; same place upstream uses) |
§1.5 Vendoring strategy for Zod schemas
Section titled “§1.5 Vendoring strategy for Zod schemas”The four KH validation files form the schema bundle vendored into
task-view’s packages/schemas/:
lib/validation/task-list-schema.ts(226 lines,TaskListSchema,TaskSchema,SubtaskSchema,TaskListStatus,SubtaskStatus,parseTaskListWithWarnings,TaskListWarning)lib/validation/roadmap-schema.ts(260 lines,RoadmapSchema,RoadmapSectionSchema,RoadmapItemSchema,DocLinkSchema,RoadmapPriority,RoadmapStatus,ColumnSet)lib/validation/backlog-schema.ts(144 lines,BacklogSchema,BacklogItemSchema,BacklogStatus,BacklogItemType)lib/validation/work-status.ts(104 lines,WorkStatusmaster enum + per-surface.exclude(...)derivations +Prioritymaster)
Vendored copies replace @/lib/validation/... import paths with
@task-view/schemas/.... The vendored files become the canonical schemas
from the tool’s perspective — KH may evolve its lib/validation/*
independently; updates flow into the tool via explicit re-vendoring
(documented in the tool’s CONTRIBUTING.md). Schema-drift detection runs
in CI (per §3.5 below) so re-vendoring is not a silent event.
Why vendor rather than publish + npm-install: the schemas need to remain
in sync with the source ledger files in each consuming project (here, KH).
Publishing them as a separate npm package would add a release cycle for
schema changes that today only need to land in KH. Vendoring is the same
strategy upstream Plannotator uses for @plannotator/shared (workspace-
local package). Re-vendoring is a manual cp lib/validation/*.ts .../task-view/packages/schemas/src/ + commit; the CI drift check makes
the re-vendor a non-silent event.
§2 Routing + schema detection
Section titled “§2 Routing + schema detection”§2.1 Three-way schema discrimination
Section titled “§2.1 Three-way schema discrimination”The tool routes by document_name value at load time per PRODUCT inv 4.
The three known values:
document_name literal | Schema discriminator | Renderer mode |
|---|---|---|
"Knowledge Hub Task List" | TaskListSchema | Task-list mode (inv 7-13) |
"Knowledge Hub Roadmap" | RoadmapSchema | Roadmap mode (inv 14-19) |
"Product Backlog" | BacklogSchema (z.string().min(1) field, value match) | Backlog mode (inv 20-25) |
Note the asymmetry called out in PRODUCT inv 4: TaskListSchema and
RoadmapSchema both use z.literal('Knowledge Hub Task List') /
z.literal('Knowledge Hub Roadmap'). BacklogSchema.document_name is
z.string().min(1) — Zod alone can’t discriminate Backlog from arbitrary
JSON with a document_name string field; routing therefore anchors on the
known canonical value "Product Backlog", not on the schema’s field
shape. This implementation match:
function detectSchema(parsed: unknown): | { kind: 'task-list'; data: TaskList } | { kind: 'roadmap'; data: Roadmap } | { kind: 'backlog'; data: BacklogDocument } | { kind: 'unknown'; documentName: string | null } { if (!parsed || typeof parsed !== 'object') return { kind: 'unknown', documentName: null }; const dn = (parsed as { document_name?: unknown }).document_name; if (dn === 'Knowledge Hub Task List') return { kind: 'task-list', data: TaskListSchema.parse(parsed) }; if (dn === 'Knowledge Hub Roadmap') return { kind: 'roadmap', data: RoadmapSchema.parse(parsed) }; if (dn === 'Product Backlog') return { kind: 'backlog', data: BacklogSchema.parse(parsed) }; return { kind: 'unknown', documentName: typeof dn === 'string' ? dn : null };}On { kind: 'unknown' }, server returns a 422 with the unmatched
document_name value (or null) and the CLI exits non-zero per inv 4 +
inv 48.
§2.2 Record-level path resolution
Section titled “§2.2 Record-level path resolution”PRODUCT inv 6: launching with docs/reference/tasks/ID-20.md (a mirror,
not a canonical JSON) must resolve to the parent ledger and preselect
the named record.
Resolution rule (deterministic):
- If the given path ends with
.md, ascend one directory level. - Look for sibling JSON files with
document_nameliterals matching one of the three known values (Knowledge Hub Task List,Knowledge Hub Roadmap,Product Backlog). - If exactly one match, load that JSON, route via §2.1, then look up the
record whose
idmatches the mirror’s filename stem (with inverse-→ original character substitution per §3.2 record-id rule). - If zero or multiple matches, exit non-zero with the same error formatting as inv 43 (“no ledger found” / “multiple ledgers found”).
Pre-selection: the URL emitted to openBrowser includes a query-string
fragment (?record=ID-20) so the SPA scrolls/expands the matching record
on first paint.
§2.3 Working-directory inference
Section titled “§2.3 Working-directory inference”PRODUCT inv 43: when no path argument is given, scan CWD for JSON files
matching the three document_name literals.
Implementation: Bun.glob('*.json', { cwd: process.cwd() }), then for
each match read the first 4 KiB, JSON.parse (lenient — failure is just
“skip”), check for one of the three document_name strings. Exactly-one
match → open; zero → exit with friendly message; multiple → print numbered
list + exit (re-invocation required).
The scan does NOT recurse into subdirectories. Repos can carry ledgers in
different locations (KH’s current docs/reference/, KH’s planned
.planning/task-management/ post-ID-22); the user supplies the path
explicitly when ledgers aren’t at the repo root.
§3 Mirror generator (per-record .md output)
Section titled “§3 Mirror generator (per-record .md output)”§3.1 Output layout
Section titled “§3.1 Output layout”PRODUCT inv 5: mirrors land in a sibling directory of the canonical JSON, named to match the document type:
| Canonical JSON | Mirror directory |
|---|---|
<dir>/task-list.json | <dir>/tasks/ |
<dir>/product-roadmap.json | <dir>/roadmap/ |
<dir>/product-backlog.json | <dir>/backlog/ |
In KH today: docs/reference/tasks/, docs/reference/roadmap/,
docs/reference/backlog/. Post-ID-22 (Liam-ratified Task to relocate
ledgers to .planning/task-management/): same naming convention.
PRODUCT inv 46 verifies this — the tool needs zero updates when ID-22
ships.
§3.2 Record-id filename rule (Liam-ratified OQ-C)
Section titled “§3.2 Record-id filename rule (Liam-ratified OQ-C)”The mirror filename stem is the raw id value with any
filesystem-unsafe character substituted to -. The unsafe set is the
POSIX-portable filename-character complement:
unsafe = / \ : * ? " < > | (and control chars 0x00-0x1F + 0x7F)Substitution is 1:1. Examples:
| Canonical id | Filename |
|---|---|
"20" (Task) | ID-20.md |
"15" (Task) | ID-15.md |
"3.1.8" (Roadmap) | 3.1.8.md |
"9.18" (Roadmap) | 9.18.md |
"30" (Backlog) | C2-PA5.md |
"45" | OPS-43.1.md |
Note the Task-list filename gets an ID- prefix because Task ids are bare
integer strings (e.g. "20") — ID-20.md reads as a record identifier on
disk where bare 20.md would not. Roadmap and Backlog ids already carry
their own prefixes/structure so they’re used as-is. Prefix rule:
- Task-list mode: filename =
ID-${task.id}.md. - Roadmap mode: filename =
${item.id}.mdfor items;section-${section.id}.mdfor section narrative pages. - Backlog mode: filename =
${item.id}.md.
Discriminating prefixes are documented in the tool’s README + applied deterministically.
§3.3 Mirror content shape
Section titled “§3.3 Mirror content shape”Mirrors are CommonMark + YAML frontmatter. The frontmatter block exposes
the structured fields the editable RecordFrontmatterCard reads; the
markdown body holds the rendered free-text content.
Task-list mode mirror (per inv 7-13):
---type: taskid: "20"title: Per-Task .md mirror generator + render surfacestatus: in_progresspriority: musteffort_estimate: ~6-10h across 20.5+ implementation subtasksowner: Engineeringupdated: 2026-05-20T23:15:00.000Zsession_refs: [kh-prod-readiness-S55, kh-prod-readiness-S56]commit_refs: [68f43b37, 8ca52ffb]dependencies: []cross_doc_links: - path: docs/research/per-task-file-mirror-design.md anchor: null raw: per-task-file-mirror-design.mdpriority_note: Liam promoted at S55 close — workflow visibility now a recurring friction pointstatus_note: S56 — 20.2 RESEARCH done...---
# ID-20: Per-Task .md mirror generator + render surface
Generate human-readable .md mirrors of each Task...
## Subtasks
### ID-20.1: RESEARCH — per-Task .md file shape design
- **Status:** done- **Dependencies:** _none_- **Updated:** _unset_
Lay out file layout, generator script signature...
**Test strategy:** Research doc exists at docs/research/per-task-file-mirror-design.md...
**Details:**
Authored at docs/research/per-task-file-mirror-design.md during S55 Wave 4...
<info added on 2026-05-20>Research doc landed at docs/research/per-task-file-mirror-design.md...</info added on 2026-05-20>The journal blocks (<info added on ...>) render in the Viewer as
visually distinct indented sections (per inv 8). The frontmatter table
displays the structured fields above; the markdown body displays
descriptions, test strategies, details, and journals.
Roadmap mode mirror (per inv 14-19):
---type: roadmap-itemid: "3.1.8"section_id: "3.1"title: ...phase_label: nullpriority: highpriority_note: nullseverity: nullstatus: pendingstatus_note: nullowner: nulleffort_estimate: ...depends_on: []blocks: []coordinates_with: []session_refs: []commit_refs: []cross_doc_links: []---
# 3.1.8: ...
(description as Markdown)Plus a section-${id}.md per section (rendered from RoadmapSectionSchema).
Backlog mode mirror (per inv 20-25):
---type: backlog-itemid: "30"description: ...type_field: featurestatus: readyeffort_estimate: ...priority: shouldtrack: ...dependencies: []surfaced: ...notes: ...details: ... (optional; promotion-ready badge if present per inv 24)testStrategy: ... (optional)---
# 30
(description / notes / details / testStrategy as Markdown)(Note: type is reserved as the document-class discriminator in
frontmatter; the Zod BacklogItem.type field surfaces as type_field.
A 1:1 mapping inside the mirror generator handles this rename.)
§3.4 Idempotency + orphan deletion (PRODUCT inv 5)
Section titled “§3.4 Idempotency + orphan deletion (PRODUCT inv 5)”Each regeneration:
- Generate the full set of mirror files for the current ledger.
- List existing files in the mirror directory.
- Delete any mirror whose record no longer exists in the canonical JSON.
- Write mirrors via atomic write-to-temp + rename (same primitive as the patch server per §5.3).
Idempotency check: identical canonical input must produce byte-identical mirror output. Deterministic ordering of frontmatter keys + stable JSON field iteration order satisfies this. A snapshot test in the Testing-and- validation table (inv 5) verifies it.
§3.5 Schema-drift CI guard (separate from the tool itself)
Section titled “§3.5 Schema-drift CI guard (separate from the tool itself)”Per §1.5 vendoring strategy, the tool ships frozen Zod schemas. KH may
evolve lib/validation/* independently. A CI check in KH detects drift:
- New KH CI job
task-view-vendor-driftruns on PRs touchinglib/validation/{task-list-schema,roadmap-schema,backlog-schema,work-status}.ts. - The job fetches the tagged
task-viewrelease vendor bundle (packages/schemas/src/*.ts) and diffs. - On drift, the job warns (does NOT block); PR description must reference
whether the consuming
task-viewneeds a re-vendor. - The check lives in
.github/workflows/task-view-vendor-drift.yml; the schema files form an inputs list in.github/workflows/ci.ymlif they change.
This guard does NOT live in the task-view repo (the tool can’t know
where KH’s lib/validation/ is). It lives in each consuming project.
§4 Viewer rendering (per-ledger-mode differences)
Section titled “§4 Viewer rendering (per-ledger-mode differences)”§4.1 Rendering surface — shared shell
Section titled “§4.1 Rendering surface — shared shell”All three modes share the Viewer.tsx shell post-strip (per §1.3). The
RecordFrontmatterCard (replacing FrontmatterCard) is mode-aware:
- For Task-list mode, displays:
Status(dropdown),Priority(dropdown),Effort estimate(text input),Owner(text input),Updated(read-only),Session refs(comma-separated text input → array),Commit refs(comma-separated text input → array; rendered as GitHub links viagetRepoInfo()),Dependencies(comma-separated text input → array; rendered as inline links toID-${depId}.md),Cross-doc links(per-entry form per inv 35). - For Roadmap mode:
ID(read-only),Section ID(read-only link),Phase label(text input),Owner(text input + inheritance qualifier per inv 18),Effort estimate(text input),Priority(dropdown — nullable, “(unset)” option per inv 30),Priority note(text input),Severity(text input),Status(dropdown — nullable, “(unset)”),Status note(text input),Session refs,Commit refs. - For Backlog mode: all Zod-schema fields per inv 21. Promotion-ready
badge if
detailsortestStrategyis present per inv 24. Blocked banner per inv 25.
§4.2 Per-mode behaviour table
Section titled “§4.2 Per-mode behaviour table”| Behaviour | Task-list mode (inv) | Roadmap mode (inv) | Backlog mode (inv) |
|---|---|---|---|
| Frontmatter table (mode-specific fields) | 7 | 15, 16 | 21 |
| Subtask block list per Task | 7-8 | n/a | n/a |
Empty-Subtasks empty state (_No subtasks._) | 9 | n/a | n/a |
Empty section empty state (_No items in this section._) | n/a | 17 | n/a |
| CommonMark + GFM + diagrams render floor | 10 | 10 | 10 |
| Cross-doc-link rendering + broken-target marker | 11 | 11 | 11 (via inv 21 details/notes) |
| Inter-record dependency links + broken-target | 12 | 16 (depends_on / blocks / coordinates_with) | 22 |
| In-page anchor for sibling-Subtask deps | 13 | n/a | n/a |
| Section index page | n/a | 14 | 20 (item index) |
| Per-item page | n/a | 16 | 21 |
| Owner inheritance qualifier | n/a | 18 | n/a |
forward_looking_only honoured (no shipped-framing) | n/a | 19 | n/a |
| Filter bar (Track / Status / Priority) | n/a | n/a | 23 |
| Promotion-ready badge | n/a | n/a | 24 |
| Blocked banner | n/a | n/a | 25 |
| Navigation strip (prev/next/back-to-index) | 7 (last bullet) | 14 (back link) | 20 (back link) |
§4.3 Index page implementation
Section titled “§4.3 Index page implementation”Roadmap (inv 14) and Backlog (inv 20) need index pages; Task-list mode
gets a similar index.md at tasks/index.md listing every Task with id,
title, status, priority, plus a count of Subtasks. The index page is
generated alongside per-record mirrors (same generator pass).
URL paths in the SPA:
/— auto-route to the index page for the loaded ledger./record/:id— per-record page (Task / Roadmap item / Backlog item)./section/:id— Roadmap section page (per inv 15)./?track=&status=&priority=— Backlog index with filters per inv 23 + URL-state per “filter state is reflected in the URL query string” requirement.
§4.4 In-page anchors for Subtask deps (inv 13)
Section titled “§4.4 In-page anchors for Subtask deps (inv 13)”Within a Task page, sibling Subtask links resolve to in-page anchors
(#subtask-${subtaskId}) generated via Plannotator’s slugify.ts. The
anchor scrolls to the Subtask block per inv 13. The Plannotator scroll
viewport hook (useScrollViewport) provides the smooth-scroll behaviour
needed.
§4.5 Broken-target marker (inv 11, 12, 13, 22)
Section titled “§4.5 Broken-target marker (inv 11, 12, 13, 22)”A “missing-target” marker is a CSS-styled span (no Warm Meridian token
dependency per inv 54 — the tool ships its own neutral palette). Visual
treatment: text rendered with strikethrough + a small (missing) suffix.
Examples:
- Missing dependency:
ID-99 (missing)with strikethrough onID-99. - Missing cross-doc link:
docs/foo/bar.md (missing target)with link greyed out.
A one-line page-top warning (“This Task references dependencies that don’t exist in the ledger: ID-99”) surfaces missing-target counts per inv 12.
§4.6 Owner inheritance display (inv 18)
Section titled “§4.6 Owner inheritance display (inv 18)”For Roadmap items where owner === null, the per-item page renders:
Owner: Engineering (inherited from §3.1)The qualifier text is generated at render time from the parent section’s
owner lookup. When both are null, the field displays —.
§5 Patch server (validation + atomic write + mtime collision)
Section titled “§5 Patch server (validation + atomic write + mtime collision)”§5.1 Endpoint surface
Section titled “§5.1 Endpoint surface”| Endpoint | Method | Body shape | Response |
|---|---|---|---|
/api/ledger | GET | n/a | { kind, data, mirrorDir, mtime } |
/api/ledger/record/:recordId | GET | n/a | { record, mirror, mtime } |
/api/ledger/record/:recordId | PATCH | { patches: FieldPatch[], baseMtime: string } | { ok: true, newMtime } | { ok: false, error } |
/api/ledger/regen | POST | { baseMtime?: string } | { ok: true } | { ok: false, error } |
Where FieldPatch is:
type FieldPatch = { fieldPath: string[]; // e.g. ['tasks', '<index-or-id>', 'status'] newValue: unknown;};For Task-list:
['tasks', taskId, 'status'](taskId is the string id)['tasks', taskId, 'priority']['tasks', taskId, 'description']['tasks', taskId, 'priority_note']['tasks', taskId, 'status_note']['tasks', taskId, 'effort_estimate']['tasks', taskId, 'owner']['tasks', taskId, 'dependencies'](array — fully replaced per inv 34)['tasks', taskId, 'session_refs']['tasks', taskId, 'commit_refs']['tasks', taskId, 'cross_doc_links'](array ofDocLinkSchema)['tasks', taskId, 'subtasks', subtaskId, 'description'](subtaskId is the integer id)['tasks', taskId, 'subtasks', subtaskId, 'testStrategy']['tasks', taskId, 'subtasks', subtaskId, 'details']['tasks', taskId, 'subtasks', subtaskId, 'status']['tasks', taskId, 'subtasks', subtaskId, 'dependencies']
(Equivalent paths for Roadmap and Backlog per their schemas.)
§5.2 Patch application algorithm
Section titled “§5.2 Patch application algorithm”applyPatch(canonical, patches, baseMtime): current_mtime = stat(canonicalPath).mtime if current_mtime > baseMtime: return { ok: false, error: 'mtime mismatch — ledger changed underneath you', currentMtime }
for patch in patches: walk canonical along patch.fieldPath last segment: set to patch.newValue (replacement, not merge) if walk fails or final mutation is not type-compatible: return { ok: false, error: 'patch path invalid', detail }
parsed = schema.parse(canonical) // throws on validation failure serialized = JSON.stringify(parsed, null, 2) // 2-space indent matches existing ledgers atomicWrite(canonicalPath, serialized) regenerateMirrors(parsed, mirrorDir) // PRODUCT inv 38 return { ok: true, newMtime: stat(canonicalPath).mtime }schema.parse is the chosen schema’s .parse(...) (TaskListSchema /
RoadmapSchema / BacklogSchema). Validation failure surfaces the Zod
ZodError to the client (inv 29 — error inline near textarea, textarea
preserved open).
§5.3 Atomic write-to-temp + rename (PRODUCT inv 36)
Section titled “§5.3 Atomic write-to-temp + rename (PRODUCT inv 36)”The PRODUCT spec dropped the “write-to-temp” implementation language at Checker review (per S57 nit-fix). TECH names the implementation:
async function atomicWrite(path: string, content: string): Promise<void> { const tmp = `${path}.tmp.${process.pid}.${Date.now()}`; await Bun.write(tmp, content); await fs.promises.rename(tmp, path); // POSIX-atomic on same FS}POSIX rename(2) is atomic on the same filesystem. macOS APFS, Linux ext4
- XFS + btrfs, Windows NTFS via Bun’s polyfill all satisfy this. Cross-filesystem renames are explicitly out of scope (the tmp lives next to the target, same FS by construction).
On rename failure: the tmp file is deleted; the canonical file
unchanged. Server returns 5xx + the underlying error.
§5.4 mtime collision detection (PRODUCT inv 37)
Section titled “§5.4 mtime collision detection (PRODUCT inv 37)”baseMtime is the file mtime the viewer read when it loaded the ledger.
Sent with every PATCH request. Server stats the canonical file’s
current mtime before patching:
- If
current_mtime <= baseMtime: proceed (the viewer’s view is fresh enough; safe to patch). - If
current_mtime > baseMtime: reject with 409 Conflict + the latest mtime + a hint that the user should reload. The viewer per inv 37 displays the “Reload from disk” button + preserves the textarea content in localStorage.
The mtime check happens before the patch is applied, not after, so the patch never lands on a stale base.
§5.5 Multi-field save (PRODUCT inv 38)
Section titled “§5.5 Multi-field save (PRODUCT inv 38)”Multi-field saves are submitted as a single PATCH with multiple
FieldPatch entries. All patches must apply or all are rolled back:
multiPatch(canonical, patches, baseMtime): snapshot = structuredClone(canonical) for patch in patches: apply(snapshot, patch) parsed = schema.parse(snapshot) // single validation pass serialize + atomicWrite (single I/O) regenerateMirrors (single pass) return { ok: true }Mirror regeneration happens once at the end per inv 38, not once per field.
§5.6 Tier 2.2 hook interaction (CLAUDE.md)
Section titled “§5.6 Tier 2.2 hook interaction (CLAUDE.md)”The KH .claude/settings.json Tier 2.2 PreToolUse hook (ID-19.3, S57)
blocks Write/Edit/MultiEdit to absolute /Users/liamj/Documents/development/knowledge-hub*
paths from CWDs that are not a prefix of the target file_path. This
matters because:
- The
task-viewpatch server runs as a Bun process. It writes viaBun.write+fs.rename, NOT via the Claude Code Write tool. The hook fires only on Write/Edit/MultiEdit tool calls. Patch server writes are therefore safe by construction. - When a sub-agent invokes
/task-viewvia the plugin slash command, the sub-agent invokestask-viewas a subprocess (Bun spawn). The subprocess inherits the sub-agent’s CWD. Writes happen from the subprocess via its own I/O — no Claude tool interception, no hook fire. - The only Write/Edit tool calls in the
task-viewlifecycle are hypothetical follow-on Claude calls (sub-agent edits something after the user closes the browser). Those follow CWD-respecting discipline per the existing hook.
§5.7 Concurrency: file locking
Section titled “§5.7 Concurrency: file locking”The tool is single-user local-machine per PRODUCT inv 44 — no remote clients. Multiple browser tabs against the same ledger ARE possible (user opens two tabs to compare records). The mtime check in §5.4 covers the cross-tab race: tab A saves at T1, tab B’s baseMtime is T0, tab B rejected. No filesystem lock needed because the cross-tab race is the same as the cross-process race.
The case where the user runs task-view <path> twice in two terminals
against the same canonical JSON is identical: each instance has its
own loaded baseMtime; the second to save loses. This is the desired
behaviour per inv 37.
§5.8 Loopback-only enforcement (PRODUCT inv 44)
Section titled “§5.8 Loopback-only enforcement (PRODUCT inv 44)”Bun.serve({ port, hostname: '127.0.0.1' }). The hostname: '127.0.0.1'
explicit bind rejects any 0.0.0.0 binding. CORS not relevant because
all requests are same-origin (localhost:PORT → localhost:PORT).
§6 Plugin + CLI dual-invocation packaging
Section titled “§6 Plugin + CLI dual-invocation packaging”§6.1 CLI binary (bin/task-view.js)
Section titled “§6.1 CLI binary (bin/task-view.js)”Shape (per upstream Plannotator bin/plannotator.js pattern):
#!/usr/bin/env bunimport { startTaskViewServer } from '@task-view/server/ledger';import { openBrowser } from '@task-view/server/browser';import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({ args: process.argv.slice(2), options: { 'no-browser': { type: 'boolean' }, 'port': { type: 'string' }, 'check': { type: 'boolean' }, }, allowPositionals: true,});
const path = positionals[0] ?? await inferPathFromCwd();if (!path) { /* friendly "no ledger found" + exit 1 */ }
if (values.check) { await runRegenCheck(path); // exits non-zero on drift; for CI per inv 42}
const server = await startTaskViewServer({ path, port: values.port });if (!values['no-browser']) { await openBrowser(server.url);}console.log(`Server ready at ${server.url} — close the tab to exit`);await server.waitForExit();CLI is invokable as task-view (when installed to $PATH) or
bunx task-view (one-shot) or via bun /path/to/task-view/bin/task-view.js.
§6.2 Plugin entry point (.claude-plugin/plugin.json)
Section titled “§6.2 Plugin entry point (.claude-plugin/plugin.json)”Plugin manifest shape (per Plannotator upstream + Claude plugin convention):
{ "name": "task-view", "version": "0.1.0", "description": "Render and edit KH workflow ledgers (task-list / roadmap / backlog) in the browser.", "commands": [ { "name": "/task-view", "description": "Launch task-view against the current project's ledger.", "script": "node_modules/.bin/task-view" // or fork-distributed binary path } ]}Slash command invocation in any Claude Code session:
/task-view [optional-path-to-ledger.json]Launches the server, opens the browser, prints the readiness message, waits for browser close per PRODUCT inv 41.
§6.3 Both entry points share one server
Section titled “§6.3 Both entry points share one server”CLI and plugin both call startTaskViewServer(...). Same Bun process
shape. Same routes. The plugin manifest invokes the CLI binary; the CLI
binary calls into @task-view/server/ledger. There’s no separate
“plugin server” — the plugin is a thin manifest pointing at the binary.
§6.4 --check flag for CI usage (PRODUCT inv 42)
Section titled “§6.4 --check flag for CI usage (PRODUCT inv 42)”task-view --check <path> runs one-shot:
- Read canonical JSON.
- Parse via §2.1 detectSchema.
- Generate mirrors (idempotently per §3.4).
- Compare against existing mirrors on disk.
- Exit 0 if no drift; exit non-zero with diff summary if drift.
KH CI integrates this in .github/workflows/ci.yml as a separate
job-or-step: task-view --check docs/reference/task-list.json (+ the
two siblings). On drift, CI fails and the developer regenerates mirrors
locally. This is the KH-side glue, not the tool itself.
§6.5 Browser-close detection (PRODUCT inv 50)
Section titled “§6.5 Browser-close detection (PRODUCT inv 50)”Per the upstream Plannotator pattern but adjusted: the server tracks
last-request timestamp. If now - last_request > 30 seconds AND at least
one request has been served (so a slow browser start doesn’t kill the
server before it loads), process.exit(0). Threshold is a constant in
packages/server/ledger.ts, not user-configurable per inv 50.
§6.6 Port retry policy (PRODUCT inv 49)
Section titled “§6.6 Port retry policy (PRODUCT inv 49)”The upstream MAX_RETRIES = 5 in annotate.ts:87 carries forward
unchanged. On port collision, retry with port + 1 (or another random
port — Bun’s behaviour) up to 5 times, then exit with “could not bind”
per inv 49.
§6.7 $PATH install convention
Section titled “§6.7 $PATH install convention”The tool publishes a release tarball + npm package. Install routes:
bunx task-view(one-shot, no install).bun install -g task-view→~/.bun/install/global/...+ symlink to$PATH.- Claude plugin marketplace:
claude plugins install task-view→ installs to~/.claude/plugins/task-view/+ auto-discovers slash command.
The repo’s README.md documents all three paths.
§7 Cross-project packaging (this tool MUST NOT live inside knowledge-hub/)
Section titled “§7 Cross-project packaging (this tool MUST NOT live inside knowledge-hub/)”§7.1 Repository-external rule (PRODUCT inv 1, 3, 45, 46)
Section titled “§7.1 Repository-external rule (PRODUCT inv 1, 3, 45, 46)”task-view is a workflow developer tool consumed by KH (and potentially
other projects). It must NOT live inside the knowledge-hub repository
because:
- It’s not Knowledge Hub product code (per CLAUDE.md project overview: “Knowledge Hub is a knowledge base platform” — task-view is workflow tooling for any KH-like project).
- KH’s
package.json,tsconfig.json, ESLint config, and CI workflows are KH-product-tuned (Vitest, Next.js routes, Supabase client, MCP tools). Bundling task-view inside KH would either pollute the KH build or sit in a workspaces sub-package — both have downsides (pollution = slower KH builds; sub-package = mixed-purpose repo). - PRODUCT inv 45 + 46 require cross-project tool independence — works against any project’s ledgers without modification. A KH-internal tool can’t satisfy that requirement honestly.
- The fork+rename Plannotator base is already cross-project — placing the rename inside KH would re-couple it.
§7.2 Where the tool lives
Section titled “§7.2 Where the tool lives”| Component | Location |
|---|---|
| Repo root | git@github.com:liam-jons/task-view.git (new GitHub repo, separate from knowledge-hub) |
| Source | Liam’s ~/Documents/development/task-view/ working tree (renamed from ~/Documents/development/plannotator/) |
| Binary | bin/task-view.js in repo; symlinked to $PATH via global install |
| Plugin | Same repo at .claude-plugin/plugin.json; installed via Claude plugin marketplace |
| Vendored schemas | packages/schemas/src/{task-list,roadmap,backlog,work-status}-schema.ts |
| KH consumption | ~/.claude/plugins/task-view/ (auto-installed on plugin install) OR $PATH (CLI) — no KH code references this path. |
The Plannotator fork directory at
/Users/liamj/Documents/development/plannotator is renamed to
/Users/liamj/Documents/development/task-view. Git remote points at the
new GitHub repo. Upstream is unset (permanent divergence per PRODUCT inv 1).
§7.3 What lives in KH (the glue)
Section titled “§7.3 What lives in KH (the glue)”KH carries zero code for task-view. The only KH-side artefacts are:
- The three ledger JSON files (already there:
docs/reference/task-list.json,docs/reference/product-roadmap.json,docs/reference/product-backlog.json). - Mirror output files (new:
docs/reference/tasks/*.md,docs/reference/roadmap/*.md,docs/reference/backlog/*.md). - A CI job in
.github/workflows/ci.ymlinvokingtask-view --check(per §6.4 + §3.5). - A schema-drift guard CI workflow
task-view-vendor-drift.yml(per §3.5). - (Optional) A line in CLAUDE.md
Commandstable documentingtask-viewinvocation pattern for new agents.
Per PRODUCT inv 46, when ID-22 ships and the ledgers move to
.planning/task-management/, the only changes are: (a) ledger paths in
the CI commands, and (b) the canonical JSON paths in agent dispatches.
The tool itself receives no changes.
§7.4 Tool dependencies (none of them are KH)
Section titled “§7.4 Tool dependencies (none of them are KH)”task-view depends on:
bunruntime (>=1.0).zod(latest 3.x).- Vendored copies of the four KH schema files (per §1.5).
- Plannotator upstream’s React + Tailwind + highlight.js + mermaid +
graphviz-wasm + react-dom + react (all already declared in
packages/ui/package.json). - Cross-platform Node
fs.promises.renameshim (Bun supports this natively).
No Supabase. No Next.js. No Vercel. No @/lib/... import paths. The tool
ships standalone.
§7.5 Why not a workspaces sub-package in KH
Section titled “§7.5 Why not a workspaces sub-package in KH”Even if Liam decided to host task-view inside the KH monorepo, the
ergonomics break:
- KH’s
bun installwould pull task-view deps (React + Vite + a different highlight.js version + mermaid) into KH’s lockfile, slowing all KH builds. - KH’s ESLint config carries product-specific rules (
local/no-unchecked-supabase-error,local/no-silent-promise-catch) that don’t apply to a generic Plannotator fork. - KH’s CI runs 7 jobs in parallel against the KH source tree. Adding task-view source would add build/test load with zero KH-side benefit.
- The
task-viewrename loses meaning when bundled inside KH (knowledge-hub/apps/task-view/...is confusing).
A separate repo is the right boundary. The schema-drift CI guard makes the vendoring traceable.
Testing and validation
Section titled “Testing and validation”This section maps every numbered PRODUCT invariant to a concrete test or
verification step. The mapping is one-to-one per the Checker
acceptance criterion. Tests live in the task-view repo’s tests/
directory (Bun-test fixtures); KH-side validation lives in KH’s CI per
§3.5 + §6.4.
Mapping table — PRODUCT invariants → TECH proposed changes / tests
Section titled “Mapping table — PRODUCT invariants → TECH proposed changes / tests”| PRODUCT inv | TECH § | Test / verification location |
|---|---|---|
| 1 (fork identity, permanent divergence) | §1.1, §1.4 (rename map) | Manual: GitHub repo exists; no upstream remote set; LICENSE inherits MIT+Apache. |
| 2 (plugin + CLI dual entry, same server) | §6.1, §6.2, §6.3 | tests/integration/dual-entry.test.ts — boot via CLI and via plugin invocation; assert both serve identical routes. |
| 3 (zero KH runtime deps; schemas vendored) | §1.5 vendoring strategy | tests/unit/schemas.test.ts — vendored schemas parse representative ledger JSON without lib/validation/ imports. |
| 4 (document_name routing; Backlog non-literal) | §2.1 detectSchema function | tests/unit/route-detection.test.ts — all 3 known values route correctly; non-literal Backlog matched on value not field shape; unknown document_name exits non-zero. |
| 5 (mirror generation, sibling dir, idempotent, orphan delete) | §3.1, §3.3, §3.4 | tests/unit/mirror-idempotency.test.ts — repeat regen produces byte-identical output; orphan removal verified by deleting a Task and re-running. |
| 6 (record-level path resolves to parent) | §2.2 record-level resolution | tests/unit/path-resolution.test.ts — invoke with mirror path; assert parent ledger loaded + record preselected. |
| 7 (Task-list mode: frontmatter + description + Subtasks + nav strip) | §3.3 Task-list mirror shape, §4.1 RecordFrontmatterCard | tests/integration/task-list-render.test.ts — render a Task; assert presence of frontmatter table rows + description + Subtask headings + nav strip elements. |
| 8 (Subtask block: small frontmatter + description + testStrategy + details + journal-block styling) | §3.3 Task-list Subtask shape, §4.2 row “Subtask block list” | tests/integration/subtask-block.test.ts — render a Subtask; assert journal blocks visually distinct (CSS class assertion). |
9 (empty Subtasks → _No subtasks._) | §4.2 row “Empty Subtasks” | tests/integration/empty-subtasks.test.ts — fixture Task with subtasks: []. |
| 10 (CommonMark + GFM + diagrams floor; parser kept) | §1.3 retained parser, §4.1 BlockRenderer kept | tests/unit/parser.test.ts — already present in upstream; re-run intact. |
| 11 (cross-doc-link rendering + broken-target marker) | §4.5 broken-target marker | tests/integration/cross-doc-links.test.ts — link to existing path renders; link to missing path renders with marker. |
| 12 (dependency links + missing-target marker + page-top warning) | §4.5 broken-target marker | tests/integration/dependency-links.test.ts — Task with deps including one missing id; assert warning banner + link styling. |
| 13 (per-Subtask sibling-only deps → in-page anchor) | §4.4 in-page anchors | tests/integration/sibling-subtask-deps.test.ts — Task with Subtask deps; assert anchor scrolls to sibling block. |
| 14 (Roadmap index page lists sections with ID/Title/Owner/Item count) | §4.3 index page, §4.2 row “Section index” | tests/integration/roadmap-index.test.ts. |
| 15 (per-section page: frontmatter + narrative + spec_links + items table) | §3.3 Roadmap mirror shape, §4.1 RecordFrontmatterCard | tests/integration/roadmap-section.test.ts. |
| 16 (per-item page: frontmatter + description + dependency cross-refs) | §3.3 Roadmap item shape, §4.2 row “Inter-record dependency links” | tests/integration/roadmap-item.test.ts. |
17 (empty section → _No items in this section._) | §4.2 row “Empty section” | tests/integration/empty-section.test.ts. |
| 18 (Roadmap owner inheritance qualifier) | §4.6 owner inheritance display | tests/integration/owner-inheritance.test.ts — item with null owner; assert qualifier text rendered. |
| 19 (Roadmap forward_looking_only respected; no shipped UI) | §4.2 row “forward_looking_only honoured” | tests/integration/no-shipped-ui.test.ts — UI has no “shipped” affordance. |
| 20 (Backlog index page: columns ID/Description/Type/Status/Priority/Track/Effort, sorted) | §4.3 index page | tests/integration/backlog-index.test.ts. |
| 21 (Backlog per-item: frontmatter + description + notes + details + testStrategy) | §3.3 Backlog mirror shape | tests/integration/backlog-item.test.ts. |
| 22 (Backlog dependencies → inline links + missing-target marker) | §4.5 broken-target marker | tests/integration/backlog-deps.test.ts. |
| 23 (Backlog index filter dropdowns Track / Status / Priority + URL query string) | §4.3 index page URL paths | tests/integration/backlog-filters.test.ts — set filters; assert URL query string reflects state. |
| 24 (Promotion-ready badge when details OR testStrategy present) | §4.2 row “Promotion-ready badge”, §4.1 RecordFrontmatterCard | tests/integration/promotion-badge.test.ts. |
| 25 (Backlog blocked banner) | §4.2 row “Blocked banner” | tests/integration/blocked-banner.test.ts. |
| 26 (every free-text field editable via pencil affordance) | §4.1 RecordFrontmatterCard + editable description blocks | tests/integration/edit-affordances.test.ts — click pencil; assert textarea appears. |
| 27 (textarea autosized + pre-populated + Save/Cancel + Cmd+Enter/Esc) | §4.1 textarea wiring | tests/integration/textarea-controls.test.ts — keyboard shortcuts assert. |
| 28 (Subtask.details journal blocks editable as part of textarea, no auto-injection) | §3.3 Task-list mirror shape (full raw details), §5.1 patch path | tests/integration/details-edit.test.ts — edit details with journal; assert raw string preserved with edit applied. |
| 29 (structured patch wire format, schema validation on save, inline error with textarea preserved) | §5.1 endpoint surface, §5.2 patch application | tests/integration/save-error.test.ts — submit invalid value; assert ZodError surfaced; textarea still open. |
| 30 (Zod-enum dropdowns; nullable shows “(unset)”) | §4.1 RecordFrontmatterCard dropdown wiring | tests/integration/enum-dropdown.test.ts — all Zod enum values present; nullable shows “(unset)“. |
| 31 (dropdown populates from canonical Zod at render; legacy aliases excluded) | §1.5 vendored schemas | tests/integration/dropdown-schema-source.test.ts — assert dropdown options match TaskListStatus._def.values exactly. |
| 32 (no state-machine enforcement in dropdown) | §4.1 dropdown wiring, OQ-4 default | tests/integration/no-transition-enforcement.test.ts — flip pending → done direct; succeeds. |
| 33 (enum-edit save discipline matches free-text) | §5.2 patch application | tests/integration/enum-save.test.ts — patch with enum field; schema validation succeeds; mirror regenerates. |
| 34 (cross-ref array fields → comma-separated textarea editor) | §5.1 endpoint dependencies/session_refs/commit_refs paths | tests/integration/array-edit.test.ts — edit comma-separated; assert array parsing trims whitespace; rejects malformed entries (Subtask cross-Task dep). |
| 35 (cross_doc_links per-entry form) | §4.1 RecordFrontmatterCard cross_doc_links control | tests/integration/cross-doc-links-edit.test.ts — add/edit/delete DocLink entries via form. |
| 36 (atomic write: tmp + rename) | §5.3 atomic write | tests/unit/atomic-write.test.ts — simulate crash mid-write; assert canonical file integrity. |
| 37 (mtime collision detection + reload button + localStorage draft preserved) | §5.4 mtime check, §6 PRODUCT integration of localStorage | tests/integration/mtime-collision.test.ts — modify file underneath; assert 409 + reload button + draft preservation. |
| 38 (multi-field save: single PATCH, mirror regen once) | §5.5 multi-field save | tests/integration/multi-field-save.test.ts. |
| 39 (external regen via project Stop hook + CI guard; tool doesn’t watch FS) | §3.5 schema-drift CI guard, §6.4 —check flag | tests/integration/external-regen.test.ts — out-of-band edit; reload reads fresh canonical. |
| 40 (viewer tolerates mirror absence; generates on the fly) | §3.4 first-run regeneration | tests/integration/first-run.test.ts — empty mirror dir on launch; assert mirrors created. |
| 41 (plugin slash command launches + opens browser + prints readiness; waits for close) | §6.2 plugin manifest, §6.3 shared server | tests/integration/plugin-launch.test.ts. |
42 (CLI binary same; --no-browser, --port, --check flags) | §6.1 CLI binary, §6.4 --check flag | tests/integration/cli-flags.test.ts. |
| 43 (no-path invocation scans CWD for known document_name; numbered list / friendly miss) | §2.3 working-directory inference | tests/integration/cwd-scan.test.ts. |
| 44 (loopback-only on 127.0.0.1) | §5.8 loopback-only enforcement | tests/unit/loopback-bind.test.ts — assert serve bound to 127.0.0.1; remote bind fails. |
| 45 (same binary works against any project’s conformant ledgers) | §1.5 vendored schemas, §7.2 location | tests/integration/cross-project.test.ts — run against a fixture project’s ledgers; assert correct render. |
| 46 (ID-22 ledger-relocation requires no viewer change) | §3.1 mirror layout (sibling dir of canonical), §7.3 KH glue | Manual: re-run after ID-22 moves ledgers to .planning/task-management/; assert no tool changes needed. |
| 47 (empty ledger: empty-state page, no creation flow) | §4.3 index page, §3.4 generator | tests/integration/empty-ledger.test.ts. |
| 48 (schema validation fail on load: formatted ZodError + exit non-zero) | §2.1 detectSchema, §5.2 patch validation | tests/integration/schema-fail-load.test.ts. |
| 49 (port collision: retry 5x then “could not bind” error) | §6.6 port retry policy | tests/integration/port-retry.test.ts. |
| 50 (browser-close detection: 30s idle + at-least-one-request → exit) | §6.5 browser-close detection | tests/integration/browser-close.test.ts. |
| 51 (failed save: textarea content in localStorage; cleared on success) | §5.1 PATCH error handling + browser localStorage wiring | tests/integration/localstorage-drafts.test.ts. |
| 52 (cross-platform: macOS/Linux/Windows-WSL2; URL paths forward-slash) | §1.3 retained browser.ts cross-platform helper | tests/unit/path-separators.test.ts. |
| 53 (Plannotator keyboard nav inherited; Tab/Enter/Esc/Cmd+Enter only) | §1.3 retained KeyboardShortcuts (simplified) | tests/integration/keyboard-nav.test.ts. |
| 54 (text-only dropdowns; no colour-coded badges in base; future styling via own design system) | §1.3 retained ThemeProvider disconnected from KH tokens | Manual: visual inspection; assert plain text rendering. |
| 55 (UK English in UI; DD/MM/YYYY display for tool-formatted dates; canonical ISO 8601 preserved in writes) | §3.3 mirror shape (raw ISO preserved); §4.1 RecordFrontmatterCard date display | tests/integration/uk-locale.test.ts — UI strings; date formatting. |
Coverage verification: all PRODUCT invariants 1-55 mapped. Every test
is colocated with the proposed change it verifies; the test list above is
the implementation Subtask testStrategy source per PRODUCT inv 38
promotion convention.
Existing parser tests inherited from Plannotator
Section titled “Existing parser tests inherited from Plannotator”The fork inherits packages/ui/utils/parser.test.ts,
packages/ui/components/InlineMarkdown.test.ts, diagramLanguages.test.ts,
shortcuts.test.ts. These continue to pass post-strip; they verify the
markdown rendering floor that inv 10 depends on.
End-to-end flow — Liam opens a Task in task-view
Section titled “End-to-end flow — Liam opens a Task in task-view”sequenceDiagram participant Liam participant CC as Claude Code participant CLI as task-view CLI (Bun) participant Server as task-view server participant FS as Filesystem participant Browser
Liam->>CC: /task-view docs/reference/task-list.json CC->>CLI: spawn `task-view docs/reference/task-list.json` CLI->>FS: read task-list.json FS-->>CLI: JSON bytes CLI->>CLI: detectSchema → kind: task-list CLI->>CLI: TaskListSchema.parse(JSON) CLI->>FS: read docs/reference/tasks/ existing mirrors CLI->>FS: write missing/changed mirrors (atomic) CLI->>FS: delete orphan mirrors CLI->>Server: startTaskViewServer({ ledger, mirrorDir }) Server->>Server: Bun.serve on 127.0.0.1:RANDOM CLI->>Browser: openBrowser('http://localhost:PORT/?record=ID-20') Browser->>Server: GET / Server-->>Browser: HTML shell + SPA bundle Browser->>Server: GET /api/ledger Server-->>Browser: { kind: 'task-list', data: TaskList, mirrorDir, mtime } Browser->>Browser: render Task ID-20 frontmatter + Subtasks Liam->>Browser: edits Subtask 20.4 status pending → in_progress Browser->>Server: PATCH /api/ledger/record/20 { patches: [{ fieldPath: ['tasks', '20', 'subtasks', '4', 'status'], newValue: 'in_progress' }], baseMtime } Server->>FS: stat task-list.json (mtime check) FS-->>Server: current mtime == baseMtime Server->>Server: apply patch in-memory Server->>Server: TaskListSchema.parse(patched) Server->>FS: write task-list.json.tmp Server->>FS: rename .tmp → task-list.json (atomic) Server->>FS: regen mirrors (atomic per-file) Server-->>Browser: 200 OK { newMtime } Browser->>Browser: re-render frontmatter row Liam->>Browser: closes tab Server->>Server: detect idle 30s → exit 0 CLI-->>CC: process exits cleanlyRisks and mitigations
Section titled “Risks and mitigations”| Risk | Mitigation |
|---|---|
Schema drift between vendored copies and KH lib/validation/* | task-view-vendor-drift.yml CI guard in KH per §3.5; re-vendor is a documented manual step in CONTRIBUTING.md. |
| Plannotator upstream API breaks the strip ledger | Fork is permanently divergent (PRODUCT inv 1). No rebase commitment. Frozen at v0.19.18. |
| Patch lands on stale base (race with workflow agent) | mtime check before patch per §5.4; 409 Conflict + reload button + localStorage draft per inv 37. |
| Atomic write fails on cross-FS rename | Tmp file lives next to canonical (same FS by construction); fs.rename is POSIX-atomic on same FS. |
| Mirror regen on every save = many small writes | Mirrors are small (~5-20 KiB each); atomic-write cost is negligible; multi-field save regens once per submit (§5.5). |
| Tier 2.2 hook fires unexpectedly during patch | Patch server writes via Bun.write + fs.rename, NOT via Claude Write tool. Hook is irrelevant to subprocess I/O (§5.6). |
--check flag in CI fails sporadically due to mirror non-determinism | Deterministic key ordering enforced in generator; idempotency test in Testing-and-validation table inv 5 verifies. |
| User opens two tabs and one stale-loses without warning | Expected behaviour per inv 37 (mtime collision returns 409 + reload button). |
| Browser-close detection misses long-running sessions | 30s idle threshold + at-least-one-request gate; long-lived sessions stay open as long as user clicks/navigates (PRODUCT inv 50). |
Vendored schemas use Zod 3.x types that break under tool’s older zod install | task-view pins same Zod major as KH; CI guard catches version drift. |
Plugin install path conflicts with another plugin named task-view | New plugin in Claude marketplace; namespace collision check at publish time. |
Parallelization
Section titled “Parallelization”The implementation splits into chained slices that benefit from a single operator (Liam-supervised Claude session). Two slices can run in worktree-isolated parallel:
- Slice 1: Fork prep (§1.1-§1.5) — repo creation, rename, strip, vendor schemas. Standalone — no dependency on other slices.
- Slice 2: Generator + schema detection (§2 + §3) — pure logic, no UI dependency. Tested via Bun unit tests.
- Slice 3: Patch server (§5) — depends on Slice 2 for schema imports.
- Slice 4: Viewer rendering (§4) — depends on Slice 1 (stripped UI surface) + Slice 2 (mirror shape). Can begin in parallel with Slice 3 once Slice 1+2 land.
- Slice 5: CLI + plugin packaging (§6) — depends on Slice 3 + Slice 4.
- Slice 6: KH-side CI glue (§3.5 + §6.4) — independent; lives in KH
repo. Can land any time after
--checkflag exists.
Parallelization opportunity: Slices 3 and 4 can run in parallel worktrees
after Slices 1+2 merge. Use isolation: "worktree" per CLAUDE.md
“Parallel agent isolation” convention.
Follow-ups
Section titled “Follow-ups”| Item | Notes |
|---|---|
FU-1: Re-vendoring procedure documented in task-view/CONTRIBUTING.md | Document the exact cp commands + when to re-vendor (schema-drift CI warns; bump tool version). |
| FU-2: Plugin marketplace publish | Required for Claude /task-view slash command auto-discovery across machines. Out of scope for first release; manual install via claude plugins install path/to/task-view works initially. |
| FU-3: First non-KH consumer | When a non-KH project adopts the tool, verify cross-project invariants 45-46 end-to-end (vendored schemas, ID-22-relocation independence). |
| FU-4: Roadmap mode index sort policy | PRODUCT inv 14 doesn’t specify section sort order; default is canonical ledger order (matches sections[] array). If Liam later wants alpha or by-owner sort, surface as a viewer setting. |
| FU-5: Backlog filter persistence beyond URL | URL state covers same-tab bookmarking per inv 23. Cross-tab persistence (localStorage) is plausible enhancement but not in PRODUCT scope. |
| FU-6: Mirror-format documentation in tool’s README | The mirror shape (§3.3) is a stable contract; document for users editing mirrors out-of-band (though the tool doesn’t watch mirrors per inv 39). |
Open questions surfaced by this TECH
Section titled “Open questions surfaced by this TECH”Three OQs surfaced (within ≤3 ceiling per workflow convention):
OQ-T1 — Distribution channel for first releasable cut
Section titled “OQ-T1 — Distribution channel for first releasable cut”The tool needs a release route before KH CI can task-view --check.
Options:
- npm + Claude plugin marketplace: standard distribution. Requires Liam to set up npm publish (one-time) + Claude plugin marketplace publish.
- GitHub Release tarball only: simpler bootstrap; install via
bun install -g github:liam-jons/task-view. Defers npm + marketplace publish to FU-2.
Default proposal: GitHub Release tarball for v0.1.0; npm + marketplace publish before v0.2.0 (FU-2). Permits an initial KH integration before the formal release pipeline exists.
OQ-T2 — Schema bundle versioning + re-vendoring trigger
Section titled “OQ-T2 — Schema bundle versioning + re-vendoring trigger”When KH lib/validation/* evolves, the tool’s vendored copies need
re-vendoring. Triggers:
- Automatic CI bump: the schema-drift CI guard auto-opens a PR to the tool’s repo when KH lands schema changes. Higher engineering ceremony; requires a CI bot account.
- Manual annotated bump: the drift warning in KH’s CI surfaces in the PR description (“re-vendor task-view before next release”) and Liam triggers manually. Lower ceremony; depends on Liam reading PR descriptions.
Default proposal: manual annotated bump for now (lower ceremony, manageable cadence — KH schemas are stable). Reconsider automation when schema-change cadence justifies it.
OQ-T3 — Patch server logging policy
Section titled “OQ-T3 — Patch server logging policy”The patch server logs requests for debugging. Considerations:
- Patches contain ledger content (descriptions, journal blocks). Some ledger entries reference customer/internal information.
- Logs at INFO level would include request bodies on PATCH.
Default proposal: server emits DEBUG-level structured logs to stderr
(silent at default INFO threshold); the log line shape is { method, path, fieldPaths: string[], mtime, outcome } — fieldPaths only, NEVER
the patch values themselves. Patch-value contents stay out of logs. Set
TASK_VIEW_LOG=debug to enable full DEBUG. Logs do NOT write to disk;
stderr only (so plugin invocation output doesn’t accumulate).
End of TECH.md.