Skip to content

Canonical-pipeline → task-list migration (Linear-style umbrella/tag model) — TECH

Canonical-pipeline → task-list migration (Linear-style umbrella/tag model) — TECH

Section titled “Canonical-pipeline → task-list migration (Linear-style umbrella/tag model) — TECH”

Spec status header Slug: canonical-pipeline-task-list-migration Version: v1.0 (draft, S64 W2b) Last verified: 22/05/2026 Linked PRODUCT: docs/specs/id-31-canonical-pipeline-task-list-migration/PRODUCT.md Authored by: task-planner under write-tech-spec, dispatched per ID-31.3 brief. Fresh Planner instance per Q-PLANNER-2 / B4 — independent review pass over PRODUCT.md by a Planner who did not author PRODUCT.md. Status: DRAFT — awaits Checker promotion of Subtask 31.3 in_progress → done, then dispatch of Subtask 31.4 PLAN.md.


The Knowledge Hub Task ledger (docs/reference/task-list.json) is a single-document ledger conforming to a strict Zod schema (lib/validation/task-list-schema.ts, 254L). It encodes a flat tasks[] array with two-level nesting (Task → Subtask). There is no umbrella entity anywhere in the schema, the file system, or runtime tooling.

ModuleLinesPurpose
lib/validation/task-list-schema.ts254SubtaskSchema + TaskSchema + TaskListSchema (root) + parseTaskListWithWarnings() helper for 25-Subtask soft-ceiling.
lib/validation/backlog-schema.ts180Pattern mirror — same idioms, strict mode, single root document.
lib/validation/roadmap-schema.ts259Pattern mirror — also exports DocLinkSchema (re-used by Task cross_doc_links[]).
lib/validation/work-status.tsn/aTaskListStatus enum (done, pending, in_progress, blocked, deferred, cancelled, spec_needed, imp_deferred). Subtask subset drops cancelled / spec_needed / imp_deferred.
__tests__/validation/task-list-schema.test.tsn/aExisting Vitest unit coverage of TaskListSchema.
__tests__/validation/backlog-schema-roundtrip.test.tsn/aRound-trip pattern reference — reads live JSON, parses via schema, asserts no warnings.
__tests__/validation/roadmap-roundtrip.test.tsn/aSame pattern, roadmap surface.

1.2 Existing runtime tooling that touches the ledgers

Section titled “1.2 Existing runtime tooling that touches the ledgers”
Skill / agentReadsWrites
.claude/skills/update-roadmap-backlog/SKILL.md (448L)product-backlog.json, product-roadmap.json, task-list.jsonproduct-backlog.json, product-roadmap.json, task-list.json (via Promote mode at line 364+). Four operation modes: Create, Update, Delete, Promote.
.claude/skills/workflow-orchestration/SKILL.mdAll three ledgerstask-list.json status transitions (Orchestrator-owned per §6.3 of docs/plans/phase-0-investigation/kh-sdlc-workflow.md).
.claude/agents/workflow-curator.mdAll three ledgersAll three ledgers (via the skill above).

1.3 T1-T14 mapping snapshot (per RESEARCH §3, ratified S62 W3)

Section titled “1.3 T1-T14 mapping snapshot (per RESEARCH §3, ratified S62 W3)”

The canonical-pipeline plan in docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md has 14 enumerated Tasks T1-T14. Post-OQ-1-ALT ratification, T0 is added as a retrospective umbrella Task. Current status as of S64 W0c (read from docs/reference/task-list.json HEAD + cross-branch read of origin/content-items-investigation):

TnProposed Task IDStatus todayShipping evidence
T0next-available (≥36)not yet openedWill land via this migration. Subtask granularity 4-8 per shipping cadence (Inv 11).
T1next-available + 1not yet opened (retro done at open)Commits 2f142936, ddebada1, f324fe93, a89440f1, c8fd217b (S242, content-items-investigation branch).
T2next-available + 2not yet opened (retro done at open)WP2a commits f6a13f4e, c8cf4e8f (S245); T2 SQL 38242fef (S246); prod-apply 2f98c8cf (S247).
T3next-available + 3not yet opened (retro done at open)Migration 20260514150238_enable_rls_auto_event_trigger_and_grants_pattern.sql (S239, verified S243).
T4next-available + 4not yet opened (retro done at open)10 commits dc1d7850..776d3a73 (S248); ID-23 followup 95b660ef (S249).
T5next-available + 5not yet opened (retro done at open)S248 commit 44ff65c7 + S251 W1B 8-commit Phase A-G chain (c84631f8..45317dc6).
T6next-available + 6not yet opened (retro done at open)Migrations 20260520225456_t6_q_a_pairs_full_schema.sql (2f3428c7), 20260520231524_t6_q_a_search_rpcs.sql (6614d8c3), 20260521095209_t6_followup_revoke_public_execute_anon_inherit_fix.sql (b3a4f792); integration test 275bc9e5.
T7future-JIT (per Inv 6)not openedGated on T8.
T8ID-28 anchored (per Inv 5)in_progress on content-items-investigation; 28.1 done S252 (58162181), 28.2-28.4 pendingSpec chain authored on content-items-investigation per cross-branch read.
T9-T14future-JIT (per Inv 6)not openedAwaiting JIT triggers per OQ-3 ratification.

ID-space audit (cross-branch read, performed at TECH-authoring time):

  • origin/main: max ID = 23.
  • origin/content-items-investigation: max ID = 29 (adds 28, 29).
  • origin/production-readiness (this branch): max ID = 35 (adds 24-27 + spec migration Tasks 30-35 opened S62 W2-W4 + S64 W0c).
  • Highest occupied across branches: 35. Next-available cascade for T0-T6 starts at ID-36.

PRODUCT.md prescribes 17 invariants spanning ledger schema (Inv 7-9), retrospective journal-block discipline (Inv 11-12), cross-branch ID coordination (Inv 10), cascade resolution rules (Inv 3-4), JIT forward-Task discipline (Inv 6), and PLAN.md ↔ Task description sync (Inv 13-14). Five P-OQs surfaced for ratification (P-OQ-1..5). Each invariant needs a concrete mechanism — a file, a schema, a function, a test path, or a documented discipline-with-checkpoint — before the migration can be decomposed into implementation Subtasks (the {31.4} PLAN.md scope).


§2 Proposed changes (one-to-one mapping against PRODUCT invariants)

Section titled “§2 Proposed changes (one-to-one mapping against PRODUCT invariants)”

Seventeen rows, one per PRODUCT invariant. Each row carries Proposed change / Mechanism / Test verification.

#Inv (PRODUCT.md)Proposed changeMechanismTest verification
1Inv 1 — Retro umbrella Task T0 (4-8 Subtasks, done at open, citing pre-phase substrate).Open one Task (id = next-available, ≥36) with title T0 — Phase 0.9 investigation substrate (architecture sub-docs + drift audit + ratification arc), status done, 4-8 Subtasks each done with journal blocks per Inv 11-12.Append a new entry under task-list.json#/tasks[] via Promote-shape (see §3.3) or manual edit committed at retro-opening. Subtask details carries the journal block per Inv 12.New test __tests__/docs/canonical-pipeline-t0-presence.test.ts: asserts a Task exists with title prefix T0 — Phase 0.9 investigation and status === 'done' and subtasks.length >= 4 and subtasks.length <= 8 and every Subtask status === 'done'.
2Inv 2 — Six retro Tasks T1-T6 opened done with shipping-cadence Subtasks.Open six Tasks (ids T0+1 through T0+6) with titles T{n} — <topic>, slim ≤200-word description, cross_doc_links referencing PLAN.md §4.n, populated commit_refs[] + session_refs[], Subtasks per shipping cadence (Inv 11) with journal blocks (Inv 12).Six append operations on task-list.json#/tasks[] — one commit per Task per P-OQ-5 default.New test __tests__/docs/canonical-pipeline-t1-t6-presence.test.ts: for each title prefix T{n} — (n ∈ {1..6}), asserts status === 'done', description.length <= 1500 chars (≤200 words ≈ ≤1500 chars), commit_refs[].length > 0, cross_doc_links[] includes one entry with path === 'docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md' and section === '§4.n'.
3Inv 3 — Final Task ID assignment locks at PLAN dispatch, not PRODUCT.TECH.md specifies the resolution algorithm (§4); the PLAN dispatch ({31.4}) reads cross-branch task-list.json snapshots at PLAN-dispatch time, computes the cascade, writes the final ID mapping table into 31.4 PLAN.md output. PRODUCT.md stays ID-agnostic — only references T0..T14 symbols.No code/schema change required. The cascade algorithm (§4) is procedural — runs at {31.4} PLAN dispatch and again at retro-Task-open time as a sanity check.No automated test. Manual gate at {31.4} PLAN dispatch: the PLAN.md output table must list T0-T14 → ID-N mapping; the Checker verifies the table against a fresh git show origin/<branch>:docs/reference/task-list.json MAX-ID query.
4Inv 4 — Cascade documentation invariant (PRODUCT.md documents the shape, not specific IDs).TECH §4 carries the resolution algorithm. PLAN.md output ({31.4}) carries the resolved table.Algorithm specified once in §4 below. Re-runnable each time a new T-Task is opened.Tested as part of Inv 3 — manual gate.
5Inv 5 — T8 = ID-28 anchor; cross-branch collision resolved S62 W4 commit 9e498e2f.The cascade algorithm in §4 hard-codes T8 → ID-28 as a fixed point. T1-T6 cascade around it: T0 = MAX-ID + 1, T1-T6 = T0+1..T0+6, T7 = T0+7 (skipping ID-28 in the cascade), T9-T14 = T0+8 onwards (also skipping ID-28).§4 algorithm.New test __tests__/docs/canonical-pipeline-t8-anchor.test.ts: if a Task with title prefix T8 — exists on any active branch, asserts its id === "28" exactly. Skip gracefully if T8 not yet opened on the branch under test (gated test).
6Inv 6 — Forward Tasks T7 + T9-T14 open just-in-time, not upfront.This Task does NOT open T7 or T9-T14. The migration land asserts (via the gated test in row 5) that the absence is intentional. The orchestrator opens each forward Task only when becoming the next-up dispatch.No code change. Procedural discipline encoded in §6 below + invoked at each future Task open.No automated test. Inverse-gated test in __tests__/docs/canonical-pipeline-forward-jit.test.ts: if a Task with title prefix T7 — or T9 —T14 — exists, asserts subtasks.length >= 1 AND subtasks[0].title starts with RESEARCH or PRODUCT (i.e. spec-chain seeded at open, not empty pending placeholder).
7Inv 7 — umbrellas.json exists with prescribed top-level shape (document_name, document_purpose, last_updated, related_documents, umbrellas[]).New file docs/reference/umbrellas.json authored to schema. New Zod schema lib/validation/umbrellas-schema.ts mirroring roadmap-schema.ts / backlog-schema.ts strict-mode idioms (see §3 below).Author the file once at first commit. The schema enforces the top-level shape via .strict() mode. last_updated re-uses the same kh-{prod-readiness|main}-S\d+ regex + ≤200-char cap + single-line discipline as the other ledgers.New test __tests__/validation/umbrellas-schema.test.ts (unit) + __tests__/validation/umbrellas-roundtrip.test.ts (round-trip — reads live docs/reference/umbrellas.json, parses, asserts no schema errors).
8Inv 8 — Each umbrellas[] entry has fields id, title, substrate_doc, task_ids[], status, phase. Many-to-many (Task may appear in multiple umbrellas).UmbrellaEntrySchema Zod object with strict mode. id is kebab-case string regex; title is min(1) string; substrate_doc is min(1) string (relative path); task_ids is z.array(z.string().regex(BARE_ID_REGEX)) (same regex as Task.id); status is enum; phase is min(1) string.See §3.2 below for the schema definition. Many-to-many is the natural consequence of task_ids being a curated list — no schema rejection if a Task id appears in two umbrellas.Unit test in umbrellas-schema.test.ts: valid entry per each enum value (5 status states × 1 phase example), invalid status rejected, invalid id-shape rejected, multi-membership accepted (one Task id in two umbrella entries → both parse).
9Inv 9 — umbrellas.json round-trips with task-list.json: every id in any task_ids[] references a real Task.Integration test reads both docs/reference/umbrellas.json and docs/reference/task-list.json, builds the set of real Task ids, asserts every umbrellas[].task_ids[] member is in that set.Test fails build (Vitest red) on broken reference. Orphan Tasks (in task-list, not in any umbrella) emit a console.warn log but do not fail the test — P-OQ-2 default (zero memberships allowed but flagged as soft warning).New test __tests__/docs/umbrellas-task-list-roundtrip.test.ts: parses both JSON files via their schemas; for each umbrellas[].task_ids[] entry, asserts presence in tasks[]; collects orphan Tasks; emits console.warn listing orphan ids; passes if zero broken references regardless of orphan count.
10Inv 10 — Orchestrator assigns NEW Task IDs by querying all active branches; rename on collision uses chore(s{NN}-w{N}): rename ID-{old} → ID-{new} pattern.Document the discipline inside .claude/skills/workflow-orchestration/SKILL.md (new sub-section “Task ID assignment: cross-branch MAX-ID discipline”). No CI guard per OQ-5 ratification. The S62 W4 ID-28 rename (9e498e2f) is the worked example.Skill body edit only — no code or schema change.No automated test. Audited by Liam-eye on each PR that opens a new Task.
11Inv 11 — Retrospective Subtasks match actual shipping cadence (1-3 per WP if single-session, per-session-WP if multi-session).Procedural discipline at retro-Task-open time. Documented inside this TECH.md §6 (migration sequencing) + cross-referenced from {31.4} PLAN.md.Documentation-only; the orchestrator hand-decomposes each retro Task per the discipline.No automated test. Gated by the {31.5+} implementation Subtask Checker — Checker verifies Subtask count against PLAN.md §4.n shipping-cadence summary.
12Inv 12 — Each retro Subtask carries <info added on YYYY-MM-DDTHH:MM:SS.sssZ> journal block in details (opening sentence + Commits block + migration refs + PLAN.md §4.n acceptance close + follow-up flags).New helper lib/validation/umbrellas-helpers.ts exports formatRetrospectiveJournalBlock(input: RetrospectiveOpeningInput): string returning the formatted journal text. Used by the orchestrator at retro-Task open. Pattern: see §5 below.Helper signature and template specified in §5. The orchestrator pastes the helper output into the Subtask details at open time.New test __tests__/validation/umbrellas-helpers.test.ts: asserts helper output starts with <info added on , contains RETROSPECTIVE OPENING, contains Commits (S{NN}, contains PLAN.md §4.{n} acceptance criteria all met., ends with </info added on . Round-trips a fixture input.
13Inv 13 — PLAN.md stays as canonical substrate; Task descriptions reference via cross_doc_links[] as { "type": "spec_substrate", "path": "...", "section": "§4.n" }.Re-use existing DocLinkSchema from roadmap-schema.ts (already imported by task-list-schema.ts). The type value "spec_substrate" is a new convention — verify it’s already permitted by DocLinkSchema (it should be, since DocLinkSchema.type is z.string() in current shape; verify at impl time).No schema change if DocLinkSchema.type accepts free strings; otherwise add "spec_substrate" to its enum. Audit happens in {31.4} PLAN dispatch.New test __tests__/docs/canonical-pipeline-cross-doc-links.test.ts: for each retro Task (T0-T6), asserts at least one cross_doc_links[] entry with type === 'spec_substrate' and path === 'docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md'.
14Inv 14 — PLAN.md backlinks added manually at Task-open time; absence does not break round-trip.Procedural discipline; no automation. The orchestrator MAY append See Task ID-NN to PLAN.md §4.n header text at retro-Task-open time (single batch commit acceptable for T0-T6).Documentation-only.No automated test. Round-trip test in row 9 (umbrellas-task-list-roundtrip.test.ts) verifies the umbrellas → task-list direction; the inverse (Task → PLAN.md backlink) is NOT tested — explicit per Inv 14 spec text.
15Inv 15 — UK English throughout umbrellas.json, retro journal blocks, Task descriptions.Standard KH discipline — same lint convention as other ledgers. No new test infrastructure.Standard editorial discipline. UK words: “organisation”, “colour”, “behaviour”. DD/MM/YYYY date format.Existing project-wide guardrails (no new test). Checker verifies during {31.5+} per-Subtask review.
16Inv 16 — Status-machine compliance for retro Tasks (done at open is the explicit exception).The schema accepts done as a Task-level status (it’s in TaskListStatus enum). The discipline gate is that retro Tasks are the ONLY case where a Task opens directly in done (skipping pending → in_progress).Procedural — no schema change. Documented as the explicit exception in §6 below + cross-referenced from kh-sdlc-workflow.md §3.5 state machine.No automated test. Manual orchestrator gate at each retro-Task open.
17Inv 17 — umbrellas.json parses against the TECH-spec’d Zod schema; orchestrator updates task-list.json + umbrellas.json in the same commit when affecting umbrella membership.Schema in §3 below. Commit-coupling discipline documented in §6 migration sequencing + cross-referenced from update-roadmap-backlog SKILL.md when its Promote mode is extended to know about umbrellas (Inv 17 + §6.4).The schema enforces the file is well-formed. Commit-coupling is procedural; no automated guard. (Considered: a CI guard that fails if a Task is added/removed to/from task-list.json without an accompanying umbrellas.json diff. Deferred per P-OQ-2 default — orphans are warnings, not failures.)Round-trip test in row 9. Manual orchestrator gate for commit-coupling.

One-to-one mapping check: 17 invariants, 17 rows. Verified by Planner before write.


§3 umbrellas.json schema (Option C ratified S62 W3, OQ-4)

Section titled “§3 umbrellas.json schema (Option C ratified S62 W3, OQ-4)”

docs/reference/umbrellas.json — sibling to the other three ledgers (task-list.json, product-backlog.json, product-roadmap.json). The Astro docs site renderer (ID-9, ID-20 follow-up) loads it from this canonical path.

3.2 Zod schema (new module lib/validation/umbrellas-schema.ts)

Section titled “3.2 Zod schema (new module lib/validation/umbrellas-schema.ts)”
/**
* umbrellas-schema.ts — Zod schema for the KH umbrella (Linear-Initiative analogue)
* surface (TECH §3, PRODUCT inv 7–9).
*
* Mirrors the structure of `roadmap-schema.ts` (Zod, strict, typed exports).
* Three exported schemas:
* - UmbrellaStatus — 4-value enum (proposed | in_progress | done | archived)
* - UmbrellaEntrySchema — single umbrella object (PRODUCT inv 8)
* - UmbrellasSchema — root document (PRODUCT inv 7)
*
* Many-to-many semantics: a Task id may appear in multiple `task_ids[]` arrays
* across umbrellas — schema imposes no uniqueness across umbrellas (PRODUCT inv 8).
*
* Round-trip with `task-list.json` is enforced by integration test
* `__tests__/docs/umbrellas-task-list-roundtrip.test.ts`, NOT by the schema
* (PRODUCT inv 9 — broken reference fails the test; orphan Task fires a console.warn
* but passes — P-OQ-2 default).
*
* No barrel re-export.
*/
import { z } from 'zod';
import { BARE_ID_REGEX } from '@/lib/validation/schemas';
// ──────────────────────────────────────────────────────────────────────────────
// UmbrellaStatus enum (PRODUCT inv 8)
// ──────────────────────────────────────────────────────────────────────────────
export const UmbrellaStatus = z.enum([
'proposed',
'in_progress',
'done',
'archived',
]);
export type UmbrellaStatus = z.infer<typeof UmbrellaStatus>;
// ──────────────────────────────────────────────────────────────────────────────
// UmbrellaEntrySchema (PRODUCT inv 8)
// ──────────────────────────────────────────────────────────────────────────────
export const UmbrellaEntrySchema = z
.object({
/** Kebab-case stable identifier (URL slug). Lowercase + hyphen + digits only. */
id: z
.string()
.regex(
/^[a-z][a-z0-9-]*[a-z0-9]$/,
'Umbrella id must be kebab-case (lowercase, hyphens, digits; no leading/trailing hyphen).',
),
/** Human-readable display name (Title Case, UK English). */
title: z.string().min(1),
/** Relative path from repo root to canonical substrate doc. */
substrate_doc: z.string().min(1),
/**
* Array of Task id strings (matches `task-list.json#/tasks[].id` regex —
* bare-digit). Order matters for rendering (insertion order = display order).
* Many-to-many: a Task id may also appear in another umbrella entry's
* task_ids[]. The cross-doc validation test (see Inv 9) verifies the
* referenced ids exist in `task-list.json`.
*/
task_ids: z.array(z.string().regex(BARE_ID_REGEX, 'task_ids[] entries must be bare-digit Task ids')),
/** 4-value status enum (mapped to aggregate umbrella phase, not auto-derived). */
status: UmbrellaStatus,
/** Short string identifier for project phase (e.g. "Phase 1", "Phase 2"). */
phase: z.string().min(1),
})
.strict();
export type UmbrellaEntry = z.infer<typeof UmbrellaEntrySchema>;
// ──────────────────────────────────────────────────────────────────────────────
// UmbrellasSchema — root document (PRODUCT inv 7)
// ──────────────────────────────────────────────────────────────────────────────
export const UmbrellasSchema = z
.object({
/** Literal document identifier (PRODUCT inv 7). */
document_name: z.literal('umbrellas'),
/** One-paragraph human-readable purpose. */
document_purpose: z.string().min(1),
/**
* Freshness marker — same shape as task-list/backlog/roadmap (200-char cap,
* single-line, single session-id, kh-{prod-readiness|main}-S{N} prefix).
*/
last_updated: z
.string()
.min(1)
.max(200, 'last_updated must be ≤200 chars.')
.regex(
/^kh-(prod-readiness|main)-S\d+/,
'last_updated must start with "kh-{prod-readiness|main}-S{N}" session-id prefix.',
)
.refine((s) => !s.includes('\n'), {
message: 'last_updated must be a single line (no newlines).',
})
.refine(
(s) => (s.match(/\bkh-(prod-readiness|main)-S\d+/g) ?? []).length === 1,
{ message: 'last_updated must contain exactly one session-id.' },
),
/** Array of repo-relative paths to related documents. */
related_documents: z.array(z.string()),
/** Array of umbrella entries — empty allowed (PRODUCT inv 7). */
umbrellas: z.array(UmbrellaEntrySchema),
})
.strict();
export type Umbrellas = z.infer<typeof UmbrellasSchema>;

3.3 Initial populated set (P-OQ-1 default — canonical-pipeline only at first commit + 4 stubs)

Section titled “3.3 Initial populated set (P-OQ-1 default — canonical-pipeline only at first commit + 4 stubs)”

Per P-OQ-1 default ratification (recommended pending Liam override):

{
"document_name": "umbrellas",
"document_purpose": "Cross-Task strategic umbrellas (Linear Initiative analogue) — curated task_ids[] arrays + substrate_doc per umbrella for navigation, querying, and rendering on the Astro docs site (ID-9 follow-up).",
"last_updated": "kh-prod-readiness-S64 W2c close-out — curator added canonical-pipeline (umbrella) + 4 stubs (production-readiness, sales-proposals, intelligence-workspaces, docs-foundation)",
"related_documents": [
"docs/reference/task-list.json",
"docs/reference/product-roadmap.json",
"docs/reference/product-backlog.json"
],
"umbrellas": [
{
"id": "canonical-pipeline",
"title": "Canonical Pipeline Implementation",
"substrate_doc": "docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md",
"task_ids": ["28", "36", "37", "38", "39", "40", "41"],
"status": "in_progress",
"phase": "Phase 1"
},
{
"id": "production-readiness",
"title": "Production Readiness",
"substrate_doc": "docs/tracks/production-readiness.md",
"task_ids": [],
"status": "in_progress",
"phase": "Phase 1"
},
{
"id": "sales-proposals",
"title": "Sales Proposals (Phase 3)",
"substrate_doc": "docs/specs/sales-proposal-workspaces/PRODUCT.md",
"task_ids": [],
"status": "proposed",
"phase": "Phase 3"
},
{
"id": "intelligence-workspaces",
"title": "Intelligence Workspaces (typed Shape B)",
"substrate_doc": "docs/specs/intelligence-workspaces/TECH.md",
"task_ids": [],
"status": "in_progress",
"phase": "Phase 1"
},
{
"id": "docs-foundation",
"title": "Astro + Starlight Docs Foundation",
"substrate_doc": "docs/specs/id-9-astro-starlight-docs-foundation/PRODUCT.md",
"task_ids": [],
"status": "in_progress",
"phase": "Phase 1"
}
]
}

The exact task_ids[] array for canonical-pipeline is illustrative; the resolved list lands at {31.4} PLAN dispatch when the cascade algorithm (§4) executes. Note that the T0-T6 retro Task ids are inserted at retro-Task-open time (one per retro-open commit per P-OQ-5 default), so the initial commit’s canonical-pipeline entry may carry an empty or partial task_ids[] and grow per retro-open.

3.4 Schema-roundtrip test (Inv 17 + Inv 9)

Section titled “3.4 Schema-roundtrip test (Inv 17 + Inv 9)”
__tests__/validation/umbrellas-roundtrip.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { UmbrellasSchema } from '@/lib/validation/umbrellas-schema';
describe('umbrellas.json round-trip', () => {
it('parses cleanly via UmbrellasSchema', () => {
const raw = readFileSync('docs/reference/umbrellas.json', 'utf-8');
const parsed = UmbrellasSchema.parse(JSON.parse(raw));
expect(parsed.umbrellas.length).toBeGreaterThan(0);
});
});
// __tests__/docs/umbrellas-task-list-roundtrip.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { UmbrellasSchema } from '@/lib/validation/umbrellas-schema';
import { TaskListSchema } from '@/lib/validation/task-list-schema';
describe('umbrellas ↔ task-list round-trip (PRODUCT inv 9)', () => {
const umbrellas = UmbrellasSchema.parse(
JSON.parse(readFileSync('docs/reference/umbrellas.json', 'utf-8')),
);
const taskList = TaskListSchema.parse(
JSON.parse(readFileSync('docs/reference/task-list.json', 'utf-8')),
);
const realTaskIds = new Set(taskList.tasks.map((t) => t.id));
it('every umbrellas[].task_ids[] entry references a real Task', () => {
const broken: { umbrella: string; missing_task_id: string }[] = [];
for (const u of umbrellas.umbrellas) {
for (const id of u.task_ids) {
if (!realTaskIds.has(id)) {
broken.push({ umbrella: u.id, missing_task_id: id });
}
}
}
expect(broken).toEqual([]);
});
it('warns (but does not fail) on orphan Tasks (P-OQ-2 default)', () => {
const assigned = new Set<string>();
for (const u of umbrellas.umbrellas) for (const id of u.task_ids) assigned.add(id);
const orphans = taskList.tasks
.map((t) => t.id)
.filter((id) => !assigned.has(id));
if (orphans.length > 0) {
// Soft warning per PRODUCT inv 9 + P-OQ-2.
console.warn(`Umbrella round-trip: ${orphans.length} orphan Task(s) (no umbrella membership):`, orphans);
}
expect(true).toBe(true); // never fail
});
});

3.5 Migration (where the file lives, how it loads, how skills know about it)

Section titled “3.5 Migration (where the file lives, how it loads, how skills know about it)”
  • File location: docs/reference/umbrellas.json (new). Created at first commit by the orchestrator running update-roadmap-backlog’s extended Promote/Create mode (see §6.4 below for the skill extension).
  • Load path at runtime: No runtime app loader yet (the Astro docs site ID-9 + ID-20 will be the consumer). Until then, the file is read at test-time only — by the round-trip tests above and the per-Subtask journal lookup at retro-open time. The orchestrator reads it via Bash + jq at Task-open time to determine umbrella membership.
  • Skill awareness: .claude/skills/update-roadmap-backlog/SKILL.md is extended per §6.4 below to know about the umbrellas surface — specifically, the Promote mode’s destination-shape options gain an optional umbrella_id parameter that triggers a same-commit edit to umbrellas.json adding the new Task id to the named umbrella’s task_ids[] array.
  • Per-Subtask journal-block lookup: The retrospective journal-block helper (§5 below, formatRetrospectiveJournalBlock()) takes an optional umbrella_id input parameter that maps to the relevant umbrella’s id field, used in the journal text (e.g. Umbrella: canonical-pipeline). The lookup is constant-time via a Map<string, UmbrellaEntry> built once per helper invocation.

PRODUCT Inv 3 + Inv 4 prescribe that PRODUCT.md is ID-agnostic and the final T0-T14 → ID-N mapping locks at {31.4} PLAN dispatch (and is re-checked at each retro-Task open). This section specifies the deterministic algorithm.

INPUT:
- Active branches: ["origin/main", "origin/production-readiness", "origin/content-items-investigation"]
- Fixed-point anchor: T8 → ID-28 (per PRODUCT inv 5)
- The set of T-Tasks to cascade: {T0, T1, T2, T3, T4, T5, T6, T7, T9, T10, T11, T12, T13, T14}
STEP 1 — Cross-branch MAX-ID query.
For each branch B in active branches:
raw_json = git show ${B}:docs/reference/task-list.json
parsed = JSON.parse(raw_json)
max_id_B = max(int(t.id) for t in parsed.tasks)
MAX_ID_ACROSS_BRANCHES = max(max_id_B for B in active branches)
// Discount any Task ids that this migration has reserved but not yet opened
// (e.g. the {31.x} subtask chain is on this branch but the T0-T6 retro Task IDs
// are not yet present on any branch — they're being assigned NOW).
STEP 2 — Compute T0 base.
T0_id = MAX_ID_ACROSS_BRANCHES + 1
// As of TECH-author time: MAX = 35 (production-readiness). T0 → 36.
STEP 3 — Cascade T1-T6 (sequential, retro tasks).
For n in 1..6:
T{n}_id = T0_id + n
// T1 → 37, T2 → 38, T3 → 39, T4 → 40, T5 → 41, T6 → 42 (illustrative;
// recompute at PLAN dispatch time per Step 1).
STEP 4 — T7 cascade (forward JIT, skipping ID-28 since it's anchored to T8).
T7_id = T6_id + 1
IF T7_id == 28 OR T7_id == ANCHOR (T8):
T7_id += 1 // skip the anchor
// T7 → 43 (using T6 = 42 example).
STEP 5 — T8 anchored.
T8_id = 28 (FIXED — never recomputed)
STEP 6 — T9-T14 cascade (forward JIT).
candidate = T7_id + 1
For n in 9..14:
IF candidate == 28: candidate += 1 // skip anchor
T{n}_id = candidate
candidate += 1
// T9 → 44, T10 → 45, T11 → 46, T12 → 47, T13 → 48, T14 → 49 (illustrative).
OUTPUT:
Final mapping table {T0: 36, T1: 37, ..., T7: 43, T8: 28, T9: 44, ..., T14: 49}
  • umbrellas.json canonical-pipeline.task_ids[] carries the resolved Task ids (as bare-digit strings). This is the source of truth for umbrella → Task membership. Order in the array follows T-number order (T0 first, then T1-T6, then T7, T8, T9-T14) per renderer convention.
  • task-list.json#/tasks[].id carries the Task id. There is no umbrella_id field on Task — the reverse lookup is via the round-trip test in §3.4.

This is a deliberate normalisation choice: umbrellas know their members; Tasks do not know their umbrellas. The Astro renderer (ID-9, ID-20 follow-up) inverts the mapping at render-time if needed.

Edge caseResolution
Task in 2 umbrellas (e.g. T2 = canonical-pipeline + intelligence-workspaces)Allowed (PRODUCT inv 8). The Task id appears in both umbrellas’ task_ids[] arrays. The round-trip test (§3.4) does not flag this. The Astro renderer (future) will surface multi-membership in the Task’s render-time metadata.
Umbrella retired (status archived)Tasks listed in task_ids[] are NOT removed from the array. The umbrella entry stays in umbrellas.json with status: "archived". The Astro renderer hides archived umbrellas from default views but the data persists for audit. Migration step: orchestrator sets status field; no task_ids[] edit.
Task cancelled or rejected (status: cancelled in task-list.json)The Task id stays in any umbrella’s task_ids[] for traceability. The Astro renderer surfaces cancelled-Task badges. Schema does not enforce removal.
Cross-branch ID collision (re-opened)Per Inv 10: orchestrator MAX-ID-checks pre-open. If post-open collision detected at merge time, rename via chore(s{NN}-w{N}): rename ID-{old} → ID-{new} commit; updates task-list.json + umbrellas.json + any cross_doc_links in one commit. S62 W4 ID-28 rename (9e498e2f) is the worked example.
Anchor reassignment (would T8 ever move off ID-28?)Not contemplated. T8 = ID-28 is treated as a load-bearing fact for the canonical-pipeline migration. Should the user later request reassignment, that’s a separate migration with its own spec chain — out of scope here.

PRODUCT Inv 12 prescribes the journal-block contents for retrospective Subtasks. The shape is deterministic enough to encode in a helper.

// lib/validation/umbrellas-helpers.ts (NEW module — sibling to umbrellas-schema.ts)
import type { UmbrellaEntry } from '@/lib/validation/umbrellas-schema';
export interface RetrospectiveOpeningInput {
/** Session counter when the retro is being opened (NOT the original work session). */
retro_open_session: string; // e.g. "kh-prod-readiness-S64"
/** Original work session for the shipped piece. */
original_session: string; // e.g. "S242"
/** Original work branch. */
original_branch: string; // e.g. "content-items-investigation"
/** Path to the continuation prompt that documented the original work. */
continuation_prompt_path: string; // e.g. "docs/continuation-prompts/continuation-prompt-kh-s242-main-t1-specs-and-phase-0-close.md"
/** Load-bearing commits in chronological order. */
commits: Array<{ sha8: string; message_line: string }>;
/** Migration file references (paths from repo root), optional. */
migration_files?: string[];
/** PLAN.md section number this retro Task delivered against (e.g. "4.1"). */
plan_md_section: string;
/** Follow-up flags / known gaps (one-liners), optional. */
followup_flags?: string[];
/** Optional umbrella id for the cross-reference line (Inv 8 — Map<string, UmbrellaEntry>). */
umbrella_id?: string;
}
/**
* Format a retrospective journal block per PRODUCT inv 12.
*
* Output shape:
*
* <info added on YYYY-MM-DDTHH:MM:SS.sssZ>
* RETROSPECTIVE OPENING — Task opened in `done` status post-implementation.
* Original work happened S{NN} per `docs/continuation-prompts/<file>.md`.
*
* Commits (S{NN}, <branch>):
* - <sha8> — <message-line>
* ...
*
* Migration files: (optional)
* - supabase/migrations/<timestamp>_<name>.sql
*
* Follow-up flags: (optional)
* - <flag>
*
* Umbrella: <id> (optional, when umbrella_id provided)
*
* PLAN.md §4.{n} acceptance criteria all met.
* </info added on YYYY-MM-DDTHH:MM:SS.sssZ>
*
* NOTE: The opening + closing `<info added on …>` timestamps must match exactly.
* The helper generates a single ISO 8601 timestamp using `new Date().toISOString()`
* at invocation time. Callers wanting a fixed timestamp (test reproducibility)
* inject via `Date.now()` mock — standard Vitest pattern.
*
* UK English throughout output (Inv 15).
*/
export function formatRetrospectiveJournalBlock(
input: RetrospectiveOpeningInput,
): string {
// Implementation lands at impl Subtask {31.5+}. Output template + signature
// specified above; reference test in `__tests__/validation/umbrellas-helpers.test.ts`
// verifies behaviour against a fixture input. Not implemented here — TECH.md
// specifies, PLAN.md decomposes, Executor implements.
throw new Error('Not implemented (see TECH.md §5 + impl Subtask {31.5+}).');
}

5.2 Worked example output (T1 retrospective Subtask 30.1, for reference)

Section titled “5.2 Worked example output (T1 retrospective Subtask 30.1, for reference)”
<info added on 2026-05-22T10:00:00.000Z>
RETROSPECTIVE OPENING — Task opened in `done` status post-implementation.
Original work happened S242 per `docs/continuation-prompts/continuation-prompt-kh-s242-main-t1-specs-and-phase-0-close.md`.
Commits (S242, content-items-investigation):
- 2f142936 — docs(spec): procurement-workspaces PRODUCT + TECH — S242 W3 fix-pass per verifier findings
- ddebada1 — docs(spec): procurement-workspaces PRODUCT + TECH (absorbs EP8 v5) — S242 T4.7
- f324fe93 — docs(spec): content-model-invariants PRODUCT — S242 W3 fix-pass per verifier findings
- a89440f1 — docs(verify): content-model-invariants verifier report — S242 WP1.3
- c8fd217b — docs(verify): procurement-workspaces verifier report — S242 WP1.4
Umbrella: canonical-pipeline
PLAN.md §4.1 acceptance criteria all met.
</info added on 2026-05-22T10:00:00.000Z>

5.3 Existing journal blocks unchanged (T0 retro discipline)

Section titled “5.3 Existing journal blocks unchanged (T0 retro discipline)”

Existing per-Subtask journal blocks in task-list.json (e.g. ID-31.1 31.2 details on this branch) are NOT retroactively edited to add Umbrella: lines. New retro Tasks opened by this migration (T0-T6) get the line; old Tasks do not. This is a forward- only adoption — no historical rewriting per standard KH discipline.


The {31.4} PLAN.md output prescribes the exact Subtask shape; this section specifies the file boundaries and dependency order so the Planner has a clear starting point.

Section titled “6.1 Recommended decomposition (target: 5-7 implementation Subtasks)”

The decomposition below is the recommended starting point — the {31.4} Planner adapts based on the latest task-list.json state at PLAN-dispatch time.

SubTitleFile boundarySibling-only deps
31.5Author lib/validation/umbrellas-schema.ts + unit testlib/validation/umbrellas-schema.ts (new) + __tests__/validation/umbrellas-schema.test.ts (new)none
31.6Author lib/validation/umbrellas-helpers.ts (formatRetrospectiveJournalBlock) + unit testlib/validation/umbrellas-helpers.ts (new) + __tests__/validation/umbrellas-helpers.test.ts (new)[5]
31.7Author initial docs/reference/umbrellas.json (canonical-pipeline + 4 stubs per P-OQ-1 default)docs/reference/umbrellas.json (new)[5]
31.8Add round-trip tests umbrellas-roundtrip.test.ts + umbrellas-task-list-roundtrip.test.ts__tests__/validation/umbrellas-roundtrip.test.ts (new) + __tests__/docs/umbrellas-task-list-roundtrip.test.ts (new)[5, 7]
31.9Extend .claude/skills/update-roadmap-backlog/SKILL.md Promote mode to know about umbrellas.claude/skills/update-roadmap-backlog/SKILL.md (edit)[5, 7]
31.10Open retrospective Tasks T0-T6 (7 commits per P-OQ-5 default; resolved IDs per §4 algorithm)docs/reference/task-list.json + docs/reference/umbrellas.json (edit per retro-open)[5, 6, 7, 8, 9]
31.11Add presence + cross-doc-links tests for retro Tasks (Inv 1, 2, 5, 6, 13)__tests__/docs/canonical-pipeline-{t0-presence, t1-t6-presence, t8-anchor, forward-jit, cross-doc-links}.test.ts (new — 5 files)[10]

Subtask count: 7. Well within the 25-soft-ceiling. Sibling-only deps verified across all rows (every dep references a same-Task sibling). No cross-Task references needed.

Before the orchestrator opens any retro Task at {31.10}, run:

Terminal window
bun run test -- umbrellas-schema umbrellas-helpers umbrellas-roundtrip

All four test files must be green (zero failures) — this confirms the schema + helper + round-trip infrastructure is ready to receive the retro-Task land.

6.3 Retro-Task open commit pattern (per P-OQ-5 default — one commit per Task)

Section titled “6.3 Retro-Task open commit pattern (per P-OQ-5 default — one commit per Task)”
Terminal window
# For each retro Task n in {0, 1, 2, 3, 4, 5, 6}:
# 1. Compute resolved id via §4 algorithm (fresh cross-branch MAX-ID query)
# 2. Compose Task entry per PRODUCT inv 2 (or inv 1 for T0)
# 3. Generate journal block per §5 helper for each Subtask
# 4. Edit task-list.json: append Task entry
# 5. Edit umbrellas.json: append id to canonical-pipeline.task_ids[]
# 6. Bump last_updated on BOTH files (single-line, ≤200 chars, single session-id)
# 7. Validate: TaskListSchema.parse() + UmbrellasSchema.parse() + round-trip test
# 8. Commit:
# chore(s64-w2-wp{X}): open Task ID-{N} T{n} retrospective (`done`) — <one-line summary>

Total: 7 commits for the retro-Task land (1 × T0 + 6 × T1-T6).

6.4 update-roadmap-backlog skill extension (Subtask 31.9)

Section titled “6.4 update-roadmap-backlog skill extension (Subtask 31.9)”

The skill’s Promote mode currently has two destination_shape options: new_top_level_task and new_subtask_under_task_id. Extend by adding:

New input fieldTypeRequired?Purpose
umbrella_idstring (kebab-case) or nullOptional (default null)When supplied AND destination_shape === 'new_top_level_task': triggers a same-commit edit to umbrellas.json appending the new Task id to the named umbrella’s task_ids[] array. When null: no umbrella edit (Task is orphan per P-OQ-2 default — warning only). When destination_shape is subtask: ignored (subtasks inherit parent Task’s umbrella membership).

The skill body gains a new step 9. Umbrella membership edit (optional):

9. **Umbrella membership edit (optional).** If input.umbrella_id is non-null:
- Load docs/reference/umbrellas.json.
- Parse via UmbrellasSchema.
- Find umbrella entry with id === input.umbrella_id; if not found, error.
- Append destination Task id to that umbrella's task_ids[] (skip if already present — idempotent).
- Bump umbrellas.json last_updated per the field-discipline rule.
- Validate via UmbrellasSchema.parse(); abort on failure.

This extension is the Subtask 31.9 scope. The skill’s existing 8 steps and the last_updated discipline apply unchanged.

6.5 Forward-Task open (T7, T9-T14) discipline

Section titled “6.5 Forward-Task open (T7, T9-T14) discipline”

When the orchestrator opens each forward Task JIT (per Inv 6):

  1. Compute fresh resolved id via §4 algorithm (re-run cross-branch MAX-ID).
  2. Open the Task with spec-chain Subtasks ({N.1 RESEARCH, N.2 PRODUCT, N.3 TECH, N.4 PLAN}) per spec-driven-implementation skill.
  3. Call update-roadmap-backlog Create (NOT Promote — these are not backlog items) with umbrella_id: "canonical-pipeline".
  4. Optionally append (see Task ID-NN) backlink to PLAN.md §4.n header per Inv 14.

§7 Testing and validation table (per invariant)

Section titled “§7 Testing and validation table (per invariant)”
InvariantTest fileTest typeAsserts
Inv 1 (T0 retro)__tests__/docs/canonical-pipeline-t0-presence.test.tsDocT0 Task exists, status done, 4-8 Subtasks all done
Inv 2 (T1-T6 retro)__tests__/docs/canonical-pipeline-t1-t6-presence.test.tsDocEach Task title prefix T{n} — present, done, ≤200-word desc, commit_refs non-empty
Inv 3, 4 (cascade docs)(manual gate at PLAN dispatch)n/aChecker manual verify
Inv 5 (T8 anchor)__tests__/docs/canonical-pipeline-t8-anchor.test.tsDoc (gated)If Task title T8 — present, id === "28"
Inv 6 (forward JIT)__tests__/docs/canonical-pipeline-forward-jit.test.tsDoc (gated)If T7/T9-T14 present, spec-chain seeded
Inv 7 (umbrellas.json shape)__tests__/validation/umbrellas-schema.test.ts + umbrellas-roundtrip.test.tsUnit + round-tripRoot parses via UmbrellasSchema
Inv 8 (entry shape)__tests__/validation/umbrellas-schema.test.tsUnitEach enum value + invalid rejected + multi-membership accepted
Inv 9 (round-trip with task-list)__tests__/docs/umbrellas-task-list-roundtrip.test.tsDocEvery task_ids[] entry resolves; orphans warn (don’t fail)
Inv 10 (cross-branch coord)(manual + skill body)n/aLiam-eye on each new-Task PR
Inv 11 (shipping-cadence)(manual at retro-open)n/aOrchestrator hand-decomposes per PLAN.md §4.n
Inv 12 (journal block format)__tests__/validation/umbrellas-helpers.test.tsUnitHelper output matches fixture template
Inv 13 (cross_doc_links shape)__tests__/docs/canonical-pipeline-cross-doc-links.test.tsDocEach retro Task has cross_doc_links[] entry with PLAN.md path
Inv 14 (PLAN backlinks manual)n/an/aDiscipline only
Inv 15 (UK English)(manual + existing project guards)n/aEditorial discipline
Inv 16 (status-machine retro exception)(manual at retro-open)n/aDocumented exception
Inv 17 (schema-roundtrip + commit-coupling)__tests__/validation/umbrellas-roundtrip.test.ts + (manual for commit-coupling)Unit + manualSchema validates; commit-coupling is procedural

Test file count: 6 new test files (3 in __tests__/validation/, 3 in __tests__/docs/). Smoke-test command: bun run test -- umbrellas canonical-pipeline.


RiskLikelihoodImpactMitigation
Umbrella drift — Task moves between umbrellas mid-flight without round-trip test catching the moveMediumLowThe round-trip test (Inv 9) catches broken references (missing Task id). It does NOT catch re-assignments (Task id removed from umbrella A, added to umbrella B). Mitigation: orchestrator commit-couples umbrella edits per Inv 17 — both files in one commit. Audit trail surfaces via git log -- umbrellas.json. If drift surfaces post-merge as a problem, add a __tests__/docs/umbrella-membership-stability.test.ts that diffs vs git show HEAD~10:umbrellas.json — deferred until evidence warrants.
Retrospective umbrella shape — T0 carries dozens of legacy Tasks (S180-S241 era), making the entry unwieldyMediumMediumT0 is a Task (not the umbrella). The umbrella canonical-pipeline carries Task ids only — T0 plus T1-T14 (15 ids). The T0 Task itself has 4-8 Subtasks (Inv 1) collapsing the ~15 doc landings into shipping-cadence groupings. Mitigation: rely on Inv 1 cap (4-8 Subtasks) to prevent T0 explosion. If pre-phase work needs deeper granularity, open separate Tasks T0a, T0b, etc. (with cascade renumbering). Default per P-OQ-4: T0 is one Task; canonical-pipeline umbrella is a distinct ledger entity.
Interaction with existing cross_doc_links field — type "spec_substrate" may not be in DocLinkSchema.type enumLowLowVerify at impl-time. DocLinkSchema source: lib/validation/roadmap-schema.ts. If type is an enum, the new value "spec_substrate" must be added (single-line change). If type is z.string(), no change needed. Verify Subtask 31.5 includes this check; pre-emptive read in §2 row 13.
Cascade race condition — multiple branches simultaneously open new Tasks consuming the same MAX+1 idLowMediumThe MAX-ID discipline (Inv 10) is human-orchestrator-only, no CI guard (per OQ-5 ratification). Mitigation: the S62 W4 ID-28 rename pattern (9e498e2f) is the worked recovery procedure. If recurrence becomes frequent, escalate to T-OQ-3 (below) and consider CI guard.
{31.10} retro-open commit batching — 7 commits is verboseMediumLowP-OQ-5 default = one commit per Task. Alternative = single batched commit. If Liam ratifies single batched commit, collapse Subtask 31.10 to a single execution; if P-OQ-5 default holds, 7 commits is the expected output. No mitigation needed — explicit P-OQ choice.
Astro renderer doesn’t exist yet — umbrellas.json has no live consumerLowLowPer Inv 7 / Non-goal: Astro renderer is a follow-up Task (downstream of ID-9 / ID-20). Until then, umbrellas.json is consumed only by tests + retro-Task-open helper. Mitigation: defer renderer concern. The schema is the load-bearing contract; the renderer follows.
Schema-roundtrip test failure on merge to main — stale task_ids[] referencesMediumMediumThe round-trip test (§3.4) is the canary. On a merge where Task ids appear/disappear, the test catches it pre-merge if run locally. CI enforcement via bun run test blocks the PR. Mitigation: orchestrator runs bun run test -- umbrellas after every merge that touches task-list.json or umbrellas.json.

SubtaskEffort estimate
31.5 — umbrellas-schema.ts + unit test30 min
31.6 — umbrellas-helpers.ts (formatRetrospectiveJournalBlock) + unit test45 min
31.7 — Initial umbrellas.json (P-OQ-1 default — 1 + 4 stubs)15 min
31.8 — Round-trip tests (2 new test files)30 min
31.9 — Extend update-roadmap-backlog skill body (Promote mode + new step)30 min
31.10 — Open retro Tasks T0-T6 (7 commits, hand-composed journal blocks)2h
31.11 — Presence + cross-doc-links tests (5 new test files)1h
Total≈ 5h

The 31.10 retro-Task open is the dominant cost — composing 7 Tasks with shipping- cadence Subtasks + journal blocks is hand-composed work, not automated. The helper (§5) reduces per-Subtask formatting cost but doesn’t generate Task descriptions or commit_refs[] population.

Effort exceeds the 2h-per-Subtask soft ceiling for 31.10. Recommend splitting 31.10 into 31.10a (T0 open), 31.10b (T1-T3 opens), 31.10c (T4-T6 opens) at {31.4} PLAN dispatch to keep each sub-execution under 2h. Decision deferred to the {31.4} Planner.


Five TECH-level open questions surface from this authoring pass. Each has a recommended default suitable for {31.4} PLAN dispatch to assume unless Liam overrides.

T-OQ-1 — lib/validation/umbrellas-helpers.ts location

Section titled “T-OQ-1 — lib/validation/umbrellas-helpers.ts location”

Should the retrospective-journal-block helper live in lib/validation/ (next to the schema) or in a new lib/ledger/ module (proposed by the brief)? The brief suggested lib/ledger/ but no such module exists today; all schema-adjacent helpers (e.g. lib/validation/jsonb.ts) live under lib/validation/.

Default: lib/validation/umbrellas-helpers.ts. Consistent with the existing pattern (schema.ts + helpers.ts in the same dir). Creating lib/ledger/ for one file is premature — defer until 2+ ledger helpers exist.

Alternative: Create lib/ledger/ with umbrellas-helpers.ts. Pro: anticipates future ledger-domain organisation (could also house task-list-helpers.ts, backlog-helpers.ts). Con: premature, single-file dir.

T-OQ-2 — update-roadmap-backlog skill — Create mode vs Promote mode for new Tasks

Section titled “T-OQ-2 — update-roadmap-backlog skill — Create mode vs Promote mode for new Tasks”

The skill’s Promote mode is documented as backlog → task-list MOVE only. New Tasks NOT originating from backlog (e.g. forward-JIT canonical-pipeline Tasks T7/T9-T14) currently use no clearly-named skill path. Should the skill grow a new mode (e.g. OpenTask) or do new Tasks use Create against task-list.json?

Default: Create against task-list.json. Add task-list as a target option to the Create mode’s “Resolve target → file” mapping (currently roadmap + backlog only). The Subtask 31.9 scope includes this Create-target extension alongside the Promote-umbrella extension.

Alternative: New mode OpenTask. Pro: cleaner semantics. Con: more maintenance, more skill API surface.

T-OQ-3 — CI guard for cross-branch MAX-ID collision

Section titled “T-OQ-3 — CI guard for cross-branch MAX-ID collision”

Per Inv 10 (and OQ-5 ratification), no CI guard exists. If multiple sessions on different branches open new Tasks concurrently and the MAX-ID discipline fails (human error), a collision merges in. The S62 W4 rename pattern is the recovery, but the cost grows with frequency.

Default: Defer CI guard until recurrence. The S62 W4 ID-28 incident is the only known collision to date. If a second collision occurs within 6 months, re-engage T-OQ-3 with a concrete CI guard proposal (likely a workflow_run job that queries gh api repos/.../contents/docs/reference/task-list.json?ref=<branch> for each active branch and diffs).

Alternative: Build CI guard now. Pro: prevents recurrence. Con: requires GitHub API tokens, cross-branch read pattern, and is heavyweight for an incident rate of 1/year.

T-OQ-4 — Round-trip test runtime cost — read both JSONs per test

Section titled “T-OQ-4 — Round-trip test runtime cost — read both JSONs per test”

The round-trip test (§3.4) reads both umbrellas.json and task-list.json once. At scale (e.g. when task-list.json grows to 100+ Tasks), this is fine (<10ms). But re-reading both files in every CI run may be a redundant cost if other tests also do this.

Default: Accept per-test re-read. Vitest’s beforeAll() could hoist the read into a shared fixture, but the test runs in O(ms) regardless. No optimisation needed.

Alternative: Centralise via a __tests__/helpers/ledger-fixtures.ts. Pro: DRY. Con: indirection for a 2-line read.

T-OQ-5 — Future Astro renderer schema needs

Section titled “T-OQ-5 — Future Astro renderer schema needs”

The Astro renderer for umbrellas (per Inv 7 + Non-goal — downstream follow-up of ID-9 / ID-20) will need additional fields not currently in UmbrellaEntrySchema (e.g. display_order: number, description: string, tags: string[]). Should we anticipate these now or add when the renderer lands?

Default: Add when renderer lands. PRODUCT.md prescribed 6 fields per entry; honour that and grow when concrete consumer needs surface. Schema is .strict() so additions are explicit and audited.

Alternative: Add description: string + display_order: number now (low-risk additions that the renderer is likely to want). Pro: avoids a schema-migration beat later. Con: speculative.


End of TECH.md. Subtask 31.3 status in_progress → done pending Checker promotion. Subtask 31.4 PLAN.md dispatch may begin once TECH.md ratified.