feat(work): pnpm work prd-ship + auto-flip integration in sandcastle
Closes the PRD-lifecycle gap surfaced by the user: when sandcastle
finishes an epic's last task, the seed PRD should auto-flip from
approved -> shipped. Builds the mechanism, wires it into the work
CLI + state index + reviewer prompt + docs.
scripts/work/prd-ship.mjs (new):
- parseFrontmatter / serializeFrontmatter — minimal YAML-ish parser
sufficient for PRD frontmatter (scalar + list shapes)
- flipPrdStatus — pure function: takes PRD text, returns new text
with status=shipped + shipped=<date> + optional shipping-commits.
Refuses to flip draft, idempotent fail-soft on already-shipped,
rejects unexpected statuses
- deriveShippingCommits — best-effort git log of the linked epic
folder for the --auto-commits flag
- findPrdPath — id -> path lookup under docs/work/prds/
- runCli — wiring for `pnpm work prd-ship <id> [--commits|--auto-commits]`
scripts/work/prd-ship.test.mjs (new, 17 tests):
- Frontmatter parser handles scalars + lists + missing frontmatter
- flipPrdStatus covers all transitions + refusals + body/key preservation
- findPrdPath + serializeFrontmatter coverage
scripts/work/state-builder.mjs:
- Epic entries gain a `prd` field
- New computeNeedsPrdShip surfaces epics done with PRD status not yet
shipped: state.needs_prd_ship[] with action commands
scripts/work/cli.mjs:
- New subcommand `pnpm work prd-ship <id>`
.sandcastle/reviewer.prompt.md:
- "Epic close-out: PRD status flip" section instructing reviewer to
check _state.json.needs_prd_ship and run the suggested action
- JSON output extends with prd_shipped: "<id>" | null
docs/work/README.md:
- "PRD lifecycle" section documenting the 4 statuses + auto-flip
Future PRDs follow the lifecycle automatically: decomposer refuses
draft, human flips to approved, sandcastle ships the epic, reviewer
runs prd-ship on the final task, PRD lands as shipped with its
commit trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildState } from "./state-builder.mjs";
|
||||
import { runCli as runPrdShip } from "./prd-ship.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||
@@ -115,10 +116,13 @@ function mark(status) {
|
||||
|
||||
function usage() {
|
||||
console.log(
|
||||
"Usage: pnpm work <rebuild-state|status|next|ready|blocked|dispatch>",
|
||||
"Usage: pnpm work <rebuild-state|status|next|ready|blocked|dispatch|prd-ship>",
|
||||
);
|
||||
console.log(
|
||||
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle)",
|
||||
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle)",
|
||||
);
|
||||
console.log(
|
||||
" prd-ship <id> Flip a PRD's status to `shipped` (run after its epic completes)",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -135,4 +139,10 @@ else if (cmd === "dispatch") {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (cmd === "prd-ship") {
|
||||
const exitCode = runPrdShip(process.argv.slice(3), {
|
||||
repoRoot: REPO_ROOT,
|
||||
workRoot: WORK_ROOT,
|
||||
});
|
||||
process.exit(exitCode);
|
||||
} else usage();
|
||||
|
||||
Reference in New Issue
Block a user