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.
**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**:
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:
- 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`)
- 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`)

View File

@@ -1,6 +1,6 @@
# 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)
@@ -54,8 +54,8 @@ The approved PRD:
## 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.
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.
3. For each Requirement, write `docs/work/<epic-id>/<NN>-<story-slug>/_story.md`:
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/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.
- 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").

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).
**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**:
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": {
"binder-wrap-helper": {
"status": "done",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
*
* Decomposer dispatcher — takes an approved PRD and invokes the decomposer
* 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
* (refuses to proceed on draft / in-review / shipped). Safe anywhere.
@@ -82,7 +82,7 @@ export function printDecomposePlan(prdId, prdPath, frontmatter) {
console.log();
console.log(` Decomposer prompt: .sandcastle/decomposer.prompt.md`);
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("To run for real:");
@@ -205,7 +205,7 @@ export async function executeDecompose(prdId, prdPath, prdText) {
console.log();
console.log("=== Suggested next steps ===");
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(
` 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);
if (state.ready.length === 0) return null;
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;
const storyContent = fs.readFileSync(storyPath, "utf8");
const { bulletLine, bulletIndex } = findFirstUncheckedBullet(storyContent);
@@ -198,7 +204,7 @@ export function readFrontmatterStatus(content) {
* files, applied at the parent-epic granularity.
*/
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;
const content = fs.readFileSync(epicFile, "utf8");
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.
*/
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");
if (!fs.existsSync(epicFile)) return false;
const epicContent = fs.readFileSync(epicFile, "utf8");
@@ -500,7 +506,10 @@ function applyApprovedState(next) {
const filesToStage = [path.relative(REPO_ROOT, next.storyPath)];
if (epicFlipped || epicBulletTicked) {
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

View File

@@ -144,11 +144,13 @@ export function flipPrdStatus(text, { shippedDate, commits } = {}) {
*/
export function deriveShippingCommits(repoRoot, prdId, workRoot) {
// 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;
for (const entry of entries) {
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;
const { frontmatter } = parseFrontmatter(fs.readFileSync(epicFile, "utf8"));
if (frontmatter.prd === prdId) {
@@ -160,7 +162,7 @@ export function deriveShippingCommits(repoRoot, prdId, workRoot) {
try {
const log = execSync(
`git log --format=%h --reverse -- docs/work/${epicDir}/`,
`git log --format=%h --reverse -- docs/work/epics/${epicDir}/`,
{ cwd: repoRoot, encoding: "utf8" },
);
return log.trim().split("\n").filter(Boolean);

View File

@@ -1,13 +1,10 @@
import fs from "node:fs";
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
* state object. Each epic folder must contain `_epic.md`; each story
* subfolder must contain `_story.md`.
* Walk the `docs/work/epics/` tree under `workRoot` and return a structured
* state object. Each epic folder under `epics/` must contain `_epic.md`;
* each story subfolder must contain `_story.md`.
*
* Returns:
* {
@@ -34,11 +31,11 @@ export function buildState(workRoot) {
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)) {
if (SKIP_FOLDERS.has(entry) || SKIP_FILES.has(entry)) continue;
const epicDir = path.join(workRoot, entry);
for (const entry of fs.readdirSync(epicsRoot)) {
const epicDir = path.join(epicsRoot, entry);
if (!fs.statSync(epicDir).isDirectory()) continue;
const epicFile = path.join(epicDir, "_epic.md");
if (!fs.existsSync(epicFile)) continue;