@golden-frijoles/kit 0.27.2 → 0.29.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/dist/build-order.mjs +19 -6
- package/dist/lib/epic-kickoff.mjs +33 -3
- package/dist/lib/golden-onboarding.mjs +14 -19
- package/dist/lib/kickoff-cli.mjs +8 -0
- package/dist/roadmap-extract.mjs +2 -12
- package/dist/skeleton/Roadmap/00-ideas/README.md +15 -12
- package/dist/skeleton/Roadmap/WAYS-OF-WORKING.md +9 -5
- package/package.json +1 -1
package/dist/build-order.mjs
CHANGED
|
@@ -58,12 +58,7 @@ function line(r) {
|
|
|
58
58
|
if (r.sprint_progress) meta.push(r.sprint_progress);
|
|
59
59
|
if (r.risk) meta.push(`risk: ${r.risk}`);
|
|
60
60
|
if (r.appetite) meta.push(`appetite ${r.appetite}`);
|
|
61
|
-
|
|
62
|
-
const warn =
|
|
63
|
-
r.grain === 'Seed' && r.status === 'Queued' && !r.underwritten_by
|
|
64
|
-
? ' — ⚠️ no underwriter (set `underwritten_by:` at the betting table)'
|
|
65
|
-
: '';
|
|
66
|
-
return `- [${r.name}](${link}) — ${meta.join(' · ')} · _${r.stage_source}_${warn}`;
|
|
61
|
+
return `- [${r.name}](${link}) — ${meta.join(' · ')} · _${r.stage_source}_`;
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
function render(rows) {
|
|
@@ -130,6 +125,24 @@ if (unfunded.length) {
|
|
|
130
125
|
process.exit(1);
|
|
131
126
|
}
|
|
132
127
|
|
|
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/. `groom` funds a bet at its approval
|
|
130
|
+
// gate (`fund.mjs`), in the same commit as the scaffold, so this only fails on work that skipped the gate.
|
|
131
|
+
const BETS = join(REPO, 'Roadmap', 'bets');
|
|
132
|
+
const cycleOf = (v) => String(v).replace(/^Roadmap\/bets\//, '').replace(/\.md$/, '');
|
|
133
|
+
const liveBets = rows.filter(
|
|
134
|
+
(r) => (r.grain === 'Epic' && ['Scaffolded', 'In progress'].includes(r.status)) || (r.grain === 'Seed' && r.status === 'Queued')
|
|
135
|
+
);
|
|
136
|
+
// A cycle name is kebab-case: anything else (a `/`, a `..`) names no cycle file and is never turned into a path.
|
|
137
|
+
const paidBy = (r) => r.underwritten_by && /^[a-z0-9][a-z0-9-]*$/.test(cycleOf(r.underwritten_by)) && existsSync(join(BETS, `${cycleOf(r.underwritten_by)}.md`));
|
|
138
|
+
const unpaid = liveBets.filter((r) => !paidBy(r));
|
|
139
|
+
if (unpaid.length) {
|
|
140
|
+
console.error('Live bets with no funding record (`underwritten_by:` must name a Roadmap/bets/<cycle>.md):');
|
|
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 "$GROOM/fund.mjs" --slug <slug> --displaced "<…>" --next | --after <slug>');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
const content = render(rows);
|
|
134
147
|
|
|
135
148
|
if (LIVE) {
|
|
@@ -145,7 +145,7 @@ const FLAG_RE = /\b[a-z][a-z0-9_]*\.[a-z0-9_]+_enabled\b|\bgf flags (?:create|se
|
|
|
145
145
|
const NEGATED_RE =
|
|
146
146
|
/\b(?:no|without|zero|not an?|nor an?)\s+(?:new\s+)?(?:db\s+|database\s+|schema\s+)?migrations?\b/gi;
|
|
147
147
|
|
|
148
|
-
export function buildEpicRules({ risk, texts }) {
|
|
148
|
+
export function buildEpicRules({ risk, texts, appetite = null, slug = '<slug>' }) {
|
|
149
149
|
const all = texts.join('\n').replace(NEGATED_RE, '');
|
|
150
150
|
const rules = [];
|
|
151
151
|
if (String(risk).toUpperCase() === 'HIGH')
|
|
@@ -160,12 +160,19 @@ export function buildEpicRules({ risk, texts }) {
|
|
|
160
160
|
rules.push(
|
|
161
161
|
'- **Flag:** create it in Golden Frijoles in every env and ACTIVATE it; `gf flags get <key>` must show production.'
|
|
162
162
|
);
|
|
163
|
+
// fund-at-approval D7 — an L bet is funded one wave at a time; the re-bet is one line, asked where the builder stops.
|
|
164
|
+
if (String(appetite).toUpperCase() === 'L')
|
|
165
|
+
rules.push(
|
|
166
|
+
`- **L bet:** it is funded one wave at a time. When you stop at a wave boundary, ask the product owner one line — ` +
|
|
167
|
+
`"fund the next wave of \`${slug}\`? what does it displace?" — and on yes run groom's ` +
|
|
168
|
+
`\`fund.mjs --slug ${slug} --displaced "<…>"\` (position kept) before the next wave starts.`
|
|
169
|
+
);
|
|
163
170
|
return rules.length ? `\nFor this epic:\n${rules.join('\n')}\n` : '';
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
export function buildEpicKickoff({ macro, slug, epicTitle, risk, sprints, templateText, texts = [] }) {
|
|
173
|
+
export function buildEpicKickoff({ macro, slug, epicTitle, risk, sprints, templateText, texts = [], appetite = null }) {
|
|
167
174
|
return sub(templateText, {
|
|
168
|
-
EPIC_RULES: buildEpicRules({ risk, texts }),
|
|
175
|
+
EPIC_RULES: buildEpicRules({ risk, texts, appetite, slug }),
|
|
169
176
|
MACRO: macro,
|
|
170
177
|
SLUG: slug,
|
|
171
178
|
EPIC_TITLE: epicTitle,
|
|
@@ -176,6 +183,28 @@ export function buildEpicKickoff({ macro, slug, epicTitle, risk, sprints, templa
|
|
|
176
183
|
});
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
// `appetite:` from a doc's frontmatter (the README's own first, else its seed's — finops D20), or null.
|
|
187
|
+
export function parseAppetite(text) {
|
|
188
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(text ?? '')?.[1] ?? '';
|
|
189
|
+
return /^appetite:\s*"?([SML])"?\s*(?:#.*)?$/m.exec(fm)?.[1] ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The epic's seed: the one whose `epic:` names `<macro>/<slug>` (as the extractor matches it — a seed's slug may differ
|
|
193
|
+
// from its epic's), else `seeds/<slug>.md`; '' when there is none.
|
|
194
|
+
function readSeed(read, list, dir, macro, slug) {
|
|
195
|
+
const seeds = join(dir, '..', '..', '00-ideas', 'seeds');
|
|
196
|
+
const pointer = new RegExp(`^epic:\\s*"?${macro}/${slug}"?\\s*(?:#.*)?$`, 'm');
|
|
197
|
+
try {
|
|
198
|
+
for (const name of list(seeds).filter((n) => String(n).endsWith('.md'))) {
|
|
199
|
+
const text = read(join(seeds, String(name)));
|
|
200
|
+
if (pointer.test(/^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? '')) return text;
|
|
201
|
+
}
|
|
202
|
+
return read(join(seeds, `${slug}.md`));
|
|
203
|
+
} catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
179
208
|
// The epic README header line carries `**Risk:** <low|high>`; the frontmatter doesn't. Falls back to
|
|
180
209
|
// 'high' — the WAYS-OF-WORKING rule is "when unsure, treat it as high", and a kickoff that under-declares
|
|
181
210
|
// risk is the one that skips the mandatory fresh-reviewer pass.
|
|
@@ -261,6 +290,7 @@ export function epicKickoffFromDir({
|
|
|
261
290
|
sprints,
|
|
262
291
|
templateText: EPIC_KICKOFF_TEMPLATE,
|
|
263
292
|
texts: [readmeText, ...sprints.map((s) => s.text)],
|
|
293
|
+
appetite: parseAppetite(readmeText) ?? parseAppetite(readSeed(read, list, dir, macro, slug)),
|
|
264
294
|
}),
|
|
265
295
|
};
|
|
266
296
|
}
|
|
@@ -157,29 +157,24 @@ export function compareVersions(a, b) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* The ONE install prompt a stranger pastes into their agent (golden-frijoles-plugin S3.3/S3.4
|
|
160
|
+
* The ONE install prompt a stranger pastes into their agent (golden-frijoles-plugin S3.3/S3.4;
|
|
161
|
+
* account-from-the-terminal S1.2, D2).
|
|
161
162
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
163
|
+
* golden-beans `apps/web/lib/install-prompt.ts` is the SOURCE: `installPrompt(siteUrl)`, built from
|
|
164
|
+
* `getSiteUrl()` there because the prompt now sends the agent to read `<site>/install.md` before it
|
|
165
|
+
* installs anything. This is its transcription against the PRODUCTION origin — never a preview URL —
|
|
166
|
+
* and golden-beans' `install-prompt.test.ts` imports this constant and asserts it equals
|
|
167
|
+
* `installPrompt('https://goldenfrijoles.com')`, so a one-sided edit goes red there.
|
|
167
168
|
* `scripts/check-onboarding-parity.mjs` asserts this string appears VERBATIM in the repo README.md and
|
|
168
|
-
* the `golden-frijoles` umbrella SKILL.md
|
|
169
|
-
* source (golden-beans `apps/web/lib/install-prompt.ts`) changing first.
|
|
170
|
-
*
|
|
171
|
-
* Text: the audit's §3.1 prompt (`Roadmap/00-ideas/audits/golden-frijoles-unification-2026-09-23.md`,
|
|
172
|
-
* golden-beans checkout), verbatim, its soft-wrapped blockquote lines joined into the one paragraph it
|
|
173
|
-
* renders as.
|
|
169
|
+
* the `golden-frijoles` umbrella SKILL.md. Never hand-edit this without the source changing first.
|
|
174
170
|
*/
|
|
175
171
|
export const INSTALL_PROMPT =
|
|
176
|
-
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'Then use the golden-frijoles skill when working on this project, and start with its setup.';
|
|
172
|
+
'Set up Golden Frijoles in this project. ' +
|
|
173
|
+
'1. Read https://goldenfrijoles.com/install.md before installing anything. ' +
|
|
174
|
+
'2. Tell me in a few lines what it installs, what changes on this machine and which services it contacts. ' +
|
|
175
|
+
'Offer me a security review, and wait for my go-ahead. ' +
|
|
176
|
+
'3. Install it the way install.md says for the agent you are. ' +
|
|
177
|
+
'4. Run the golden-frijoles skill and start its setup.';
|
|
183
178
|
|
|
184
179
|
/**
|
|
185
180
|
* The value of `name` in a dotenv file, or null.
|
package/dist/lib/kickoff-cli.mjs
CHANGED
|
@@ -67,6 +67,14 @@ export function findEpicDir(repoRoot, slug, { exists = existsSync, list = readdi
|
|
|
67
67
|
}
|
|
68
68
|
const hits = macros.filter((m) => exists(join(roadmapDir, m, slug, 'README.md')));
|
|
69
69
|
if (hits.length === 0) {
|
|
70
|
+
// A seed with no epic yet (dogfood F33): say what it is and the one command that makes it buildable.
|
|
71
|
+
if (exists(join(roadmapDir, '00-ideas', 'seeds', `${slug}.md`))) {
|
|
72
|
+
return {
|
|
73
|
+
error:
|
|
74
|
+
`"${slug}" is a seed with no epic yet — a kickoff builds an epic. Fund it at the approval gate, then scaffold it ` +
|
|
75
|
+
`(a fixed-scope seed needs nothing but its slug): node "$GROOM/scaffold-epic.mjs" --slug ${slug}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
70
78
|
return { error: `no epic found for slug "${slug}" under any Roadmap/*/ dir in "${repoRoot}" (searched: ${macros.join(', ') || '(none)'})` };
|
|
71
79
|
}
|
|
72
80
|
if (hits.length > 1) {
|
package/dist/roadmap-extract.mjs
CHANGED
|
@@ -101,13 +101,6 @@ const SEED_STATUS_LABEL = {
|
|
|
101
101
|
shipped: 'Shipped',
|
|
102
102
|
archived: 'Archived',
|
|
103
103
|
};
|
|
104
|
-
const PRIORITY_LABEL = {
|
|
105
|
-
'wave-0': 'Wave 0 Enablers',
|
|
106
|
-
'wave-1': 'Wave 1',
|
|
107
|
-
'wave-2': 'Wave 2',
|
|
108
|
-
'wave-3': 'Wave 3',
|
|
109
|
-
'wave-4': 'Wave 4',
|
|
110
|
-
};
|
|
111
104
|
const TYPE_LABEL = { feature: 'Feature', spike: 'Spike', chore: 'Chore', bug: 'Bug', epic: 'Epic' };
|
|
112
105
|
|
|
113
106
|
/**
|
|
@@ -523,7 +516,6 @@ export function buildRows({
|
|
|
523
516
|
const totStories = boardSprints.reduce((a, s) => a + s.total, 0);
|
|
524
517
|
const doneStories = boardSprints.reduce((a, s) => a + s.done, 0);
|
|
525
518
|
const area = AREA_NAMES[e.area] || e.area;
|
|
526
|
-
const priority = seed.priority ? PRIORITY_LABEL[seed.priority] || seed.priority : null;
|
|
527
519
|
const riskWord = epicFm.risk || seed.risk;
|
|
528
520
|
const risk = riskWord ? (riskWord === 'high' ? 'High' : 'Low') : null;
|
|
529
521
|
const readmePath = `Roadmap/${epicKey}/README.md`;
|
|
@@ -561,12 +553,12 @@ export function buildRows({
|
|
|
561
553
|
stage,
|
|
562
554
|
stage_source: stageSource,
|
|
563
555
|
area,
|
|
564
|
-
priority,
|
|
565
556
|
type: TYPE_LABEL[epicFm.type || seed.type] || 'Epic',
|
|
566
557
|
risk,
|
|
567
558
|
// finops D20 — the README's own appetite first (an epic groomed after FinOps carries it), else the seed's.
|
|
568
559
|
appetite: epicFm.appetite || seed.appetite || null,
|
|
569
|
-
|
|
560
|
+
// fund-at-approval D8 — the README's own first (an epic with no seed carries it there), else the seed's.
|
|
561
|
+
underwritten_by: epicFm.underwritten_by || seed.underwritten_by || null,
|
|
570
562
|
sprint_progress: totStories ? `${doneStories}/${totStories} stories` : `${sprints.length} sprints`,
|
|
571
563
|
build_order: buildOrder,
|
|
572
564
|
build_order_num: buildOrderNum(buildOrder),
|
|
@@ -598,7 +590,6 @@ export function buildRows({
|
|
|
598
590
|
status: sp.status,
|
|
599
591
|
status_date: when(`Roadmap/${epicKey}/sprint-${sp.n}.md`, '^(\\*\\*)?Status:'),
|
|
600
592
|
area,
|
|
601
|
-
priority,
|
|
602
593
|
type: 'Sprint',
|
|
603
594
|
risk,
|
|
604
595
|
sprint_progress: sp.total ? `${sp.done}/${sp.total} stories` : '—',
|
|
@@ -625,7 +616,6 @@ export function buildRows({
|
|
|
625
616
|
stage,
|
|
626
617
|
stage_source: stageSource,
|
|
627
618
|
area: AREA_NAMES[s.area] || s.area || null,
|
|
628
|
-
priority: s.priority ? PRIORITY_LABEL[s.priority] || s.priority : null,
|
|
629
619
|
type: TYPE_LABEL[s.type] || 'Feature',
|
|
630
620
|
risk: s.risk ? (s.risk === 'high' ? 'High' : 'Low') : null,
|
|
631
621
|
appetite: s.appetite || null,
|
|
@@ -22,12 +22,11 @@ slug: example-feature-idea # kebab; matches the filename
|
|
|
22
22
|
status: raw # raw | ready | queued | scaffolded | in-progress | shipped | archived
|
|
23
23
|
area: "01" # macro-section number, matching Roadmap/README.md's table
|
|
24
24
|
type: feature # feature | spike | chore | epic
|
|
25
|
-
priority: null # a wave/priority label, or null
|
|
26
25
|
appetite: null # S | M | L — the budget, set at shaping; REQUIRED before `queued`
|
|
27
|
-
underwritten_by: null #
|
|
26
|
+
underwritten_by: null # the cycle that PAID for it (Roadmap/bets/<cycle>.md, bare name) — fund.mjs
|
|
28
27
|
risk: low # low | high
|
|
29
28
|
epic: null # path to the scaffolded epic, or null until scaffolded
|
|
30
|
-
build_order: null #
|
|
29
|
+
build_order: null # integer position in the one global sequence — fund.mjs places it
|
|
31
30
|
updated: <date>
|
|
32
31
|
---
|
|
33
32
|
```
|
|
@@ -64,10 +63,11 @@ One field is authoritative at each stage — they never both drive the board:
|
|
|
64
63
|
|
|
65
64
|
`appetite` (S | M | L) is the **budget the idea is worth**, fixed at shaping *before* the solution
|
|
66
65
|
is designed — sessions + an implied token band, never a time estimate (see WAYS-OF-WORKING →
|
|
67
|
-
*Betting & appetite*). `underwritten_by` names the **
|
|
68
|
-
`Roadmap/bets/<
|
|
69
|
-
fine in the funnel, impossible on the board: `build-order.mjs`
|
|
70
|
-
no `appetite`, and
|
|
66
|
+
*Betting & appetite*). `underwritten_by` names the **cycle that paid for it** — the bare name of a
|
|
67
|
+
`Roadmap/bets/<cycle>.md` file (`wave-2026-10`), written by `groom`'s `fund.mjs` at the approval gate.
|
|
68
|
+
`null` means nobody has paid for it yet — fine in the funnel, impossible on the board: `build-order.mjs`
|
|
69
|
+
**hard-fails** a `queued` seed with no `appetite`, and a live bet (`queued`, or an epic scaffolded or in
|
|
70
|
+
progress) whose `underwritten_by` is missing or names no cycle file. Like `status`, `appetite` is an
|
|
71
71
|
enforced enum — a present-but-unrecognized value fails the board, it never falls back silently.
|
|
72
72
|
|
|
73
73
|
## How seeds flow (no file moves)
|
|
@@ -76,10 +76,13 @@ enforced enum — a present-but-unrecognized value fails the board, it never fal
|
|
|
76
76
|
from a brain-dump).
|
|
77
77
|
2. **Scope** — `groom` fills out the Definition-of-Ready (appetite included) and flips
|
|
78
78
|
`status: ready`.
|
|
79
|
-
3. **
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`status: scaffolded
|
|
79
|
+
3. **Fund + scaffold, in one answer** — the approval gate is the betting table. On "approve", `groom`
|
|
80
|
+
runs its own `fund.mjs` (a row in the month's `Roadmap/bets/wave-YYYY-MM.md`, created on first use,
|
|
81
|
+
recording what the bet displaced; `underwritten_by:`; a `build_order` placed with `--next` or
|
|
82
|
+
`--after <slug>`, renumbering only the queue) and then `scaffold-epic.mjs` (the epic/sprint docs; the
|
|
83
|
+
seed gets `epic:` + `status: scaffolded`), committed together. Both ship inside the `groom` skill,
|
|
84
|
+
`golden-frijoles` plugin. A fixed-scope seed scaffolds from its slug alone, its acceptance criteria
|
|
85
|
+
becoming sprint 1's stories. "Approve, don't fund" leaves the seed `ready` and scaffolds nothing.
|
|
86
|
+
**No file ever moves between folders** — the frontmatter carries the state.
|
|
84
87
|
|
|
85
88
|
Filenames are kebab-case and match `slug`. Audits live in `audits/`, never in `seeds/`.
|
|
@@ -92,11 +92,15 @@ and session context. Fixed appetite, variable scope.
|
|
|
92
92
|
| **M** | one wave: an architect session + builder fan-out + review rounds | appetite exhausted → stop, back to shaping |
|
|
93
93
|
| **L** | a multi-wave epic | per-wave: each wave is re-bet at the boundary |
|
|
94
94
|
|
|
95
|
-
Four rules: **an exhausted bet returns to shaping**, never extends in flight; **
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
Four rules: **an exhausted bet returns to shaping**, never extends in flight; **the approval gate is the
|
|
96
|
+
betting table** — approving a pitch in `groom` funds it in the same answer (`fund.mjs`: a row in the month's
|
|
97
|
+
cycle file `Roadmap/bets/wave-YYYY-MM.md` recording what it displaced, `underwritten_by:`, and a build
|
|
98
|
+
position), in the same commit as the scaffold, while "approve, don't fund" leaves it `ready` and scaffolds
|
|
99
|
+
nothing; **nothing scaffolded is unfunded** — `build-order.mjs` fails a live bet with no `underwritten_by:`;
|
|
100
|
+
and **uphill work stays on the strongest model**. An L bet is re-bet at each wave boundary with one line
|
|
101
|
+
when its builder stops there ("fund the next wave of `<slug>`? what does it displace?"; yes runs `fund.mjs`
|
|
102
|
+
again, position kept). Fixed scope (appetite S) and reactive/ops work go through the same gate, straight to
|
|
103
|
+
a builder. Why it works this way:
|
|
100
104
|
[`references/shapeup/`](https://github.com/golden-frijoles/skills/blob/main/template/references/shapeup/README.md).
|
|
101
105
|
|
|
102
106
|
## Review & merge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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": {
|