From eac711ecec183fbb86022b337c55a76090fb2d8b Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:44:54 +0200 Subject: [PATCH 1/5] docs(work): scaffold work-system-v1 epic + story --- .../01-state-builder-and-cli/_story.md | 33 +++++++++++++++++++ docs/work/work-system-v1/_epic.md | 28 ++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/work/work-system-v1/01-state-builder-and-cli/_story.md create mode 100644 docs/work/work-system-v1/_epic.md diff --git a/docs/work/work-system-v1/01-state-builder-and-cli/_story.md b/docs/work/work-system-v1/01-state-builder-and-cli/_story.md new file mode 100644 index 0000000..587d392 --- /dev/null +++ b/docs/work/work-system-v1/01-state-builder-and-cli/_story.md @@ -0,0 +1,33 @@ +--- +id: 01-state-builder-and-cli +epic: work-system-v1 +title: State builder + pnpm work CLI +type: technical-story +status: in-progress +feature: scripts +depends-on: [] +blocks: [] +--- + +## Goal +`pnpm work status` shows the current epic/story state across `docs/work/`. +`pnpm work next` prints the next unblocked story. `pnpm work rebuild-state` +regenerates `_state.json` from markdown. + +## In scope +- `scripts/work/state-builder.mjs` — pure function, fully tested +- `scripts/work/cli.mjs` — argument dispatch + I/O wrapping the builder +- `docs/work/_state.json` — initial committed snapshot +- pnpm work script entry + +## Out of scope +- Per-task state (tasks are markdown checkboxes, not separate entries) +- Orchestrator dispatch logic +- Dependency-graph analysis + +## Tasks +- [ ] Epic + story scaffold +- [ ] state-builder.mjs + tests +- [ ] cli.mjs + tests +- [ ] _state.json initial commit + pnpm work script entry +- [ ] Final verification + closeout diff --git a/docs/work/work-system-v1/_epic.md b/docs/work/work-system-v1/_epic.md new file mode 100644 index 0000000..0a16ef4 --- /dev/null +++ b/docs/work/work-system-v1/_epic.md @@ -0,0 +1,28 @@ +--- +id: work-system-v1 +prd: null +title: Work system v1 (MVP) — state tracking + pnpm work CLI +type: epic +status: in-progress +features: [scripts] +created: 2026-05-13 +--- + +## Goal +Filesystem-only state tracking for `docs/work/` + a `pnpm work` CLI with +status / next / rebuild-state subcommands. Foundation for future +orchestration; no agent dispatch in v1. + +## Why +Agents and humans both need a fast way to see "what's the next unblocked +work?" and "where are we?" — without scanning every story file by hand. + +## Out of scope (v2+) +- Sandcastle / agent dispatch +- PRD and ADR elicitation skills +- Decomposer / implementer / reviewer prompts +- DAG analysis (depends-on / blocks) +- Pre-commit hooks (the state file is rebuilt manually for now) + +## Stories +- [ ] [01 — State builder + CLI](01-state-builder-and-cli/_story.md) From 6b57d76dc22d75186ceff5dd7fa9ed32a1abc221 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:46:28 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat(scripts):=20work=20state-builder=20?= =?UTF-8?q?=E2=80=94=20walks=20docs/work/=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/work/state-builder.mjs | 118 +++++++++++++++++++++ scripts/work/state-builder.test.mjs | 152 ++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 scripts/work/state-builder.mjs create mode 100644 scripts/work/state-builder.test.mjs diff --git a/scripts/work/state-builder.mjs b/scripts/work/state-builder.mjs new file mode 100644 index 0000000..5a7f42b --- /dev/null +++ b/scripts/work/state-builder.mjs @@ -0,0 +1,118 @@ +import fs from "node:fs"; +import path from "node:path"; + +const SKIP_FOLDERS = new Set(["_templates", "prds"]); +const SKIP_FILES = new Set(["README.md", "_state.json"]); + +/** + * Walk the `docs/work/` tree starting at `workRoot` and return a structured + * state object. Each epic folder must contain `_epic.md`; each story + * subfolder must contain `_story.md`. + * + * Returns: + * { + * updated_at: ISO string, + * epics: { + * : { + * status: "todo" | "in-progress" | "done", + * title: string, + * stories: { + * : { + * status: "todo" | "in-progress" | "done", + * title: string, + * ac_total: number, // total checkboxes in Tasks section + * ac_completed: number, // - [x] checkboxes + * } + * } + * } + * } + * } + */ +export function buildState(workRoot) { + const state = { + updated_at: new Date().toISOString(), + epics: {}, + }; + + if (!fs.existsSync(workRoot)) return state; + + for (const entry of fs.readdirSync(workRoot)) { + if (SKIP_FOLDERS.has(entry) || SKIP_FILES.has(entry)) continue; + const epicDir = path.join(workRoot, entry); + if (!fs.statSync(epicDir).isDirectory()) continue; + const epicFile = path.join(epicDir, "_epic.md"); + if (!fs.existsSync(epicFile)) continue; + + const epicMeta = parseFrontmatter(epicFile); + const epicEntry = { + status: epicMeta.status ?? "todo", + title: epicMeta.title ?? entry, + stories: {}, + }; + + for (const sub of fs.readdirSync(epicDir)) { + const subPath = path.join(epicDir, sub); + if (!fs.statSync(subPath).isDirectory()) continue; + const storyFile = path.join(subPath, "_story.md"); + if (!fs.existsSync(storyFile)) continue; + const storyMeta = parseFrontmatter(storyFile); + const storyContent = fs.readFileSync(storyFile, "utf8"); + const { total, completed } = countTaskCheckboxes(storyContent); + epicEntry.stories[storyMeta.id ?? sub] = { + status: storyMeta.status ?? "todo", + title: storyMeta.title ?? sub, + ac_total: total, + ac_completed: completed, + }; + } + + state.epics[epicMeta.id ?? entry] = epicEntry; + } + + return state; +} + +/** + * Read a markdown file's YAML frontmatter (between leading `---` delimiters) + * and return a flat object of string-valued keys. Numeric / bool values are + * left as strings; this is enough for our needs (status, id, title fields). + */ +export function parseFrontmatter(filePath) { + const src = fs.readFileSync(filePath, "utf8"); + const match = src.match(/^---\n([\s\S]+?)\n---/); + if (!match) return {}; + const out = {}; + for (const line of match[1].split("\n")) { + const m = line.match(/^([\w-]+):\s*(.*)$/); + if (!m) continue; + let value = m[2].trim(); + if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); + if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1); + out[m[1]] = value; + } + return out; +} + +/** + * Count `- [x]` and `- [ ]` checkboxes inside the file's `## Tasks` section. + * Returns { total, completed }. + */ +export function countTaskCheckboxes(content) { + const lines = content.split("\n"); + let inTasks = false; + let total = 0; + let completed = 0; + for (const line of lines) { + if (line.startsWith("## ")) { + inTasks = /^##\s+Tasks\b/i.test(line); + continue; + } + if (!inTasks) continue; + const m = line.match(/^[\s>-]*\[(.)\]/); + if (m) { + total++; + if (m[1] === "x" || m[1] === "X") completed++; + } + } + return { total, completed }; +} diff --git a/scripts/work/state-builder.test.mjs b/scripts/work/state-builder.test.mjs new file mode 100644 index 0000000..960a4fd --- /dev/null +++ b/scripts/work/state-builder.test.mjs @@ -0,0 +1,152 @@ +import { describe, it, expect } from "vitest"; +import path from "node:path"; +import os from "node:os"; +import fs from "node:fs"; +import { buildState, parseFrontmatter, countTaskCheckboxes } from "./state-builder.mjs"; + +function makeWorkTree({ epics }) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-state-")); + for (const [epicId, epicData] of Object.entries(epics)) { + const epicDir = path.join(root, epicId); + fs.mkdirSync(epicDir, { recursive: true }); + fs.writeFileSync( + path.join(epicDir, "_epic.md"), + `--- +id: ${epicId} +title: ${epicData.title ?? epicId} +status: ${epicData.status ?? "todo"} +--- + +`, + ); + for (const [storyId, storyData] of Object.entries(epicData.stories ?? {})) { + const storyDir = path.join(epicDir, storyId); + fs.mkdirSync(storyDir, { recursive: true }); + const tasksBlock = (storyData.tasks ?? []) + .map((t) => `- [${t.done ? "x" : " "}] ${t.label}`) + .join("\n"); + fs.writeFileSync( + path.join(storyDir, "_story.md"), + `--- +id: ${storyId} +title: ${storyData.title ?? storyId} +status: ${storyData.status ?? "todo"} +--- + +## Tasks +${tasksBlock} +`, + ); + } + } + return root; +} + +describe("buildState", () => { + it("returns an empty epics map for a non-existent workRoot", () => { + const state = buildState("/nonexistent/path"); + expect(state.epics).toEqual({}); + expect(typeof state.updated_at).toBe("string"); + }); + + it("collects epics + stories with status + task counts", () => { + const root = makeWorkTree({ + epics: { + epic1: { + status: "in-progress", + stories: { + story1: { + status: "done", + tasks: [ + { label: "a", done: true }, + { label: "b", done: true }, + ], + }, + story2: { + status: "todo", + tasks: [ + { label: "c", done: false }, + { label: "d", done: false }, + { label: "e", done: false }, + ], + }, + }, + }, + }, + }); + + const state = buildState(root); + expect(state.epics).toEqual({ + epic1: { + status: "in-progress", + title: "epic1", + stories: { + story1: { status: "done", title: "story1", ac_total: 2, ac_completed: 2 }, + story2: { status: "todo", title: "story2", ac_total: 3, ac_completed: 0 }, + }, + }, + }); + }); + + it("skips _templates and prds folders", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip-")); + fs.mkdirSync(path.join(root, "_templates"), { recursive: true }); + fs.writeFileSync(path.join(root, "_templates", "_epic.md"), "---\nid: x\n---"); + fs.mkdirSync(path.join(root, "prds"), { recursive: true }); + fs.writeFileSync(path.join(root, "prds", "_epic.md"), "---\nid: y\n---"); + expect(buildState(root).epics).toEqual({}); + }); + + it("skips directories without _epic.md", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip2-")); + fs.mkdirSync(path.join(root, "incomplete"), { recursive: true }); + expect(buildState(root).epics).toEqual({}); + }); +}); + +describe("countTaskCheckboxes", () => { + it("counts both unchecked and checked", () => { + const content = `## Tasks +- [x] done one +- [ ] open one +- [X] capital X also counts +`; + expect(countTaskCheckboxes(content)).toEqual({ total: 3, completed: 2 }); + }); + + it("only counts inside the ## Tasks section", () => { + const content = `## Tasks +- [x] in tasks + +## Notes +- [x] outside, should not count +`; + expect(countTaskCheckboxes(content)).toEqual({ total: 1, completed: 1 }); + }); + + it("returns zeros when no Tasks section exists", () => { + expect(countTaskCheckboxes(`## Goal\n\nFoo\n`)).toEqual({ total: 0, completed: 0 }); + }); +}); + +describe("parseFrontmatter", () => { + it("extracts simple string keys", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-")); + const fp = path.join(dir, "f.md"); + fs.writeFileSync(fp, `--- +id: x +title: A title +status: done +--- + +Body`); + expect(parseFrontmatter(fp)).toEqual({ id: "x", title: "A title", status: "done" }); + }); + + it("returns {} when no frontmatter present", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm2-")); + const fp = path.join(dir, "f.md"); + fs.writeFileSync(fp, "# Just a heading\n"); + expect(parseFrontmatter(fp)).toEqual({}); + }); +}); From be8e89baed5b3c0574c49f013a3486d27585ae22 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:46:51 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat(scripts):=20pnpm=20work=20CLI=20?= =?UTF-8?q?=E2=80=94=20rebuild-state,=20status,=20next?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/work/cli.mjs | 87 +++++++++++++++++++++++++++++++++++++++ scripts/work/cli.test.mjs | 42 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 scripts/work/cli.mjs create mode 100644 scripts/work/cli.test.mjs diff --git a/scripts/work/cli.mjs b/scripts/work/cli.mjs new file mode 100644 index 0000000..6440349 --- /dev/null +++ b/scripts/work/cli.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +/** + * pnpm work — CLI for the local work-system. + * + * Subcommands: + * rebuild-state Rewrites docs/work/_state.json from the current markdown + * status Prints a tree of all epics + their stories + * next Prints the first non-done story in the first non-done epic + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildState } from "./state-builder.mjs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, "..", ".."); +const WORK_ROOT = path.join(REPO_ROOT, "docs", "work"); +const STATE_FILE = path.join(WORK_ROOT, "_state.json"); + +function rebuildState() { + const state = buildState(WORK_ROOT); + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + "\n"); + console.log(`Rebuilt ${path.relative(REPO_ROOT, STATE_FILE)} with ${Object.keys(state.epics).length} epic(s).`); +} + +function printStatus() { + const state = buildState(WORK_ROOT); + const epicIds = Object.keys(state.epics).sort(); + if (epicIds.length === 0) { + console.log("No epics found under docs/work/."); + return; + } + for (const epicId of epicIds) { + const epic = state.epics[epicId]; + const storyIds = Object.keys(epic.stories).sort(); + const storyTotals = storyIds.reduce( + (acc, sid) => ({ + total: acc.total + epic.stories[sid].ac_total, + done: acc.done + epic.stories[sid].ac_completed, + }), + { total: 0, done: 0 }, + ); + const epicMark = mark(epic.status); + console.log(`${epicMark} ${epicId} — ${epic.title} (${storyTotals.done}/${storyTotals.total} tasks done)`); + for (const sid of storyIds) { + const s = epic.stories[sid]; + console.log(` ${mark(s.status)} ${sid} (${s.ac_completed}/${s.ac_total}) — ${s.title}`); + } + } +} + +function printNext() { + const state = buildState(WORK_ROOT); + const epicIds = Object.keys(state.epics).sort(); + for (const epicId of epicIds) { + const epic = state.epics[epicId]; + if (epic.status === "done") continue; + const storyIds = Object.keys(epic.stories).sort(); + for (const sid of storyIds) { + const s = epic.stories[sid]; + if (s.status !== "done") { + console.log(`${epicId} / ${sid} — ${s.title}`); + console.log(` status: ${s.status}, tasks: ${s.ac_completed}/${s.ac_total}`); + return; + } + } + } + console.log("All epics + stories are done. ✓"); +} + +function mark(status) { + if (status === "done") return "✓"; + if (status === "in-progress") return "→"; + if (status === "blocked") return "✗"; + return "○"; +} + +function usage() { + console.log("Usage: pnpm work "); + process.exit(2); +} + +const cmd = process.argv[2]; +if (cmd === "rebuild-state") rebuildState(); +else if (cmd === "status") printStatus(); +else if (cmd === "next") printNext(); +else usage(); diff --git a/scripts/work/cli.test.mjs b/scripts/work/cli.test.mjs new file mode 100644 index 0000000..901289f --- /dev/null +++ b/scripts/work/cli.test.mjs @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +import { execSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CLI = path.join(__dirname, "cli.mjs"); + +function run(args) { + try { + return execSync(`node "${CLI}" ${args}`, { encoding: "utf8" }); + } catch (e) { + return e.stdout + e.stderr; + } +} + +describe("pnpm work cli", () => { + it("prints usage when no subcommand is given", () => { + const out = run(""); + expect(out).toContain("Usage:"); + expect(out).toContain("rebuild-state"); + expect(out).toContain("status"); + expect(out).toContain("next"); + }); + + it("rebuild-state writes _state.json", () => { + const out = run("rebuild-state"); + expect(out).toContain("Rebuilt"); + expect(out).toContain("epic"); + }); + + it("status prints a tree", () => { + const out = run("status"); + // We expect at least one epic-line marker character + expect(out).toMatch(/[✓→○]/); + }); + + it("next prints the next non-done story OR confirms all done", () => { + const out = run("next"); + expect(out.length).toBeGreaterThan(0); + }); +}); From 9858d49787c85ce5cd5121143e77d15d53743793 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:47:03 +0200 Subject: [PATCH 4/5] feat: wire pnpm work CLI + initial _state.json snapshot --- docs/work/_state.json | 107 ++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 108 insertions(+) create mode 100644 docs/work/_state.json diff --git a/docs/work/_state.json b/docs/work/_state.json new file mode 100644 index 0000000..0917b79 --- /dev/null +++ b/docs/work/_state.json @@ -0,0 +1,107 @@ +{ + "updated_at": "2026-05-13T05:46:57.686Z", + "epics": { + "agent-workflow-docs-v1": { + "status": "done", + "title": "Agent-workflow docs rollout", + "stories": { + "01-docs-rewrite": { + "status": "done", + "title": "Surface conformance system across top-level docs", + "ac_total": 8, + "ac_completed": 8 + } + } + }, + "conformance-hardening-v1": { + "status": "done", + "title": "Conformance hardening v1 — AST manifest parsing + dev-seed boot assertion", + "stories": { + "01-ast-manifest-source": { + "status": "done", + "title": "Replace regex manifest source parser with AST", + "ac_total": 5, + "ac_completed": 5 + }, + "02-dev-seed-assertion": { + "status": "done", + "title": "Extend assertFeatureConformance to all bind-dev-seed paths", + "ac_total": 6, + "ac_completed": 6 + } + } + }, + "conformance-system-v1": { + "status": "done", + "title": "Conformance system v1", + "stories": { + "01-define-feature-helper": { + "status": "done", + "title": "defineFeature helper + Instrumented/Captured/Audited brands", + "ac_total": 9, + "ac_completed": 9 + }, + "02-boot-assertions": { + "status": "done", + "title": "assertFeatureConformance + boot wiring", + "ac_total": 11, + "ac_completed": 11 + }, + "03-a-structural-eslint-rules": { + "status": "done", + "title": "Structural ESLint rules (feature-must-have-manifest, usecase-must-have-test-file, required-cores-installed)", + "ac_total": 10, + "ac_completed": 10 + }, + "03-b-ast-eslint-rules": { + "status": "done", + "title": "AST-aware ESLint rules (no-undeclared-event-publish, no-undeclared-audit)", + "ac_total": 8, + "ac_completed": 8 + }, + "04-ci-drift-gate": { + "status": "done", + "title": "CI drift gate — pnpm conformance with cross-feature event closure", + "ac_total": 6, + "ac_completed": 6 + }, + "05-generator-updates": { + "status": "done", + "title": "Generator updates — emit feature.manifest.ts + self-asserting bind-production", + "ac_total": 7, + "ac_completed": 7 + }, + "06-feature-migrations": { + "status": "done", + "title": "Migrate blog/media/navigation/marketing-pages to conformance pattern", + "ac_total": 7, + "ac_completed": 7 + } + } + }, + "frontend-conformance-v1": { + "status": "done", + "title": "Frontend conformance rules v1", + "stories": { + "01-frontend-rules": { + "status": "done", + "title": "Three structural frontend conformance ESLint rules", + "ac_total": 6, + "ac_completed": 6 + } + } + }, + "work-system-v1": { + "status": "in-progress", + "title": "Work system v1 (MVP) — state tracking + pnpm work CLI", + "stories": { + "01-state-builder-and-cli": { + "status": "in-progress", + "title": "State builder + pnpm work CLI", + "ac_total": 5, + "ac_completed": 0 + } + } + } + } +} diff --git a/package.json b/package.json index 8ff87ba..d0d0065 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test:stories": "turbo run test:stories", "typecheck": "turbo run typecheck", "conformance": "node scripts/conformance.mjs", + "work": "node scripts/work/cli.mjs", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"" }, From ee315352ffd2c45fde2b7612e4f5ef83c5115e41 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:47:33 +0200 Subject: [PATCH 5/5] docs(work): close work-system-v1 epic --- docs/work/_state.json | 8 ++++---- .../01-state-builder-and-cli/_story.md | 12 ++++++------ docs/work/work-system-v1/_epic.md | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/work/_state.json b/docs/work/_state.json index 0917b79..118e279 100644 --- a/docs/work/_state.json +++ b/docs/work/_state.json @@ -1,5 +1,5 @@ { - "updated_at": "2026-05-13T05:46:57.686Z", + "updated_at": "2026-05-13T05:47:28.219Z", "epics": { "agent-workflow-docs-v1": { "status": "done", @@ -92,14 +92,14 @@ } }, "work-system-v1": { - "status": "in-progress", + "status": "done", "title": "Work system v1 (MVP) — state tracking + pnpm work CLI", "stories": { "01-state-builder-and-cli": { - "status": "in-progress", + "status": "done", "title": "State builder + pnpm work CLI", "ac_total": 5, - "ac_completed": 0 + "ac_completed": 5 } } } diff --git a/docs/work/work-system-v1/01-state-builder-and-cli/_story.md b/docs/work/work-system-v1/01-state-builder-and-cli/_story.md index 587d392..a6392f0 100644 --- a/docs/work/work-system-v1/01-state-builder-and-cli/_story.md +++ b/docs/work/work-system-v1/01-state-builder-and-cli/_story.md @@ -3,7 +3,7 @@ id: 01-state-builder-and-cli epic: work-system-v1 title: State builder + pnpm work CLI type: technical-story -status: in-progress +status: done feature: scripts depends-on: [] blocks: [] @@ -26,8 +26,8 @@ regenerates `_state.json` from markdown. - Dependency-graph analysis ## Tasks -- [ ] Epic + story scaffold -- [ ] state-builder.mjs + tests -- [ ] cli.mjs + tests -- [ ] _state.json initial commit + pnpm work script entry -- [ ] Final verification + closeout +- [x] Epic + story scaffold +- [x] state-builder.mjs + tests +- [x] cli.mjs + tests +- [x] _state.json initial commit + pnpm work script entry +- [x] Final verification + closeout diff --git a/docs/work/work-system-v1/_epic.md b/docs/work/work-system-v1/_epic.md index 0a16ef4..3a85c70 100644 --- a/docs/work/work-system-v1/_epic.md +++ b/docs/work/work-system-v1/_epic.md @@ -3,7 +3,7 @@ id: work-system-v1 prd: null title: Work system v1 (MVP) — state tracking + pnpm work CLI type: epic -status: in-progress +status: done features: [scripts] created: 2026-05-13 --- @@ -25,4 +25,4 @@ work?" and "where are we?" — without scanning every story file by hand. - Pre-commit hooks (the state file is rebuilt manually for now) ## Stories -- [ ] [01 — State builder + CLI](01-state-builder-and-cli/_story.md) +- [x] [01 — State builder + CLI](01-state-builder-and-cli/_story.md)