@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/dist/intent-reader.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// node scripts/intent-reader.mjs --epic <slug> [--timeout <seconds>]
|
|
5
5
|
//
|
|
6
6
|
// The epic kickoff's lock step names this command on every run. With `intent.reader` off — the default — it prints
|
|
7
|
-
// one line and exits before touching any CLI, so it costs nothing. Turned on (`
|
|
7
|
+
// one line and exits before touching any CLI, so it costs nothing. Turned on (`frijoles-kit config set intent.reader on`),
|
|
8
8
|
// it hands the epic's seed (the pitch file, nothing else) to the first of codex, agy and vibe that is installed,
|
|
9
9
|
// once, with a hard timeout, and asks Jev one Noul: would that reader build the same thing as the lock's plan? The
|
|
10
10
|
// answer is the fifth signal, "agreement", and it is written into the epic README's `## Intent match` section.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Any failure — no reader CLI, a capped or logged-out CLI, a timeout, an empty or unstructured reply, Jev unable to
|
|
14
14
|
// look — prints exactly ONE line, `reader skipped: <why>`, and exits 0. There is no retry and no second family after
|
|
15
15
|
// one has been asked: free-tier CLIs can be finicky, and three reads of every pitch cost more time than they return
|
|
16
|
-
// (the product owner's call at
|
|
16
|
+
// (the product owner's call at refining). Exit 1 is kept for a usage error only.
|
|
17
17
|
//
|
|
18
18
|
// ── NEVER CLAUDE ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
19
19
|
// Planning and building run on Claude; a Claude reader adds no independent read. `FAMILIES` holds no Claude entry
|
|
@@ -156,7 +156,7 @@ export function readerSection({ family, agreement, seedComponents, seedTotal, da
|
|
|
156
156
|
? `- Seed score: **${seedTotal ?? '—'}** (${Object.entries(seedComponents)
|
|
157
157
|
.map(([k, v]) => `${SIGNAL_NAMES[k]} ${f2(v)}`)
|
|
158
158
|
.join(' · ')})`
|
|
159
|
-
: '- Seed score: none recorded (run `node scripts/intent-match.mjs <seed> --write` at
|
|
159
|
+
: '- Seed score: none recorded (run `node scripts/intent-match.mjs <seed> --write` at refine Stage 3.5)';
|
|
160
160
|
const lines = [
|
|
161
161
|
'## Intent match',
|
|
162
162
|
'',
|
|
@@ -235,7 +235,7 @@ export async function run(argv, io) {
|
|
|
235
235
|
const setting = io.setting();
|
|
236
236
|
if (setting !== 'on') {
|
|
237
237
|
io.out(
|
|
238
|
-
`intent reader: off (intent.reader: ${JSON.stringify(setting)}) — no reader asked. Turn it on with \`
|
|
238
|
+
`intent reader: off (intent.reader: ${JSON.stringify(setting)}) — no reader asked. Turn it on with \`frijoles-kit config set intent.reader on\`.`
|
|
239
239
|
);
|
|
240
240
|
return 0;
|
|
241
241
|
}
|
package/dist/jev-eval.mjs
CHANGED
|
@@ -423,7 +423,7 @@ export function formatReport(report) {
|
|
|
423
423
|
export function liveRefusal(config, getKey) {
|
|
424
424
|
if (config.egress !== true)
|
|
425
425
|
// false: no text leaves this machine; null: nobody has said yes yet (D12). `--live` sends fixtures.
|
|
426
|
-
return `jev-eval --live: jev.egress is ${JSON.stringify(config.egress)}, not true — refusing to send fixtures to Jev. Say yes with \`
|
|
426
|
+
return `jev-eval --live: jev.egress is ${JSON.stringify(config.egress)}, not true — refusing to send fixtures to Jev. Say yes with \`frijoles-kit config set jev.egress true\` first.`;
|
|
427
427
|
if (!getKey())
|
|
428
428
|
return 'jev-eval --live needs TYPESAFE_API_KEY: put TYPESAFE_API_KEY=… in .env.local (or the environment).';
|
|
429
429
|
return null;
|
package/dist/lib/board-text.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Pure — the caller passes the rows and how fresh their facts are.
|
|
7
7
|
|
|
8
|
-
import { STAGES, groupByStage } from './stage.mjs';
|
|
8
|
+
import { STAGES, groupByStage, stageWord } from './stage.mjs';
|
|
9
9
|
|
|
10
10
|
const SHIPPED_SHOWN = 5;
|
|
11
11
|
|
|
@@ -36,7 +36,7 @@ export function renderBoardText(rows, facts = { mode: 'docs' }) {
|
|
|
36
36
|
const out = [`Board — six stages, ${fresh}`, ''];
|
|
37
37
|
for (const stage of STAGES) {
|
|
38
38
|
const list = columns[stage];
|
|
39
|
-
out.push(`${stage} (${list.length})`);
|
|
39
|
+
out.push(`${stageWord(stage)} (${list.length})`);
|
|
40
40
|
const shown = stage === 'Shipped' ? list.slice(0, SHIPPED_SHOWN) : list;
|
|
41
41
|
for (const row of shown) out.push(cardLine(row));
|
|
42
42
|
if (stage === 'Shipped' && list.length > shown.length)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// config-registry.mjs — every setting Golden Frijoles can ask for, in one table (golden-frijoles-plugin S4.3, D11).
|
|
2
2
|
//
|
|
3
3
|
// This is the audit §4.3 table as data. Setup reads it for its questions, a script reads it before asking
|
|
4
|
-
// just-in-time (lib/config.mjs `needSetting`), and `
|
|
4
|
+
// just-in-time (lib/config.mjs `needSetting`), and `frijoles doctor` reads it to say which modules are configured. A new
|
|
5
5
|
// setting is a new row here, never a question hard-coded in a skill.
|
|
6
6
|
//
|
|
7
7
|
// key dotted, `<section>.<name>`: the path in golden-frijoles.config.json
|
|
8
8
|
// module the product module doctor groups it under (Plan · Build · Ship · Measure · Spend · Operate)
|
|
9
|
-
// askWhen 'setup' | 'first-
|
|
10
|
-
// | 'first-high-risk-
|
|
9
|
+
// askWhen 'setup' | 'first-refine' | 'first-pr' | 'first-smoke' | 'first-report' | 'first-prune'
|
|
10
|
+
// | 'first-high-risk-refine' | 'never-yet' (declared so the schema is ready; nothing reads it this wave)
|
|
11
11
|
// default what a skipped question means. `null` = "unanswered", which a rail must treat as its safest choice
|
|
12
12
|
// question the plain words an agent asks, once
|
|
13
|
-
// choices optional allowed values; `store: 'env'` means the answer lives in .env.local (
|
|
13
|
+
// choices optional allowed values; `store: 'env'` means the answer lives in .env.local (frijoles init), never here
|
|
14
14
|
//
|
|
15
15
|
// Zero deps, no imports: config.mjs imports THIS, so it must not import back.
|
|
16
16
|
|
|
@@ -42,7 +42,7 @@ export const REGISTRY = Object.freeze([
|
|
|
42
42
|
choices: ['later', 'now'],
|
|
43
43
|
store: 'env',
|
|
44
44
|
question:
|
|
45
|
-
'An account adds flags you can roll out and turn off, each bet measured on its read date, every product in one place and an outcome report you can send. Sign in now (recommended:
|
|
45
|
+
'An account adds flags you can roll out and turn off, each bet measured on its read date, every product in one place and an outcome report you can send. Sign in now (recommended: frijoles login, then frijoles init), or later?',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
key: 'board.sink',
|
|
@@ -81,14 +81,14 @@ export const REGISTRY = Object.freeze([
|
|
|
81
81
|
{
|
|
82
82
|
key: 'roadmap.areas',
|
|
83
83
|
module: 'Plan',
|
|
84
|
-
askWhen: 'first-
|
|
84
|
+
askWhen: 'first-refine',
|
|
85
85
|
default: ['09 Platform & Infra'],
|
|
86
86
|
question: 'Which areas (macro-sections) does this roadmap have? 09 Platform & Infra is reserved.',
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
key: 'ways.fillIns',
|
|
90
90
|
module: 'Plan',
|
|
91
|
-
askWhen: 'first-
|
|
91
|
+
askWhen: 'first-refine',
|
|
92
92
|
default: 'Roadmap/fill-ins.yml',
|
|
93
93
|
question: 'Where are this project\'s WAYS-OF-WORKING fill-ins?',
|
|
94
94
|
},
|
|
@@ -140,7 +140,7 @@ export const REGISTRY = Object.freeze([
|
|
|
140
140
|
{
|
|
141
141
|
key: 'routines',
|
|
142
142
|
module: 'Operate',
|
|
143
|
-
// never-yet: declares the section so readSection accepts it, while `
|
|
143
|
+
// never-yet: declares the section so readSection accepts it, while `frijoles doctor` (which skips never-yet
|
|
144
144
|
// rows) never reports Operate unconfigured for it — its values are per-routine fill-ins that
|
|
145
145
|
// routine-bootstrap.mjs names itself when one is missing (#191 review).
|
|
146
146
|
askWhen: 'never-yet',
|
|
@@ -157,7 +157,7 @@ export const REGISTRY = Object.freeze([
|
|
|
157
157
|
{
|
|
158
158
|
key: 'ship.killSwitchPolicy',
|
|
159
159
|
module: 'Ship',
|
|
160
|
-
askWhen: 'first-high-risk-
|
|
160
|
+
askWhen: 'first-high-risk-refine',
|
|
161
161
|
default: 'every-risk-high-story-names-its-flag',
|
|
162
162
|
question: 'Should every risk:high story name its kill-switch flag?',
|
|
163
163
|
},
|
|
@@ -173,7 +173,7 @@ export const REGISTRY = Object.freeze([
|
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
// semantic-lint D4: the rules the lint rail's selectors run (id, globs, allowlist, patterns, question). never-yet,
|
|
176
|
-
// like `routines`: it declares the section so `config get/set` accept it, and `
|
|
176
|
+
// like `routines`: it declares the section so `config get/set` accept it, and `frijoles doctor` (which skips never-yet
|
|
177
177
|
// rows and prints one line per MODULE) never reports Build unconfigured because a project has no lint rules.
|
|
178
178
|
key: 'lint.rules',
|
|
179
179
|
module: 'Build',
|
package/dist/lib/config.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Types for lib/config.mjs, the one config core (golden-frijoles-plugin D10). Shipped in @golden-frijoles/kit as
|
|
2
|
-
// `@golden-frijoles/kit/config` so the TypeScript `
|
|
2
|
+
// `@golden-frijoles/kit/config` so the TypeScript `frijoles` CLI imports the same rules instead of re-implementing them.
|
|
3
3
|
|
|
4
4
|
export declare const CONFIG_FILENAME: 'golden-frijoles.config.json';
|
|
5
5
|
export declare const SECTIONS: readonly string[];
|
package/dist/lib/config.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Secrets never go in this file, only the NAMES of the env vars that hold them. `setKey` refuses a value that looks
|
|
16
16
|
// like a credential.
|
|
17
17
|
//
|
|
18
|
-
// Zero deps. Imported by the kit's `
|
|
18
|
+
// Zero deps. Imported by the kit's `frijoles-kit config` and, via `@golden-frijoles/kit/config`, by the `frijoles` CLI (D10).
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
21
|
import { join, resolve, sep } from 'node:path';
|
|
@@ -24,7 +24,7 @@ import { REGISTRY } from './config-registry.mjs';
|
|
|
24
24
|
|
|
25
25
|
// One import for every front end (D10): the CLI's doctor and setup read the registry through this module too.
|
|
26
26
|
export { REGISTRY, MODULES } from './config-registry.mjs';
|
|
27
|
-
// The project root this core reads and writes under, exported so the `
|
|
27
|
+
// The project root this core reads and writes under, exported so the `frijoles` CLI resolves the SAME directory (D10).
|
|
28
28
|
export { projectRoot } from './project-root.mjs';
|
|
29
29
|
|
|
30
30
|
export const CONFIG_FILENAME = 'golden-frijoles.config.json';
|
|
@@ -271,7 +271,7 @@ export const REDACTED = '<redacted: looks like a secret; keep it in .env.local>'
|
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
273
|
* Pure — `value` with every secret-looking string replaced by REDACTED. `loadConfig` and `getKey` are what
|
|
274
|
-
* `config list/get` and `
|
|
274
|
+
* `config list/get` and `frijoles doctor` PRINT, and a legacy file may still hold a literal token the write guard never saw
|
|
275
275
|
* (copy-in review). Rails read through `readSection`, which is never redacted.
|
|
276
276
|
*/
|
|
277
277
|
export function redactSecrets(key, value) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// epic-kickoff.mjs — the epic-mode kickoff, as one pure builder (board-sinks-and-scrumban D17).
|
|
2
2
|
//
|
|
3
|
-
// It used to live only in the
|
|
3
|
+
// It used to live only in the refine skill (`emit-epic-kickoff.mjs` + `templates/epic-kickoff.md`), which the kit does
|
|
4
4
|
// not ship and an installed extractor cannot import (lock C11). The Hub's "Ready to build" card has to carry the same
|
|
5
5
|
// kickoff the CLI prints — a kickoff composed twice is the exact drift `emit-epic-kickoff` was written to end — so
|
|
6
6
|
// the builder, the parsers it reads the docs with and the template text all live here, in the ONE source
|
|
7
|
-
// (`template/scripts/`). The
|
|
8
|
-
// writes into `
|
|
7
|
+
// (`template/scripts/`). The refine skill imports a byte-identical copy that `skills/scripts/render-hook-vendor.mjs`
|
|
8
|
+
// writes into `refine/vendor/` and checks in CI; the extractor imports this file directly.
|
|
9
9
|
//
|
|
10
10
|
// The parsers moved here from `emit-kickoff.mjs` unchanged; that file re-exports them, so the per-sprint generator
|
|
11
11
|
// and this one still read the doc format with ONE set of parsers.
|
|
@@ -150,7 +150,7 @@ export function buildEpicRules({ risk, texts, appetite = null, slug = '<slug>' }
|
|
|
150
150
|
const rules = [];
|
|
151
151
|
if (String(risk).toUpperCase() === 'HIGH')
|
|
152
152
|
rules.push(
|
|
153
|
-
'- **High risk:** the fresh `
|
|
153
|
+
'- **High risk:** the fresh `verifier` pass is mandatory on every PR, on top of the routed external passes.'
|
|
154
154
|
);
|
|
155
155
|
if (MIGRATION_RE.test(all))
|
|
156
156
|
rules.push(
|
|
@@ -158,13 +158,13 @@ export function buildEpicRules({ risk, texts, appetite = null, slug = '<slug>' }
|
|
|
158
158
|
);
|
|
159
159
|
if (FLAG_RE.test(all))
|
|
160
160
|
rules.push(
|
|
161
|
-
'- **Flag:** create it in Golden Frijoles in every env and ACTIVATE it; `
|
|
161
|
+
'- **Flag:** create it in Golden Frijoles in every env and ACTIVATE it; `frijoles flags get <key>` must show production.'
|
|
162
162
|
);
|
|
163
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
164
|
if (String(appetite).toUpperCase() === 'L')
|
|
165
165
|
rules.push(
|
|
166
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
|
-
`"
|
|
167
|
+
`"Start the next part of \`${slug}\`? What waits for it?" — and on yes run refine's ` +
|
|
168
168
|
`\`fund.mjs --slug ${slug} --displaced "<…>"\` (position kept) before the next wave starts.`
|
|
169
169
|
);
|
|
170
170
|
return rules.length ? `\nFor this epic:\n${rules.join('\n')}\n` : '';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// ── Why this is a module and not a sentence in three READMEs ───────────────────────────────────
|
|
4
4
|
// golden-flags-by-default S1.4's acceptance is that the text the agent prints, the install page's
|
|
5
|
-
// CLI block and `
|
|
5
|
+
// CLI block and `frijoles init`'s own next-step line are **one surface**: "the three must say the same
|
|
6
6
|
// thing". Two hand-written copies of `npx @golden-frijoles/cli init` agree right up until one of
|
|
7
7
|
// them is edited, and the one that drifts is always the one nobody runs.
|
|
8
8
|
//
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// ── The cross-repo half, stated honestly ──────────────────────────────────────────────────────
|
|
14
14
|
// The other two surfaces live in the Golden Frijoles product repo — its `/install` page reads
|
|
15
|
-
// `apps/web/lib/cli-install.ts`, and `
|
|
15
|
+
// `apps/web/lib/cli-install.ts`, and `frijoles init` prints from `packages/cli/src/commands/init.ts`.
|
|
16
16
|
// This file cannot import either: they are a different repo, and a template that npm-installs a
|
|
17
17
|
// product's web app to read a string would be absurd. The weld there is the same discipline in the
|
|
18
18
|
// other direction (`cli-install.test.ts` reads `packages/cli/package.json` off disk). Here the
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
// reading it.
|
|
23
23
|
//
|
|
24
24
|
// ── The env var names are D6 of the CLI epic, and D3 of this one ──────────────────────────────
|
|
25
|
-
// `
|
|
25
|
+
// `frijoles init` writes exactly three names, and deliberately does NOT write `flag_sync` or an ingest
|
|
26
26
|
// key: a verb whose job is "let this app READ its flags" must not put wider credentials on disk as
|
|
27
27
|
// a side effect. `flag_sync` is an operator/deploy credential and belongs in CI secrets — minted on
|
|
28
|
-
// purpose with `
|
|
28
|
+
// purpose with `frijoles keys create --type flag_sync`. Never both in one place.
|
|
29
29
|
//
|
|
30
30
|
// Zero deps — Node 18+.
|
|
31
31
|
|
|
@@ -34,7 +34,9 @@ export const CLI_PACKAGE = '@golden-frijoles/cli';
|
|
|
34
34
|
export const SDK_PACKAGE = '@golden-frijoles/sdk';
|
|
35
35
|
|
|
36
36
|
/** The binary `npm i -g` puts on PATH. */
|
|
37
|
-
export const CLI_BIN = '
|
|
37
|
+
export const CLI_BIN = 'frijoles';
|
|
38
|
+
/** The CLI's name before 1.0 (plugin-1-0 D2). Still found, so an older install keeps working until it upgrades. */
|
|
39
|
+
export const LEGACY_CLI_BIN = 'gf';
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* The minimum CLI this template's contract is written against.
|
|
@@ -47,8 +49,8 @@ export const CLI_BIN = 'gf';
|
|
|
47
49
|
export const MIN_CLI_VERSION = '0.1.0';
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* The env var names `
|
|
51
|
-
* the file and its reader are generated together in `
|
|
52
|
+
* The env var names `frijoles init` writes and the app reads. ONE definition, on purpose (CLI epic D6):
|
|
53
|
+
* the file and its reader are generated together in `frijoles init`, and this is that pair's address on
|
|
52
54
|
* the template side. `preflight.mjs` and `apps/example-app/flags.mjs` both read it from here.
|
|
53
55
|
*/
|
|
54
56
|
export const ENV_KEYS = {
|
|
@@ -57,7 +59,7 @@ export const ENV_KEYS = {
|
|
|
57
59
|
environment: 'GOLDEN_FRIJOLES_ENVIRONMENT',
|
|
58
60
|
};
|
|
59
61
|
|
|
60
|
-
/** The file `
|
|
62
|
+
/** The file `frijoles init` writes, and refuses to write if it cannot get it into `.gitignore`. */
|
|
61
63
|
export const ENV_FILE = '.env.local';
|
|
62
64
|
|
|
63
65
|
/** The route a `flag_read` key is exercised against — the one that actually serves it. */
|
|
@@ -95,8 +97,8 @@ export const KILL_SWITCH_STORY = [
|
|
|
95
97
|
* How to read the activation check's answer, in the CLI's OWN vocabulary.
|
|
96
98
|
*
|
|
97
99
|
* ⚠️ **This line shipped wrong once, and the way it was wrong is worth keeping.** It said
|
|
98
|
-
* `
|
|
99
|
-
* separate errors: `
|
|
100
|
+
* `frijoles flags ls --env production` and told the reader to look for the words *"never turned on"*. Two
|
|
101
|
+
* separate errors: `frijoles flags ls` accepts only `--project` and prints all three environments as
|
|
100
102
|
* columns, so `--env` is a hard usage error that exits 1 before it ever reaches auth; and *"never
|
|
101
103
|
* turned on here"* is the **web console's** wording, while the CLI's `describeServing` prints `—`.
|
|
102
104
|
* A reader following it literally would have run a command that cannot run, looking for a string
|
|
@@ -174,7 +176,7 @@ export const INSTALL_PROMPT =
|
|
|
174
176
|
'2. Tell me in a few lines what it installs, what changes on this machine and which services it contacts. ' +
|
|
175
177
|
'Offer me a security review, and wait for my go-ahead. ' +
|
|
176
178
|
'3. Install it the way install.md says for the agent you are. ' +
|
|
177
|
-
'4. Run the
|
|
179
|
+
'4. Run the setup skill from the golden-frijoles plugin.';
|
|
178
180
|
|
|
179
181
|
/**
|
|
180
182
|
* The value of `name` in a dotenv file, or null.
|
|
@@ -182,7 +184,7 @@ export const INSTALL_PROMPT =
|
|
|
182
184
|
* ⚠️ **The LAST assignment wins.** `dotenv` assigns in file order, so a later line overrides an
|
|
183
185
|
* earlier one — and a reader that returns the FIRST will confidently report on a value the running
|
|
184
186
|
* app does not use. Two duplicate lines is not exotic: it is what a hand-edit plus a re-run of
|
|
185
|
-
* `
|
|
187
|
+
* `frijoles init` produces. Same rule, same reason, as the CLI's own `readEnvValue`.
|
|
186
188
|
*
|
|
187
189
|
* No interpolation, deliberately: this reads a file to decide whether a credential is present, and
|
|
188
190
|
* expanding `$VAR` would make that answer depend on the environment doing the reading.
|
package/dist/lib/kickoff-cli.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// `emit-epic-kickoff.mjs` and `emit-kickoff.mjs` run in THREE places, the same bytes in each:
|
|
5
5
|
// • a project's own `scripts/` (copied from the template),
|
|
6
6
|
// • the kit's `dist/` (`npx -y @golden-frijoles/kit emit-epic-kickoff …`, far from the project),
|
|
7
|
-
// • the
|
|
8
|
-
// `projectRoot()` answers the first two. For the third it answers `
|
|
7
|
+
// • the refine skill's `vendor/` inside the plugin (the build-view mod's `/build`, and refine's Stage 8).
|
|
8
|
+
// `projectRoot()` answers the first two. For the third it answers `refine/`, which holds no Roadmap/, so the root takes
|
|
9
9
|
// one more rung (see `resolveRepoRoot`). The epic lookup lived twice, byte-for-byte, in the two CLIs; it lives here.
|
|
10
10
|
//
|
|
11
11
|
// Zero deps; no side effects at import.
|
|
@@ -32,7 +32,7 @@ export function parseArgs(argv) {
|
|
|
32
32
|
/**
|
|
33
33
|
* The project whose Roadmap/ the kickoff is built from, first match wins:
|
|
34
34
|
* 1. `--repo-root <path>` (the mod always passes it),
|
|
35
|
-
* 2. `GF_PROJECT_ROOT` (`
|
|
35
|
+
* 2. `GF_PROJECT_ROOT` (`frijoles-kit --root` sets it), taken as given: a person named it,
|
|
36
36
|
* 3. `projectRoot()` when it holds a Roadmap/ (a project's own `scripts/`, or the installed kit's walk from cwd),
|
|
37
37
|
* — a copied set answers its own parent, so the template's own copy here answers `skills/template/` (whose
|
|
38
38
|
* skeleton Roadmap/ has no epics); that is project-root.mjs's copied-mode rule, and no doc runs that copy,
|
|
@@ -72,7 +72,7 @@ export function findEpicDir(repoRoot, slug, { exists = existsSync, list = readdi
|
|
|
72
72
|
return {
|
|
73
73
|
error:
|
|
74
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 "$
|
|
75
|
+
`(a fixed-scope seed needs nothing but its slug): node "$REFINE/scaffold-epic.mjs" --slug ${slug}`,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
return { error: `no epic found for slug "${slug}" under any Roadmap/*/ dir in "${repoRoot}" (searched: ${macros.join(', ') || '(none)'})` };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A script in this set runs in one of two places:
|
|
4
4
|
// • COPIED: the project's own `scripts/<x>.mjs` (a project spawned from the template, or a deliberate fork).
|
|
5
|
-
// • INSTALLED: `@golden-frijoles/kit`, run by `npx …
|
|
5
|
+
// • INSTALLED: `@golden-frijoles/kit`, run by `npx … frijoles-kit <x>` from npm's cache, far from the project.
|
|
6
6
|
// The same bytes run in both. What differs is only where "the project" and "my own files" are, so every
|
|
7
7
|
// path goes through exactly one of:
|
|
8
8
|
//
|
|
@@ -50,7 +50,7 @@ export function todayUtc(now = new Date()) {
|
|
|
50
50
|
* shipped before the result record existed are never due (the no-backfill rule).
|
|
51
51
|
*/
|
|
52
52
|
export function readDateOf({ readDate, targetMetric, shippedAt }) {
|
|
53
|
-
// A day written at
|
|
53
|
+
// A day written at refining that passes before the epic ships is read on ship day (codex review, #290): there was
|
|
54
54
|
// nothing out there to read before then, and "overdue since before release" would be a false alarm.
|
|
55
55
|
const shipped = dayOf(shippedAt);
|
|
56
56
|
if (isDay(readDate))
|
|
@@ -67,7 +67,7 @@ export function isLate({ verdictAt, shippedAt }) {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Whether a read is due today: a shipped epic with a target, no verdict yet, and a read date that has arrived. An epic
|
|
70
|
-
* not yet shipped is never due, even past a read date written at
|
|
70
|
+
* not yet shipped is never due, even past a read date written at refining: there is nothing out there to read.
|
|
71
71
|
*/
|
|
72
72
|
export function isReadDue({ targetMetric, verdict, readDate, shipped }, today = todayUtc()) {
|
|
73
73
|
if (!shipped || !targetMetric || verdict || !isDay(readDate)) return false;
|
|
@@ -41,7 +41,7 @@ export const EPIC_FIELDS = ['title', 'area', 'risk', 'type', 'phase', 'sprints_t
|
|
|
41
41
|
export const SPRINT_FIELDS = ['epic', 'sprint', 'title', 'risk', 'phase', 'stories_total', 'stories'];
|
|
42
42
|
export const STORY_FIELDS = ['id', 'title', 'as_a', 'i_want', 'so_that', 'risk', 'status'];
|
|
43
43
|
|
|
44
|
-
// finops D6/D17 — an epic's quote (written at
|
|
44
|
+
// finops D6/D17 — an epic's quote (written at refining) and its actual (stamped at close), declared ONCE here.
|
|
45
45
|
// All optional: an epic scaffolded before FinOps has none, and an absent field is never a zero (D4). The numeric
|
|
46
46
|
// ones are numbers >= 0 or null; a `*_basis` says where the number came from, in words.
|
|
47
47
|
export const FINOPS_NUMERIC_FIELDS = ['quote_low_usd', 'quote_high_usd', 'actual_usd', 'actual_mtok'];
|
|
@@ -55,7 +55,7 @@ export const FINOPS_FIELDS = [
|
|
|
55
55
|
'actual_basis',
|
|
56
56
|
];
|
|
57
57
|
|
|
58
|
-
// result-record D1/D2 — the result record: what an epic should move (written at
|
|
58
|
+
// result-record D1/D2 — the result record: what an epic should move (written at refining) and its verdict (written at
|
|
59
59
|
// the read), declared ONCE here. All optional: no target is "no target", never an error. Dates are calendar days.
|
|
60
60
|
export const VERDICTS = ['proven', 'disproven', 'unclear'];
|
|
61
61
|
export const TARGET_FIELDS = ['hypothesis', 'target_metric', 'target_from', 'target_to', 'read_date'];
|
|
@@ -140,7 +140,7 @@ export function validateResultFields(fm) {
|
|
|
140
140
|
return offenses;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// one-epic-page D11 — the epic's flag, decided at
|
|
143
|
+
// one-epic-page D11 — the epic's flag, decided at refine Stage 6b and copied into the README by the scaffold. The SDK's
|
|
144
144
|
// flag-key grammar (`FLAG_KEY` in apps/web/lib/flag-admin-operation.ts). Optional: `flag_key: null` (no flag) is the
|
|
145
145
|
// default and never an error; a value that is not a key is named, never pushed (the extract sends null for it).
|
|
146
146
|
export const FLAG_KEY_RE = /^[a-z][a-z0-9_.-]{0,127}$/;
|
package/dist/lib/stage.mjs
CHANGED
|
@@ -25,6 +25,16 @@ import { branchCandidates } from './work-branch.mjs';
|
|
|
25
25
|
/** The six stages, in board order (D1 — locked by the product owner, 2026-10-01). */
|
|
26
26
|
export const STAGES = Object.freeze(['To groom', 'Grooming', 'Ready to build', 'Building', 'QA', 'Shipped']);
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* What each stage is CALLED where a person reads it (plugin-1-0 D6): plain agile, Backlog → Refining → Ready. The keys
|
|
30
|
+
* above are stored data and never change; the board, the build view and the terminal board print through `stageWord`.
|
|
31
|
+
* One known exception: the Notion sync writes the KEY into its `Stage` select, because a select option is data matched
|
|
32
|
+
* by name (renaming it is a migration of that database: the product owner's call). The console's twin is
|
|
33
|
+
* `apps/web/lib/screen-words.ts → stageLabel`, and a test keeps the two equal.
|
|
34
|
+
*/
|
|
35
|
+
export const STAGE_WORDS = Object.freeze({ 'To groom': 'Backlog', Grooming: 'Refining', 'Ready to build': 'Ready' });
|
|
36
|
+
export const stageWord = (stage) => STAGE_WORDS[stage] ?? stage;
|
|
37
|
+
|
|
28
38
|
const DOCS_STAGE = {
|
|
29
39
|
raw: 'To groom',
|
|
30
40
|
ready: 'Grooming',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// strategy-files.mjs — where the strategy coaches' files live, and the few shapes every strategy script reads
|
|
2
2
|
// (coaches-v2 D1, D4, D7).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// refine's own reader (`skills/refine/strategy.mjs`) ships inside the plugin and cannot import from the kit, so it keeps
|
|
5
5
|
// its own STRATEGY_DIR and KINDS; `skills/scripts/strategy-templates.test.mjs` fails while the two disagree, so a
|
|
6
6
|
// rename lands in both or in neither.
|
|
7
7
|
//
|
package/dist/lib/surface.mjs
CHANGED
|
@@ -132,7 +132,7 @@ function parseBlockLine(rest) {
|
|
|
132
132
|
if (/^[a-z][\w-]*:(\s|$)/.test(rest)) {
|
|
133
133
|
throw (
|
|
134
134
|
'`- <kind>: "…"` is the old list shape — write `- <kind> "words"` (for example `- head "Orders"`); ' +
|
|
135
|
-
'see
|
|
135
|
+
'see refine/references/intent-and-visuals.md'
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
const tokens = tokenize(rest);
|
package/dist/preflight.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// true until someone acts, and re-running changes nothing. Saying so loudly is the whole job.
|
|
21
21
|
//
|
|
22
22
|
// • A REJECTED CREDENTIAL is a hard failure too (exit 1). A 401 is not an outage: the key is
|
|
23
|
-
// revoked, expired or for another project, and the remedy is another `
|
|
23
|
+
// revoked, expired or for another project, and the remedy is another `frijoles init`.
|
|
24
24
|
//
|
|
25
25
|
// • AN UNREACHABLE DEPLOYMENT IS A WARNING (exit 0). A transient outage, a captive-portal proxy,
|
|
26
26
|
// a 404 from a deployment with flag serving switched off, an offline laptop on a plane — none
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
// ── Where this runs: at init and at session start, NOT as a CI gate ───────────────────────────
|
|
40
40
|
// `.env.local` is gitignored — deliberately, it holds a live credential — so a CI checkout does not
|
|
41
41
|
// have one and this check would fail there for a reason that is not a defect. Run it on a fresh
|
|
42
|
-
// spawn, at session start, and after `
|
|
42
|
+
// spawn, at session start, and after `frijoles init`. If you DO want it in CI, inject the same variables
|
|
43
43
|
// from CI secrets (the reader below falls back to the process environment for exactly that) and
|
|
44
44
|
// pass `--offline` if your runner has no egress.
|
|
45
45
|
//
|
|
46
46
|
// ── What it does NOT prove ─────────────────────────────────────────────────────────────────────
|
|
47
47
|
// That any particular flag exists, or that it is ACTIVATED. Definitions are catalog-as-code and
|
|
48
|
-
// activations are not (D4) — `
|
|
48
|
+
// activations are not (D4) — `frijoles flags get <key>` is the verb that answers that, per flag and per
|
|
49
49
|
// environment, and a kill-switch story names it as its own step. `—` in its SERVING column means
|
|
50
50
|
// never activated here.
|
|
51
51
|
//
|
|
@@ -63,6 +63,7 @@ import { join, resolve } from 'node:path';
|
|
|
63
63
|
import { fileURLToPath } from 'node:url';
|
|
64
64
|
import {
|
|
65
65
|
CLI_BIN,
|
|
66
|
+
LEGACY_CLI_BIN,
|
|
66
67
|
CLI_NPX_INIT,
|
|
67
68
|
CLI_GLOBAL_INSTALL,
|
|
68
69
|
DEFAULT_API_URL,
|
|
@@ -87,7 +88,7 @@ const ORDER = ['cli', 'cli-version', 'sdk', 'project', 'flag-read-key', 'snapsho
|
|
|
87
88
|
/**
|
|
88
89
|
* The whole decision, as a pure function of what was found.
|
|
89
90
|
*
|
|
90
|
-
* Every impure act — spawning `
|
|
91
|
+
* Every impure act — spawning `frijoles`, reading `.env.local`, the HTTP probe — happens in `main()` and
|
|
91
92
|
* arrives here as data, so all six states the sprint names (no project · no key · CLI absent · CLI
|
|
92
93
|
* outdated · all good · unreachable) are unit-testable without a network, a CLI or a filesystem.
|
|
93
94
|
*
|
|
@@ -136,7 +137,7 @@ export function evaluatePreflight({ cli, sdk = { found: false, source: null }, e
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// ── 3. can the APP actually read a flag — is the SDK installed ────────────────────────────
|
|
139
|
-
// ⚠️ **Added after review.** The CLI and the SDK are different halves: `
|
|
140
|
+
// ⚠️ **Added after review.** The CLI and the SDK are different halves: `frijoles` creates and kills
|
|
140
141
|
// flags, the SDK reads them. Without this check a project reached FIVE GREEN CHECKS, including a
|
|
141
142
|
// live snapshot, while every flag resolved to its call-site default forever — because nothing had
|
|
142
143
|
// ever installed `@golden-frijoles/sdk`. The seam imports it dynamically (deliberately: that is
|
|
@@ -226,18 +227,21 @@ export function evaluatePreflight({ cli, sdk = { found: false, source: null }, e
|
|
|
226
227
|
warnings: warned.length,
|
|
227
228
|
exitCode: failed.length === 0 ? 0 : 1,
|
|
228
229
|
// The remedy is printed whenever the project is not wired — which is exactly the set of
|
|
229
|
-
// failures `
|
|
230
|
+
// failures `frijoles init` fixes. A stale CLI is not one of them, so it does not drag the install
|
|
230
231
|
// block in behind it.
|
|
231
232
|
showOnboarding: failed.some((c) => c.id === 'project' || c.id === 'flag-read-key' || c.id === 'snapshot'),
|
|
232
233
|
checks,
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
/** Is `
|
|
237
|
+
/** Is `frijoles` runnable, and what does it say its version is? */
|
|
237
238
|
export function findCli({ cwd = REPO, spawn = spawnSync } = {}) {
|
|
238
239
|
const candidates = [
|
|
239
240
|
{ source: `${CLI_BIN} (PATH)`, command: CLI_BIN },
|
|
240
241
|
{ source: `node_modules/.bin/${CLI_BIN}`, command: join(cwd, 'node_modules', '.bin', CLI_BIN) },
|
|
242
|
+
// An install from before 1.0 has only the old name. Found, but said, so the version check can ask for an upgrade.
|
|
243
|
+
{ source: `${LEGACY_CLI_BIN} (PATH, the old name)`, command: LEGACY_CLI_BIN },
|
|
244
|
+
{ source: `node_modules/.bin/${LEGACY_CLI_BIN} (the old name)`, command: join(cwd, 'node_modules', '.bin', LEGACY_CLI_BIN) },
|
|
241
245
|
];
|
|
242
246
|
for (const candidate of candidates) {
|
|
243
247
|
const result = spawn(candidate.command, ['--version'], { encoding: 'utf8', timeout: 20_000 });
|
|
@@ -325,7 +329,7 @@ export function readEnvFile(path, processEnv = process.env) {
|
|
|
325
329
|
*
|
|
326
330
|
* Four answers, and the fourth is the one D1 turns on:
|
|
327
331
|
* live — a snapshot came back, for the environment this project says it is.
|
|
328
|
-
* dead — 401. Unknown, revoked, expired. Configuration: re-run `
|
|
332
|
+
* dead — 401. Unknown, revoked, expired. Configuration: re-run `frijoles init`.
|
|
329
333
|
* wrong-environment — it resolves, for a DIFFERENT environment. A `flag_read` key is scoped to
|
|
330
334
|
* one, so this is a production config holding a development credential with
|
|
331
335
|
* nothing anywhere saying so — the worst shape a flag bug has.
|
package/dist/quote.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The quote is the p25–p75 of `actual_usd` over SHIPPED epics at the same appetite — the middle half of what epics
|
|
9
9
|
// that size actually cost here. Until there are MIN_HISTORY of them it is WIDE_DEFAULT, labelled `wide` with its n,
|
|
10
|
-
// never a guess dressed as a calibration (D4). It is recomputed at every
|
|
10
|
+
// never a guess dressed as a calibration (D4). It is recomputed at every refinement, so it moves as history accumulates.
|
|
11
11
|
// Epics are read through the extractor (`buildRows`), so appetite follows the board's own rule (README, else seed —
|
|
12
12
|
// D20) and no second frontmatter reader exists.
|
|
13
13
|
//
|
package/dist/review-route.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// ── The rule ──────────────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
// The highest-preference family that did NOT build the diff runs the general pass.
|
|
6
6
|
// If the PR touches a security path, the NEXT eligible family runs the lean security lens.
|
|
7
|
-
// The fresh `
|
|
7
|
+
// The fresh `verifier` subagent runs too, in whatever scope the project's review-config.json sets.
|
|
8
8
|
//
|
|
9
9
|
// That is the whole policy. It replaces a 273-line router with a four-row table, a two-passes-per-PR
|
|
10
10
|
// count, a HIGH-only branch on the fresh reviewer, and the REFUND-ASK / --fallback-after / DARK-layer
|
|
@@ -84,7 +84,7 @@ export function planReview({ builder, available = PREFERENCE, securityPass = fal
|
|
|
84
84
|
else notes.push(`The security lens is triggered but no family can run it. Say so in the PR body.`);
|
|
85
85
|
}
|
|
86
86
|
notes.push(
|
|
87
|
-
'The fresh `
|
|
87
|
+
'The fresh `verifier` subagent runs as well — context independence is a different axis from family independence.'
|
|
88
88
|
);
|
|
89
89
|
return { builder, general, security, notes };
|
|
90
90
|
}
|
|
@@ -107,7 +107,7 @@ export function renderPlan(plan, pr, repo) {
|
|
|
107
107
|
l.push(' Run:');
|
|
108
108
|
if (plan.general) l.push(` ${arg(plan.general)}`);
|
|
109
109
|
if (plan.security) l.push(` ${arg(plan.security)} --lens security`);
|
|
110
|
-
l.push(` then: use the
|
|
110
|
+
l.push(` then: use the verifier subagent on PR #${pr}`);
|
|
111
111
|
return l.join('\n');
|
|
112
112
|
}
|
|
113
113
|
|
package/dist/roadmap-extract.mjs
CHANGED
|
@@ -613,7 +613,7 @@ export function buildRows({
|
|
|
613
613
|
area,
|
|
614
614
|
type: TYPE_LABEL[epicFm.type || seed.type] || 'Epic',
|
|
615
615
|
risk,
|
|
616
|
-
// finops D20 — the README's own appetite first (an epic
|
|
616
|
+
// finops D20 — the README's own appetite first (an epic refined after FinOps carries it), else the seed's.
|
|
617
617
|
appetite: epicFm.appetite || seed.appetite || null,
|
|
618
618
|
// fund-at-approval D8 — the README's own first (an epic with no seed carries it there), else the seed's.
|
|
619
619
|
underwritten_by: epicFm.underwritten_by || seed.underwritten_by || null,
|
package/dist/routines/README.md
CHANGED
|
@@ -138,9 +138,9 @@ restored.)
|
|
|
138
138
|
1. **Install the Claude GitHub App** on `<root-repo>` (root repo).
|
|
139
139
|
2. **Create the routine** from `roadmap-hygiene.prompt.md`.
|
|
140
140
|
3. **Trigger:** Schedule, **weekly — Mon 14:00 UTC** (after the 08:00 nightly `notion-sync.yml`).
|
|
141
|
-
4. **Env/connectors:** **No Notion connector** — no `.mcp.json`, no `NOTION_TOKEN`. The routine
|
|
141
|
+
4. **Env/connectors:** **No Notion connector** — no `.mcp.json`, no `NOTION_TOKEN`. The routine refines
|
|
142
142
|
the `00-ideas` funnel, flags status-drift, runs `node scripts/build-order.mjs`, invokes the
|
|
143
|
-
|
|
143
|
+
doc-hygiene script (`node scripts/doc-hygiene.mjs` — always-read-set size + LEARNINGS/poster
|
|
144
144
|
dedupe-staleness candidates), and opens a `claude/` **docs PR** with the regenerated
|
|
145
145
|
`BUILD-ORDER.md` + any new `DOC-HYGIENE-REPORT-*.md` + a drift report. Network = GitHub.
|
|
146
146
|
5. **Propagation:** after the product owner merges the docs PR, the existing `notion-sync.yml` propagates
|
|
@@ -289,7 +289,7 @@ routine (cap-safe) — see the budget table below.
|
|
|
289
289
|
Shipped by `ops-routines-reporting` (the origin project's `ops-routines-reporting` epic)
|
|
290
290
|
S3 — a dedicated **weekly** routine (mirroring Routine C's precedent of a standalone weekly schedule,
|
|
291
291
|
rather than a day-of-week-gated step folded into the nightly `ops-nightly` routine). One step: the
|
|
292
|
-
`
|
|
292
|
+
`report` skill's weekly chapter (`scripts/weekly-recap.mjs`) gathers the week's merged PRs (every configured repo),
|
|
293
293
|
shipped/closed epics (README frontmatter `status:` flips), a merges-to-main deploy count per app repo,
|
|
294
294
|
and a short retro digest per shipped epic — then posts one Telegram message.
|
|
295
295
|
|
|
@@ -324,7 +324,7 @@ and a short retro digest per shipped epic — then posts one Telegram message.
|
|
|
324
324
|
(reads `gh` + `git log` across every configured repo; posts to Telegram; writes `claude/pmo-reports-log`).
|
|
325
325
|
|
|
326
326
|
Shipped by `pmo-operational-reports` (the origin project's `pmo-operational-reports` epic)
|
|
327
|
-
S3. One step: the `
|
|
327
|
+
S3. One step: the `report` skill's PMO chapter (`scripts/pmo-report.mjs --weekly`) gathers scrum/DORA/doc-ops
|
|
328
328
|
metrics, renders a story-deck URL, posts headline numbers plus the deck link to Telegram, and
|
|
329
329
|
then advances the PMO window log.
|
|
330
330
|
|