@golden-frijoles/kit 0.32.0 → 0.34.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/epic-read.mjs +122 -51
- package/dist/lib/cross-agent-cli.mjs +2 -2
- package/dist/lib/roadmap-contract.mjs +20 -0
- package/dist/roadmap-extract.mjs +19 -1
- package/package.json +1 -1
package/dist/epic-read.mjs
CHANGED
|
@@ -1,12 +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> #
|
|
4
|
+
// node scripts/epic-read.mjs --epic <slug> # fetches the number through gf, drafts
|
|
5
|
+
// node scripts/epic-read.mjs --epic <slug> --experiment smart-defaults # … and cites the A/B decision record
|
|
5
6
|
// node scripts/epic-read.mjs --epic <slug> --actual 72 --evidence north-star:invoices_paid_on_time@2026-11-04
|
|
6
7
|
// node scripts/epic-read.mjs --epic <slug> --evidence "traffic too low (n = 18)" # no number: unclear
|
|
7
8
|
// node scripts/epic-read.mjs --epic <slug> --verdict proven --evidence https://… # an owner verdict
|
|
8
9
|
// … --write # the approval: stamp verdict_* into the README
|
|
9
|
-
// options: --today YYYY-MM-DD (default: today, UTC)
|
|
10
|
+
// options: --project <slug> (gf's; default: the remembered one) · --today YYYY-MM-DD (default: today, UTC)
|
|
11
|
+
// · --repo-root <dir> · --json · GF_BIN=<path to gf> (default: gf on PATH)
|
|
10
12
|
//
|
|
11
13
|
// ── The shape (result-record D8) ─────────────────────────────────────────────────────────────────────────────────
|
|
12
14
|
// The agent does the legwork, the owner decides. Without `--write` this prints a draft and the exact command that
|
|
@@ -22,15 +24,19 @@
|
|
|
22
24
|
// • More than 90 days after shipping, the read is still written, and said to be late (the extract marks it).
|
|
23
25
|
// • An epic shipped with no target can be read too — an owner verdict and its evidence, one epic per run.
|
|
24
26
|
//
|
|
25
|
-
// ──
|
|
26
|
-
//
|
|
27
|
-
// `
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
27
|
+
// ── The agent fetches the number (result-record S3, D17) ─────────────────────────────────────────────────────────
|
|
28
|
+
// With a target and no `--actual`, this runs `gf north-star readings <target_metric> --to <today> --json` and takes
|
|
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 `gf experiments decision <key> --json`; a recorded decision
|
|
31
|
+
// becomes the evidence, `ab:<key>`. gf is spawned without a shell (`$GF_BIN`, else `gf` on PATH), so a shell alias of
|
|
32
|
+
// the same name never applies. `--actual` / `--evidence` still win: they are the owner's word. No gf, not signed in, no
|
|
33
|
+
// project chosen, or any failure is one "could not fetch (why)" line, and the read asks the owner as before — it never
|
|
34
|
+
// fails because the platform is not linked. Grounded is now "the readings route knows this input".
|
|
35
|
+
|
|
31
36
|
// Zero deps — Node 18+.
|
|
32
37
|
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
33
38
|
import { join, resolve } from 'node:path';
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
34
40
|
import { fileURLToPath } from 'node:url';
|
|
35
41
|
import { projectRoot } from './lib/project-root.mjs';
|
|
36
42
|
import {
|
|
@@ -39,10 +45,9 @@ import {
|
|
|
39
45
|
parseDocFrontmatter,
|
|
40
46
|
validateResultFields,
|
|
41
47
|
} from './lib/roadmap-contract.mjs';
|
|
42
|
-
import { READ_CAP_DAYS, isLate, readDateOf, todayUtc, isDay } from './lib/result-dates.mjs';
|
|
48
|
+
import { READ_CAP_DAYS, addDays, dayOf, isLate, readDateOf, todayUtc, isDay } from './lib/result-dates.mjs';
|
|
43
49
|
import { stampFrontmatter } from './lib/frontmatter-stamp.mjs';
|
|
44
50
|
import { buildRows } from './roadmap-extract.mjs';
|
|
45
|
-
import { apiKeyFrom } from './roadmap-push.mjs';
|
|
46
51
|
|
|
47
52
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
48
53
|
/** `2026-11-04` → `4 Nov 2026`. */
|
|
@@ -133,41 +138,90 @@ export function planRead({ fm, shippedAt, shipped, today, actual = null, evidenc
|
|
|
133
138
|
return { state: 'ready', target, drafted, owner, fields, late, readDate, derived };
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
/** `
|
|
137
|
-
export
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
/** Run one `gf … --json` and read its single JSON document. Never throws; `why` says what went wrong in words. */
|
|
142
|
+
export function runGf(args, { env = process.env, spawnFn = spawnSync } = {}) {
|
|
143
|
+
const bin = env.GF_BIN || 'gf';
|
|
144
|
+
const res = spawnFn(bin, [...args, '--json'], { encoding: 'utf8', timeout: 30_000, env, shell: false });
|
|
145
|
+
if (res.error)
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
why:
|
|
149
|
+
res.error.code === 'ENOENT'
|
|
150
|
+
? 'gf is not installed (npm i -g @golden-frijoles/cli, then gf login)'
|
|
151
|
+
: `gf did not run: ${res.error.message}`,
|
|
152
|
+
};
|
|
153
|
+
let body = null;
|
|
141
154
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
146
|
-
if (!res.ok) return { checked: false, why: `the engine answered ${res.status}` };
|
|
147
|
-
const body = await res.json();
|
|
148
|
-
const keys = (body?.metrics ?? []).flatMap((m) => (m?.inputs ?? []).map((i) => i?.key)).filter(Boolean);
|
|
149
|
-
return { checked: true, grounded: keys.includes(metric), keys };
|
|
150
|
-
} catch (err) {
|
|
151
|
-
return { checked: false, why: `could not reach ${baseUrl}: ${err?.message ?? err}` };
|
|
155
|
+
body = JSON.parse(res.stdout || 'null');
|
|
156
|
+
} catch {
|
|
157
|
+
body = null;
|
|
152
158
|
}
|
|
159
|
+
if (body && body.ok === true) return { ok: true, body };
|
|
160
|
+
if (res.status === 2) return { ok: false, code: 'unauthorized', why: 'gf is not signed in (run gf login)' };
|
|
161
|
+
const code = body?.code ?? null;
|
|
162
|
+
return { ok: false, code, why: body?.error ?? `gf exited ${res.status}` };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The number and its evidence, fetched through gf (D17). Pure apart from the injected `run`.
|
|
167
|
+
* Returns `{ fetched, actual?, evidence?, reading?, decision?, grounded, why? }`.
|
|
168
|
+
*/
|
|
169
|
+
export function fetchEvidence({ metric, experiment = null, today, shippedAt, project = null, run }) {
|
|
170
|
+
const scope = project ? ['--project', project] : [];
|
|
171
|
+
const out = { fetched: false, grounded: null };
|
|
172
|
+
const readings = run(['north-star', 'readings', metric, '--to', today, ...scope]);
|
|
173
|
+
if (!readings.ok) {
|
|
174
|
+
// `not_found` is also what an unknown or foreign PROJECT answers; only the input's own sentence means "not grounded".
|
|
175
|
+
if (readings.code === 'not_found' && /No North Star input/.test(readings.why ?? '')) out.grounded = false;
|
|
176
|
+
out.why = readings.why;
|
|
177
|
+
} else {
|
|
178
|
+
out.grounded = true;
|
|
179
|
+
const latest = readings.body.latest;
|
|
180
|
+
const shipped = dayOf(shippedAt);
|
|
181
|
+
if (!latest) out.why = `no reading of ${metric} yet`;
|
|
182
|
+
else if (shipped && latest.date < shipped)
|
|
183
|
+
out.why = `no reading of ${metric} since it shipped (the latest is ${latest.date})`;
|
|
184
|
+
else {
|
|
185
|
+
out.fetched = true;
|
|
186
|
+
out.reading = latest;
|
|
187
|
+
out.actual = latest.value;
|
|
188
|
+
out.evidence = `north-star:${metric}@${latest.date}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (experiment) {
|
|
192
|
+
const decision = run(['experiments', 'decision', experiment, ...scope]);
|
|
193
|
+
if (decision.ok && decision.body.decisions?.state === 'decided') {
|
|
194
|
+
out.decision = decision.body.decisions.current;
|
|
195
|
+
out.evidence = `ab:${experiment}`; // the decision record is the stronger pointer; the reading stays the actual
|
|
196
|
+
} else if (decision.ok) out.decisionWhy = `${experiment} has no decision recorded yet`;
|
|
197
|
+
else out.decisionWhy = decision.why;
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
153
200
|
}
|
|
154
201
|
|
|
155
202
|
/** The text a person reads. Pure. */
|
|
156
|
-
export function renderPlan(plan, { slug,
|
|
203
|
+
export function renderPlan(plan, { slug, fetched, write, written }) {
|
|
157
204
|
const out = [];
|
|
158
205
|
const t = plan.target;
|
|
159
206
|
const head = t.metric
|
|
160
207
|
? `${slug}: ${t.metric}${t.from !== null && t.to !== null ? ` ${fig(t.from)} → ${fig(t.to)}` : ''}${t.hypothesis ? ` — "${t.hypothesis}"` : ''}`
|
|
161
208
|
: `${slug}: no target`;
|
|
162
209
|
out.push(head);
|
|
163
|
-
if (
|
|
164
|
-
if (grounded
|
|
210
|
+
if (fetched) {
|
|
211
|
+
if (fetched.grounded === true)
|
|
212
|
+
out.push(` grounded: ${t.metric} is one of the project's North Star inputs`);
|
|
213
|
+
if (fetched.grounded === false)
|
|
214
|
+
out.push(` not grounded: ${t.metric} is not one of the project's North Star inputs`);
|
|
215
|
+
if (fetched.fetched)
|
|
216
|
+
out.push(
|
|
217
|
+
` fetched: ${fig(fetched.reading.value)} on ${longDay(fetched.reading.date)} (gf north-star readings)`
|
|
218
|
+
);
|
|
219
|
+
else if (fetched.why) out.push(` could not fetch the number: ${fetched.why}`);
|
|
220
|
+
if (fetched.decision)
|
|
165
221
|
out.push(
|
|
166
|
-
|
|
167
|
-
? ` grounded: ${t.metric} is one of the project's North Star inputs`
|
|
168
|
-
: ` not grounded: ${t.metric} is not one of the project's North Star inputs (${grounded.keys.join(', ') || 'none'})`
|
|
222
|
+
` decision: ${fetched.decision.outcome ?? 'recorded'}${fetched.decision.chosenVariantKey ? ` (${fetched.decision.chosenVariantKey})` : ''} (gf experiments decision)`
|
|
169
223
|
);
|
|
170
|
-
else if (
|
|
224
|
+
else if (fetched.decisionWhy) out.push(` decision: ${fetched.decisionWhy}`);
|
|
171
225
|
}
|
|
172
226
|
switch (plan.state) {
|
|
173
227
|
case 'already-read':
|
|
@@ -236,7 +290,7 @@ const arg = (argv, name) => {
|
|
|
236
290
|
return i === -1 ? null : (argv[i + 1] ?? null);
|
|
237
291
|
};
|
|
238
292
|
|
|
239
|
-
export async function main(argv, {
|
|
293
|
+
export async function main(argv, { spawnFn = spawnSync, env = process.env, stdout = process.stdout } = {}) {
|
|
240
294
|
const slug = arg(argv, '--epic');
|
|
241
295
|
if (!slug) {
|
|
242
296
|
process.stderr.write('epic-read: --epic <slug> is required\n');
|
|
@@ -268,24 +322,41 @@ export async function main(argv, { fetchFn = fetch, env = process.env, stdout =
|
|
|
268
322
|
const row = buildRows({ root, facts: { mode: 'docs', prs: [], branches: [] } }).find(
|
|
269
323
|
(r) => r.grain === 'Epic' && r.slug === slug
|
|
270
324
|
);
|
|
271
|
-
const
|
|
325
|
+
const shippedAt = row?.shipped_at ?? null;
|
|
326
|
+
const evidenceFlag = arg(argv, '--evidence');
|
|
327
|
+
const base = {
|
|
272
328
|
fm: parsed.data,
|
|
273
|
-
shippedAt
|
|
329
|
+
shippedAt,
|
|
274
330
|
shipped: row?.stage === 'Shipped',
|
|
275
331
|
today,
|
|
276
|
-
actual,
|
|
277
|
-
evidence: arg(argv, '--evidence'),
|
|
278
332
|
verdict: arg(argv, '--verdict'),
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
333
|
+
};
|
|
334
|
+
let plan = planRead({ ...base, actual, evidence: evidenceFlag });
|
|
335
|
+
// D17 — fetch only when there is a read to make and the owner has not given the number: never before the read date,
|
|
336
|
+
// never for an epic already read or not shipped, never over the owner's own --actual.
|
|
337
|
+
let fetched = null;
|
|
338
|
+
const readable = !['already-read', 'not-shipped', 'not-due'].includes(plan.state);
|
|
339
|
+
// The owner's word wins: a number (--actual), a reason or pointer (--evidence) or a verdict (--verdict) means the
|
|
340
|
+
// owner has answered, and a fetched number must not overrule them (fresh review, #293).
|
|
341
|
+
const ownerAnswered = actual !== null || evidenceFlag !== null || base.verdict !== null;
|
|
342
|
+
if (readable && plan.target.metric && !ownerAnswered) {
|
|
343
|
+
fetched = fetchEvidence({
|
|
344
|
+
metric: plan.target.metric,
|
|
345
|
+
experiment: arg(argv, '--experiment'),
|
|
346
|
+
// Complete days only: today's telemetry count is a partial day (fresh review, #293).
|
|
347
|
+
today: addDays(today, -1),
|
|
348
|
+
shippedAt,
|
|
349
|
+
project: arg(argv, '--project'),
|
|
350
|
+
run: (args) => runGf(args, { env, spawnFn }),
|
|
351
|
+
});
|
|
352
|
+
// Only a reading is a number to draft from; a decision on its own only names the pointer for the owner to confirm.
|
|
353
|
+
if (fetched.fetched)
|
|
354
|
+
plan = planRead({
|
|
355
|
+
...base,
|
|
356
|
+
actual: fetched.actual ?? null,
|
|
357
|
+
evidence: evidenceFlag ?? fetched.evidence ?? null,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
289
360
|
const write = argv.includes('--write');
|
|
290
361
|
let written = null;
|
|
291
362
|
if (write && plan.state === 'ready') {
|
|
@@ -297,16 +368,16 @@ export async function main(argv, { fetchFn = fetch, env = process.env, stdout =
|
|
|
297
368
|
const refused = { ...plan, state: 'refused', reasons: offenses };
|
|
298
369
|
stdout.write(
|
|
299
370
|
argv.includes('--json')
|
|
300
|
-
? `${JSON.stringify({ slug, ...refused,
|
|
301
|
-
: `${renderPlan(refused, { slug,
|
|
371
|
+
? `${JSON.stringify({ slug, ...refused, fetched, written: null })}\n`
|
|
372
|
+
: `${renderPlan(refused, { slug, fetched, write })}\n`
|
|
302
373
|
);
|
|
303
374
|
return 1;
|
|
304
375
|
}
|
|
305
376
|
writeFileSync(path, next);
|
|
306
377
|
written = path.slice(root.length + 1);
|
|
307
378
|
}
|
|
308
|
-
if (argv.includes('--json')) stdout.write(`${JSON.stringify({ slug, ...plan,
|
|
309
|
-
else stdout.write(`${renderPlan(plan, { slug,
|
|
379
|
+
if (argv.includes('--json')) stdout.write(`${JSON.stringify({ slug, ...plan, fetched, written })}\n`);
|
|
380
|
+
else stdout.write(`${renderPlan(plan, { slug, fetched, write, written })}\n`);
|
|
310
381
|
if (plan.state === 'refused') return 1;
|
|
311
382
|
if (write && plan.state !== 'ready') return 1; // asked to write, and there was nothing approvable to write
|
|
312
383
|
return 0;
|
|
@@ -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-
|
|
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.
|
|
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({
|
package/dist/roadmap-extract.mjs
CHANGED
|
@@ -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(
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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": {
|