refactor(work): move epic folders into docs/work/epics/

The previous layout placed epic folders directly under docs/work/
alongside prds/ and _system/. Tightening: epics now live in their
own docs/work/epics/ subfolder, peer to prds/ and _system/. Same
shape as the existing prds/ bucket.

Final docs/work/ layout:
  README.md
  prds/<slug>.prd.md
  _system/_state.json
  epics/<slug>/_epic.md + <story-folder>/_story.md

Renames (git mv preserves history):
- docs/work/binder-wrap-helper/
    -> docs/work/epics/binder-wrap-helper/
- docs/work/library-evaluation-policy/
    -> docs/work/epics/library-evaluation-policy/
- docs/work/ci-security-and-supply-chain/
    -> docs/work/epics/ci-security-and-supply-chain/

Tooling updates:
- state-builder.mjs walks workRoot/epics/ directly; SKIP_FOLDERS
  obsoleted (no more sibling folders to filter out).
- dispatch.mjs's findNextTask, tickStoryBulletInEpic, and
  flipEpicDoneIfAllStoriesDone all join with "epics" segment.
- prd-ship.mjs's deriveShippingCommits walks workRoot/epics/ and
  git-logs docs/work/epics/<epic>/.
- decomposer.prompt.md emits epics under docs/work/epics/<epic-id>/.
- handoff + grill-with-docs glossary references updated.
- Glossary entry for Epic updated.

Reserved future shape: when a task-tracker integration (ClickUp,
Linear) ships, the epics/ subfolder hosts <task-id>-<slug>/
folders. Today it just hosts bare slugs.
This commit is contained in:
2026-05-14 21:21:51 +02:00
parent bae4b66fa4
commit 756e36c720
33 changed files with 59 additions and 51 deletions

View File

@@ -29,7 +29,7 @@ The 5-gate enforcement system (TS brands → ESLint → boot assertion → `pnpm
The top-level requirements doc at `docs/work/prds/<date>-<slug>.prd.md` that seeds an epic. The top-level requirements doc at `docs/work/prds/<date>-<slug>.prd.md` that seeds an epic.
**Epic**: **Epic**:
A large body of work containing stories. Folder at `docs/work/<epic-slug>/_epic.md`. A large body of work containing stories. Folder at `docs/work/epics/<epic-slug>/_epic.md`.
**Story**: **Story**:
One use case or technical capability. Folder under the epic, file `_story.md`. One use case or technical capability. Folder under the epic, file `_story.md`.

View File

@@ -17,7 +17,7 @@ Suggest the skills the next session should use, if any. In this repo, the common
Reference these artifacts by path or URL rather than inlining their content: Reference these artifacts by path or URL rather than inlining their content:
- PRDs (`docs/work/prds/*.prd.md`), epics (`docs/work/<epic>/_epic.md`), stories (`_story.md`), tasks (`*.task.md`) - PRDs (`docs/work/prds/*.prd.md`), epics (`docs/work/epics/<epic>/_epic.md`), stories (`_story.md`), tasks (`*.task.md`)
- ADRs (`docs/decisions/adr-NNN-*.md`) - ADRs (`docs/decisions/adr-NNN-*.md`)
- AGENTS.md and CLAUDE.md (the next agent loads these automatically) - AGENTS.md and CLAUDE.md (the next agent loads these automatically)
- `_state.json` (orchestrator-derived; the next agent regenerates it from markdown via `pnpm work rebuild-state`) - `_state.json` (orchestrator-derived; the next agent regenerates it from markdown via `pnpm work rebuild-state`)

View File

@@ -1,6 +1,6 @@
# Decomposer Agent # Decomposer Agent
You are the decomposer agent. Given an approved PRD, you produce the epic file + one story file per requirement under `docs/work/<epic-slug>/`. Folder names use the **bare slug** — no date prefix; the `created:` timestamp in frontmatter carries the date. Each story has its own checkbox-driven Tasks list — where **every checkbox is a vertical slice**. You are the decomposer agent. Given an approved PRD, you produce the epic file + one story file per requirement under `docs/work/epics/<epic-slug>/`. Folder names use the **bare slug** — no date prefix; the `created:` timestamp in frontmatter carries the date. Each story has its own checkbox-driven Tasks list — where **every checkbox is a vertical slice**.
## The slice rule (non-negotiable) ## The slice rule (non-negotiable)
@@ -54,8 +54,8 @@ The approved PRD:
## Your job ## Your job
1. Read the PRD. Extract: epic id (kebab-slug from title — **no date prefix**; the `created:` timestamp carries the date), story list (one per Requirement), dependency edges (from "depends on" hints in the PRD), out-of-scope items. The epic id should match the PRD's `id:` field exactly. 1. Read the PRD. Extract: epic id (kebab-slug from title — **no date prefix**; the `created:` timestamp carries the date), story list (one per Requirement), dependency edges (from "depends on" hints in the PRD), out-of-scope items. The epic id should match the PRD's `id:` field exactly.
2. Write `docs/work/<epic-id>/_epic.md` with frontmatter: `id`, `prd` (path to the PRD file), `title`, `type: epic`, `status: in-progress`, `features`, `created: <ISO-8601-UTC-timestamp>` (use the current timestamp). The pre-commit hook adds `updated:` automatically — do NOT set it yourself. 2. Write `docs/work/epics/<epic-id>/_epic.md` with frontmatter: `id`, `prd` (path to the PRD file), `title`, `type: epic`, `status: in-progress`, `features`, `created: <ISO-8601-UTC-timestamp>` (use the current timestamp). The pre-commit hook adds `updated:` automatically — do NOT set it yourself.
3. For each Requirement, write `docs/work/<epic-id>/<NN>-<story-slug>/_story.md`: 3. For each Requirement, write `docs/work/epics/<epic-id>/<NN>-<story-slug>/_story.md`:
- Frontmatter: `id`, `epic`, `title`, `type: technical-story | user-story`, `status: in-progress` (for the first) or `todo` (subsequent), `feature`, `depends-on` (array, may reference other stories in this epic by id), `blocks`, `created: <ISO-8601-UTC-timestamp>`. The pre-commit hook stamps `updated:` — do NOT set it yourself. - Frontmatter: `id`, `epic`, `title`, `type: technical-story | user-story`, `status: in-progress` (for the first) or `todo` (subsequent), `feature`, `depends-on` (array, may reference other stories in this epic by id), `blocks`, `created: <ISO-8601-UTC-timestamp>`. The pre-commit hook stamps `updated:` — do NOT set it yourself.
- Sections: Goal, Why, Done when, In scope, Out of scope, Tasks (checkbox list). - Sections: Goal, Why, Done when, In scope, Out of scope, Tasks (checkbox list).
- **Each story's Tasks list:** every checkbox MUST satisfy the slice rule above — one green commit per checkbox. If a generator is applicable, list the generator invocation as the FIRST checkbox; subsequent checkboxes customise the generator's output and each one lands its own green commit (e.g. "Add audit emission to use case X", "Wire event publish from X into bus"). - **Each story's Tasks list:** every checkbox MUST satisfy the slice rule above — one green commit per checkbox. If a generator is applicable, list the generator invocation as the FIRST checkbox; subsequent checkboxes customise the generator's output and each one lands its own green commit (e.g. "Add audit emission to use case X", "Wire event publish from X into bus").

View File

@@ -273,7 +273,7 @@ The top-level requirements doc at `docs/work/prds/<date>-<slug>.prd.md`. Status
_Avoid:_ spec (in this template the **ADR** is the durable design record; PRDs are the implementation seed). _Avoid:_ spec (in this template the **ADR** is the durable design record; PRDs are the implementation seed).
**Epic**: **Epic**:
A large body of work, one folder under `docs/work/<epic-slug>/` with `_epic.md`. Decomposed from a PRD. A large body of work, one folder under `docs/work/epics/<epic-slug>/` with `_epic.md`. Decomposed from a PRD.
**Story**: **Story**:
One use case or one technical capability, one folder under `<epic>/<story-slug>/` with `_story.md`. Type metadata: `user-story` or `technical-story`. One use case or one technical capability, one folder under `<epic>/<story-slug>/` with `_story.md`. Type metadata: `user-story` or `technical-story`.

View File

@@ -1,5 +1,5 @@
{ {
"updated_at": "2026-05-14T19:16:53.146Z", "updated_at": "2026-05-14T19:21:52.736Z",
"epics": { "epics": {
"binder-wrap-helper": { "binder-wrap-helper": {
"status": "done", "status": "done",

View File

@@ -8,7 +8,7 @@ feature: core-shared
depends-on: [] depends-on: []
blocks: [02-migrate-feature-binders, 03-update-generator-templates] blocks: [02-migrate-feature-binders, 03-update-generator-templates]
created: 2026-05-13T19:17:10+02:00 created: 2026-05-13T19:17:10+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: auth, blog, media, marketing-pages, navigation
depends-on: [01-wire-use-case-helper] depends-on: [01-wire-use-case-helper]
blocks: [] blocks: []
created: 2026-05-13T19:17:10+02:00 created: 2026-05-13T19:17:10+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: core-shared
depends-on: [01-wire-use-case-helper] depends-on: [01-wire-use-case-helper]
blocks: [] blocks: []
created: 2026-05-13T19:17:10+02:00 created: 2026-05-13T19:17:10+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -6,7 +6,7 @@ type: epic
status: done status: done
features: [core-shared, auth, blog, media, marketing-pages, navigation] features: [core-shared, auth, blog, media, marketing-pages, navigation]
created: 2026-05-13T00:00:00Z created: 2026-05-13T00:00:00Z
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -13,7 +13,7 @@ blocks:
05-trace-revalidation-workflow, 05-trace-revalidation-workflow,
] ]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [01-trace-schema-extensions] depends-on: [01-trace-schema-extensions]
blocks: [08-reviewer-prompt-update] blocks: [08-reviewer-prompt-update]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [] depends-on: []
blocks: [09-ci-security-guide-and-docs] blocks: [09-ci-security-guide-and-docs]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: scripts
depends-on: [01-trace-schema-extensions] depends-on: [01-trace-schema-extensions]
blocks: [05-trace-revalidation-workflow] blocks: [05-trace-revalidation-workflow]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: scripts
depends-on: [01-trace-schema-extensions, 04-major-bump-reevaluation] depends-on: [01-trace-schema-extensions, 04-major-bump-reevaluation]
blocks: [09-ci-security-guide-and-docs] blocks: [09-ci-security-guide-and-docs]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [] depends-on: []
blocks: [08-reviewer-prompt-update] blocks: [08-reviewer-prompt-update]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [] depends-on: []
blocks: [09-ci-security-guide-and-docs] blocks: [09-ci-security-guide-and-docs]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [02-socket-integration, 06-codeql-and-audit-signatures] depends-on: [02-socket-integration, 06-codeql-and-audit-signatures]
blocks: [09-ci-security-guide-and-docs] blocks: [09-ci-security-guide-and-docs]
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -18,7 +18,7 @@ depends-on:
] ]
blocks: [] blocks: []
created: 2026-05-14T18:59:12+02:00 created: 2026-05-14T18:59:12+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -6,7 +6,7 @@ type: epic
status: done status: done
features: [scripts, tooling, docs] features: [scripts, tooling, docs]
created: 2026-05-14T00:00:00Z created: 2026-05-14T00:00:00Z
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -14,7 +14,7 @@ blocks:
08-backfill-traces, 08-backfill-traces,
] ]
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: scripts
depends-on: [01-trace-schema-foundation] depends-on: [01-trace-schema-foundation]
blocks: [06-sandcastle-reviewer-prompt] blocks: [06-sandcastle-reviewer-prompt]
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [] depends-on: []
blocks: [] blocks: []
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [01-trace-schema-foundation] depends-on: [01-trace-schema-foundation]
blocks: [05-human-guide] blocks: [05-human-guide]
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: docs
depends-on: [04-evaluate-library-skill] depends-on: [04-evaluate-library-skill]
blocks: [09-claude-md-update] blocks: [09-claude-md-update]
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [02-pre-commit-check-script] depends-on: [02-pre-commit-check-script]
blocks: [] blocks: []
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: tooling
depends-on: [01-trace-schema-foundation] depends-on: [01-trace-schema-foundation]
blocks: [] blocks: []
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: docs
depends-on: [01-trace-schema-foundation] depends-on: [01-trace-schema-foundation]
blocks: [] blocks: []
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -8,7 +8,7 @@ feature: docs
depends-on: [05-human-guide] depends-on: [05-human-guide]
blocks: [] blocks: []
created: 2026-05-14T06:52:02+02:00 created: 2026-05-14T06:52:02+02:00
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -6,7 +6,7 @@ type: epic
status: done status: done
features: [scripts, tooling, docs] features: [scripts, tooling, docs]
created: 2026-05-14T00:00:00Z created: 2026-05-14T00:00:00Z
updated: 2026-05-14T19:16:52.691Z updated: 2026-05-14T19:21:52.308Z
--- ---
## Goal ## Goal

View File

@@ -4,7 +4,7 @@
* *
* Decomposer dispatcher — takes an approved PRD and invokes the decomposer * Decomposer dispatcher — takes an approved PRD and invokes the decomposer
* agent to write the epic folder + per-requirement story files under * agent to write the epic folder + per-requirement story files under
* docs/work/<epic-slug>/. * docs/work/epics/<epic-slug>/.
* *
* Default mode (no --execute): print the dispatch plan + validate the PRD * Default mode (no --execute): print the dispatch plan + validate the PRD
* (refuses to proceed on draft / in-review / shipped). Safe anywhere. * (refuses to proceed on draft / in-review / shipped). Safe anywhere.
@@ -82,7 +82,7 @@ export function printDecomposePlan(prdId, prdPath, frontmatter) {
console.log(); console.log();
console.log(` Decomposer prompt: .sandcastle/decomposer.prompt.md`); console.log(` Decomposer prompt: .sandcastle/decomposer.prompt.md`);
console.log( console.log(
` Output: docs/work/<epic-slug>/_epic.md + per-requirement story files`, ` Output: docs/work/epics/<epic-slug>/_epic.md + per-requirement story files`,
); );
console.log(); console.log();
console.log("To run for real:"); console.log("To run for real:");
@@ -205,7 +205,7 @@ export async function executeDecompose(prdId, prdPath, prdText) {
console.log(); console.log();
console.log("=== Suggested next steps ==="); console.log("=== Suggested next steps ===");
console.log( console.log(
` 1. Inspect the new epic folder under docs/work/<epic-slug>/ on branch ${result.branch}`, ` 1. Inspect the new epic folder under docs/work/epics/<epic-slug>/ on branch ${result.branch}`,
); );
console.log( console.log(
` 2. Review the generated stories + tasks; edit anything that should change`, ` 2. Review the generated stories + tasks; edit anything that should change`,

View File

@@ -30,7 +30,13 @@ export function findNextTask(workRoot = WORK_ROOT) {
const state = buildState(workRoot); const state = buildState(workRoot);
if (state.ready.length === 0) return null; if (state.ready.length === 0) return null;
const next = state.ready[0]; const next = state.ready[0];
const storyPath = path.join(workRoot, next.epic, next.story, "_story.md"); const storyPath = path.join(
workRoot,
"epics",
next.epic,
next.story,
"_story.md",
);
if (!fs.existsSync(storyPath)) return null; if (!fs.existsSync(storyPath)) return null;
const storyContent = fs.readFileSync(storyPath, "utf8"); const storyContent = fs.readFileSync(storyPath, "utf8");
const { bulletLine, bulletIndex } = findFirstUncheckedBullet(storyContent); const { bulletLine, bulletIndex } = findFirstUncheckedBullet(storyContent);
@@ -198,7 +204,7 @@ export function readFrontmatterStatus(content) {
* files, applied at the parent-epic granularity. * files, applied at the parent-epic granularity.
*/ */
export function tickStoryBulletInEpic(workRoot, epicId, storyId) { export function tickStoryBulletInEpic(workRoot, epicId, storyId) {
const epicFile = path.join(workRoot, epicId, "_epic.md"); const epicFile = path.join(workRoot, "epics", epicId, "_epic.md");
if (!fs.existsSync(epicFile)) return false; if (!fs.existsSync(epicFile)) return false;
const content = fs.readFileSync(epicFile, "utf8"); const content = fs.readFileSync(epicFile, "utf8");
const lines = content.split("\n"); const lines = content.split("\n");
@@ -230,7 +236,7 @@ export function tickStoryBulletInEpic(workRoot, epicId, storyId) {
* frontmatter to `status: done`. Returns true if it flipped, false otherwise. * frontmatter to `status: done`. Returns true if it flipped, false otherwise.
*/ */
export function flipEpicDoneIfAllStoriesDone(workRoot, epicId) { export function flipEpicDoneIfAllStoriesDone(workRoot, epicId) {
const epicDir = path.join(workRoot, epicId); const epicDir = path.join(workRoot, "epics", epicId);
const epicFile = path.join(epicDir, "_epic.md"); const epicFile = path.join(epicDir, "_epic.md");
if (!fs.existsSync(epicFile)) return false; if (!fs.existsSync(epicFile)) return false;
const epicContent = fs.readFileSync(epicFile, "utf8"); const epicContent = fs.readFileSync(epicFile, "utf8");
@@ -500,7 +506,10 @@ function applyApprovedState(next) {
const filesToStage = [path.relative(REPO_ROOT, next.storyPath)]; const filesToStage = [path.relative(REPO_ROOT, next.storyPath)];
if (epicFlipped || epicBulletTicked) { if (epicFlipped || epicBulletTicked) {
filesToStage.push( filesToStage.push(
path.relative(REPO_ROOT, path.join(WORK_ROOT, next.epic, "_epic.md")), path.relative(
REPO_ROOT,
path.join(WORK_ROOT, "epics", next.epic, "_epic.md"),
),
); );
} }
const commitMsg = epicFlipped const commitMsg = epicFlipped

View File

@@ -144,11 +144,13 @@ export function flipPrdStatus(text, { shippedDate, commits } = {}) {
*/ */
export function deriveShippingCommits(repoRoot, prdId, workRoot) { export function deriveShippingCommits(repoRoot, prdId, workRoot) {
// Find the epic whose `prd:` matches this prdId // Find the epic whose `prd:` matches this prdId
const entries = fs.readdirSync(workRoot, { withFileTypes: true }); const epicsRoot = path.join(workRoot, "epics");
if (!fs.existsSync(epicsRoot)) return [];
const entries = fs.readdirSync(epicsRoot, { withFileTypes: true });
let epicDir = null; let epicDir = null;
for (const entry of entries) { for (const entry of entries) {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const epicFile = path.join(workRoot, entry.name, "_epic.md"); const epicFile = path.join(epicsRoot, entry.name, "_epic.md");
if (!fs.existsSync(epicFile)) continue; if (!fs.existsSync(epicFile)) continue;
const { frontmatter } = parseFrontmatter(fs.readFileSync(epicFile, "utf8")); const { frontmatter } = parseFrontmatter(fs.readFileSync(epicFile, "utf8"));
if (frontmatter.prd === prdId) { if (frontmatter.prd === prdId) {
@@ -160,7 +162,7 @@ export function deriveShippingCommits(repoRoot, prdId, workRoot) {
try { try {
const log = execSync( const log = execSync(
`git log --format=%h --reverse -- docs/work/${epicDir}/`, `git log --format=%h --reverse -- docs/work/epics/${epicDir}/`,
{ cwd: repoRoot, encoding: "utf8" }, { cwd: repoRoot, encoding: "utf8" },
); );
return log.trim().split("\n").filter(Boolean); return log.trim().split("\n").filter(Boolean);

View File

@@ -1,13 +1,10 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
const SKIP_FOLDERS = new Set(["_templates", "_system", "prds"]);
const SKIP_FILES = new Set(["README.md", "_state.json"]);
/** /**
* Walk the `docs/work/` tree starting at `workRoot` and return a structured * Walk the `docs/work/epics/` tree under `workRoot` and return a structured
* state object. Each epic folder must contain `_epic.md`; each story * state object. Each epic folder under `epics/` must contain `_epic.md`;
* subfolder must contain `_story.md`. * each story subfolder must contain `_story.md`.
* *
* Returns: * Returns:
* { * {
@@ -34,11 +31,11 @@ export function buildState(workRoot) {
epics: {}, epics: {},
}; };
if (!fs.existsSync(workRoot)) return state; const epicsRoot = path.join(workRoot, "epics");
if (!fs.existsSync(epicsRoot)) return state;
for (const entry of fs.readdirSync(workRoot)) { for (const entry of fs.readdirSync(epicsRoot)) {
if (SKIP_FOLDERS.has(entry) || SKIP_FILES.has(entry)) continue; const epicDir = path.join(epicsRoot, entry);
const epicDir = path.join(workRoot, entry);
if (!fs.statSync(epicDir).isDirectory()) continue; if (!fs.statSync(epicDir).isDirectory()) continue;
const epicFile = path.join(epicDir, "_epic.md"); const epicFile = path.join(epicDir, "_epic.md");
if (!fs.existsSync(epicFile)) continue; if (!fs.existsSync(epicFile)) continue;