@golden-frijoles/kit 0.32.0 → 0.33.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/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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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": {
|