feat(work): auto-tick epic story bullets when a story finishes

- Add tickStoryBulletInEpic(workRoot, epic, story) helper that finds
  the bullet in the parent epic's `## Stories` section linking to the
  given story folder and flips `- [ ]` to `- [x]`. Idempotent.
- applyApprovedState now ticks the parent epic bullet whenever a
  story flips to status: done (alongside the existing per-task tick
  and epic-status flip). Epic file gets staged on either ticked-or-
  flipped, not just flipped.
- Backfill all 3 existing epics: 21 bullets ticked to match their
  already-done story statuses (binder-wrap-helper x3, library-
  evaluation-policy x9, ci-security-and-supply-chain x9).
This commit is contained in:
2026-05-14 21:05:06 +02:00
parent dd58974067
commit 3fc5c0f1ca
5 changed files with 59 additions and 23 deletions

View File

@@ -18,6 +18,6 @@ Five of `pnpm fallow`'s top-ten clone groups come from binder pairs across featu
## Stories
- [ ] [01 — Introduce `wireUseCase` helper in core-shared](01-wire-use-case-helper/_story.md)
- [ ] [02 — Migrate all five feature binders to `wireUseCase`](02-migrate-feature-binders/_story.md)
- [ ] [03 — Update generator templates to emit `wireUseCase` call shape](03-update-generator-templates/_story.md)
- [x] [01 — Introduce `wireUseCase` helper in core-shared](01-wire-use-case-helper/_story.md)
- [x] [02 — Migrate all five feature binders to `wireUseCase`](02-migrate-feature-binders/_story.md)
- [x] [03 — Update generator templates to emit `wireUseCase` call shape](03-update-generator-templates/_story.md)

View File

@@ -18,12 +18,12 @@ The repo's security posture has zero security tooling. ADR-022 + the library-eva
## Stories
- [ ] [01 — Trace schema extensions (socketRisk + lastRevalidated)](01-trace-schema-extensions/_story.md)
- [ ] [02 — Socket integration (skill + CI)](02-socket-integration/_story.md)
- [ ] [03 — Renovate adoption](03-renovate-adoption/_story.md)
- [ ] [04 — Major-bump re-evaluation flow](04-major-bump-reevaluation/_story.md)
- [ ] [05 — Trace revalidation workflow](05-trace-revalidation-workflow/_story.md)
- [ ] [06 — CodeQL workflow + pnpm audit signatures](06-codeql-and-audit-signatures/_story.md)
- [ ] [07 — Gitleaks pre-commit hook](07-gitleaks-precommit/_story.md)
- [ ] [08 — Sandcastle reviewer prompt update](08-reviewer-prompt-update/_story.md)
- [ ] [09 — CI security guide + CLAUDE.md](09-ci-security-guide-and-docs/_story.md)
- [x] [01 — Trace schema extensions (socketRisk + lastRevalidated)](01-trace-schema-extensions/_story.md)
- [x] [02 — Socket integration (skill + CI)](02-socket-integration/_story.md)
- [x] [03 — Renovate adoption](03-renovate-adoption/_story.md)
- [x] [04 — Major-bump re-evaluation flow](04-major-bump-reevaluation/_story.md)
- [x] [05 — Trace revalidation workflow](05-trace-revalidation-workflow/_story.md)
- [x] [06 — CodeQL workflow + pnpm audit signatures](06-codeql-and-audit-signatures/_story.md)
- [x] [07 — Gitleaks pre-commit hook](07-gitleaks-precommit/_story.md)
- [x] [08 — Sandcastle reviewer prompt update](08-reviewer-prompt-update/_story.md)
- [x] [09 — CI security guide + CLAUDE.md](09-ci-security-guide-and-docs/_story.md)

View File

@@ -18,12 +18,12 @@ The repo's narrow third-party surface is uncodified. New dependencies enter via
## Stories
- [ ] [01 — Trace schema module + docs/library-decisions/ foundation](01-trace-schema-foundation/_story.md)
- [ ] [02 — Pre-commit check script](02-pre-commit-check-script/_story.md)
- [ ] [03 — Claude PreToolUse / PostToolUse hooks](03-claude-hooks/_story.md)
- [ ] [04 — evaluate-library skill](04-evaluate-library-skill/_story.md)
- [ ] [05 — Human guide: docs/guides/adding-a-library.md](05-human-guide/_story.md)
- [ ] [06 — Sandcastle reviewer prompt update](06-sandcastle-reviewer-prompt/_story.md)
- [ ] [07 — Generator pre-shipped traces for optional cores](07-generator-pre-shipped-traces/_story.md)
- [ ] [08 — Backfill traces for existing runtime deps](08-backfill-traces/_story.md)
- [ ] [09 — CLAUDE.md Key Conventions bullet](09-claude-md-update/_story.md)
- [x] [01 — Trace schema module + docs/library-decisions/ foundation](01-trace-schema-foundation/_story.md)
- [x] [02 — Pre-commit check script](02-pre-commit-check-script/_story.md)
- [x] [03 — Claude PreToolUse / PostToolUse hooks](03-claude-hooks/_story.md)
- [x] [04 — evaluate-library skill](04-evaluate-library-skill/_story.md)
- [x] [05 — Human guide: docs/guides/adding-a-library.md](05-human-guide/_story.md)
- [x] [06 — Sandcastle reviewer prompt update](06-sandcastle-reviewer-prompt/_story.md)
- [x] [07 — Generator pre-shipped traces for optional cores](07-generator-pre-shipped-traces/_story.md)
- [x] [08 — Backfill traces for existing runtime deps](08-backfill-traces/_story.md)
- [x] [09 — CLAUDE.md Key Conventions bullet](09-claude-md-update/_story.md)

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-05-14T18:17:30.697Z",
"updated_at": "2026-05-14T19:05:08.479Z",
"epics": {
"2026-05-13-binder-wrap-helper": {
"status": "done",

View File

@@ -191,6 +191,40 @@ export function readFrontmatterStatus(content) {
return m ? m[1].trim() : null;
}
/**
* Tick the bullet in an epic's `## Stories` section that links to the given
* story folder. Idempotent: returns false if the bullet is already ticked or
* not present. Mirrors the per-task tick that already happens inside story
* files, applied at the parent-epic granularity.
*/
export function tickStoryBulletInEpic(workRoot, epicId, storyId) {
const epicFile = path.join(workRoot, epicId, "_epic.md");
if (!fs.existsSync(epicFile)) return false;
const content = fs.readFileSync(epicFile, "utf8");
const lines = content.split("\n");
let inStories = false;
let changed = false;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("## ")) {
inStories = /^##\s+Stories\b/i.test(lines[i]);
continue;
}
if (!inStories) continue;
if (
lines[i].includes(`(${storyId}/_story.md)`) ||
lines[i].includes(`(./${storyId}/_story.md)`)
) {
if (/\[\s\]/.test(lines[i])) {
lines[i] = lines[i].replace(/\[\s\]/, "[x]");
changed = true;
}
break;
}
}
if (changed) fs.writeFileSync(epicFile, lines.join("\n"));
return changed;
}
/**
* If all stories in an epic are `status: done`, flip the epic's own
* frontmatter to `status: done`. Returns true if it flipped, false otherwise.
@@ -457,12 +491,14 @@ function applyApprovedState(next) {
fs.writeFileSync(next.storyPath, content);
let epicFlipped = false;
let epicBulletTicked = false;
if (storyFlipped) {
epicBulletTicked = tickStoryBulletInEpic(WORK_ROOT, next.epic, next.story);
epicFlipped = flipEpicDoneIfAllStoriesDone(WORK_ROOT, next.epic);
}
const filesToStage = [path.relative(REPO_ROOT, next.storyPath)];
if (epicFlipped) {
if (epicFlipped || epicBulletTicked) {
filesToStage.push(
path.relative(REPO_ROOT, path.join(WORK_ROOT, next.epic, "_epic.md")),
);