@golden-frijoles/kit 0.26.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // emit-epic-kickoff.mjs — print the finished, paste-ready EPIC-MODE kickoff for a groomed epic.
3
+ //
4
+ // ── Why this exists ──────────────────────────────────────────────────────────────────────────────────
5
+ // Epic mode — one orchestrated run across a whole epic, sprint files as internal integration boundaries
6
+ // — is the DEFAULT unit of work. But the only generator that existed was `emit-kickoff.mjs`, which emits
7
+ // a single-sprint prompt, so every epic-mode kickoff was composed BY HAND. Hand-composed prompts are
8
+ // exactly where a process contract erodes: the architecture-lock pass gets summarised away, the review
9
+ // policy reverts to whatever the composing agent remembered, and nobody notices because the prompt still
10
+ // looks complete. This is the sibling generator, and the two now split cleanly:
11
+ //
12
+ // emit-epic-kickoff.mjs → the DEFAULT. One prompt, whole epic, one orchestrator.
13
+ // emit-kickoff.mjs → the EXCEPTION. A single-sprint epic, or a sprint whose outcome genuinely
14
+ // changes the next sprint's scope (so the next kickoff can't be written yet).
15
+ //
16
+ // Same contract as its sibling: it resolves the epic by SEARCHING Roadmap/*/<slug>/ under the project root
17
+ // (lib/kickoff-cli.mjs: --repo-root, else GF_PROJECT_ROOT, else the project around cwd), reads the epic README and EVERY sprint-N.md, substitutes into EPIC_KICKOFF_TEMPLATE (lib/epic-kickoff.mjs)
18
+ // and prints to stdout. It writes no file — read + print, editorial control stays with the caller.
19
+ //
20
+ // The prompt POINTS at the process instead of restating it. WAYS-OF-WORKING → *Epic-mode builds* is the
21
+ // one copy of the doctrine; an earlier template restated it verbatim and the two had already drifted on the
22
+ // review policy. The test fails if a section the template names is missing, so the pointer can't dangle.
23
+ // What stays in the prompt: the few non-negotiables that went missing when prompts were hand-composed, and
24
+ // rules picked from THIS epic's docs (high risk, a migration, a flag) — see buildEpicRules.
25
+ //
26
+ // Usage, from the project root (the project's own `scripts/` copy wins, else the kit):
27
+ // npx -y @golden-frijoles/kit emit-epic-kickoff --epic <slug> [--repo-root <path>]
28
+ // npx -y @golden-frijoles/kit emit-epic-kickoff --list [--repo-root <path>] # the epics a kickoff can start
29
+ // # (live-build-view S2.4): `scaffolded` or `in-progress`, build order first
30
+ //
31
+ // The kickoff's one home is `/build <slug>` (the plugin's mod runs groom's vendored copy of THIS file and fills the
32
+ // prompt with its output); hosts without the mod run it through the kit. It is never saved to a file — the epic docs
33
+ // are the state, regenerate it.
34
+ //
35
+ // One source, three homes (kickoff-generator-path C1): this file ships in the kit, a project spawned from the template
36
+ // carries it in `scripts/`, and the groom skill holds a byte-identical copy in `vendor/` (render-hook-vendor.mjs).
37
+
38
+ import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
39
+ import { fileURLToPath } from 'node:url';
40
+ import { join } from 'node:path';
41
+
42
+ // The builder, its parsers and the template text live in ONE file, `lib/epic-kickoff.mjs` (board-sinks-and-scrumban
43
+ // D17): the Hub's Ready-to-build card carries the kickoff the extractor builds from that same file, so the card and
44
+ // this CLI cannot print two different prompts. Re-exported for the spec.
45
+ import { parseFrontmatter } from './emit-kickoff.mjs';
46
+ import { parseArgs, resolveRepoRoot, findEpicDir } from './lib/kickoff-cli.mjs';
47
+ import {
48
+ sprintNumFromFilename,
49
+ listSprintFiles,
50
+ buildSprintFileList,
51
+ compactStory,
52
+ buildSprintBreakdown,
53
+ buildEpicRules,
54
+ buildEpicKickoff,
55
+ parseEpicRisk,
56
+ epicKickoffFromDir,
57
+ EPIC_KICKOFF_TEMPLATE,
58
+ } from './lib/epic-kickoff.mjs';
59
+ import { wipWarning } from './lib/wip.mjs';
60
+ export {
61
+ sprintNumFromFilename,
62
+ listSprintFiles,
63
+ buildSprintFileList,
64
+ compactStory,
65
+ buildSprintBreakdown,
66
+ buildEpicRules,
67
+ buildEpicKickoff,
68
+ parseEpicRisk,
69
+ EPIC_KICKOFF_TEMPLATE,
70
+ };
71
+
72
+ // ── filesystem helpers (thin, not unit-tested — exercised by the real run) ──────────────────
73
+
74
+ function die(msg) {
75
+ console.error(`emit-epic-kickoff: ${msg}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ const STARTABLE = ['scaffolded', 'in-progress'];
80
+
81
+ /**
82
+ * The epics a kickoff can start, from each README's frontmatter: `{ slug, title, status, build_order }`, status
83
+ * scaffolded or in-progress, sorted by build_order (unordered last), then slug. Pure over `read`/`list` for the spec.
84
+ */
85
+ export function listEpics(repoRoot, { read = (p) => readFileSync(p, 'utf8'), exists = existsSync, list = readdirSync } = {}) {
86
+ const roadmapDir = join(repoRoot, 'Roadmap');
87
+ if (!exists(roadmapDir)) return [];
88
+ const out = [];
89
+ for (const macro of list(roadmapDir)) {
90
+ if (!/^\d{2}-/.test(macro)) continue;
91
+ let slugs = [];
92
+ try {
93
+ slugs = list(join(roadmapDir, macro));
94
+ } catch {
95
+ continue;
96
+ }
97
+ for (const slug of slugs) {
98
+ const readme = join(roadmapDir, macro, slug, 'README.md');
99
+ if (!exists(readme)) continue;
100
+ const fm = parseFrontmatter(read(readme));
101
+ if (!STARTABLE.includes(fm.status)) continue;
102
+ const order = Number.parseInt(fm.build_order, 10);
103
+ out.push({ slug, title: String(fm.title ?? slug).replace(/^"(.*)"$/, '$1'), status: fm.status, build_order: Number.isFinite(order) ? order : null });
104
+ }
105
+ }
106
+ return out.sort((a, b) => (a.build_order ?? Infinity) - (b.build_order ?? Infinity) || a.slug.localeCompare(b.slug));
107
+ }
108
+
109
+ /** The list as printed: one epic per line, the slug first so it can be typed after `/build `. */
110
+ export function formatEpicList(epics) {
111
+ if (!epics.length) return 'No scaffolded or in-progress epics under Roadmap/ — groom one first.';
112
+ return epics.map((e) => `${e.slug} ${e.title} (${e.status}${e.build_order !== null ? `, #${e.build_order}` : ''})`).join('\n');
113
+ }
114
+
115
+ function main() {
116
+ const args = parseArgs(process.argv.slice(2));
117
+ if (args.list) {
118
+ process.stdout.write(`${formatEpicList(listEpics(resolveRepoRoot(args)))}\n`);
119
+ return;
120
+ }
121
+ if (!args.epic || args.epic === true) {
122
+ console.error('emit-epic-kickoff: missing required flag: --epic');
123
+ console.error('Run with --epic <slug> [--repo-root <path>]');
124
+ process.exit(1);
125
+ }
126
+
127
+ const slug = String(args.epic);
128
+ const repoRoot = resolveRepoRoot(args);
129
+ const found = findEpicDir(repoRoot, slug);
130
+ if (found.error) die(found.error);
131
+ const { macro, dir } = found;
132
+
133
+ const readmeText = readFileSync(join(dir, 'README.md'), 'utf8');
134
+ const frontmatter = parseFrontmatter(readmeText);
135
+ if (frontmatter.slug && frontmatter.slug !== slug) {
136
+ die(`Roadmap/${macro}/${slug}/README.md frontmatter slug "${frontmatter.slug}" != requested "${slug}"`);
137
+ }
138
+
139
+ let built;
140
+ try {
141
+ built = epicKickoffFromDir({ macro, slug, dir });
142
+ } catch (err) {
143
+ die(err.message);
144
+ }
145
+
146
+ // A single-sprint "epic" is the documented exception, not the default — say so instead of emitting a
147
+ // whole-epic orchestration prompt for one sprint's worth of work.
148
+ if (built.sprints.length === 1) {
149
+ process.stderr.write(
150
+ `⚠ "${slug}" has one sprint. Epic mode buys nothing here — the per-sprint kickoff is the right ` +
151
+ `tool: npx -y @golden-frijoles/kit emit-kickoff --epic ${slug} --sprint 1\n` +
152
+ ` (Emitting the epic-mode prompt anyway.)\n\n`
153
+ );
154
+ }
155
+
156
+ // S3.4 — scrumban's pull as advice: one line on stderr when Building is at its WIP limit; the kickoff still prints.
157
+ const wip = wipWarning({ root: repoRoot, excluding: slug });
158
+ if (wip) process.stderr.write(`${wip}\n\n`);
159
+
160
+ process.stdout.write(built.kickoff);
161
+ }
162
+
163
+ // realpath on both sides (D6): run through a symlinked path (npx's bin link, macOS /tmp → /private/tmp) a plain
164
+ // compare is false and the script would exit 0 having printed nothing.
165
+ const isMain = (() => {
166
+ try {
167
+ return !!process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
168
+ } catch {
169
+ return false;
170
+ }
171
+ })();
172
+ if (isMain) main();
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ // emit-kickoff.mjs — print the finished, paste-ready Stage-8 Claude Code kickoff for a groomed
3
+ // sprint. Planning-only helper for the `groom` skill (Stage 8). The invariant preamble (the
4
+ // orientation reads, plan-mode, escalate-don't-guess triggers, etc.) lives in
5
+ // `templates/kickoff.md` — this script's whole point is that boilerplate stops being retyped by
6
+ // hand per sprint; only the sprint-specific delta (epic title, sprint number + its own title,
7
+ // story list) is read out of the epic's own docs and substituted in.
8
+ //
9
+ // Usage, from the project root (the project's own `scripts/` copy wins, else the kit):
10
+ // npx -y @golden-frijoles/kit emit-kickoff --epic <slug> --sprint N [--repo-root <path>]
11
+ //
12
+ // One source, three homes (kickoff-generator-path C1): this file ships in the kit, a project spawned from the template
13
+ // carries it in `scripts/`, and the groom skill holds a byte-identical copy in `vendor/` (render-hook-vendor.mjs).
14
+ // The template is read from this file's own directory, which is the kit root in all three.
15
+ //
16
+ // Resolves the epic dir by SEARCHING Roadmap/*/<slug>/ under the project root (lib/kickoff-cli.mjs) — the
17
+ // macro-area prefix (e.g. "09-platform-infra") isn't knowable from the slug alone. Reads that
18
+ // epic's README.md (frontmatter + H1 title) and sprint-<N>.md (H1 + `### Story N.M — <title>`
19
+ // headings), then substitutes into templates/kickoff.md and prints the result to stdout.
20
+ //
21
+ // It does NOT write any file — this is a read + print tool, same "advisory, editorial control
22
+ // stays with the caller" stance as the other groom/cross-agent scripts in this repo.
23
+
24
+ import { readFileSync, existsSync, realpathSync } from 'node:fs';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { dirname, join } from 'node:path';
27
+ import { parseArgs, resolveRepoRoot, findEpicDir } from './lib/kickoff-cli.mjs';
28
+ export { parseArgs };
29
+
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+ const TPL = join(__dirname, 'templates');
32
+
33
+ // ── pure helpers (exported for the co-located test) ─────────────────────────────────────────
34
+
35
+ // The doc parsers live in the epic kickoff builder now (board-sinks-and-scrumban D17), so the per-sprint and epic
36
+ // generators — and the Hub card's kickoff, which the extractor builds from the same file — read the doc format with
37
+ // ONE set of parsers. Re-exported for this file's spec and for any caller that imported them from here.
38
+ import {
39
+ sub,
40
+ parseFrontmatter,
41
+ stripFrontmatter,
42
+ parseEpicTitle,
43
+ parseSprintHeader,
44
+ parseStoryHeadings,
45
+ sprintBranch,
46
+ sprintBase,
47
+ } from './lib/epic-kickoff.mjs';
48
+ export { sub, parseFrontmatter, stripFrontmatter, parseEpicTitle, parseSprintHeader, parseStoryHeadings };
49
+
50
+ export function buildStoryList(headings) {
51
+ if (!headings.length) return '(no `### Story N.M — <title>` headings found in the sprint doc)';
52
+ return headings.map((h) => `- ${h}`).join('\n');
53
+ }
54
+
55
+ export function buildKickoff({ macro, slug, sprintNum, epicTitle, sprintTitle, storyList, templateText }) {
56
+ return sub(templateText, {
57
+ MACRO: macro,
58
+ SLUG: slug,
59
+ N: String(sprintNum),
60
+ BRANCH: sprintBranch(slug, sprintNum),
61
+ BASE: sprintBase(slug, sprintNum),
62
+ EPIC_TITLE: epicTitle,
63
+ SPRINT_TITLE: sprintTitle,
64
+ STORY_LIST: storyList,
65
+ });
66
+ }
67
+
68
+ // ── filesystem helpers (thin, not unit-tested — exercised by the real run) ──────────────────
69
+
70
+ function die(msg) {
71
+ console.error(`emit-kickoff: ${msg}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ function main() {
76
+ const args = parseArgs(process.argv.slice(2));
77
+ const missing = ['epic', 'sprint'].filter((k) => !args[k] || args[k] === true);
78
+ if (missing.length) {
79
+ console.error(`emit-kickoff: missing required flag(s): ${missing.map((m) => '--' + m).join(', ')}`);
80
+ console.error('Run with --epic <slug> --sprint N [--repo-root <path>]');
81
+ process.exit(1);
82
+ }
83
+
84
+ const slug = String(args.epic);
85
+ const sprintNum = String(args.sprint);
86
+ if (!/^\d+$/.test(sprintNum)) die(`--sprint must be a number (got "${sprintNum}")`);
87
+ const repoRoot = resolveRepoRoot(args);
88
+
89
+ const found = findEpicDir(repoRoot, slug);
90
+ if (found.error) die(found.error);
91
+ const { macro, dir } = found;
92
+
93
+ const readmePath = join(dir, 'README.md');
94
+ const sprintPath = join(dir, `sprint-${sprintNum}.md`);
95
+ if (!existsSync(sprintPath)) die(`no sprint-${sprintNum}.md under Roadmap/${macro}/${slug}/`);
96
+
97
+ const readmeText = readFileSync(readmePath, 'utf8');
98
+ const sprintText = readFileSync(sprintPath, 'utf8');
99
+
100
+ const frontmatter = parseFrontmatter(readmeText);
101
+ if (frontmatter.slug && frontmatter.slug !== slug) {
102
+ die(`Roadmap/${macro}/${slug}/README.md frontmatter slug "${frontmatter.slug}" != requested "${slug}"`);
103
+ }
104
+
105
+ const epicTitle = parseEpicTitle(readmeText);
106
+ if (!epicTitle) die(`couldn't find an H1 title in Roadmap/${macro}/${slug}/README.md`);
107
+
108
+ const sprintHeader = parseSprintHeader(sprintText);
109
+ if (!sprintHeader) die(`couldn't parse the H1 of Roadmap/${macro}/${slug}/sprint-${sprintNum}.md (expected "# <epic title> — Sprint <N>: <sprint title>")`);
110
+
111
+ const storyHeadings = parseStoryHeadings(sprintText);
112
+ const storyList = buildStoryList(storyHeadings);
113
+
114
+ const templatePath = join(TPL, 'kickoff.md');
115
+ if (!existsSync(templatePath)) die(`missing template: ${templatePath}`);
116
+ const templateText = readFileSync(templatePath, 'utf8');
117
+
118
+ const out = buildKickoff({ macro, slug, sprintNum, epicTitle, sprintTitle: sprintHeader.sprintTitle, storyList, templateText });
119
+ process.stdout.write(out);
120
+ }
121
+
122
+ // realpath on both sides (D6): run through a symlinked path (npx's bin link, macOS /tmp → /private/tmp) a plain
123
+ // compare is false and the script would exit 0 having printed nothing.
124
+ const isMain = (() => {
125
+ try {
126
+ return !!process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
127
+ } catch {
128
+ return false;
129
+ }
130
+ })();
131
+ if (isMain) main();
@@ -0,0 +1,76 @@
1
+ // kickoff-cli.mjs — the argv, project-root and epic-lookup half both kickoff generators share
2
+ // (kickoff-generator-path C2).
3
+ //
4
+ // `emit-epic-kickoff.mjs` and `emit-kickoff.mjs` run in THREE places, the same bytes in each:
5
+ // • a project's own `scripts/` (copied from the template),
6
+ // • the kit's `dist/` (`npx -y @golden-frijoles/kit emit-epic-kickoff …`, far from the project),
7
+ // • the groom skill's `vendor/` inside the plugin (the build-view mod's `/build`, and groom's Stage 8).
8
+ // `projectRoot()` answers the first two. For the third it answers `groom/`, which holds no Roadmap/, so the root takes
9
+ // one more rung (see `resolveRepoRoot`). The epic lookup lived twice, byte-for-byte, in the two CLIs; it lives here.
10
+ //
11
+ // Zero deps; no side effects at import.
12
+
13
+ import { existsSync, readdirSync } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ import { findProjectRoot, projectRoot } from './project-root.mjs';
16
+
17
+ /** `--key value` pairs; a flag with no value (or followed by another flag) is `true`. */
18
+ export function parseArgs(argv) {
19
+ const a = {};
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const t = argv[i];
22
+ if (t.startsWith('--')) {
23
+ const key = t.slice(2);
24
+ const next = argv[i + 1];
25
+ if (next === undefined || next.startsWith('--')) { a[key] = true; }
26
+ else { a[key] = next; i++; }
27
+ }
28
+ }
29
+ return a;
30
+ }
31
+
32
+ /**
33
+ * The project whose Roadmap/ the kickoff is built from, first match wins:
34
+ * 1. `--repo-root <path>` (the mod always passes it),
35
+ * 2. `GF_PROJECT_ROOT` (`gf-kit --root` sets it), taken as given: a person named it,
36
+ * 3. `projectRoot()` when it holds a Roadmap/ (a project's own `scripts/`, or the installed kit's walk from cwd),
37
+ * — a copied set answers its own parent, so the template's own copy here answers `skills/template/` (whose
38
+ * skeleton Roadmap/ has no epics); that is project-root.mjs's copied-mode rule, and no doc runs that copy,
39
+ * 4. the nearest directory holding Roadmap/ or .git, walking up from cwd (the plugin's vendored copy),
40
+ * 5. cwd.
41
+ * Pure over its options for the spec.
42
+ */
43
+ export function resolveRepoRoot(
44
+ args,
45
+ { env = process.env, cwd = process.cwd(), exists = existsSync, project = () => projectRoot({ env, cwd, exists }) } = {}
46
+ ) {
47
+ const flag = args['repo-root'];
48
+ if (flag && flag !== true) return resolve(cwd, String(flag));
49
+ if (env.GF_PROJECT_ROOT) return resolve(cwd, env.GF_PROJECT_ROOT);
50
+ const root = project();
51
+ if (exists(join(root, 'Roadmap'))) return root;
52
+ return findProjectRoot(cwd, { exists }) ?? resolve(cwd);
53
+ }
54
+
55
+ /**
56
+ * Search Roadmap/*\/<slug>/ under repoRoot — the macro-area prefix isn't knowable from the slug alone.
57
+ * Returns `{ macro, dir }`, or `{ error }` naming what was searched (zero hits, or more than one).
58
+ */
59
+ export function findEpicDir(repoRoot, slug, { exists = existsSync, list = readdirSync } = {}) {
60
+ const roadmapDir = join(repoRoot, 'Roadmap');
61
+ if (!exists(roadmapDir)) return { error: `no Roadmap/ dir under "${repoRoot}" (pass --repo-root <path>)` };
62
+ let macros;
63
+ try {
64
+ macros = list(roadmapDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
65
+ } catch (e) {
66
+ return { error: `couldn't read ${roadmapDir}: ${e.message}` };
67
+ }
68
+ const hits = macros.filter((m) => exists(join(roadmapDir, m, slug, 'README.md')));
69
+ if (hits.length === 0) {
70
+ return { error: `no epic found for slug "${slug}" under any Roadmap/*/ dir in "${repoRoot}" (searched: ${macros.join(', ') || '(none)'})` };
71
+ }
72
+ if (hits.length > 1) {
73
+ return { error: `ambiguous slug "${slug}" — found under multiple macro-areas: ${hits.map((m) => `Roadmap/${m}/${slug}`).join(', ')}` };
74
+ }
75
+ return { macro: hits[0], dir: join(roadmapDir, hits[0], slug) };
76
+ }
@@ -0,0 +1,40 @@
1
+ // wip.mjs — scrumban's pull, as advice at the moment of pulling (board-sinks-and-scrumban S3.4, D9, D22).
2
+ //
3
+ // `emit-epic-kickoff` asks this before it prints a kickoff: if Building is already at its limit (`board.wip.Building` in
4
+ // golden-frijoles.config.json), it says so in ONE line that names the limit and the cards in Building — and the kickoff
5
+ // is printed anyway. A WIP limit here is advice, never a gate (D9): the person pulling may have a good reason.
6
+ //
7
+ // Building is counted from the SAME rows every client reads (`buildRows`, the stage resolver), with the facts in the
8
+ // local snapshot — no network at kickoff time, and the age of those facts is said in the warning.
9
+
10
+ import { buildRows } from '../roadmap-extract.mjs';
11
+ import { gatherFacts } from './stage-facts.mjs';
12
+ import { getKey } from './config.mjs';
13
+
14
+ /**
15
+ * The warning line, or null when there is no limit, Building is under it, or the roadmap cannot be read (advice must
16
+ * never break the kickoff it decorates). `excluding` is the epic being pulled — it is not yet Building.
17
+ */
18
+ export function wipWarning({ root, excluding = null, gather = gatherFacts } = {}) {
19
+ try {
20
+ const wip = getKey('board.wip', { root });
21
+ const limit = wip && Number.isInteger(wip.Building) && wip.Building > 0 ? wip.Building : null;
22
+ if (limit === null) return null;
23
+ const facts = gather({ root, mode: 'snapshot' });
24
+ const building = buildRows({ facts, root, dates: false }).filter(
25
+ (r) => r.grain !== 'Sprint' && r.stage === 'Building' && r.slug !== excluding
26
+ );
27
+ if (building.length < limit) return null;
28
+ const age =
29
+ facts.mode === 'snapshot'
30
+ ? ` (facts from ${facts.origin.replace('@', ' of ')})`
31
+ : ' (docs only: no snapshot of git facts yet)';
32
+ const names = building.map((r) => r.name).join('; ');
33
+ return (
34
+ `⚠ WIP: Building is ${building.length > limit ? 'over' : 'at'} its limit of ${limit} — ${names}${age}. ` +
35
+ `Pulling this starts one more; finish or hand one off first, or go ahead knowingly. The kickoff follows.`
36
+ );
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
@@ -0,0 +1,26 @@
1
+ Start by pushing this sprint's branch, before anything else — it is what moves the card to Building on the board:
2
+ `git switch -c {{BRANCH}} {{BASE}} && git push -u origin {{BRANCH}}` (stacked on the previous sprint; branch from
3
+ `origin/main` instead once that sprint has merged. Resuming? `git switch {{BRANCH}}`).
4
+
5
+ Read AGENTS.md, Roadmap/WAYS-OF-WORKING.md and Roadmap/LEARNINGS.md. Skim team memory.
6
+ Then read Roadmap/{{MACRO}}/{{SLUG}}/README.md and Roadmap/{{MACRO}}/{{SLUG}}/sprint-{{N}}.md.
7
+
8
+ You're building Sprint {{N}} of "{{EPIC_TITLE}}". Enter plan mode, confirm the plan as user stories with me,
9
+ then build one story at a time per WAYS-OF-WORKING. If you're one of
10
+ several builders running in parallel, work in your own isolated `git worktree`, not the shared root checkout.
11
+ Reuse before rebuild (see "What already exists"). Escalate rather than guess: stop and hand back on any trigger in
12
+ the ONE list, WAYS-OF-WORKING → *Escalate, don't guess* — default to escalate when unsure. Commit per story with path-limited adds
13
+ (`git add <your files>` + `git commit -- <those paths>`, never `git add -A` — a shared worktree races the
14
+ index). Follow this project's own copy/localization conventions (see AGENTS.md). Add one api spec per testable story; name the
15
+ QA/smoke stage and state any browser smoke owed to me. When the deterministic gate (tsc + build + Playwright
16
+ api) is green, open a draft PR declaring the risk tier, then flip it ready-for-review — and write the SPRINT
17
+ SMOKE WALKTHROUGH (below) into sprint-{{N}}.md before you call the sprint done.
18
+ Review (WAYS-OF-WORKING → Review & merge): CI always; then run
19
+ `node scripts/review-route.mjs --builder <who-wrote-it> <PR#>` and take what it prints — one external
20
+ general pass, a security lens when the changed paths trigger it, and the fresh `pr-reviewer` subagent, in
21
+ the scope `scripts/review-config.json` sets. A family never reviews its own diff, so don't pick `--agent`
22
+ by hand; a capped family is routed past with `--exclude <family>`, and if only one can run it runs both
23
+ prompts and you say so in the PR body. You merge your own PR on a green gate once findings are resolved.
24
+
25
+ Sprint {{N}} of "{{EPIC_TITLE}}" — "{{SPRINT_TITLE}}" — stories:
26
+ {{STORY_LIST}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golden-frijoles/kit",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {