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:
@@ -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`.
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user