From adabb3428de67cf6e493ab5b1308e1cd68472ebc Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:59:53 +0200 Subject: [PATCH 1/4] docs(work): scaffold dag-and-readiness-v1 epic + stories --- docs/work/_state.json | 26 ++++++++++++++++++- .../01-extended-state/_story.md | 17 ++++++++++++ .../02-dag-computation/_story.md | 16 ++++++++++++ .../03-cli-subcommands/_story.md | 18 +++++++++++++ docs/work/dag-and-readiness-v1/_epic.md | 21 +++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 docs/work/dag-and-readiness-v1/01-extended-state/_story.md create mode 100644 docs/work/dag-and-readiness-v1/02-dag-computation/_story.md create mode 100644 docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md create mode 100644 docs/work/dag-and-readiness-v1/_epic.md diff --git a/docs/work/_state.json b/docs/work/_state.json index 4e9b00f..2f3d122 100644 --- a/docs/work/_state.json +++ b/docs/work/_state.json @@ -1,5 +1,5 @@ { - "updated_at": "2026-05-13T05:56:35.878Z", + "updated_at": "2026-05-13T05:59:55.392Z", "epics": { "agent-workflow-docs-v1": { "status": "done", @@ -79,6 +79,30 @@ } } }, + "dag-and-readiness-v1": { + "status": "in-progress", + "title": "DAG + readiness — depends-on / blocks parsing", + "stories": { + "01-extended-state": { + "status": "in-progress", + "title": "Frontmatter array parsing + extended state shape", + "ac_total": 4, + "ac_completed": 0 + }, + "02-dag-computation": { + "status": "todo", + "title": "DAG computation — ready + blocked story sets", + "ac_total": 3, + "ac_completed": 0 + }, + "03-cli-subcommands": { + "status": "todo", + "title": "CLI ready / blocked subcommands", + "ac_total": 5, + "ac_completed": 0 + } + } + }, "frontend-conformance-v1": { "status": "done", "title": "Frontend conformance rules v1", diff --git a/docs/work/dag-and-readiness-v1/01-extended-state/_story.md b/docs/work/dag-and-readiness-v1/01-extended-state/_story.md new file mode 100644 index 0000000..15f4001 --- /dev/null +++ b/docs/work/dag-and-readiness-v1/01-extended-state/_story.md @@ -0,0 +1,17 @@ +--- +id: 01-extended-state +epic: dag-and-readiness-v1 +title: Frontmatter array parsing + extended state shape +type: technical-story +status: in-progress +feature: scripts +depends-on: [] +blocks: [02-dag-computation] +--- + +## Tasks + +- [ ] Story scaffold +- [ ] Extend parseFrontmatter to parse array values like `depends-on: [a, b]` +- [ ] Extend buildState to include depends_on + blocks per story entry +- [ ] Update existing tests; add new array-parsing tests diff --git a/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md b/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md new file mode 100644 index 0000000..b9136a0 --- /dev/null +++ b/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md @@ -0,0 +1,16 @@ +--- +id: 02-dag-computation +epic: dag-and-readiness-v1 +title: DAG computation — ready + blocked story sets +type: technical-story +status: todo +feature: scripts +depends-on: [01-extended-state] +blocks: [03-cli-subcommands] +--- + +## Tasks + +- [ ] computeReadyBlocked(state) — pure function +- [ ] Tests +- [ ] Wire into buildState output diff --git a/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md b/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md new file mode 100644 index 0000000..f56ce95 --- /dev/null +++ b/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md @@ -0,0 +1,18 @@ +--- +id: 03-cli-subcommands +epic: dag-and-readiness-v1 +title: CLI ready / blocked subcommands +type: technical-story +status: todo +feature: scripts +depends-on: [02-dag-computation] +blocks: [] +--- + +## Tasks + +- [ ] `pnpm work ready` — prints ready stories +- [ ] `pnpm work blocked` — prints blocked stories with waiting_on +- [ ] Update `pnpm work next` to use the ready set +- [ ] CLI smoke tests +- [ ] Final verification + closeout diff --git a/docs/work/dag-and-readiness-v1/_epic.md b/docs/work/dag-and-readiness-v1/_epic.md new file mode 100644 index 0000000..77842b4 --- /dev/null +++ b/docs/work/dag-and-readiness-v1/_epic.md @@ -0,0 +1,21 @@ +--- +id: dag-and-readiness-v1 +prd: null +title: DAG + readiness — depends-on / blocks parsing +type: epic +status: in-progress +features: [scripts] +created: 2026-05-13 +--- + +## Goal + +Extend the state builder + CLI so `pnpm work next` / `pnpm work ready` / +`pnpm work blocked` answer "what's actually unblocked right now?" +from the depends-on graph instead of just the first non-done story. + +## Stories + +- [ ] [01 — Frontmatter array parsing + extended state shape](01-extended-state/_story.md) +- [ ] [02 — DAG computation: ready + blocked](02-dag-computation/_story.md) +- [ ] [03 — CLI ready / blocked subcommands](03-cli-subcommands/_story.md) From 23fedac1a8b76efcc489c4b410024a4ff026060e Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 08:04:38 +0200 Subject: [PATCH 2/4] feat(scripts): state-builder reads depends-on + blocks from frontmatter --- eslint.config.js | 33 ++++ scripts/work/state-builder.mjs | 91 +++++++++- scripts/work/state-builder.test.mjs | 251 +++++++++++++++++++++++++++- 3 files changed, 363 insertions(+), 12 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..bfe4305 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,33 @@ +/** + * Root ESLint config — covers scripts/ and any other plain-JS files at the + * repo root. Package-level configs (packages/*, apps/*) define their own + * eslint.config.js and take precedence for files inside those directories. + */ +import js from "@eslint/js"; + +// Minimal Node.js globals for scripts — avoids a peer-dep on `globals`. +const nodeGlobals = { + process: "readonly", + console: "readonly", + URL: "readonly", + Buffer: "readonly", + __dirname: "readonly", + __filename: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", +}; + +export default [ + { ignores: ["node_modules/**", "packages/**", "apps/**", "turbo/**"] }, + js.configs.recommended, + { + files: ["scripts/**/*.{mjs,cjs,js}"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: nodeGlobals, + }, + }, +]; diff --git a/scripts/work/state-builder.mjs b/scripts/work/state-builder.mjs index 5a7f42b..5c5158f 100644 --- a/scripts/work/state-builder.mjs +++ b/scripts/work/state-builder.mjs @@ -63,19 +63,84 @@ export function buildState(workRoot) { title: storyMeta.title ?? sub, ac_total: total, ac_completed: completed, + depends_on: Array.isArray(storyMeta["depends-on"]) + ? storyMeta["depends-on"] + : [], + blocks: Array.isArray(storyMeta.blocks) ? storyMeta.blocks : [], }; } state.epics[epicMeta.id ?? entry] = epicEntry; } + const { ready, blocked } = computeReadyBlocked(state); + state.ready = ready; + state.blocked = blocked; + return state; } +/** + * Given a built state object, compute the ready + blocked story sets. + * + * "ready" = not done AND all depends_on stories are done + * "blocked" = not done AND at least one depends_on story is not done + * + * depends_on references are either same-epic (just story id) or cross-epic + * (`/`). + * + * Returns { ready: [...], blocked: [...] } where each entry is: + * { epic, story, title, [waiting_on?] } + */ +export function computeReadyBlocked(state) { + // Build a flat map: "/" => { status, title, depends_on } + const flat = new Map(); + for (const [epic, epicEntry] of Object.entries(state.epics)) { + for (const [story, storyEntry] of Object.entries(epicEntry.stories)) { + flat.set(`${epic}/${story}`, { + epic, + story, + status: storyEntry.status, + title: storyEntry.title, + depends_on: storyEntry.depends_on, + }); + } + } + + // Resolve a depends_on reference (which may be `/` or just ``) + // relative to the current epic. + function resolveRef(ref, currentEpic) { + if (ref.includes("/")) return ref; + return `${currentEpic}/${ref}`; + } + + const ready = []; + const blocked = []; + for (const entry of flat.values()) { + if (entry.status === "done") continue; + const refs = entry.depends_on.map((r) => resolveRef(r, entry.epic)); + const waitingOn = refs.filter((r) => { + const dep = flat.get(r); + return !dep || dep.status !== "done"; + }); + if (waitingOn.length === 0) { + ready.push({ epic: entry.epic, story: entry.story, title: entry.title }); + } else { + blocked.push({ + epic: entry.epic, + story: entry.story, + title: entry.title, + waiting_on: waitingOn, + }); + } + } + return { ready, blocked }; +} + /** * 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). + * and return a flat object of string or array values. Numeric / bool values are + * left as strings; array values like `[a, b, "c"]` are parsed into JS arrays. */ export function parseFrontmatter(filePath) { const src = fs.readFileSync(filePath, "utf8"); @@ -86,8 +151,26 @@ export function parseFrontmatter(filePath) { 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); + // Array values like [a, b, "c"] or [] + if (value.startsWith("[") && value.endsWith("]")) { + const inner = value.slice(1, -1).trim(); + if (inner === "") { + out[m[1]] = []; + } else { + out[m[1]] = inner.split(",").map((s) => { + let v = s.trim(); + if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); + if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1); + return v; + }); + } + continue; + } + // Strings (with optional quotes) + 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; diff --git a/scripts/work/state-builder.test.mjs b/scripts/work/state-builder.test.mjs index 960a4fd..e3dad9d 100644 --- a/scripts/work/state-builder.test.mjs +++ b/scripts/work/state-builder.test.mjs @@ -2,7 +2,12 @@ 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"; +import { + buildState, + parseFrontmatter, + countTaskCheckboxes, + computeReadyBlocked, +} from "./state-builder.mjs"; function makeWorkTree({ epics }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-state-")); @@ -81,8 +86,22 @@ describe("buildState", () => { 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 }, + story1: { + status: "done", + title: "story1", + ac_total: 2, + ac_completed: 2, + depends_on: [], + blocks: [], + }, + story2: { + status: "todo", + title: "story2", + ac_total: 3, + ac_completed: 0, + depends_on: [], + blocks: [], + }, }, }, }); @@ -91,7 +110,10 @@ describe("buildState", () => { 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.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({}); @@ -102,6 +124,56 @@ describe("buildState", () => { fs.mkdirSync(path.join(root, "incomplete"), { recursive: true }); expect(buildState(root).epics).toEqual({}); }); + + it("parses depends-on and blocks frontmatter arrays", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-dep-")); + const epicDir = path.join(root, "epic1"); + fs.mkdirSync(epicDir, { recursive: true }); + fs.writeFileSync( + path.join(epicDir, "_epic.md"), + `--- +id: epic1 +title: epic1 +status: todo +--- +`, + ); + const story1Dir = path.join(epicDir, "story1"); + fs.mkdirSync(story1Dir, { recursive: true }); + fs.writeFileSync( + path.join(story1Dir, "_story.md"), + `--- +id: story1 +title: story1 +status: todo +depends-on: [] +blocks: [story2, other-epic/x] +--- +`, + ); + const story2Dir = path.join(epicDir, "story2"); + fs.mkdirSync(story2Dir, { recursive: true }); + fs.writeFileSync( + path.join(story2Dir, "_story.md"), + `--- +id: story2 +title: story2 +status: todo +depends-on: [story1] +blocks: [] +--- +`, + ); + + const state = buildState(root); + expect(state.epics.epic1.stories.story1.depends_on).toEqual([]); + expect(state.epics.epic1.stories.story1.blocks).toEqual([ + "story2", + "other-epic/x", + ]); + expect(state.epics.epic1.stories.story2.depends_on).toEqual(["story1"]); + expect(state.epics.epic1.stories.story2.blocks).toEqual([]); + }); }); describe("countTaskCheckboxes", () => { @@ -125,7 +197,10 @@ describe("countTaskCheckboxes", () => { }); it("returns zeros when no Tasks section exists", () => { - expect(countTaskCheckboxes(`## Goal\n\nFoo\n`)).toEqual({ total: 0, completed: 0 }); + expect(countTaskCheckboxes(`## Goal\n\nFoo\n`)).toEqual({ + total: 0, + completed: 0, + }); }); }); @@ -133,14 +208,21 @@ 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, `--- + fs.writeFileSync( + fp, + `--- id: x title: A title status: done --- -Body`); - expect(parseFrontmatter(fp)).toEqual({ 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", () => { @@ -149,4 +231,157 @@ Body`); fs.writeFileSync(fp, "# Just a heading\n"); expect(parseFrontmatter(fp)).toEqual({}); }); + + it("parses array frontmatter values", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-arr-")); + const fp = path.join(dir, "f.md"); + fs.writeFileSync( + fp, + `--- +id: x +items: [a, b, "c"] +empty: [] +--- +`, + ); + expect(parseFrontmatter(fp)).toEqual({ + id: "x", + items: ["a", "b", "c"], + empty: [], + }); + }); +}); + +describe("computeReadyBlocked", () => { + it("returns ready for stories whose depends_on are all done", () => { + const state = { + updated_at: "x", + epics: { + e: { + status: "in-progress", + title: "e", + stories: { + a: { + status: "done", + title: "a", + ac_total: 0, + ac_completed: 0, + depends_on: [], + blocks: [], + }, + b: { + status: "todo", + title: "b", + ac_total: 0, + ac_completed: 0, + depends_on: ["a"], + blocks: [], + }, + }, + }, + }, + }; + const { ready, blocked } = computeReadyBlocked(state); + expect(ready).toEqual([{ epic: "e", story: "b", title: "b" }]); + expect(blocked).toEqual([]); + }); + + it("returns blocked for stories whose depends_on are not done", () => { + const state = { + updated_at: "x", + epics: { + e: { + status: "in-progress", + title: "e", + stories: { + a: { + status: "todo", + title: "a", + ac_total: 0, + ac_completed: 0, + depends_on: [], + blocks: [], + }, + b: { + status: "todo", + title: "b", + ac_total: 0, + ac_completed: 0, + depends_on: ["a"], + blocks: [], + }, + }, + }, + }, + }; + const { ready, blocked } = computeReadyBlocked(state); + expect(ready).toEqual([{ epic: "e", story: "a", title: "a" }]); + expect(blocked).toEqual([ + { epic: "e", story: "b", title: "b", waiting_on: ["e/a"] }, + ]); + }); + + it("resolves cross-epic refs", () => { + const state = { + updated_at: "x", + epics: { + e1: { + status: "done", + title: "e1", + stories: { + a: { + status: "done", + title: "a", + ac_total: 0, + ac_completed: 0, + depends_on: [], + blocks: [], + }, + }, + }, + e2: { + status: "in-progress", + title: "e2", + stories: { + b: { + status: "todo", + title: "b", + ac_total: 0, + ac_completed: 0, + depends_on: ["e1/a"], + blocks: [], + }, + }, + }, + }, + }; + const { ready, blocked } = computeReadyBlocked(state); + expect(ready).toEqual([{ epic: "e2", story: "b", title: "b" }]); + expect(blocked).toEqual([]); + }); + + it("skips done stories", () => { + const state = { + updated_at: "x", + epics: { + e: { + status: "done", + title: "e", + stories: { + a: { + status: "done", + title: "a", + ac_total: 1, + ac_completed: 1, + depends_on: [], + blocks: [], + }, + }, + }, + }, + }; + const { ready, blocked } = computeReadyBlocked(state); + expect(ready).toEqual([]); + expect(blocked).toEqual([]); + }); }); From 4cf979aaa56f43a5ff3c3d8c94b2b14a8f8f7e83 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 08:05:19 +0200 Subject: [PATCH 3/4] feat(scripts): pnpm work ready + blocked subcommands, DAG-aware next --- scripts/work/cli.mjs | 72 ++++++++++++++++++++++++++++++--------- scripts/work/cli.test.mjs | 12 +++++++ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/scripts/work/cli.mjs b/scripts/work/cli.mjs index 6440349..504088a 100644 --- a/scripts/work/cli.mjs +++ b/scripts/work/cli.mjs @@ -20,7 +20,9 @@ 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).`); + console.log( + `Rebuilt ${path.relative(REPO_ROOT, STATE_FILE)} with ${Object.keys(state.epics).length} epic(s).`, + ); } function printStatus() { @@ -41,31 +43,67 @@ function printStatus() { { total: 0, done: 0 }, ); const epicMark = mark(epic.status); - console.log(`${epicMark} ${epicId} — ${epic.title} (${storyTotals.done}/${storyTotals.total} tasks done)`); + 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}`); + 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; + if (state.ready.length === 0) { + if (state.blocked.length > 0) { + console.log("No ready stories. Blocked:"); + for (const b of state.blocked) { + console.log( + ` ${b.epic} / ${b.story} — waiting on: ${b.waiting_on.join(", ")}`, + ); } + } else { + console.log("All epics + stories are done. ✓"); } + return; + } + const r = state.ready[0]; + console.log(`${r.epic} / ${r.story} — ${r.title}`); + console.log(` (use \`pnpm work ready\` to see all ready stories)`); +} + +function printReady() { + const state = buildState(WORK_ROOT); + if (state.ready.length === 0) { + console.log( + "No ready stories. Run `pnpm work blocked` to see what's waiting on what.", + ); + return; + } + console.log( + `${state.ready.length} ready stor${state.ready.length === 1 ? "y" : "ies"}:`, + ); + for (const r of state.ready) { + console.log(` ${r.epic} / ${r.story} — ${r.title}`); + } +} + +function printBlocked() { + const state = buildState(WORK_ROOT); + if (state.blocked.length === 0) { + console.log("No blocked stories."); + return; + } + console.log( + `${state.blocked.length} blocked stor${state.blocked.length === 1 ? "y" : "ies"}:`, + ); + for (const b of state.blocked) { + console.log(` ${b.epic} / ${b.story} — ${b.title}`); + console.log(` waiting on: ${b.waiting_on.join(", ")}`); } - console.log("All epics + stories are done. ✓"); } function mark(status) { @@ -76,7 +114,7 @@ function mark(status) { } function usage() { - console.log("Usage: pnpm work "); + console.log("Usage: pnpm work "); process.exit(2); } @@ -84,4 +122,6 @@ const cmd = process.argv[2]; if (cmd === "rebuild-state") rebuildState(); else if (cmd === "status") printStatus(); else if (cmd === "next") printNext(); +else if (cmd === "ready") printReady(); +else if (cmd === "blocked") printBlocked(); else usage(); diff --git a/scripts/work/cli.test.mjs b/scripts/work/cli.test.mjs index 901289f..e41a037 100644 --- a/scripts/work/cli.test.mjs +++ b/scripts/work/cli.test.mjs @@ -21,6 +21,8 @@ describe("pnpm work cli", () => { expect(out).toContain("rebuild-state"); expect(out).toContain("status"); expect(out).toContain("next"); + expect(out).toContain("ready"); + expect(out).toContain("blocked"); }); it("rebuild-state writes _state.json", () => { @@ -39,4 +41,14 @@ describe("pnpm work cli", () => { const out = run("next"); expect(out.length).toBeGreaterThan(0); }); + + it("ready prints something", () => { + const out = run("ready"); + expect(out.length).toBeGreaterThan(0); + }); + + it("blocked prints something", () => { + const out = run("blocked"); + expect(out.length).toBeGreaterThan(0); + }); }); From e92610e7f19f45e43f72441822292f4057e990e3 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 08:06:21 +0200 Subject: [PATCH 4/4] docs(work): close dag-and-readiness-v1 epic --- docs/work/_state.json | 130 ++++++++++++++---- .../01-extended-state/_story.md | 10 +- .../02-dag-computation/_story.md | 8 +- .../03-cli-subcommands/_story.md | 12 +- docs/work/dag-and-readiness-v1/_epic.md | 8 +- 5 files changed, 125 insertions(+), 43 deletions(-) diff --git a/docs/work/_state.json b/docs/work/_state.json index 2f3d122..35c5fd9 100644 --- a/docs/work/_state.json +++ b/docs/work/_state.json @@ -1,5 +1,5 @@ { - "updated_at": "2026-05-13T05:59:55.392Z", + "updated_at": "2026-05-13T06:06:23.176Z", "epics": { "agent-workflow-docs-v1": { "status": "done", @@ -9,7 +9,9 @@ "status": "done", "title": "Surface conformance system across top-level docs", "ac_total": 8, - "ac_completed": 8 + "ac_completed": 8, + "depends_on": [], + "blocks": [] } } }, @@ -21,13 +23,21 @@ "status": "done", "title": "Replace regex manifest source parser with AST", "ac_total": 5, - "ac_completed": 5 + "ac_completed": 5, + "depends_on": [], + "blocks": [ + "02-dev-seed-assertion" + ] }, "02-dev-seed-assertion": { "status": "done", "title": "Extend assertFeatureConformance to all bind-dev-seed paths", "ac_total": 6, - "ac_completed": 6 + "ac_completed": 6, + "depends_on": [ + "01-ast-manifest-source" + ], + "blocks": [] } } }, @@ -39,67 +49,119 @@ "status": "done", "title": "defineFeature helper + Instrumented/Captured/Audited brands", "ac_total": 9, - "ac_completed": 9 + "ac_completed": 9, + "depends_on": [], + "blocks": [ + "02-boot-assertions" + ] }, "02-boot-assertions": { "status": "done", "title": "assertFeatureConformance + boot wiring", "ac_total": 11, - "ac_completed": 11 + "ac_completed": 11, + "depends_on": [ + "01-define-feature-helper" + ], + "blocks": [ + "03-eslint-rules" + ] }, "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 + "ac_completed": 10, + "depends_on": [ + "02-boot-assertions" + ], + "blocks": [ + "03-b-ast-aware-eslint-rules" + ] }, "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 + "ac_completed": 8, + "depends_on": [ + "03-a-structural-eslint-rules" + ], + "blocks": [ + "04-ci-drift-gate" + ] }, "04-ci-drift-gate": { "status": "done", "title": "CI drift gate — pnpm conformance with cross-feature event closure", "ac_total": 6, - "ac_completed": 6 + "ac_completed": 6, + "depends_on": [ + "03-b-ast-eslint-rules" + ], + "blocks": [ + "05-generator-updates" + ] }, "05-generator-updates": { "status": "done", "title": "Generator updates — emit feature.manifest.ts + self-asserting bind-production", "ac_total": 7, - "ac_completed": 7 + "ac_completed": 7, + "depends_on": [ + "04-ci-drift-gate" + ], + "blocks": [ + "06-feature-migrations" + ] }, "06-feature-migrations": { "status": "done", "title": "Migrate blog/media/navigation/marketing-pages to conformance pattern", "ac_total": 7, - "ac_completed": 7 + "ac_completed": 7, + "depends_on": [ + "05-generator-updates" + ], + "blocks": [] } } }, "dag-and-readiness-v1": { - "status": "in-progress", + "status": "done", "title": "DAG + readiness — depends-on / blocks parsing", "stories": { "01-extended-state": { - "status": "in-progress", + "status": "done", "title": "Frontmatter array parsing + extended state shape", "ac_total": 4, - "ac_completed": 0 + "ac_completed": 4, + "depends_on": [], + "blocks": [ + "02-dag-computation" + ] }, "02-dag-computation": { - "status": "todo", + "status": "done", "title": "DAG computation — ready + blocked story sets", "ac_total": 3, - "ac_completed": 0 + "ac_completed": 3, + "depends_on": [ + "01-extended-state" + ], + "blocks": [ + "03-cli-subcommands" + ] }, "03-cli-subcommands": { - "status": "todo", + "status": "done", "title": "CLI ready / blocked subcommands", "ac_total": 5, - "ac_completed": 0 + "ac_completed": 5, + "depends_on": [ + "02-dag-computation" + ], + "blocks": [] } } }, @@ -111,7 +173,9 @@ "status": "done", "title": "Three structural frontend conformance ESLint rules", "ac_total": 6, - "ac_completed": 6 + "ac_completed": 6, + "depends_on": [], + "blocks": [] } } }, @@ -123,19 +187,33 @@ "status": "done", "title": "Install husky + lint-staged", "ac_total": 3, - "ac_completed": 3 + "ac_completed": 3, + "depends_on": [], + "blocks": [ + "02-pre-commit-hook" + ] }, "02-pre-commit-hook": { "status": "done", "title": "Pre-commit hook script", "ac_total": 2, - "ac_completed": 2 + "ac_completed": 2, + "depends_on": [ + "01-husky-install" + ], + "blocks": [ + "03-state-sync-guard" + ] }, "03-state-sync-guard": { "status": "done", "title": "State-sync guard + tests", "ac_total": 4, - "ac_completed": 4 + "ac_completed": 4, + "depends_on": [ + "02-pre-commit-hook" + ], + "blocks": [] } } }, @@ -147,9 +225,13 @@ "status": "done", "title": "State builder + pnpm work CLI", "ac_total": 5, - "ac_completed": 5 + "ac_completed": 5, + "depends_on": [], + "blocks": [] } } } - } + }, + "ready": [], + "blocked": [] } diff --git a/docs/work/dag-and-readiness-v1/01-extended-state/_story.md b/docs/work/dag-and-readiness-v1/01-extended-state/_story.md index 15f4001..6fcae89 100644 --- a/docs/work/dag-and-readiness-v1/01-extended-state/_story.md +++ b/docs/work/dag-and-readiness-v1/01-extended-state/_story.md @@ -3,7 +3,7 @@ id: 01-extended-state epic: dag-and-readiness-v1 title: Frontmatter array parsing + extended state shape type: technical-story -status: in-progress +status: done feature: scripts depends-on: [] blocks: [02-dag-computation] @@ -11,7 +11,7 @@ blocks: [02-dag-computation] ## Tasks -- [ ] Story scaffold -- [ ] Extend parseFrontmatter to parse array values like `depends-on: [a, b]` -- [ ] Extend buildState to include depends_on + blocks per story entry -- [ ] Update existing tests; add new array-parsing tests +- [x] Story scaffold +- [x] Extend parseFrontmatter to parse array values like `depends-on: [a, b]` +- [x] Extend buildState to include depends_on + blocks per story entry +- [x] Update existing tests; add new array-parsing tests diff --git a/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md b/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md index b9136a0..1a100d0 100644 --- a/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md +++ b/docs/work/dag-and-readiness-v1/02-dag-computation/_story.md @@ -3,7 +3,7 @@ id: 02-dag-computation epic: dag-and-readiness-v1 title: DAG computation — ready + blocked story sets type: technical-story -status: todo +status: done feature: scripts depends-on: [01-extended-state] blocks: [03-cli-subcommands] @@ -11,6 +11,6 @@ blocks: [03-cli-subcommands] ## Tasks -- [ ] computeReadyBlocked(state) — pure function -- [ ] Tests -- [ ] Wire into buildState output +- [x] computeReadyBlocked(state) — pure function +- [x] Tests +- [x] Wire into buildState output diff --git a/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md b/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md index f56ce95..9d69491 100644 --- a/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md +++ b/docs/work/dag-and-readiness-v1/03-cli-subcommands/_story.md @@ -3,7 +3,7 @@ id: 03-cli-subcommands epic: dag-and-readiness-v1 title: CLI ready / blocked subcommands type: technical-story -status: todo +status: done feature: scripts depends-on: [02-dag-computation] blocks: [] @@ -11,8 +11,8 @@ blocks: [] ## Tasks -- [ ] `pnpm work ready` — prints ready stories -- [ ] `pnpm work blocked` — prints blocked stories with waiting_on -- [ ] Update `pnpm work next` to use the ready set -- [ ] CLI smoke tests -- [ ] Final verification + closeout +- [x] `pnpm work ready` — prints ready stories +- [x] `pnpm work blocked` — prints blocked stories with waiting_on +- [x] Update `pnpm work next` to use the ready set +- [x] CLI smoke tests +- [x] Final verification + closeout diff --git a/docs/work/dag-and-readiness-v1/_epic.md b/docs/work/dag-and-readiness-v1/_epic.md index 77842b4..70902e5 100644 --- a/docs/work/dag-and-readiness-v1/_epic.md +++ b/docs/work/dag-and-readiness-v1/_epic.md @@ -3,7 +3,7 @@ id: dag-and-readiness-v1 prd: null title: DAG + readiness — depends-on / blocks parsing type: epic -status: in-progress +status: done features: [scripts] created: 2026-05-13 --- @@ -16,6 +16,6 @@ from the depends-on graph instead of just the first non-done story. ## Stories -- [ ] [01 — Frontmatter array parsing + extended state shape](01-extended-state/_story.md) -- [ ] [02 — DAG computation: ready + blocked](02-dag-computation/_story.md) -- [ ] [03 — CLI ready / blocked subcommands](03-cli-subcommands/_story.md) +- [x] [01 — Frontmatter array parsing + extended state shape](01-extended-state/_story.md) +- [x] [02 — DAG computation: ready + blocked](02-dag-computation/_story.md) +- [x] [03 — CLI ready / blocked subcommands](03-cli-subcommands/_story.md)