Ledger-mirror — PRODUCT
Ledger-mirror — PRODUCT
Section titled “Ledger-mirror — PRODUCT”Status: DRAFT (ID-20.3, kh-prod-readiness-S57). Authored by task-planner under
write-product-specper S57 dispatch. Numbered invariants are independently testable against the ratified Behaviour set.
Summary
Section titled “Summary”A cross-project tooling surface — provisionally named kh-ledger-viewer (see
inv 1) — that renders any of the three Knowledge Hub planning ledgers
(task-list.json, product-roadmap.json, product-backlog.json) as a human-friendly
view-and-edit interface in the browser. The tool generates per-record Markdown mirrors
on demand, serves them in a forked Plannotator-based viewer, lets Liam edit free-text
and structured enum fields in place, and writes every change back to the canonical
JSON ledger atomically. Mirrors regenerate automatically after any JSON edit. The tool
ships as a forked-and-renamed Plannotator standalone repository, installable as both a
Claude Code plugin (slash-command invocation) and a CLI binary (between-session
invocation). Cross-project by construction: the canonical ledgers stay in each consuming
project; the tool itself lives in its own repository.
Problem
Section titled “Problem”docs/reference/task-list.json (and the sibling roadmap + backlog ledgers) is the
canonical traceability surface for KH work. Reading and editing it today means loading
the whole JSON into an editor, hand-patching the right Task/Subtask/section/item, and
trusting that schema-required fields are preserved. Liam — the human-in-the-loop — wants
to (a) view a single Task / roadmap item / backlog item in a rendered form without
loading the entire ledger into context, and (b) make small, structured edits (status
flips, priority changes, description rewrites) from a browser UI rather than a JSON
text editor. The existing Plannotator fork already renders Markdown beautifully and has
the necessary UI primitives; it just lacks edit-back and structured-field-edit. The
solution is to fork Plannotator, strip the annotation-only paths, and add the three
missing capabilities targeted at all three ledger schemas.
- Render any one ledger record (Task with subtasks / Roadmap item or section /
Backlog item) as a navigable browser view from the canonical JSON via an
auto-generated per-record
.mdmirror. - Allow in-place edit of free-text fields (descriptions,
details,notes,narrative) via textarea controls. - Allow structured edit of enum fields (status, priority, type, owner where enum-shaped) via dropdown controls populated from the canonical Zod schema.
- Write every edit back to the canonical JSON ledger atomically; never leave the mirror as the source of truth.
- Ship as a single tool that auto-detects which ledger it has been pointed at by filename and applies the correct schema.
- Be installable in two ways: as a Claude Code plugin (in-session slash command) and as a standalone CLI binary (between-session use).
- Be cross-project: live in its own repository, no KH-specific code paths.
Non-goals
Section titled “Non-goals”- Bidirectional sync where the
.mdmirror can also be edited as a source. Mirrors are always derived; canonical writes always target the JSON. - Adding new fields, schemas, or status transitions to the ledgers. The tool is a view-and-edit surface for the existing data shapes only.
- Enforcing the Task / Roadmap / Backlog state machine (e.g. blocking a status
transition from
pendingstraight todone). All valid enum values are presented as available choices; transition discipline lives in workflow-orchestration, not in the editor. - Building the per-record mirror generator itself (covered by the wider ID-20 scope; this spec defines the viewer’s expectations of the mirror file shape).
- Concurrent multi-user editing, cross-device sync, or auth. The tool is a single-user local-machine surface.
- Annotation overlay (DELETION / COMMENT / GLOBAL_COMMENT) — Plannotator’s annotation capability is stripped during the fork.
- Relocating ledgers from
docs/reference/to.planning/task-management/. That is a downstream Task (ID-22) post-Phase A of ID-15; this spec targets the current ledger locations.
Open questions
Section titled “Open questions”Three remain for Liam ratification. All other Qs from RESEARCH 20.1 (Q1–Q8) and 20.2 (OQ-1, OQ-2, OQ-4, OQ-5, OQ-7) are pre-ratified per the S57 dispatch journal block.
- Open question (tool name):
kh-ledger-vieweris a working placeholder. It communicates “ledger” (covers all three) and “viewer” (the primary affordance). Candidates:ledger-mirror(active verb, emphasises bidirectional view + write),plan-ledger(positions against Plannotator lineage; broader),tm-mirror(Taskmaster-adjacent; KH-specific). Default proposal:ledger-mirror— drops the KH prefix because it’s cross-project, swaps “viewer” for “mirror” because the tool’s defining behaviour is bidirectional reflection between rendered view and canonical JSON. - Open question (mirror persistence): Per-record
.mdmirrors are generated artefacts. Two options for where they live: (a) committed alongside their canonical JSON ledger (e.g.docs/reference/tasks/ID-20.md) so they show up ingit diffand are browsable on GitHub; (b) generated on-demand into a tool-managed scratch dir (e.g.~/.cache/ledger-mirror/<project-slug>/) and never committed. Default proposal: option (a) for consistency with the 20.1 RESEARCH §2.4 recommendation (Stop hook regen + CI guard makes drift detectable). Inv 4–6 below assume option (a). - Open question (Backlog item / Roadmap section selection key): Task ledger
keys per record by integer Task id (
ID-N.md). Backlog items key by string id (30,45). Roadmap items key by dotted-decimal id (1.4,3.1.8). Filenames must be safe for filesystem use. Default proposal: use the rawidvalue as the filename stem with a 1:1 substitution of any filesystem-unsafe character to-. SoC2-PA5.md,3.1.8.md,ID-20.md. Inv 4–6 below assume this.
Figma: none provided — viewer styling inherits from the Plannotator base (stripped) and is intentionally minimalist. UI surfaces beyond the basic Viewer / FrontmatterCard / textarea / dropdown primitives are out of scope.
Behaviour
Section titled “Behaviour”Tool identity and packaging
Section titled “Tool identity and packaging”-
The tool ships as a forked-and-renamed copy of Plannotator v0.19.18 in its own git repository (separate from
knowledge-hub, separate from upstreamplannotator). The repository name and the binary name match each other and are settled at fork time per the “Open question (tool name)” above (working nameledger-mirror). No upstream Plannotator alignment is attempted; the fork is permanently divergent. -
The tool exposes two entry points that drive the same underlying server:
- Plugin entry point: a Claude Code plugin slash command (e.g.
/ledger-mirror <path-to-ledger.json>) invokable from any Claude Code session in any project. The plugin manifest lives at<repo>/.claude-plugin/plugin.jsonand is installable via the Claude plugin marketplace mechanism. - CLI entry point: a binary (e.g.
ledger-mirror <path-to-ledger.json>) installable to the user’s$PATHfor use outside Claude Code sessions. Both entry points launch the same local HTTP server on a random free port, open the browser, and exit when the user closes the browser tab or sends a termination signal.
[task-view fork divergence — annotated S268] The “closes the browser tab” exit path is REMOVED in the task-view fork (it depended on inv 50’s idle browser-close detection, also removed). Post-fork, the server exits only on an explicit termination signal (Ctrl-C / SIGTERM).
- Plugin entry point: a Claude Code plugin slash command (e.g.
-
The tool has zero KH-specific runtime dependencies. The canonical Zod schemas for the three ledgers (
TaskListSchema,RoadmapSchema,BacklogSchema) are vendored or imported via a published package; the tool does not reach intoknowledge-hub/lib/validation/at runtime. Any project that ships ledgers matching these schemas can use the tool without modification.
Ledger detection and routing
Section titled “Ledger detection and routing”-
When the tool is launched against a ledger path, it reads the JSON file once, parses
document_name, and routes to one of three rendering modes:"Knowledge Hub Task List"→ Task-list mode (per inv 7–13)."Knowledge Hub Roadmap"→ Roadmap mode (per inv 14–19)."Product Backlog"→ Backlog mode (per inv 20–25). UnlikeTaskListSchemaandRoadmapSchema,BacklogSchema.document_nameis a non-literalz.string().min(1)field; routing matches the known canonical value, not the schema-level field shape.- Any other
document_name, or unparseable JSON → the tool displays an error page identifying the failure (path attempted, error message) and exits with non-zero status. It does not attempt to write to or repair the ledger.
-
When ledger routing succeeds, the tool generates per-record
.mdmirrors into a sibling directory of the canonical JSON (per the “mirror persistence” OQ default —docs/reference/tasks/,docs/reference/roadmap/,docs/reference/backlog/). One file per Task / Roadmap item / Backlog item. Mirrors are byte-identical across regenerations from the same canonical input (idempotent). Orphan mirrors (records no longer present in the canonical JSON) are deleted on each regen. -
When the tool is given a record-level path (e.g.
docs/reference/tasks/ID-20.md) rather than a ledger-level path, it resolves the parent ledger by walking up one directory level for the sibling JSON file matching the schema, then loads the parent ledger as in inv 4 and pre-selects the named record in the viewer.
Task-list mode — view
Section titled “Task-list mode — view”-
In Task-list mode, the viewer presents a per-Task page rendered from each
ID-N.mdmirror. The page consists of:- A frontmatter table showing
Status,Priority,Effort estimate,Owner,Updated,Session refs(comma-separated),Commit refs(comma-separated linked to the project’s GitHub when applicable),Dependencies(linked to otherID-N.mdmirrors),Cross-doc links(linked to repo paths). - The Task
descriptionrendered as Markdown. - The optional
priority_noteandstatus_notestrings, each prefixed in bold (“Priority note:”, “Status note:”) and rendered as inline italic Markdown. - A
Subtaskssection listing each Subtask as a level-3 heading withID-{taskId}.{subtaskId}: {title}and a nested block (per inv 8). - A navigation strip at the top with previous-Task / next-Task links and a “Back to ledger index” link.
- A frontmatter table showing
-
Each Subtask block within a Task page renders:
- A small frontmatter table with
Status,Dependencies(linked to sibling Subtask blocks in the same page via in-page anchor),Updated. - The Subtask
descriptionas plain text. - The Subtask
testStrategyfield (one-line acceptance prose). - The Subtask
detailsfield rendered verbatim including any<info added on ...>journal blocks. Journal blocks display visually distinct from the pre-journaldetailscontent (rendered in a subtly indented block with a “Journal” label) but are otherwise readable in line.
- A small frontmatter table with
-
When a Task’s
subtasks[]array is empty, the Subtasks section renders the message_No subtasks._in italic. The Task page does not omit the section. -
When a Task or Subtask field contains Markdown (most commonly the
detailsfield), the renderer respects standard CommonMark + GFM (tables, task lists, inline code, fenced code blocks). Plannotator’s existing Markdown parser (packages/ui/utils/parser.tsin the upstream) is the floor for rendered capability; the fork does not strip parser features. -
Cross-doc-link rendering:
cross_doc_links[]entries render as Markdown links whosehrefis computed from the link’spathfield relative to the project root. Anchors (#section-idor§2.3) append after the path. Therawfield is preserved as the link text. Broken links (path not found at render time) still render but with a visually distinct “missing target” marker (e.g. strikethrough); the viewer never silently drops a link. -
Dependencies rendering:
Task.dependencies[]entries render as inline links to the relevantID-{depId}.mdmirror in the same directory. Broken dependencies (the referenced Task id is not present in the parent ledger) render in a visually distinct “missing target” state and surface a one-line warning at the top of the page. -
Per-Subtask
dependencies[](sibling-only perTaskSchema.superRefine) render as in-page anchor links to the corresponding sibling Subtask block. Renderer assumes the schema’s superRefine has held; if a stray cross-Task dep slips through (which would mean the canonical JSON failed validation), it renders with the same “missing target” marker as cross-Task dependencies in inv 12.
Roadmap mode — view
Section titled “Roadmap mode — view”-
In Roadmap mode, the viewer presents a per-section index plus per-item pages. The index page lists every Section (
§1,§3.1,§9.18, …) as a table with columnsID,Title,Owner,Item count. Each section title links to a per-section page. -
The per-section page renders:
- A frontmatter table with
ID,Parent ID,Number,Owner,Table columns(the section’s chosen ColumnSet — informational only; not editable), and a count of items. - The section
narrativerendered as Markdown (may be empty). - A list of
spec_links[]entries each as a rendered link (same broken- link discipline as inv 11). - A table of items with the columns appropriate to the section’s
table_columnsvalue. Each item row links to a per-item page.
- A frontmatter table with
-
The per-item page renders:
- A frontmatter table with
ID,Section ID(linked back to the section page),Phase label,Owner,Effort estimate,Priority,Priority note,Severity,Status,Status note,Session refs,Commit refs. Descriptionas Markdown.- Dependency cross-references (
depends_on[],blocks[],coordinates_with[]) each as their own list section, rendered as links to other items by id when the target resolves to an item, or as plain strings when the target is a free-form reference (e.g.§5).
- A frontmatter table with
-
Sections with no items render a friendly empty state (
_No items in this section._) rather than an empty table. -
Owner inheritance: when an item has
owner: null, the per-item page displays the parent Section’sownervalue with a small “(inherited from §{sectionId})” qualifier. When both are null, the field is shown as—. -
Roadmap
forward_looking_only: trueis honoured by the viewer: there is no UI affordance to add or display “shipped” framing. Iflast_updatedcontains shipped narrative (e.g. “Phase 4 closed S35”), it renders as plain text without special treatment.
Backlog mode — view
Section titled “Backlog mode — view”-
In Backlog mode, the viewer presents an index page listing every item with columns
ID,Description,Type,Status,Priority,Track,Effort. The list is sorted bytrack, thenstatus, thenid. Each row links to a per-item page. -
The per-item page renders:
- A frontmatter table with every Zod-schema field (id, type, status,
effort_estimate, priority, track, surfaced, dependencies, notes, plus
the optional
detailsandtestStrategywhen populated). - The item
descriptionas plain text. - The
notesfield as Markdown. - The optional
detailsandtestStrategyfields rendered when present (same shape as the Task-list Subtask details rendering — see inv 8 and inv 10).
- A frontmatter table with every Zod-schema field (id, type, status,
effort_estimate, priority, track, surfaced, dependencies, notes, plus
the optional
-
Dependencies render as inline links to other backlog item per-item pages. Broken dependencies (the referenced item id is not present in
BacklogSchema.items[]) render with the missing-target marker (inv 12). -
Filters: the index page exposes a top-bar filter row with dropdowns for
Track,Status, andPriority. Each dropdown is populated from the canonical Zod enum values for that field, plus an “All” option. Filter state is reflected in the URL query string so a filtered view is bookmarkable / shareable as a local file URL. -
When an item carries the optional
details/testStrategyfields (i.e. the item has been pre-thought into promotion-ready form perBacklogItemSchemainv 38), the per-item page shows a small “Promotion-ready” badge near the frontmatter table. The badge is purely informational; the viewer does not provide a promotion action. -
When the Backlog item’s status is
blocked, the per-item page surfaces a one-line “Blocked” banner at the top of the page (same visual treatment used for missing-dependency warnings in inv 12). Other status values do not get special banners.
Edit affordances — free text
Section titled “Edit affordances — free text”-
Every free-text field surfaced in the viewer is editable in place by clicking a small pencil affordance next to the rendered value. The fields covered:
- Task-list:
Task.description,Task.priority_note,Task.status_note,Subtask.description,Subtask.testStrategy,Subtask.details. - Roadmap:
Section.narrative,RoadmapItem.description,RoadmapItem.priority_note,RoadmapItem.status_note. - Backlog:
BacklogItem.description,BacklogItem.notes,BacklogItem.details,BacklogItem.testStrategy.
- Task-list:
-
Clicking the pencil replaces the rendered Markdown block with an autosized
<textarea>pre-populated with the current raw Markdown source. A Save button commits the edit; a Cancel button discards and restores the rendered view without round-tripping. Pressing Cmd/Ctrl+Enter inside the textarea is equivalent to clicking Save; pressing Esc is equivalent to Cancel. -
Free-text edits to
Subtask.details(Task-list) andBacklogItem.details(Backlog) operate on the entire raw string including any<info added on ...>journal blocks. The journal blocks render visually distinct from pre-journal content while editing (per inv 8), but the editor presents them as part of the same textarea — Liam may freely move or delete journal blocks. No special gating, locking, or auto-timestamp-injection happens during a Liam-initiated edit. Workflow agents continue to append new journal blocks programmatically via their own write paths (the editor never injects a new journal block on Save). -
Save discipline for free-text edits: when the user clicks Save, the edited string is sent to the server as a structured patch describing
{ledger, recordId, fieldPath, newValue}rather than as a re-serialised JSON document. The server applies the patch, validates the result against the canonical Zod schema for that ledger, and writes the JSON file atomically on success. On schema-validation failure, the server returns the Zod error to the browser; the viewer displays the error inline next to the textarea and leaves the textarea open with the user’s unsaved text preserved.
Edit affordances — structured enum fields
Section titled “Edit affordances — structured enum fields”-
Every Zod-enum field surfaced in the viewer is editable as a dropdown rather than free text. The fields covered:
- Task-list:
Task.status(8 values fromTaskListStatus),Task.priority(8 values fromPriority),Subtask.status(5 values fromSubtaskStatus). - Roadmap:
RoadmapItem.status(6 values fromRoadmapStatus, nullable — null appears as “(unset)” option),RoadmapItem.priority(8 values fromRoadmapPriority, nullable). - Backlog:
BacklogItem.status(5 values fromBacklogStatus),BacklogItem.priority(8 values fromPriority),BacklogItem.type(8 values fromBacklogItemType).
- Task-list:
-
The dropdown populates from the canonical Zod enum values at render time — not from a hard-coded list in the viewer. When a schema is updated (new enum value added), the dropdown picks the new value up automatically on next render with no viewer code change. The dropdown does not include legacy aliases (e.g.
needs_specfor Backlogstatus); only canonical values are presented. -
The dropdown does NOT enforce state-machine transitions. Every valid enum value is presented as a selectable option regardless of the current value. For example, a Task with
status: pendingmay be flipped directly tostatus: donevia the dropdown even though workflow-orchestration discourages such a jump. Transition discipline lives in the orchestration layer, not the editor. -
Save discipline for enum edits matches inv 29: the change is sent as a structured patch, validated against the schema server-side, and written atomically. The viewer re-renders the affected frontmatter row on success.
Edit affordances — array fields
Section titled “Edit affordances — array fields”-
The four cross-reference array fields render as comma-separated link lists in view mode (per inv 7, 12, 13, 16) and as a textarea of comma-separated id strings in edit mode. Saving parses the comma-separated list, trims whitespace, drops empty entries, and writes the result back as a JSON array. Schema validation rejects malformed entries (e.g. an id with embedded JSON-breaking characters).
- Task-list:
Task.dependencies[],Task.session_refs[],Task.commit_refs[].Subtask.dependencies[](sibling-only — validation rejects cross-Task references per the schema’s superRefine). - Roadmap:
RoadmapItem.depends_on[],RoadmapItem.blocks[],RoadmapItem.coordinates_with[],RoadmapItem.session_refs[],RoadmapItem.commit_refs[]. - Backlog:
BacklogItem.dependencies[].
- Task-list:
-
The
cross_doc_links[]field across Task-list and Roadmap (structured {path, anchor, raw} objects) is editable in a small per-entry form rather than a comma-separated list. Each entry haspath,anchor,rawtext inputs; an “Add link” button creates a new blank row; a delete affordance removes a row. Save persists the resulting array asDocLinkSchema[].
Save discipline and concurrency
Section titled “Save discipline and concurrency”-
All writes to the canonical JSON ledger are atomic: the server reads the full JSON, applies the requested patch in-memory, validates the result against the canonical Zod schema, and on success commits the change to disk such that a crashed write never produces a partial file.
-
The server detects concurrent ledger writes (e.g. a workflow agent edits
task-list.jsonwhile the viewer has the same ledger open) by stat’ing the file’smtimebefore each patch. If themtimehas advanced since the viewer last loaded the file, the save is rejected with a “ledger changed underneath you” error. The viewer displays the error, offers a “Reload from disk” button that re-fetches the canonical JSON and re-renders all per-record mirrors, and preserves the user’s unsaved edit content in a local-storage draft so they can re-apply after reload. -
After every successful save, the server re-emits the affected per-record Markdown mirror(s) so the rendered view stays in lockstep with the JSON. Multi-field saves (multiple patches submitted in a single Save click — e.g. status + priority changed together) regenerate the mirror once at the end, not once per field.
Mirror regeneration outside the viewer
Section titled “Mirror regeneration outside the viewer”-
When the canonical JSON ledger is edited outside the viewer (workflow agent, manual edit, git operation, etc.), the per-record
.mdmirrors become stale. Regeneration in those cases is handled by a separate project-side mechanism (per ID-20.4 TECH design — Stop hook on JSON edit + CI guard). The viewer itself does not watch the filesystem; eachkh-ledger-viewerinvocation re-reads the canonical JSON and regenerates mirrors fresh. -
The viewer is robust to mirror absence: when launched against a ledger that has no per-record mirrors yet (first run, post-clean checkout, etc.), it generates them on the fly and continues. It does not require pre-existing mirror files.
Invocation and routing
Section titled “Invocation and routing”-
Plugin invocation: the slash command
/ledger-mirror [path](final command name TBC per OQ-1) launches the server, openshttp://localhost:<port>/in the user’s browser, and printsServer ready at http://localhost:<port> — close the tab to exitto the Claude Code session. The session waits for server termination before returning control. -
CLI invocation: the binary
ledger-mirror [path]does the same as the plugin entry point. Optional flags:--no-browser(do not auto-open; print the URL only),--port <N>(override random port),--check(run a one-shot mirror-regeneration sanity pass and exit non-zero on drift; intended for CI use). -
When invoked with no path argument, both entry points search the current working directory for any file matching the three known
document_nameliterals from inv 4. If exactly one is found, it is opened. If multiple are found, the tool prints a numbered list and exits; the user re-invokes with the chosen path. If none are found, the tool exits with a friendly “no ledger found” message naming the three canonical document names it recognises. -
The server listens only on
127.0.0.1(loopback). No remote access path exists. Authentication is intentionally absent — security model is “trusted local machine” because the tool ships as a developer tool.
Cross-project consistency
Section titled “Cross-project consistency”-
The same
ledger-mirrorbinary works against any project’s ledgers provided the JSON files conform to the canonical Zod schemas. The tool does not assume any particular path (docs/reference/,.planning/task-management/, anywhere else) — it accepts an absolute or project-relative path to the JSON. -
When the Knowledge Hub project executes the ID-22 ledger-relocation Task (move ledgers from
docs/reference/to.planning/task-management/), the viewer requires no changes:kh-ledger-viewer .planning/task-management/task-list.jsonworks exactly the same as the pre-relocation invocation. Per-record mirror directories follow the canonical JSON’s location.
Edge cases and error states
Section titled “Edge cases and error states”-
Empty ledger: a JSON file with
tasks: []/items: []/sections: []renders an empty-state page naming the ledger type and offering no edit actions (the schema does not define record-creation flows). Liam adds records via the canonical creation path (workflow-curator skill or manual JSON edit); the viewer is not a record-creation surface. -
Schema validation failure on load: when the canonical JSON fails Zod parse, the tool displays the formatted Zod error and exits with non-zero status. It does not attempt to partially render the JSON. Recovery is “fix the JSON and re-launch”.
-
Server port collision: if the chosen random port is already in use, the server retries with a fresh random port up to 5 times before exiting with a “could not bind” error.
-
Browser-close detection: the server polls for browser disconnection (no requests for >30 seconds after at least one successful request) and self-terminates so the invoking CLI / plugin session returns control. The threshold and polling cadence are not user-configurable; they are defined in the tool’s own configuration file.
[task-view fork divergence — annotated S268] REMOVED in the task-view fork. Static SSR pages issue no keepalive request, so the idle timer tore the server down ~30s after any load. The fork’s server now runs until explicitly stopped (Ctrl-C / SIGTERM) — a permanent-fork divergence (inv 1). Ref: task-view
packages/server/ledger.tsheader + commit8b0d163; inverted regression testpackages/server/ledger.test.tsasserts the server does NOT self-exit when idle. (Cosmetic stale JSDoc still references “§6.5 browser-close idle detection” inledger.ts— non-behavioural.) -
Patch failure with unsaved edits: when a save fails for any reason (schema validation, mtime mismatch, server crash, network error), the user’s textarea content is preserved in browser local-storage keyed by
{ledgerPath, recordId, fieldPath}so a subsequent reload of the same record re-populates the textarea. Local-storage drafts are cleared on successful save of the same{ledgerPath, recordId, fieldPath}triple. -
Cross-platform: the tool runs on macOS, Linux, and Windows (WSL2). The Bun runtime requirement is the only system-level dependency. File paths in mirror generation and write-back use OS-native separators; URL paths in rendered links use forward slashes regardless of OS.
Accessibility and theming
Section titled “Accessibility and theming”-
The viewer inherits Plannotator’s existing keyboard navigation: Tab cycles between focusable elements, Enter activates buttons, Esc cancels editing. No new keyboard shortcuts are introduced beyond Cmd/Ctrl+Enter for Save (inv 27).
-
Status, priority, and type values render as text in dropdowns. The viewer does not introduce colour-coded badges in the base implementation; if Liam later adds them, the implementation must respect Warm Meridian semantic tokens (via the tool’s own design system, not via importing KH’s
app/globals.css— the tool is project-agnostic). For now, plain text is the contract. -
UK English throughout the viewer UI (“colour”, “organisation”, “behaviour”; DD/MM/YYYY for any rendered timestamps that the viewer reformats — the canonical ISO 8601 strings in the JSON are preserved verbatim in writes, but display formatting uses DD/MM/YYYY).