@golden-frijoles/kit 0.33.0 → 0.35.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.
@@ -54,10 +54,10 @@ export const AGENT_BIN = {
54
54
  // Harmless here since AGY_MODEL/AGY_FALLBACK_MODEL below are always valid, listed model names (checked via
55
55
  // `agy models`), but it means a future typo in either constant would silently review with the WRONG model
56
56
  // instead of failing loud — watch for that if either constant is ever edited.
57
- // agy-doctor: last verified 2026-10-06 against 1.3.0.
57
+ // agy-doctor: last verified 2026-10-07 against 1.3.1.
58
58
  // ^ machine-managed marker — `node scripts/cross-agent-doctor.mjs agy --fix` rewrites it (with the constant
59
59
  // below) after a green live contract probe. Don't hand-edit the marker's shape.
60
- export const AGY_PINNED = '1.3.0';
60
+ export const AGY_PINNED = '1.3.1';
61
61
 
62
62
  // agy's `--print` mode prints NOTHING unless `--model` names a model — and, crucially, it ALSO prints
63
63
  // nothing (exit 0, empty stdout — the error lands only in agy's log, see --log-file) when the model is
@@ -140,6 +140,25 @@ export function validateResultFields(fm) {
140
140
  return offenses;
141
141
  }
142
142
 
143
+ // one-epic-page D11 — the epic's flag, decided at groom Stage 6b and copied into the README by the scaffold. The SDK's
144
+ // flag-key grammar (`FLAG_KEY` in apps/web/lib/flag-admin-operation.ts). Optional: `flag_key: null` (no flag) is the
145
+ // default and never an error; a value that is not a key is named, never pushed (the extract sends null for it).
146
+ export const FLAG_KEY_RE = /^[a-z][a-z0-9_.-]{0,127}$/;
147
+
148
+ /** `flag_key:` → offenses (`contract-flag-key-invalid`). Absent or null is fine. */
149
+ export function validateFlagKey(fm) {
150
+ const v = fm.flag_key;
151
+ if (v === undefined || v === null) return [];
152
+ // The string "null" fits the grammar but means "no flag": named here as the push schema refuses it (codex, #297).
153
+ if (typeof v === 'string' && v !== 'null' && FLAG_KEY_RE.test(v)) return [];
154
+ return [
155
+ {
156
+ rule: 'contract-flag-key-invalid',
157
+ detail: `flag_key: "${v}" is not a flag key (a lowercase letter, then a-z 0-9 _ . -; at most 128) — or null`,
158
+ },
159
+ ];
160
+ }
161
+
143
162
  // live-build-view D10 — `locked_at:` is stamped by `scripts/epic-phase.mjs lock` when the architecture lock is written;
144
163
  // the build view reads its absence as "Locking architecture". Optional (no epic before it has one), an ISO date-time
145
164
  // string when present: the command writes `"2026-10-03T20:34:34Z"` (quoted, so the frontmatter parser keeps the colons).
@@ -355,6 +374,7 @@ export function validateEpicFrontmatter(parsed, ctx = {}) {
355
374
  }
356
375
  offenses.push(...validateFinopsFields(fm));
357
376
  offenses.push(...validateResultFields(fm));
377
+ offenses.push(...validateFlagKey(fm));
358
378
  offenses.push(...validateLockedAt(fm));
359
379
  if (isInt(fm.sprints_total) && isInt(ctx.sprintCount) && fm.sprints_total !== ctx.sprintCount)
360
380
  offenses.push({
@@ -73,6 +73,7 @@ import {
73
73
  FINOPS_NUMERIC_FIELDS,
74
74
  RESULT_DAY_FIELDS,
75
75
  RESULT_FIELDS,
76
+ FLAG_KEY_RE,
76
77
  RESULT_NUMERIC_FIELDS,
77
78
  VERDICTS,
78
79
  } from './lib/roadmap-contract.mjs';
@@ -161,6 +162,21 @@ export function resultFields(fm, shippedAt) {
161
162
  return out;
162
163
  }
163
164
 
165
+ /**
166
+ * one-epic-page D11 — an epic's flag: `flag_key` off its README frontmatter (the SDK's key grammar, else null — the
167
+ * contract names a bad one) and `flag_note`, the README's own `**Flag:** …` line, which is where an epic with no flag
168
+ * says why. Both null when absent; nothing is inferred from prose about a flag the frontmatter does not name.
169
+ */
170
+ export function flagFields(fm, readme) {
171
+ const raw = typeof fm.flag_key === 'string' ? fm.flag_key.trim() : '';
172
+ // A QUOTED `flag_key: "null"` (or `~`) arrives as a string, and "null" fits the key grammar — so the blank
173
+ // spellings are refused before the pattern is asked. (Unquoted `null` is already null from this file's reader.)
174
+ const blank = raw === '' || raw === 'null' || raw === '~';
175
+ const line = /^\*\*Flag:\*\*[ \t]*(.+)$/m.exec(readme ?? '');
176
+ const note = line ? line[1].replace(/[*`]/g, '').trim().slice(0, 280) : '';
177
+ return { flag_key: !blank && FLAG_KEY_RE.test(raw) ? raw : null, flag_note: note || null };
178
+ }
179
+
164
180
  function parseFrontmatter(md) {
165
181
  if (!md.startsWith('---')) return {};
166
182
  const end = md.indexOf('\n---', 3);
@@ -583,6 +599,7 @@ export function buildRows({
583
599
  }
584
600
  }
585
601
 
602
+ const readme = readFileSync(join(e.path, 'README.md'), 'utf8');
586
603
  // Epic row
587
604
  rows.push({
588
605
  name: epicTitle(e.path, e.slug),
@@ -605,7 +622,7 @@ export function buildRows({
605
622
  build_order_num: buildOrderNum(buildOrder),
606
623
  doc_link: readmePath,
607
624
  epic_slug: null,
608
- goal: firstParagraph(readFileSync(join(e.path, 'README.md'), 'utf8'), ['Why', 'Goal', 'Problem']),
625
+ goal: firstParagraph(readme, ['Why', 'Goal', 'Problem']),
609
626
  sprints: boardSprints.map((sp) => ({ n: sp.n, title: sp.title, done: sp.done, total: sp.total })),
610
627
  links: {
611
628
  readme: readmePath,
@@ -618,6 +635,7 @@ export function buildRows({
618
635
  shipped_at: stage === 'Shipped' ? statusDay : null,
619
636
  ...finopsFields(epicFm),
620
637
  ...resultFields(epicFm, stage === 'Shipped' ? statusDay : null),
638
+ ...flagFields(epicFm, readme),
621
639
  });
622
640
 
623
641
  // Sprint rows (one per sprint-N.md), related to the Epic by slug. boardSprints already carries
@@ -76,13 +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. **Fund + scaffold, in one answer** — the approval gate is the betting table. On "approve", `groom`
79
+ 3. **Fund + scaffold, in one answer** — the approval gate is the betting table. On "Approve the plan", `groom`
80
80
  runs its own `fund.mjs` (a row in the month's `Roadmap/bets/wave-YYYY-MM.md`, created on first use,
81
81
  recording what the bet displaced; `underwritten_by:`; a `build_order` placed with `--next` or
82
82
  `--after <slug>`, renumbering only the queue) and then `scaffold-epic.mjs` (the epic/sprint docs; the
83
83
  seed gets `epic:` + `status: scaffolded`), committed together. Both ship inside the `groom` skill,
84
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.
85
+ becoming sprint 1's stories. "Park it" leaves the seed `ready` and scaffolds nothing.
86
86
  **No file ever moves between folders** — the frontmatter carries the state.
87
87
 
88
88
  Filenames are kebab-case and match `slug`. Audits live in `audits/`, never in `seeds/`.
@@ -93,10 +93,11 @@ and session context. Fixed appetite, variable scope.
93
93
  | **L** | a multi-wave epic | per-wave: each wave is re-bet at the boundary |
94
94
 
95
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:`;
96
+ betting table** — **Approve the plan** in `groom`'s Plan gate funds it in the same answer (`fund.mjs`: a row in
97
+ the month's cycle file `Roadmap/bets/wave-YYYY-MM.md` recording what it displaced, `underwritten_by:`, and a
98
+ build position), in the same commit as the scaffold, while **Park it** leaves it `ready` and scaffolds nothing
99
+ (the gates' words: groom `references/gates.md`); **nothing scaffolded is unfunded** — `build-order.mjs` fails a
100
+ live bet with no `underwritten_by:`;
100
101
  and **uphill work stays on the strongest model**. An L bet is re-bet at each wave boundary with one line
101
102
  when its builder stops there ("fund the next wave of `<slug>`? what does it displace?"; yes runs `fund.mjs`
102
103
  again, position kept). Fixed scope (appetite S) and reactive/ops work go through the same gate, straight to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golden-frijoles/kit",
3
- "version": "0.33.0",
3
+ "version": "0.35.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": {