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