Skip to content

task-view — TECH

Status: DRAFT (ID-20.4, kh-prod-readiness-S58). Authored by task-planner under write-tech-spec per 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.

  • PRODUCT spec: ./PRODUCT.md — 55 numbered Behaviour invariants (inv 1 through inv 55). Note: PRODUCT working draft uses the placeholder ledger-mirror throughout. The Liam-ratified final name is task-view. This TECH uses task-view exclusively; 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/plannotator v0.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 — master WorkStatus enum + per-surface .exclude(...) derivations + Priority master enum). TECH refers to these as the schema set this build vendors at fork time.

PRODUCT and RESEARCH use working placeholders. Liam ratified at S58 dispatch:

SurfaceWorking placeholder (PRODUCT)Ratified final (this TECH)
Tool nameledger-mirrortask-view
CLI binaryledger-mirrortask-view
Plugin slash command/ledger-mirror/task-view
RepositoryTBDtask-view (new GitHub repo)
Plugin manifest dir.claude-plugin/plugin.jsonsame (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.


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 FrontmatterCard becomes an editable RecordFrontmatterCard with 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)”
FileRoleFork action
apps/hook/server/index.tsBun 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:153Bun.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.tsAnnotation 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.tsxAnnotation 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.tsgetRepoInfo for GitHub repo display in viewer header.KEEP — drives the Commit refs GitHub linking per inv 7.
packages/server/browser.tsopenBrowser cross-platform helper (macOS/Linux/Windows/WSL).KEEP — matches inv 52 cross-platform requirement.
apps/hook/package.json bin fieldAlready 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).

FileRoleNotes
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.jsonCanonical Roadmap ledger.Same access pattern.
docs/reference/product-backlog.jsonCanonical Backlog ledger.Same access pattern.
lib/validation/task-list-schema.tsKH 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.tsKH Zod schema for Roadmap.Same vendoring rule.
lib/validation/backlog-schema.tsKH Zod schema for Backlog.Same vendoring rule.
lib/validation/work-status.tsMaster WorkStatus enum + .exclude(...) derivations + Priority master enum.Same vendoring rule. The four files form one vendored bundle.
.claude/settings.json PreToolUse Write|Edit|MultiEdit matcherTier 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 + TableBlock give 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.serve random-port retry, openBrowser cross-platform helper, and getRepoInfo GitHub linker are all directly reusable.
  • Plannotator’s Frontmatter parser 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. Create new GitHub repo task-view (account: Liam’s personal account; no ai-solution-hub org coupling since this is a cross-project tool).
  2. Bootstrap from a clean clone of upstream backnotprop/plannotator at commit pinned for v0.19.18 (use existing fork at /Users/liamj/Documents/development/plannotator as the source).
  3. First commit: rename top-level identifiers (per §1.4 below).
  4. Second commit: apply strip ledger (per §1.2 below).
  5. Third commit: install vendored Zod schemas (per §1.5 below).
  6. Fourth commit: wire the new RecordFrontmatterCard + patch-server endpoint.
  7. Tag v0.1.0-task-view as 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.

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 deleted apps/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 of remote.ts unused), code-nav.ts, config.ts, draft.ts (re-purposed), shared handlers. Delete annotate.ts (replaced by ledger.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 to task-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).

Components and modules retained:

  • packages/ui/components/Viewer.tsx — primary rendering shell. Lines 140-630 retained minus annotation pipeline; specifically FrontmatterCard (lines 110-138) replaced by RecordFrontmatterCard (see §4 below); the scroll viewport hook retained for inv 53 keyboard nav.
  • packages/ui/components/BlockRenderer.tsx — dispatches Block types 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.tsopenBrowser() cross-platform per inv 52.
  • packages/server/repo.tsgetRepoInfo() for GitHub linking per inv 7.
  • packages/server/draft.ts — content-hash draft logic; ported to browser- local-storage per inv 51.
Upstream identifierTask-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/
startAnnotateServerstartTaskViewServer
AnnotateServerOptionsTaskViewServerOptions
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)

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, WorkStatus master enum + per-surface .exclude(...) derivations + Priority master)

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.


The tool routes by document_name value at load time per PRODUCT inv 4. The three known values:

document_name literalSchema discriminatorRenderer mode
"Knowledge Hub Task List"TaskListSchemaTask-list mode (inv 7-13)
"Knowledge Hub Roadmap"RoadmapSchemaRoadmap 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.

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):

  1. If the given path ends with .md, ascend one directory level.
  2. Look for sibling JSON files with document_name literals matching one of the three known values (Knowledge Hub Task List, Knowledge Hub Roadmap, Product Backlog).
  3. If exactly one match, load that JSON, route via §2.1, then look up the record whose id matches the mirror’s filename stem (with inverse - → original character substitution per §3.2 record-id rule).
  4. 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.

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)”

PRODUCT inv 5: mirrors land in a sibling directory of the canonical JSON, named to match the document type:

Canonical JSONMirror 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 idFilename
"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}.md for items; section-${section.id}.md for section narrative pages.
  • Backlog mode: filename = ${item.id}.md.

Discriminating prefixes are documented in the tool’s README + applied deterministically.

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: task
id: "20"
title: Per-Task .md mirror generator + render surface
status: in_progress
priority: must
effort_estimate: ~6-10h across 20.5+ implementation subtasks
owner: Engineering
updated: 2026-05-20T23:15:00.000Z
session_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.md
priority_note: Liam promoted at S55 close — workflow visibility now a recurring friction point
status_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-item
id: "3.1.8"
section_id: "3.1"
title: ...
phase_label: null
priority: high
priority_note: null
severity: null
status: pending
status_note: null
owner: null
effort_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-item
id: "30"
description: ...
type_field: feature
status: ready
effort_estimate: ...
priority: should
track: ...
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:

  1. Generate the full set of mirror files for the current ledger.
  2. List existing files in the mirror directory.
  3. Delete any mirror whose record no longer exists in the canonical JSON.
  4. 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-drift runs on PRs touching lib/validation/{task-list-schema,roadmap-schema,backlog-schema,work-status}.ts.
  • The job fetches the tagged task-view release vendor bundle (packages/schemas/src/*.ts) and diffs.
  • On drift, the job warns (does NOT block); PR description must reference whether the consuming task-view needs 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.yml if 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)”

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 via getRepoInfo()), Dependencies (comma-separated text input → array; rendered as inline links to ID-${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 details or testStrategy is present per inv 24. Blocked banner per inv 25.
BehaviourTask-list mode (inv)Roadmap mode (inv)Backlog mode (inv)
Frontmatter table (mode-specific fields)715, 1621
Subtask block list per Task7-8n/an/a
Empty-Subtasks empty state (_No subtasks._)9n/an/a
Empty section empty state (_No items in this section._)n/a17n/a
CommonMark + GFM + diagrams render floor101010
Cross-doc-link rendering + broken-target marker111111 (via inv 21 details/notes)
Inter-record dependency links + broken-target1216 (depends_on / blocks / coordinates_with)22
In-page anchor for sibling-Subtask deps13n/an/a
Section index pagen/a1420 (item index)
Per-item pagen/a1621
Owner inheritance qualifiern/a18n/a
forward_looking_only honoured (no shipped-framing)n/a19n/a
Filter bar (Track / Status / Priority)n/an/a23
Promotion-ready badgen/an/a24
Blocked bannern/an/a25
Navigation strip (prev/next/back-to-index)7 (last bullet)14 (back link)20 (back link)

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

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)”
EndpointMethodBody shapeResponse
/api/ledgerGETn/a{ kind, data, mirrorDir, mtime }
/api/ledger/record/:recordIdGETn/a{ record, mirror, mtime }
/api/ledger/record/:recordIdPATCH{ patches: FieldPatch[], baseMtime: string }{ ok: true, newMtime } | { ok: false, error }
/api/ledger/regenPOST{ 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 of DocLinkSchema)
  • ['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.)

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.

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-view patch server runs as a Bun process. It writes via Bun.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-view via the plugin slash command, the sub-agent invokes task-view as 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-view lifecycle 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.

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:PORTlocalhost:PORT).


§6 Plugin + CLI dual-invocation packaging

Section titled “§6 Plugin + CLI dual-invocation packaging”

Shape (per upstream Plannotator bin/plannotator.js pattern):

#!/usr/bin/env bun
import { 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.

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:

  1. Read canonical JSON.
  2. Parse via §2.1 detectSchema.
  3. Generate mirrors (idempotently per §3.4).
  4. Compare against existing mirrors on disk.
  5. 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.

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.

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:

  1. 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).
  2. 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).
  3. 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.
  4. The fork+rename Plannotator base is already cross-project — placing the rename inside KH would re-couple it.
ComponentLocation
Repo rootgit@github.com:liam-jons/task-view.git (new GitHub repo, separate from knowledge-hub)
SourceLiam’s ~/Documents/development/task-view/ working tree (renamed from ~/Documents/development/plannotator/)
Binarybin/task-view.js in repo; symlinked to $PATH via global install
PluginSame repo at .claude-plugin/plugin.json; installed via Claude plugin marketplace
Vendored schemaspackages/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).

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.yml invoking task-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 Commands table documenting task-view invocation 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:

  • bun runtime (>=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.rename shim (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 install would 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-view rename 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.


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 invTECH §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.3tests/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 strategytests/unit/schemas.test.ts — vendored schemas parse representative ledger JSON without lib/validation/ imports.
4 (document_name routing; Backlog non-literal)§2.1 detectSchema functiontests/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.4tests/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 resolutiontests/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 RecordFrontmatterCardtests/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 kepttests/unit/parser.test.ts — already present in upstream; re-run intact.
11 (cross-doc-link rendering + broken-target marker)§4.5 broken-target markertests/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 markertests/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 anchorstests/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 RecordFrontmatterCardtests/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 displaytests/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 pagetests/integration/backlog-index.test.ts.
21 (Backlog per-item: frontmatter + description + notes + details + testStrategy)§3.3 Backlog mirror shapetests/integration/backlog-item.test.ts.
22 (Backlog dependencies → inline links + missing-target marker)§4.5 broken-target markertests/integration/backlog-deps.test.ts.
23 (Backlog index filter dropdowns Track / Status / Priority + URL query string)§4.3 index page URL pathstests/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 RecordFrontmatterCardtests/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 blockstests/integration/edit-affordances.test.ts — click pencil; assert textarea appears.
27 (textarea autosized + pre-populated + Save/Cancel + Cmd+Enter/Esc)§4.1 textarea wiringtests/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 pathtests/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 applicationtests/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 wiringtests/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 schemastests/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 defaulttests/integration/no-transition-enforcement.test.ts — flip pending → done direct; succeeds.
33 (enum-edit save discipline matches free-text)§5.2 patch applicationtests/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 pathstests/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 controltests/integration/cross-doc-links-edit.test.ts — add/edit/delete DocLink entries via form.
36 (atomic write: tmp + rename)§5.3 atomic writetests/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 localStoragetests/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 savetests/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 flagtests/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 regenerationtests/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 servertests/integration/plugin-launch.test.ts.
42 (CLI binary same; --no-browser, --port, --check flags)§6.1 CLI binary, §6.4 --check flagtests/integration/cli-flags.test.ts.
43 (no-path invocation scans CWD for known document_name; numbered list / friendly miss)§2.3 working-directory inferencetests/integration/cwd-scan.test.ts.
44 (loopback-only on 127.0.0.1)§5.8 loopback-only enforcementtests/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 locationtests/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 glueManual: 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 generatortests/integration/empty-ledger.test.ts.
48 (schema validation fail on load: formatted ZodError + exit non-zero)§2.1 detectSchema, §5.2 patch validationtests/integration/schema-fail-load.test.ts.
49 (port collision: retry 5x then “could not bind” error)§6.6 port retry policytests/integration/port-retry.test.ts.
50 (browser-close detection: 30s idle + at-least-one-request → exit)§6.5 browser-close detectiontests/integration/browser-close.test.ts.
51 (failed save: textarea content in localStorage; cleared on success)§5.1 PATCH error handling + browser localStorage wiringtests/integration/localstorage-drafts.test.ts.
52 (cross-platform: macOS/Linux/Windows-WSL2; URL paths forward-slash)§1.3 retained browser.ts cross-platform helpertests/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 tokensManual: 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 displaytests/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 cleanly

RiskMitigation
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 ledgerFork 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 renameTmp file lives next to canonical (same FS by construction); fs.rename is POSIX-atomic on same FS.
Mirror regen on every save = many small writesMirrors 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 patchPatch 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-determinismDeterministic key ordering enforced in generator; idempotency test in Testing-and-validation table inv 5 verifies.
User opens two tabs and one stale-loses without warningExpected behaviour per inv 37 (mtime collision returns 409 + reload button).
Browser-close detection misses long-running sessions30s 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 installtask-view pins same Zod major as KH; CI guard catches version drift.
Plugin install path conflicts with another plugin named task-viewNew plugin in Claude marketplace; namespace collision check at publish time.

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


ItemNotes
FU-1: Re-vendoring procedure documented in task-view/CONTRIBUTING.mdDocument the exact cp commands + when to re-vendor (schema-drift CI warns; bump tool version).
FU-2: Plugin marketplace publishRequired 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 consumerWhen 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 policyPRODUCT 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 URLURL 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 READMEThe 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).

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.

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.