@golden-frijoles/kit 0.43.0 → 1.0.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.
- package/README.md +1 -1
- package/bin.mjs +28 -13
- package/dist/build-order.mjs +6 -6
- package/dist/build-state.mjs +2 -3
- package/dist/cold-read.mjs +2 -2
- package/dist/cold-read.prompt.md +3 -1
- package/dist/config.mjs +8 -8
- package/dist/cross-panel.prompt.md +2 -2
- package/dist/cross-review.mjs +1 -1
- package/dist/cross-review.prompt.md +1 -1
- package/dist/doc-format.enforced.json +1 -1
- package/dist/doc-format.mjs +3 -3
- package/dist/doc-hygiene.mjs +3 -3
- package/dist/emit-epic-kickoff.mjs +4 -4
- package/dist/emit-kickoff.mjs +4 -4
- package/dist/epic-actuals.mjs +1 -1
- package/dist/epic-read.mjs +22 -17
- package/dist/init.mjs +7 -7
- package/dist/intent-match.mjs +6 -6
- package/dist/intent-reader.mjs +4 -4
- package/dist/jev-eval.mjs +1 -1
- package/dist/lib/board-text.mjs +2 -2
- package/dist/lib/config-registry.mjs +10 -10
- package/dist/lib/config.d.mts +1 -1
- package/dist/lib/config.mjs +3 -3
- package/dist/lib/epic-kickoff.mjs +6 -6
- package/dist/lib/golden-onboarding.mjs +14 -12
- package/dist/lib/kickoff-cli.mjs +4 -4
- package/dist/lib/project-root.mjs +1 -1
- package/dist/lib/result-dates.mjs +2 -2
- package/dist/lib/roadmap-contract.mjs +3 -3
- package/dist/lib/stage.mjs +10 -0
- package/dist/lib/strategy-files.mjs +1 -1
- package/dist/lib/surface.mjs +1 -1
- package/dist/preflight.mjs +12 -8
- package/dist/quote.mjs +1 -1
- package/dist/review-route.mjs +3 -3
- package/dist/roadmap-extract.mjs +1 -1
- package/dist/routines/README.md +4 -4
- package/dist/routines/ops-nightly.prompt.md +22 -18
- package/dist/routines/pmo-report.prompt.md +3 -3
- package/dist/routines/pr-review.prompt.md +1 -1
- package/dist/routines/roadmap-hygiene.prompt.md +13 -11
- package/dist/routines/weekly-recap.prompt.md +2 -2
- package/dist/skeleton/Roadmap/00-ideas/README.md +6 -6
- package/dist/skeleton/Roadmap/LEARNINGS.md +1 -1
- package/dist/skeleton/Roadmap/WAYS-OF-WORKING.md +6 -6
- package/dist/templates/kickoff.md +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npx -y @golden-frijoles/kit@<version> --root ../other build-order
|
|
|
15
15
|
`Roadmap/00-ideas/BUILD-ORDER.md`.
|
|
16
16
|
- **Your own copy wins.** If your project has `scripts/<name>.mjs`, the skills run that instead of the kit, so a
|
|
17
17
|
deliberate fork keeps working. That's by name, so an unrelated `scripts/preflight.mjs` of your own would be run in
|
|
18
|
-
its place. Rename yours, or pass the script through `
|
|
18
|
+
its place. Rename yours, or pass the script through `frijoles-kit` directly.
|
|
19
19
|
- **Some files stay yours even when the kit runs:** `scripts/prose/cpo-persona.md`, `scripts/prose-lessons.md`,
|
|
20
20
|
`scripts/cross-panel.prompt.md` and `scripts/doc-format.enforced.json` override the kit's defaults.
|
|
21
21
|
- **Zero dependencies.** A script that drives another tool, like Playwright or a reviewer CLI, uses *your*
|
package/bin.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// frijoles-kit — run one of the Golden Frijoles skills' scripts in the current project (golden-frijoles-plugin D1–D3).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// frijoles-kit <name> [args…] run dist/<name>.mjs against the project you're standing in
|
|
5
|
+
// frijoles-kit --root <dir> <name> …against <dir> instead (exported to the script as GF_PROJECT_ROOT)
|
|
6
|
+
// frijoles-kit --list the scripts this kit carries
|
|
7
|
+
// frijoles-kit --version
|
|
8
8
|
//
|
|
9
9
|
// The script is SPAWNED, never imported: every entry guards `main()` behind an isMain check on process.argv[1]
|
|
10
10
|
// and several end with process.exit, so an import() would silently run nothing. stdio, the exit code and a
|
|
@@ -26,7 +26,7 @@ export function listScripts(dist = DIST) {
|
|
|
26
26
|
.sort();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** Pure — split
|
|
29
|
+
/** Pure — split frijoles-kit's own flags from the script's. Only flags BEFORE the script name belong to frijoles-kit. */
|
|
30
30
|
export function parseArgs(argv) {
|
|
31
31
|
const out = { root: null, list: false, version: false, help: false, name: null, rest: [] };
|
|
32
32
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -51,7 +51,18 @@ export function parseArgs(argv) {
|
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const USAGE = 'usage:
|
|
54
|
+
const USAGE = 'usage: frijoles-kit [--root <dir>] <script> [args…] · frijoles-kit --list · frijoles-kit --version';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* plugin-1-0 D2 — started as the old name `gf-kit`? One line on stderr (stdout is a script's own output, often parsed),
|
|
58
|
+
* until the date `scripts/check-deprecations.mjs` enforces. Pure, so the test pins it.
|
|
59
|
+
*/
|
|
60
|
+
export function deprecatedNameNotice(invokedPath) {
|
|
61
|
+
const name = String(invokedPath ?? '').split(/[\\/]/).pop() ?? '';
|
|
62
|
+
// Unix only: npm's Windows shims start `node …\\bin.mjs`, so argv[1] is the entry file there (verifier, #324).
|
|
63
|
+
if (name !== 'gf-kit') return null;
|
|
64
|
+
return 'gf-kit is now frijoles-kit. gf-kit stops working on 2026-12-31 (or in kit 1.1.0).\n';
|
|
65
|
+
}
|
|
55
66
|
|
|
56
67
|
function main(argv) {
|
|
57
68
|
const args = parseArgs(argv);
|
|
@@ -69,12 +80,12 @@ function main(argv) {
|
|
|
69
80
|
return args.help ? 0 : 2;
|
|
70
81
|
}
|
|
71
82
|
if (args.root === '') {
|
|
72
|
-
process.stderr.write('
|
|
83
|
+
process.stderr.write('frijoles-kit: --root needs a directory\n');
|
|
73
84
|
return 2;
|
|
74
85
|
}
|
|
75
86
|
// A plain name from the list only — never a path, so nothing outside dist/ can be reached through it.
|
|
76
87
|
if (!scripts.includes(args.name)) {
|
|
77
|
-
process.stderr.write(`
|
|
88
|
+
process.stderr.write(`frijoles-kit: no script "${args.name}". This kit carries:\n ${scripts.join('\n ')}\n`);
|
|
78
89
|
return 2;
|
|
79
90
|
}
|
|
80
91
|
const env = { ...process.env };
|
|
@@ -83,21 +94,21 @@ function main(argv) {
|
|
|
83
94
|
// re-resolved against that cwd in every child (fresh review of #45: pmo-report read zero rows, silently).
|
|
84
95
|
const root = resolve(args.root);
|
|
85
96
|
if (!existsSync(root)) {
|
|
86
|
-
process.stderr.write(`
|
|
97
|
+
process.stderr.write(`frijoles-kit: --root ${args.root}: no such directory\n`);
|
|
87
98
|
return 2;
|
|
88
99
|
}
|
|
89
100
|
env.GF_PROJECT_ROOT = root;
|
|
90
101
|
}
|
|
91
102
|
const run = spawnSync(process.execPath, [join(DIST, `${args.name}.mjs`), ...args.rest], { stdio: 'inherit', env });
|
|
92
103
|
if (run.error) {
|
|
93
|
-
process.stderr.write(`
|
|
104
|
+
process.stderr.write(`frijoles-kit: could not start ${args.name}: ${run.error.message}\n`);
|
|
94
105
|
return 1;
|
|
95
106
|
}
|
|
96
107
|
if (run.signal) process.kill(process.pid, run.signal);
|
|
97
108
|
return run.status ?? 1;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// npm installs `
|
|
111
|
+
// npm installs `frijoles-kit` as a symlink in node_modules/.bin, so compare real paths on both sides.
|
|
101
112
|
const isMain = (() => {
|
|
102
113
|
try {
|
|
103
114
|
return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
@@ -105,4 +116,8 @@ const isMain = (() => {
|
|
|
105
116
|
return false;
|
|
106
117
|
}
|
|
107
118
|
})();
|
|
108
|
-
if (isMain)
|
|
119
|
+
if (isMain) {
|
|
120
|
+
const notice = deprecatedNameNotice(process.argv[1]);
|
|
121
|
+
if (notice) process.stderr.write(notice);
|
|
122
|
+
process.exitCode = main(process.argv.slice(2));
|
|
123
|
+
}
|
package/dist/build-order.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
18
18
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
|
-
import { STAGES, groupByStage } from './lib/stage.mjs';
|
|
21
|
+
import { STAGES, groupByStage, stageWord } from './lib/stage.mjs';
|
|
22
22
|
import { projectRoot } from './lib/project-root.mjs';
|
|
23
23
|
|
|
24
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -78,7 +78,7 @@ function render(rows) {
|
|
|
78
78
|
out.push('# Build order — the six stages');
|
|
79
79
|
out.push('');
|
|
80
80
|
out.push(`> **Generated ${now} — do not hand-edit.** One stage per initiative, decided in one place`);
|
|
81
|
-
out.push(
|
|
81
|
+
out.push(`> (\`scripts/lib/stage.mjs\`): ${STAGES.map(stageWord).join(' · ')}.`);
|
|
82
82
|
if (LIVE) {
|
|
83
83
|
out.push('> **Live:** Building and QA come from git and GitHub as of this run. Printed, never written.');
|
|
84
84
|
} else {
|
|
@@ -89,7 +89,7 @@ function render(rows) {
|
|
|
89
89
|
for (const stage of STAGES) {
|
|
90
90
|
const list = columns[stage];
|
|
91
91
|
if (LIVE_ONLY.has(stage) && !LIVE) {
|
|
92
|
-
out.push(`## ${stage} — live only`);
|
|
92
|
+
out.push(`## ${stageWord(stage)} — live only`);
|
|
93
93
|
out.push('');
|
|
94
94
|
out.push(
|
|
95
95
|
`_${STAGE_NOTE[stage]}. Not in this committed file: \`node scripts/build-order.mjs --live\` or the Hub board._`
|
|
@@ -97,7 +97,7 @@ function render(rows) {
|
|
|
97
97
|
out.push('');
|
|
98
98
|
continue;
|
|
99
99
|
}
|
|
100
|
-
out.push(`## ${stage} (${list.length})`);
|
|
100
|
+
out.push(`## ${stageWord(stage)} (${list.length})`);
|
|
101
101
|
out.push('');
|
|
102
102
|
out.push(`_${STAGE_NOTE[stage]}._`);
|
|
103
103
|
out.push('');
|
|
@@ -126,7 +126,7 @@ if (unfunded.length) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Scaffolded ⇒ funded (fund-at-approval D8). A live bet — an epic scaffolded or in progress, or a queued seed — must
|
|
129
|
-
// name the cycle that paid for it, and that cycle must be a file in Roadmap/bets/. `
|
|
129
|
+
// name the cycle that paid for it, and that cycle must be a file in Roadmap/bets/. `refine` funds a bet at its approval
|
|
130
130
|
// gate (`fund.mjs`), in the same commit as the scaffold, so this only fails on work that skipped the gate.
|
|
131
131
|
const BETS = join(REPO, 'Roadmap', 'bets');
|
|
132
132
|
const cycleOf = (v) => String(v).replace(/^Roadmap\/bets\//, '').replace(/\.md$/, '');
|
|
@@ -139,7 +139,7 @@ const unpaid = liveBets.filter((r) => !paidBy(r));
|
|
|
139
139
|
if (unpaid.length) {
|
|
140
140
|
console.error('Live bets with no funding record (`underwritten_by:` must name a Roadmap/bets/<cycle>.md):');
|
|
141
141
|
for (const r of unpaid) console.error(` - ${r.doc_link}${r.underwritten_by ? ` — no Roadmap/bets/${cycleOf(r.underwritten_by)}.md` : ''}`);
|
|
142
|
-
console.error('Fund it at the approval gate: node "$
|
|
142
|
+
console.error('Fund it at the approval gate: node "$REFINE/fund.mjs" --slug <slug> --displaced "<…>" --next | --after <slug>');
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
|
package/dist/build-state.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { parseJournal, JOURNAL_BRANCH, JOURNAL_PATH } from './lib/session-journa
|
|
|
61
61
|
import { branchCandidates, parseBranch } from './lib/work-branch.mjs';
|
|
62
62
|
import { buildRows } from './roadmap-extract.mjs';
|
|
63
63
|
import { gatherFacts } from './lib/stage-facts.mjs';
|
|
64
|
-
import { STAGES, groupByStage } from './lib/stage.mjs';
|
|
64
|
+
import { STAGES, groupByStage, stageWord } from './lib/stage.mjs';
|
|
65
65
|
import { getKey } from './lib/config.mjs';
|
|
66
66
|
|
|
67
67
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -927,14 +927,13 @@ function stageParts(state) {
|
|
|
927
927
|
}
|
|
928
928
|
|
|
929
929
|
// build-view-upgrade D3 — the stage as a track, in lib/stage.mjs's words. `To groom` shows only when it is the stage.
|
|
930
|
-
const TRACK_WORDS = { 'To groom': 'Backlog', 'Ready to build': 'Ready' };
|
|
931
930
|
export const TRACK_MARK = '◉';
|
|
932
931
|
|
|
933
932
|
/** `Grooming ─ Ready ─ ◉ Building ─ QA ─ Shipped` for a stage; `◉ Locking` in Building's place while locking. */
|
|
934
933
|
export function stageTrack(stage, locking = false) {
|
|
935
934
|
return STAGES.filter((s) => s !== 'To groom' || stage === 'To groom')
|
|
936
935
|
.map((s) => {
|
|
937
|
-
const word = s === 'Building' && locking ? 'Locking' : (
|
|
936
|
+
const word = s === 'Building' && locking ? 'Locking' : stageWord(s);
|
|
938
937
|
return s === stage ? `${TRACK_MARK} ${word}` : word;
|
|
939
938
|
})
|
|
940
939
|
.join(' ─ ');
|
package/dist/cold-read.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// cold-read.mjs — the deterministic half of the
|
|
2
|
+
// cold-read.mjs — the deterministic half of the cold-read chapter of the `strategy` skill: the brief, the run, the seal and the compare
|
|
3
3
|
// (coaches-v2 D2–D4).
|
|
4
4
|
//
|
|
5
5
|
// node scripts/cold-read.mjs brief [--out <file>] print the agent's brief (the exclusion list is in it); --out
|
|
@@ -316,7 +316,7 @@ function run(root) {
|
|
|
316
316
|
err: 'cold-read: no other model family reachable (codex is not installed). Run the brief with a same-family agent and record `family: claude (same family)`.',
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
|
-
// Codex reads the repository it is started in. Under `
|
|
319
|
+
// Codex reads the repository it is started in. Under `frijoles-kit --root <dir>` that is not this process's cwd, so move
|
|
320
320
|
// there first: otherwise it reads one repo and the read is sealed into another's strategy folder (review of #314).
|
|
321
321
|
process.chdir(root);
|
|
322
322
|
const r = tryCodex(brief, '');
|
package/dist/cold-read.prompt.md
CHANGED
|
@@ -8,7 +8,9 @@ the end, says where it goes.
|
|
|
8
8
|
Skip these, and anything whose purpose is positioning, brand, messaging, landing copy or a strategy method:
|
|
9
9
|
|
|
10
10
|
- `Roadmap/00-strategy/` (all of it): the makers' own strategy and any earlier cold read
|
|
11
|
-
- any `pmf-narrative`, `north-star`, `risk-validation` or
|
|
11
|
+
- the `strategy` skill folder (and, on an older install, any `pmf-narrative`, `north-star`, `risk-validation` or
|
|
12
|
+
`cold-read` skill folder), and the `refine` skill's `references/coaching.md` and `references/strategy.md`: the
|
|
13
|
+
coaching methods
|
|
12
14
|
- `references/`, `brand/`, landing-page and marketing copy, positioning or messaging modules
|
|
13
15
|
- `.env*`, secrets, `node_modules/`, `.git/`
|
|
14
16
|
|
package/dist/config.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// config.mjs — `
|
|
2
|
+
// config.mjs — `frijoles-kit config`: read and change golden-frijoles.config.json (golden-frijoles-plugin S4.1, D10).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// frijoles-kit config list [--json] every section's effective value, where it came from, any duplicates
|
|
5
|
+
// frijoles-kit config get <key> [--json] one key's effective value (or its registry default)
|
|
6
|
+
// frijoles-kit config set <key> <value> write one key; <value> is parsed as JSON when it is valid JSON
|
|
7
|
+
// frijoles-kit config migrate [--dry-run] fold the legacy files into golden-frijoles.config.json (never edits them)
|
|
8
8
|
//
|
|
9
9
|
// A thin front end: every rule (precedence, the secret guard, what migrate folds) lives in lib/config.mjs, which the
|
|
10
|
-
// `
|
|
10
|
+
// `frijoles` CLI imports too. Exit 0 ok · 1 usage · 2 configuration error (a malformed file, a secret, an unknown section).
|
|
11
11
|
|
|
12
12
|
import { realpathSync } from 'node:fs';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { CONFIG_FILENAME, ConfigError, getKey, loadConfig, migrate, setKey } from './lib/config.mjs';
|
|
15
15
|
|
|
16
|
-
const USAGE = 'usage:
|
|
16
|
+
const USAGE = 'usage: frijoles-kit config list|get <key>|set <key> <value>|migrate [--dry-run] [--json]';
|
|
17
17
|
|
|
18
18
|
/** Pure — a CLI value: JSON when it parses (true, 3, ["a"], null), otherwise the literal string. */
|
|
19
19
|
export function parseValue(text) {
|
|
@@ -63,7 +63,7 @@ export function main(argv, { out = (s) => process.stdout.write(s), err = (s) =>
|
|
|
63
63
|
}
|
|
64
64
|
} catch (e) {
|
|
65
65
|
if (e instanceof ConfigError) {
|
|
66
|
-
err(`
|
|
66
|
+
err(`frijoles-kit config: ${e.message}\n`);
|
|
67
67
|
return 2;
|
|
68
68
|
}
|
|
69
69
|
throw e;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
You are an **advisory second-opinion reviewer** from a different model family than the agent that is
|
|
27
|
-
|
|
27
|
+
refining this plan. You are reviewing a **proposed plan** — a scope/seed doc for a feature, bug, spike,
|
|
28
28
|
or chore — **before** it gets sliced into an epic and built. Your job is to catch the architecture blind
|
|
29
29
|
spots a same-family planner would miss, while the plan is still cheap to change.
|
|
30
30
|
|
|
@@ -80,7 +80,7 @@ confirm (or kill) your "already modeled / not modeled" assumption.
|
|
|
80
80
|
## LENS: architect-pragmatist
|
|
81
81
|
You are the **ship-it pragmatist.** Your question on every line: *what is the thinnest thing that actually
|
|
82
82
|
works and ships, and is this plan over-built for v1?* Push hard on:
|
|
83
|
-
- **Can we already do this today?** (LEARNINGS / the
|
|
83
|
+
- **Can we already do this today?** (LEARNINGS / the refine skill's "can we already do this?" stage.) Sort
|
|
84
84
|
the ask into *already-possible* (existing features + the right messaging/positioning — no build),
|
|
85
85
|
*light-enhancement* (a small story or copy/config change on an existing feature), or *genuinely-new*.
|
|
86
86
|
If buckets 1–2 hit the outcome, say so loudly — that's the win. A plan that builds net-new when copy +
|
package/dist/cross-review.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export function promptPathFor(lens) {
|
|
|
109
109
|
|
|
110
110
|
const BANNER =
|
|
111
111
|
'> **Cross-agent review — every finding is fixed, or answered on this PR, before merge. This does not authorize one.** ' +
|
|
112
|
-
'CI and the risk-tier merge rule remain the only merge authority. A fresh `
|
|
112
|
+
'CI and the risk-tier merge rule remain the only merge authority. A fresh `verifier` pass covers context independence; ' +
|
|
113
113
|
'this is the family-independence pass: one single-pass read by a model family that did not build the diff.';
|
|
114
114
|
|
|
115
115
|
const HELP = `cross-review.mjs — the cross-agent judgment-layer review for a PR diff.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
cross-review.prompt.md — the ONE shared reviewer prompt, read by BOTH readers.
|
|
3
3
|
|
|
4
4
|
• `scripts/cross-review.mjs` (the external CLI pass) sends everything below the first `---`.
|
|
5
|
-
• The fresh `
|
|
5
|
+
• The fresh `verifier` subagent reads *Shared bar*, *What to check* and *Project rules*, and ignores
|
|
6
6
|
*CLI reader only*.
|
|
7
7
|
|
|
8
8
|
One prompt, two independent readers, no drift: that is the whole design (ways-of-work-lean-pass D8).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Which Roadmap docs `node scripts/doc-format.mjs --check` FAILS on (everything else is reported, not enforced). A trailing \"/\" makes a prefix. A new project enforces all of Roadmap/ — nothing predates the
|
|
2
|
+
"$comment": "Which Roadmap docs `node scripts/doc-format.mjs --check` FAILS on (everything else is reported, not enforced). A trailing \"/\" makes a prefix. A new project enforces all of Roadmap/ — nothing predates the refine templates. A project adopting this late lists files as each area is swept.",
|
|
3
3
|
"enforced": ["Roadmap/"]
|
|
4
4
|
}
|
package/dist/doc-format.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// doc-format.mjs — check Roadmap/ epic docs (README.md, sprint-N.md, RETROSPECTIVE.md) against the
|
|
3
|
-
// canonical shape: the `
|
|
4
|
-
// skills/
|
|
3
|
+
// canonical shape: the `refine` plugin's scaffolding templates (golden-frijoles/skills,
|
|
4
|
+
// skills/refine/templates/) — proven canonical in the project this was ported from by a zero-drift
|
|
5
5
|
// control group (81 seeds, one authoring path, identical shape; epic READMEs drift because they get
|
|
6
6
|
// hand-edited after scaffolding, away from the template). dobby-foundation's CI renders a scaffolded
|
|
7
7
|
// epic and runs this checker over it, so the producer validates its own templates.
|
|
@@ -392,7 +392,7 @@ export function checkRetrospective(content) {
|
|
|
392
392
|
// long as the italic markup actually closes somewhere (immediately after the date, or at the end
|
|
393
393
|
// of the line). Require: starts with the italic-open + "Closed:" + a real date, and a closing "_"
|
|
394
394
|
// appears somewhere after that.
|
|
395
|
-
// The canonical
|
|
395
|
+
// The canonical refine scaffold intentionally has no close date yet. Accept its literal sentinel:
|
|
396
396
|
// treating an unbuilt epic as closed would make roadmap-to-notion derive a false Shipped state.
|
|
397
397
|
const isScaffoldPlaceholder = trimmed === '_Closed: <date>_';
|
|
398
398
|
const startsWithDate = /^_Closed:\s*\d{4}-\d{2}-\d{2}/.test(trimmed);
|
package/dist/doc-hygiene.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// candidates in LEARNINGS.md + the README poster. ADVISORY ONLY: this script never edits any
|
|
4
4
|
// Roadmap doc — it only reads them and (unless --check) writes ONE new dated report file under
|
|
5
5
|
// Roadmap/00-ideas/. Every flagged candidate is a hint for a human/model to verify, not a verdict —
|
|
6
|
-
// see
|
|
6
|
+
// see Step 4 of scripts/routines/roadmap-hygiene.prompt.md for the
|
|
7
7
|
// judgment pass this script's output feeds.
|
|
8
8
|
//
|
|
9
9
|
// node scripts/doc-hygiene.mjs # print the report + write Roadmap/00-ideas/DOC-HYGIENE-REPORT-<date>.md
|
|
@@ -211,12 +211,12 @@ function buildReport() {
|
|
|
211
211
|
const date = new Date().toISOString().slice(0, 10);
|
|
212
212
|
const lines = [];
|
|
213
213
|
lines.push(
|
|
214
|
-
`<!-- Advisory artifact from the doc-hygiene
|
|
214
|
+
`<!-- Advisory artifact from the doc-hygiene pass (the roadmap-hygiene routine, step 4) (scripts/doc-hygiene.mjs). Findings are proposals`,
|
|
215
215
|
` only — no LEARNINGS.md/README.md content was changed by this script. -->`,
|
|
216
216
|
'',
|
|
217
217
|
`# Doc hygiene report — ${date}`,
|
|
218
218
|
'',
|
|
219
|
-
'🧹 **doc-hygiene
|
|
219
|
+
'🧹 **doc-hygiene pass (the roadmap-hygiene routine, step 4).** Advisory only — review by hand; nothing here gates or auto-edits.',
|
|
220
220
|
'',
|
|
221
221
|
'## Always-read set size',
|
|
222
222
|
''
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// emit-epic-kickoff.mjs — print the finished, paste-ready EPIC-MODE kickoff for a
|
|
2
|
+
// emit-epic-kickoff.mjs — print the finished, paste-ready EPIC-MODE kickoff for a refined epic.
|
|
3
3
|
//
|
|
4
4
|
// ── Why this exists ──────────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
// Epic mode — one orchestrated run across a whole epic, sprint files as internal integration boundaries
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
// npx -y @golden-frijoles/kit emit-epic-kickoff --list [--repo-root <path>] # the epics a kickoff can start
|
|
29
29
|
// # (live-build-view S2.4): `scaffolded` or `in-progress`, build order first
|
|
30
30
|
//
|
|
31
|
-
// The kickoff's one home is `/build <slug>` (the plugin's mod runs
|
|
31
|
+
// The kickoff's one home is `/build <slug>` (the plugin's mod runs refine's vendored copy of THIS file and fills the
|
|
32
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
33
|
// are the state, regenerate it.
|
|
34
34
|
//
|
|
35
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
|
|
36
|
+
// carries it in `scripts/`, and the refine skill holds a byte-identical copy in `vendor/` (render-hook-vendor.mjs).
|
|
37
37
|
|
|
38
38
|
import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
@@ -108,7 +108,7 @@ export function listEpics(repoRoot, { read = (p) => readFileSync(p, 'utf8'), exi
|
|
|
108
108
|
|
|
109
109
|
/** The list as printed: one epic per line, the slug first so it can be typed after `/build `. */
|
|
110
110
|
export function formatEpicList(epics) {
|
|
111
|
-
if (!epics.length) return 'No scaffolded or in-progress epics under Roadmap/ —
|
|
111
|
+
if (!epics.length) return 'No scaffolded or in-progress epics under Roadmap/ — refine one first.';
|
|
112
112
|
return epics.map((e) => `${e.slug} ${e.title} (${e.status}${e.build_order !== null ? `, #${e.build_order}` : ''})`).join('\n');
|
|
113
113
|
}
|
|
114
114
|
|
package/dist/emit-kickoff.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// emit-kickoff.mjs — print the finished, paste-ready Stage-8 Claude Code kickoff for a
|
|
3
|
-
// sprint. Planning-only helper for the `
|
|
2
|
+
// emit-kickoff.mjs — print the finished, paste-ready Stage-8 Claude Code kickoff for a refined
|
|
3
|
+
// sprint. Planning-only helper for the `refine` skill (Stage 8). The invariant preamble (the
|
|
4
4
|
// orientation reads, plan-mode, escalate-don't-guess triggers, etc.) lives in
|
|
5
5
|
// `templates/kickoff.md` — this script's whole point is that boilerplate stops being retyped by
|
|
6
6
|
// hand per sprint; only the sprint-specific delta (epic title, sprint number + its own title,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// npx -y @golden-frijoles/kit emit-kickoff --epic <slug> --sprint N [--repo-root <path>]
|
|
11
11
|
//
|
|
12
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
|
|
13
|
+
// carries it in `scripts/`, and the refine skill holds a byte-identical copy in `vendor/` (render-hook-vendor.mjs).
|
|
14
14
|
// The template is read from this file's own directory, which is the kit root in all three.
|
|
15
15
|
//
|
|
16
16
|
// Resolves the epic dir by SEARCHING Roadmap/*/<slug>/ under the project root (lib/kickoff-cli.mjs) — the
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// headings), then substitutes into templates/kickoff.md and prints the result to stdout.
|
|
20
20
|
//
|
|
21
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
|
|
22
|
+
// stays with the caller" stance as the other refine/cross-agent scripts in this repo.
|
|
23
23
|
|
|
24
24
|
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
package/dist/epic-actuals.mjs
CHANGED
|
@@ -660,7 +660,7 @@ export async function pushUsage({
|
|
|
660
660
|
return { sent: 0, reason: `config unreadable — ${err && err.message ? err.message : err}` };
|
|
661
661
|
}
|
|
662
662
|
if (setting !== 'on')
|
|
663
|
-
return { sent: 0, reason: 'off — spend.telemetry is not on (
|
|
663
|
+
return { sent: 0, reason: 'off — spend.telemetry is not on (frijoles-kit config set spend.telemetry on)' };
|
|
664
664
|
// The throttle counts ATTEMPTS, not successes: a persistent 401 or 429 retries every 10 minutes, not every minute.
|
|
665
665
|
const last = Math.max(index.pushed_at ?? -Infinity, index.push_attempt_at ?? -Infinity);
|
|
666
666
|
if (throttle && Number.isFinite(last) && now - last < PUSH_EVERY_MS)
|
package/dist/epic-read.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// epic-read.mjs — read an epic's result on its read date: draft the verdict with its evidence; write it on approval.
|
|
3
3
|
//
|
|
4
|
-
// node scripts/epic-read.mjs --epic <slug> # fetches the number through
|
|
4
|
+
// node scripts/epic-read.mjs --epic <slug> # fetches the number through frijoles, drafts
|
|
5
5
|
// node scripts/epic-read.mjs --epic <slug> --experiment smart-defaults # … and cites the A/B decision record
|
|
6
6
|
// node scripts/epic-read.mjs --epic <slug> --actual 72 --evidence north-star:invoices_paid_on_time@2026-11-04
|
|
7
7
|
// node scripts/epic-read.mjs --epic <slug> --evidence "traffic too low (n = 18)" # no number: unclear
|
|
8
8
|
// node scripts/epic-read.mjs --epic <slug> --verdict proven --evidence https://… # an owner verdict
|
|
9
9
|
// … --write # the approval: stamp verdict_* into the README
|
|
10
|
-
// options: --project <slug> (
|
|
11
|
-
// · --repo-root <dir> · --json · GF_BIN=<path to
|
|
10
|
+
// options: --project <slug> (frijoles's; default: the remembered one) · --today YYYY-MM-DD (default: today, UTC)
|
|
11
|
+
// · --repo-root <dir> · --json · GF_BIN=<path to frijoles> (default: frijoles on PATH)
|
|
12
12
|
//
|
|
13
13
|
// ── The shape (result-record D8) ─────────────────────────────────────────────────────────────────────────────────
|
|
14
14
|
// The agent does the legwork, the owner decides. Without `--write` this prints a draft and the exact command that
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
// • An epic shipped with no target can be read too — an owner verdict and its evidence, one epic per run.
|
|
26
26
|
//
|
|
27
27
|
// ── The agent fetches the number (result-record S3, D17) ─────────────────────────────────────────────────────────
|
|
28
|
-
// With a target and no `--actual`, this runs `
|
|
28
|
+
// With a target and no `--actual`, this runs `frijoles north-star readings <target_metric> --to <today> --json` and takes
|
|
29
29
|
// `latest`: the actual is its value, the evidence `north-star:<input>@<its day>`. A reading dated before the epic shipped
|
|
30
|
-
// is no evidence for it. `--experiment <key>` also runs `
|
|
31
|
-
// becomes the evidence, `ab:<key>`.
|
|
32
|
-
// the same name never applies. `--actual` / `--evidence` still win: they are the owner's word. No
|
|
30
|
+
// is no evidence for it. `--experiment <key>` also runs `frijoles experiments decision <key> --json`; a recorded decision
|
|
31
|
+
// becomes the evidence, `ab:<key>`. frijoles is spawned without a shell (`$GF_BIN`, else `frijoles` on PATH), so a shell alias of
|
|
32
|
+
// the same name never applies. `--actual` / `--evidence` still win: they are the owner's word. No frijoles, not signed in, no
|
|
33
33
|
// project chosen, or any failure is one "could not fetch (why)" line, and the read asks the owner as before — it never
|
|
34
34
|
// fails because the platform is not linked. Grounded is now "the readings route knows this input".
|
|
35
35
|
|
|
@@ -138,17 +138,22 @@ export function planRead({ fm, shippedAt, shipped, today, actual = null, evidenc
|
|
|
138
138
|
return { state: 'ready', target, drafted, owner, fields, late, readDate, derived };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/** Run one `
|
|
141
|
+
/** Run one `frijoles … --json` and read its single JSON document. Never throws; `why` says what went wrong in words. */
|
|
142
142
|
export function runGf(args, { env = process.env, spawnFn = spawnSync } = {}) {
|
|
143
|
-
|
|
144
|
-
const
|
|
143
|
+
// `frijoles` since CLI 1.0; `gf` is the name an older install still has (plugin-1-0 D2). GF_BIN pins one.
|
|
144
|
+
const bins = env.GF_BIN ? [env.GF_BIN] : ['frijoles', 'gf'];
|
|
145
|
+
let res = null;
|
|
146
|
+
for (const bin of bins) {
|
|
147
|
+
res = spawnFn(bin, [...args, '--json'], { encoding: 'utf8', timeout: 30_000, env, shell: false });
|
|
148
|
+
if (res.error?.code !== 'ENOENT') break; // only "not installed under this name" tries the next name
|
|
149
|
+
}
|
|
145
150
|
if (res.error)
|
|
146
151
|
return {
|
|
147
152
|
ok: false,
|
|
148
153
|
why:
|
|
149
154
|
res.error.code === 'ENOENT'
|
|
150
|
-
? '
|
|
151
|
-
: `
|
|
155
|
+
? 'frijoles is not installed (npm i -g @golden-frijoles/cli, then frijoles login)'
|
|
156
|
+
: `frijoles did not run: ${res.error.message}`,
|
|
152
157
|
};
|
|
153
158
|
let body = null;
|
|
154
159
|
try {
|
|
@@ -157,13 +162,13 @@ export function runGf(args, { env = process.env, spawnFn = spawnSync } = {}) {
|
|
|
157
162
|
body = null;
|
|
158
163
|
}
|
|
159
164
|
if (body && body.ok === true) return { ok: true, body };
|
|
160
|
-
if (res.status === 2) return { ok: false, code: 'unauthorized', why: '
|
|
165
|
+
if (res.status === 2) return { ok: false, code: 'unauthorized', why: 'frijoles is not signed in (run frijoles login)' };
|
|
161
166
|
const code = body?.code ?? null;
|
|
162
|
-
return { ok: false, code, why: body?.error ?? `
|
|
167
|
+
return { ok: false, code, why: body?.error ?? `frijoles exited ${res.status}` };
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
|
-
* The number and its evidence, fetched through
|
|
171
|
+
* The number and its evidence, fetched through frijoles (D17). Pure apart from the injected `run`.
|
|
167
172
|
* Returns `{ fetched, actual?, evidence?, reading?, decision?, grounded, why? }`.
|
|
168
173
|
*/
|
|
169
174
|
export function fetchEvidence({ metric, experiment = null, today, shippedAt, project = null, run }) {
|
|
@@ -214,12 +219,12 @@ export function renderPlan(plan, { slug, fetched, write, written }) {
|
|
|
214
219
|
out.push(` not grounded: ${t.metric} is not one of the project's North Star inputs`);
|
|
215
220
|
if (fetched.fetched)
|
|
216
221
|
out.push(
|
|
217
|
-
` fetched: ${fig(fetched.reading.value)} on ${longDay(fetched.reading.date)} (
|
|
222
|
+
` fetched: ${fig(fetched.reading.value)} on ${longDay(fetched.reading.date)} (frijoles north-star readings)`
|
|
218
223
|
);
|
|
219
224
|
else if (fetched.why) out.push(` could not fetch the number: ${fetched.why}`);
|
|
220
225
|
if (fetched.decision)
|
|
221
226
|
out.push(
|
|
222
|
-
` decision: ${fetched.decision.outcome ?? 'recorded'}${fetched.decision.chosenVariantKey ? ` (${fetched.decision.chosenVariantKey})` : ''} (
|
|
227
|
+
` decision: ${fetched.decision.outcome ?? 'recorded'}${fetched.decision.chosenVariantKey ? ` (${fetched.decision.chosenVariantKey})` : ''} (frijoles experiments decision)`
|
|
223
228
|
);
|
|
224
229
|
else if (fetched.decisionWhy) out.push(` decision: ${fetched.decisionWhy}`);
|
|
225
230
|
}
|
package/dist/init.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// init.mjs — `
|
|
2
|
+
// init.mjs — `frijoles-kit init`: adopt any repo by writing the Roadmap/ skeleton (golden-frijoles-plugin S3.2, D2).
|
|
3
3
|
//
|
|
4
4
|
// node scripts/init.mjs # write the skeleton into projectRoot(); never overwrite a file
|
|
5
5
|
//
|
|
6
6
|
// ── Why ───────────────────────────────────────────────────────────────────────────────────────
|
|
7
|
-
// A stranger who pastes the install prompt into an EXISTING repo gets the plugin, but `
|
|
7
|
+
// A stranger who pastes the install prompt into an EXISTING repo gets the plugin, but `refine` needs
|
|
8
8
|
// somewhere to write on day one — `Roadmap/README.md`, `WAYS-OF-WORKING.md`, `LEARNINGS.md`, and the
|
|
9
|
-
// `00-ideas/` funnel. `
|
|
9
|
+
// `00-ideas/` funnel. `frijoles-kit init` is that one step: it writes the same skeleton a project spawned
|
|
10
10
|
// from `template/` already has, into any repo, copied or installed (D2's two roots).
|
|
11
11
|
//
|
|
12
12
|
// ── Where the skeleton's SOURCE lives ─────────────────────────────────────────────────────────
|
|
@@ -45,7 +45,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
45
45
|
import { isInstalled, kitRoot, projectRoot } from './lib/project-root.mjs';
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* The Roadmap skeleton `
|
|
48
|
+
* The Roadmap skeleton `frijoles-kit init` writes, relative both to its source root (template/, or
|
|
49
49
|
* kit/dist/skeleton/ once built) and to the project it writes into. ONE list: `build-kit.mjs` reads
|
|
50
50
|
* this export to populate `kit/dist/skeleton/`, so there is no second copy of the file names to drift.
|
|
51
51
|
*/
|
|
@@ -104,7 +104,7 @@ export function initSkeleton({
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* True when `project` resolves to the operator's own home directory — the one place `
|
|
107
|
+
* True when `project` resolves to the operator's own home directory — the one place `frijoles-kit init`
|
|
108
108
|
* must never write, even though `projectRoot()`'s walk-up can legitimately land there (a non-git
|
|
109
109
|
* folder under a dotfiles repo tracked at `$HOME`). Pure given its injected `home`.
|
|
110
110
|
*/
|
|
@@ -116,7 +116,7 @@ function main() {
|
|
|
116
116
|
const project = projectRoot();
|
|
117
117
|
if (isHomeDirectory({ project })) {
|
|
118
118
|
console.error(
|
|
119
|
-
`
|
|
119
|
+
`frijoles-kit init: refusing to run — the resolved project root is your home directory (${project}).\n` +
|
|
120
120
|
' This usually means no Roadmap/ or .git was found between the current directory and $HOME,\n' +
|
|
121
121
|
' and $HOME itself is a git repo (a dotfiles checkout, for example). Run this from inside a\n' +
|
|
122
122
|
' real project — a directory with its own .git — or pass --root explicitly.'
|
|
@@ -124,7 +124,7 @@ function main() {
|
|
|
124
124
|
return 2;
|
|
125
125
|
}
|
|
126
126
|
const { wrote, skipped } = initSkeleton({ project, log: (line) => console.log(line) });
|
|
127
|
-
console.log(`
|
|
127
|
+
console.log(`frijoles-kit init: ${wrote.length} written, ${skipped.length} already present.`);
|
|
128
128
|
return 0;
|
|
129
129
|
}
|
|
130
130
|
|
package/dist/intent-match.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// intent-match.mjs — how well a
|
|
2
|
+
// intent-match.mjs — how well a refined pitch captured the ask it was written from (intent-match S1, D9–D15).
|
|
3
3
|
//
|
|
4
4
|
// node scripts/intent-match.mjs <seed.md> print the score, the band and a route for each gap
|
|
5
5
|
// node scripts/intent-match.mjs <seed.md> --json the same, as JSON on stdout (the report goes to stderr)
|
|
@@ -7,27 +7,27 @@
|
|
|
7
7
|
// node scripts/intent-match.mjs <seed.md> --no-route skip the second call that routes each gap to an artifact
|
|
8
8
|
//
|
|
9
9
|
// ── ADVISORY, NEVER A GATE (D1) ──────────────────────────────────────────────────────────────────────────────
|
|
10
|
-
// The score can add a step to a
|
|
10
|
+
// The score can add a step to a refinement; it never blocks a scaffold and never removes the product owner's approval.
|
|
11
11
|
// Nothing reads the exit code as a verdict: 0 means "scored", 2 means "could not look", 1 means a usage error.
|
|
12
12
|
//
|
|
13
13
|
// ── ONE STATISTIC PER QUESTION (D2, D11) ─────────────────────────────────────────────────────────────────────
|
|
14
14
|
// Each signal is one number over Jev Noul/Score answers, so a later calibration can fit weights and bands without
|
|
15
15
|
// re-asking anything (the jev-reanchor spike's lesson). Coverage in = mean P(true) over the ask's claims; coverage
|
|
16
16
|
// out = mean P(true) over the pitch's acceptance criteria; clarity = mean Score ÷ 3 over the same criteria;
|
|
17
|
-
// teach-back = the product owner's answer to
|
|
17
|
+
// teach-back = the product owner's answer to refine's Stage 1 mirror (yes 1 · partly 0.5 · no 0). The total is
|
|
18
18
|
// 100 × the equal-weight mean of the signals PRESENT, and it always says "uncalibrated" and which signals it used:
|
|
19
19
|
// the 80 / 60 bands are placeholders until `intent-outcomes` has twenty answered epics to fit them against.
|
|
20
20
|
//
|
|
21
21
|
// ── THREE STATES, NEVER TWO ──────────────────────────────────────────────────────────────────────────────────
|
|
22
22
|
// No key, `jev.egress` not `true`, a state over Jev's budget, a timeout or a malformed answer all print "could not
|
|
23
|
-
// look" and NO number. A partial score dressed as a whole one is worse than none: it is the number a
|
|
23
|
+
// look" and NO number. A partial score dressed as a whole one is worse than none: it is the number a refinement would
|
|
24
24
|
// quote. The pitch is never truncated to fit — `askJev` refuses an over-budget state, and so do we.
|
|
25
25
|
//
|
|
26
26
|
// ── NOT A JEV RAIL (C2) ──────────────────────────────────────────────────────────────────────────────────────
|
|
27
27
|
// A rail (`review`, `prose`) is a guard with a regex to fall back to and an off/shadow/jev switch. This decides
|
|
28
28
|
// nothing and has no fallback, so it only reads `jev.egress` and `jev.model`: egress must be an explicit `true`.
|
|
29
29
|
//
|
|
30
|
-
// The seed format this parses is the contract in the epic README's D9 (and
|
|
30
|
+
// The seed format this parses is the contract in the epic README's D9 (and refine's `templates/scope-seed.md`):
|
|
31
31
|
// `## The ask, as given` → `### Claims` (a numbered list) → `**Teach-back:** yes | partly | no`, and the list under
|
|
32
32
|
// the first `## Acceptance…` heading. Zero deps — Node 18+.
|
|
33
33
|
|
|
@@ -60,7 +60,7 @@ export const BANDS = Object.freeze([
|
|
|
60
60
|
{ min: 0, label: 'sketch or spike first' },
|
|
61
61
|
]);
|
|
62
62
|
|
|
63
|
-
/** The artifacts a gap can be routed to (D14) — the visuals rule (
|
|
63
|
+
/** The artifacts a gap can be routed to (D14) — the visuals rule (refine Stage 4.6) draws from the same words. */
|
|
64
64
|
export const ROUTES = Object.freeze({
|
|
65
65
|
copy_deck: 'copy deck',
|
|
66
66
|
wireframe: 'wireframe',
|