@golden-frijoles/kit 0.42.0 → 0.44.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 CHANGED
@@ -15,7 +15,7 @@ npx -y @golden-frijoles/kit@<version> --root ../other build-order
15
15
  `Roadmap/00-ideas/BUILD-ORDER.md`.
16
16
  - **Your own copy wins.** If your project has `scripts/<name>.mjs`, the skills run that instead of the kit, so a
17
17
  deliberate fork keeps working. That's by name, so an unrelated `scripts/preflight.mjs` of your own would be run in
18
- its place. Rename yours, or pass the script through `gf-kit` directly.
18
+ its place. Rename yours, or pass the script through `frijoles-kit` directly.
19
19
  - **Some files stay yours even when the kit runs:** `scripts/prose/cpo-persona.md`, `scripts/prose-lessons.md`,
20
20
  `scripts/cross-panel.prompt.md` and `scripts/doc-format.enforced.json` override the kit's defaults.
21
21
  - **Zero dependencies.** A script that drives another tool, like Playwright or a reviewer CLI, uses *your*
package/bin.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // gf-kit — run one of the Golden Frijoles skills' scripts in the current project (golden-frijoles-plugin D1–D3).
2
+ // frijoles-kit — run one of the Golden Frijoles skills' scripts in the current project (golden-frijoles-plugin D1–D3).
3
3
  //
4
- // gf-kit <name> [args…] run dist/<name>.mjs against the project you're standing in
5
- // gf-kit --root <dir> <name> …against <dir> instead (exported to the script as GF_PROJECT_ROOT)
6
- // gf-kit --list the scripts this kit carries
7
- // gf-kit --version
4
+ // frijoles-kit <name> [args…] run dist/<name>.mjs against the project you're standing in
5
+ // frijoles-kit --root <dir> <name> …against <dir> instead (exported to the script as GF_PROJECT_ROOT)
6
+ // frijoles-kit --list the scripts this kit carries
7
+ // frijoles-kit --version
8
8
  //
9
9
  // The script is SPAWNED, never imported: every entry guards `main()` behind an isMain check on process.argv[1]
10
10
  // and several end with process.exit, so an import() would silently run nothing. stdio, the exit code and a
@@ -26,7 +26,7 @@ export function listScripts(dist = DIST) {
26
26
  .sort();
27
27
  }
28
28
 
29
- /** Pure — split gf-kit's own flags from the script's. Only flags BEFORE the script name belong to gf-kit. */
29
+ /** Pure — split frijoles-kit's own flags from the script's. Only flags BEFORE the script name belong to frijoles-kit. */
30
30
  export function parseArgs(argv) {
31
31
  const out = { root: null, list: false, version: false, help: false, name: null, rest: [] };
32
32
  for (let i = 0; i < argv.length; i++) {
@@ -51,7 +51,18 @@ export function parseArgs(argv) {
51
51
  return out;
52
52
  }
53
53
 
54
- const USAGE = 'usage: gf-kit [--root <dir>] <script> [args…] · gf-kit --list · gf-kit --version';
54
+ const USAGE = 'usage: frijoles-kit [--root <dir>] <script> [args…] · frijoles-kit --list · frijoles-kit --version';
55
+
56
+ /**
57
+ * plugin-1-0 D2 — started as the old name `gf-kit`? One line on stderr (stdout is a script's own output, often parsed),
58
+ * until the date `scripts/check-deprecations.mjs` enforces. Pure, so the test pins it.
59
+ */
60
+ export function deprecatedNameNotice(invokedPath) {
61
+ const name = String(invokedPath ?? '').split(/[\\/]/).pop() ?? '';
62
+ // Unix only: npm's Windows shims start `node …\\bin.mjs`, so argv[1] is the entry file there (verifier, #324).
63
+ if (name !== 'gf-kit') return null;
64
+ return 'gf-kit is now frijoles-kit. gf-kit stops working on 2026-12-31 (or in kit 1.1.0).\n';
65
+ }
55
66
 
56
67
  function main(argv) {
57
68
  const args = parseArgs(argv);
@@ -69,12 +80,12 @@ function main(argv) {
69
80
  return args.help ? 0 : 2;
70
81
  }
71
82
  if (args.root === '') {
72
- process.stderr.write('gf-kit: --root needs a directory\n');
83
+ process.stderr.write('frijoles-kit: --root needs a directory\n');
73
84
  return 2;
74
85
  }
75
86
  // A plain name from the list only — never a path, so nothing outside dist/ can be reached through it.
76
87
  if (!scripts.includes(args.name)) {
77
- process.stderr.write(`gf-kit: no script "${args.name}". This kit carries:\n ${scripts.join('\n ')}\n`);
88
+ process.stderr.write(`frijoles-kit: no script "${args.name}". This kit carries:\n ${scripts.join('\n ')}\n`);
78
89
  return 2;
79
90
  }
80
91
  const env = { ...process.env };
@@ -83,21 +94,21 @@ function main(argv) {
83
94
  // re-resolved against that cwd in every child (fresh review of #45: pmo-report read zero rows, silently).
84
95
  const root = resolve(args.root);
85
96
  if (!existsSync(root)) {
86
- process.stderr.write(`gf-kit: --root ${args.root}: no such directory\n`);
97
+ process.stderr.write(`frijoles-kit: --root ${args.root}: no such directory\n`);
87
98
  return 2;
88
99
  }
89
100
  env.GF_PROJECT_ROOT = root;
90
101
  }
91
102
  const run = spawnSync(process.execPath, [join(DIST, `${args.name}.mjs`), ...args.rest], { stdio: 'inherit', env });
92
103
  if (run.error) {
93
- process.stderr.write(`gf-kit: could not start ${args.name}: ${run.error.message}\n`);
104
+ process.stderr.write(`frijoles-kit: could not start ${args.name}: ${run.error.message}\n`);
94
105
  return 1;
95
106
  }
96
107
  if (run.signal) process.kill(process.pid, run.signal);
97
108
  return run.status ?? 1;
98
109
  }
99
110
 
100
- // npm installs `gf-kit` as a symlink in node_modules/.bin, so compare real paths on both sides.
111
+ // npm installs `frijoles-kit` as a symlink in node_modules/.bin, so compare real paths on both sides.
101
112
  const isMain = (() => {
102
113
  try {
103
114
  return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
@@ -105,4 +116,8 @@ const isMain = (() => {
105
116
  return false;
106
117
  }
107
118
  })();
108
- if (isMain) process.exitCode = main(process.argv.slice(2));
119
+ if (isMain) {
120
+ const notice = deprecatedNameNotice(process.argv[1]);
121
+ if (notice) process.stderr.write(notice);
122
+ process.exitCode = main(process.argv.slice(2));
123
+ }
@@ -316,7 +316,7 @@ function run(root) {
316
316
  err: 'cold-read: no other model family reachable (codex is not installed). Run the brief with a same-family agent and record `family: claude (same family)`.',
317
317
  };
318
318
  }
319
- // Codex reads the repository it is started in. Under `gf-kit --root <dir>` that is not this process's cwd, so move
319
+ // Codex reads the repository it is started in. Under `frijoles-kit --root <dir>` that is not this process's cwd, so move
320
320
  // there first: otherwise it reads one repo and the read is sealed into another's strategy folder (review of #314).
321
321
  process.chdir(root);
322
322
  const r = tryCodex(brief, '');
package/dist/config.mjs CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- // config.mjs — `gf-kit config`: read and change golden-frijoles.config.json (golden-frijoles-plugin S4.1, D10).
2
+ // config.mjs — `frijoles-kit config`: read and change golden-frijoles.config.json (golden-frijoles-plugin S4.1, D10).
3
3
  //
4
- // gf-kit config list [--json] every section's effective value, where it came from, any duplicates
5
- // gf-kit config get <key> [--json] one key's effective value (or its registry default)
6
- // gf-kit config set <key> <value> write one key; <value> is parsed as JSON when it is valid JSON
7
- // gf-kit config migrate [--dry-run] fold the legacy files into golden-frijoles.config.json (never edits them)
4
+ // frijoles-kit config list [--json] every section's effective value, where it came from, any duplicates
5
+ // frijoles-kit config get <key> [--json] one key's effective value (or its registry default)
6
+ // frijoles-kit config set <key> <value> write one key; <value> is parsed as JSON when it is valid JSON
7
+ // frijoles-kit config migrate [--dry-run] fold the legacy files into golden-frijoles.config.json (never edits them)
8
8
  //
9
9
  // A thin front end: every rule (precedence, the secret guard, what migrate folds) lives in lib/config.mjs, which the
10
- // `gf` CLI imports too. Exit 0 ok · 1 usage · 2 configuration error (a malformed file, a secret, an unknown section).
10
+ // `frijoles` CLI imports too. Exit 0 ok · 1 usage · 2 configuration error (a malformed file, a secret, an unknown section).
11
11
 
12
12
  import { realpathSync } from 'node:fs';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { CONFIG_FILENAME, ConfigError, getKey, loadConfig, migrate, setKey } from './lib/config.mjs';
15
15
 
16
- const USAGE = 'usage: gf-kit config list|get <key>|set <key> <value>|migrate [--dry-run] [--json]';
16
+ const USAGE = 'usage: frijoles-kit config list|get <key>|set <key> <value>|migrate [--dry-run] [--json]';
17
17
 
18
18
  /** Pure — a CLI value: JSON when it parses (true, 3, ["a"], null), otherwise the literal string. */
19
19
  export function parseValue(text) {
@@ -63,7 +63,7 @@ export function main(argv, { out = (s) => process.stdout.write(s), err = (s) =>
63
63
  }
64
64
  } catch (e) {
65
65
  if (e instanceof ConfigError) {
66
- err(`gf-kit config: ${e.message}\n`);
66
+ err(`frijoles-kit config: ${e.message}\n`);
67
67
  return 2;
68
68
  }
69
69
  throw e;
@@ -660,7 +660,7 @@ export async function pushUsage({
660
660
  return { sent: 0, reason: `config unreadable — ${err && err.message ? err.message : err}` };
661
661
  }
662
662
  if (setting !== 'on')
663
- return { sent: 0, reason: 'off — spend.telemetry is not on (gf-kit config set spend.telemetry on)' };
663
+ return { sent: 0, reason: 'off — spend.telemetry is not on (frijoles-kit config set spend.telemetry on)' };
664
664
  // The throttle counts ATTEMPTS, not successes: a persistent 401 or 429 retries every 10 minutes, not every minute.
665
665
  const last = Math.max(index.pushed_at ?? -Infinity, index.push_attempt_at ?? -Infinity);
666
666
  if (throttle && Number.isFinite(last) && now - last < PUSH_EVERY_MS)
@@ -1,14 +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> # fetches the number through gf, drafts
4
+ // node scripts/epic-read.mjs --epic <slug> # fetches the number through frijoles, drafts
5
5
  // node scripts/epic-read.mjs --epic <slug> --experiment smart-defaults # … and cites the A/B decision record
6
6
  // node scripts/epic-read.mjs --epic <slug> --actual 72 --evidence north-star:invoices_paid_on_time@2026-11-04
7
7
  // node scripts/epic-read.mjs --epic <slug> --evidence "traffic too low (n = 18)" # no number: unclear
8
8
  // node scripts/epic-read.mjs --epic <slug> --verdict proven --evidence https://… # an owner verdict
9
9
  // … --write # the approval: stamp verdict_* into the README
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
+ // options: --project <slug> (frijoles's; default: the remembered one) · --today YYYY-MM-DD (default: today, UTC)
11
+ // · --repo-root <dir> · --json · GF_BIN=<path to frijoles> (default: frijoles on PATH)
12
12
  //
13
13
  // ── The shape (result-record D8) ─────────────────────────────────────────────────────────────────────────────────
14
14
  // The agent does the legwork, the owner decides. Without `--write` this prints a draft and the exact command that
@@ -25,11 +25,11 @@
25
25
  // • An epic shipped with no target can be read too — an owner verdict and its evidence, one epic per run.
26
26
  //
27
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
28
+ // With a target and no `--actual`, this runs `frijoles north-star readings <target_metric> --to <today> --json` and takes
29
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
30
+ // is no evidence for it. `--experiment <key>` also runs `frijoles experiments decision <key> --json`; a recorded decision
31
+ // becomes the evidence, `ab:<key>`. frijoles is spawned without a shell (`$GF_BIN`, else `frijoles` on PATH), so a shell alias of
32
+ // the same name never applies. `--actual` / `--evidence` still win: they are the owner's word. No frijoles, not signed in, no
33
33
  // project chosen, or any failure is one "could not fetch (why)" line, and the read asks the owner as before — it never
34
34
  // fails because the platform is not linked. Grounded is now "the readings route knows this input".
35
35
 
@@ -138,17 +138,22 @@ export function planRead({ fm, shippedAt, shipped, today, actual = null, evidenc
138
138
  return { state: 'ready', target, drafted, owner, fields, late, readDate, derived };
139
139
  }
140
140
 
141
- /** Run one `gf … --json` and read its single JSON document. Never throws; `why` says what went wrong in words. */
141
+ /** Run one `frijoles … --json` and read its single JSON document. Never throws; `why` says what went wrong in words. */
142
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 });
143
+ // `frijoles` since CLI 1.0; `gf` is the name an older install still has (plugin-1-0 D2). GF_BIN pins one.
144
+ const bins = env.GF_BIN ? [env.GF_BIN] : ['frijoles', 'gf'];
145
+ let res = null;
146
+ for (const bin of bins) {
147
+ res = spawnFn(bin, [...args, '--json'], { encoding: 'utf8', timeout: 30_000, env, shell: false });
148
+ if (res.error?.code !== 'ENOENT') break; // only "not installed under this name" tries the next name
149
+ }
145
150
  if (res.error)
146
151
  return {
147
152
  ok: false,
148
153
  why:
149
154
  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}`,
155
+ ? 'frijoles is not installed (npm i -g @golden-frijoles/cli, then frijoles login)'
156
+ : `frijoles did not run: ${res.error.message}`,
152
157
  };
153
158
  let body = null;
154
159
  try {
@@ -157,13 +162,13 @@ export function runGf(args, { env = process.env, spawnFn = spawnSync } = {}) {
157
162
  body = null;
158
163
  }
159
164
  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)' };
165
+ if (res.status === 2) return { ok: false, code: 'unauthorized', why: 'frijoles is not signed in (run frijoles login)' };
161
166
  const code = body?.code ?? null;
162
- return { ok: false, code, why: body?.error ?? `gf exited ${res.status}` };
167
+ return { ok: false, code, why: body?.error ?? `frijoles exited ${res.status}` };
163
168
  }
164
169
 
165
170
  /**
166
- * The number and its evidence, fetched through gf (D17). Pure apart from the injected `run`.
171
+ * The number and its evidence, fetched through frijoles (D17). Pure apart from the injected `run`.
167
172
  * Returns `{ fetched, actual?, evidence?, reading?, decision?, grounded, why? }`.
168
173
  */
169
174
  export function fetchEvidence({ metric, experiment = null, today, shippedAt, project = null, run }) {
@@ -214,12 +219,12 @@ export function renderPlan(plan, { slug, fetched, write, written }) {
214
219
  out.push(` not grounded: ${t.metric} is not one of the project's North Star inputs`);
215
220
  if (fetched.fetched)
216
221
  out.push(
217
- ` fetched: ${fig(fetched.reading.value)} on ${longDay(fetched.reading.date)} (gf north-star readings)`
222
+ ` fetched: ${fig(fetched.reading.value)} on ${longDay(fetched.reading.date)} (frijoles north-star readings)`
218
223
  );
219
224
  else if (fetched.why) out.push(` could not fetch the number: ${fetched.why}`);
220
225
  if (fetched.decision)
221
226
  out.push(
222
- ` decision: ${fetched.decision.outcome ?? 'recorded'}${fetched.decision.chosenVariantKey ? ` (${fetched.decision.chosenVariantKey})` : ''} (gf experiments decision)`
227
+ ` decision: ${fetched.decision.outcome ?? 'recorded'}${fetched.decision.chosenVariantKey ? ` (${fetched.decision.chosenVariantKey})` : ''} (frijoles experiments decision)`
223
228
  );
224
229
  else if (fetched.decisionWhy) out.push(` decision: ${fetched.decisionWhy}`);
225
230
  }
package/dist/init.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // init.mjs — `gf-kit init`: adopt any repo by writing the Roadmap/ skeleton (golden-frijoles-plugin S3.2, D2).
2
+ // init.mjs — `frijoles-kit init`: adopt any repo by writing the Roadmap/ skeleton (golden-frijoles-plugin S3.2, D2).
3
3
  //
4
4
  // node scripts/init.mjs # write the skeleton into projectRoot(); never overwrite a file
5
5
  //
6
6
  // ── Why ───────────────────────────────────────────────────────────────────────────────────────
7
7
  // A stranger who pastes the install prompt into an EXISTING repo gets the plugin, but `groom` needs
8
8
  // somewhere to write on day one — `Roadmap/README.md`, `WAYS-OF-WORKING.md`, `LEARNINGS.md`, and the
9
- // `00-ideas/` funnel. `gf-kit init` is that one step: it writes the same skeleton a project spawned
9
+ // `00-ideas/` funnel. `frijoles-kit init` is that one step: it writes the same skeleton a project spawned
10
10
  // from `template/` already has, into any repo, copied or installed (D2's two roots).
11
11
  //
12
12
  // ── Where the skeleton's SOURCE lives ─────────────────────────────────────────────────────────
@@ -45,7 +45,7 @@ import { dirname, join, resolve } from 'node:path';
45
45
  import { isInstalled, kitRoot, projectRoot } from './lib/project-root.mjs';
46
46
 
47
47
  /**
48
- * The Roadmap skeleton `gf-kit init` writes, relative both to its source root (template/, or
48
+ * The Roadmap skeleton `frijoles-kit init` writes, relative both to its source root (template/, or
49
49
  * kit/dist/skeleton/ once built) and to the project it writes into. ONE list: `build-kit.mjs` reads
50
50
  * this export to populate `kit/dist/skeleton/`, so there is no second copy of the file names to drift.
51
51
  */
@@ -104,7 +104,7 @@ export function initSkeleton({
104
104
  }
105
105
 
106
106
  /**
107
- * True when `project` resolves to the operator's own home directory — the one place `gf-kit init`
107
+ * True when `project` resolves to the operator's own home directory — the one place `frijoles-kit init`
108
108
  * must never write, even though `projectRoot()`'s walk-up can legitimately land there (a non-git
109
109
  * folder under a dotfiles repo tracked at `$HOME`). Pure given its injected `home`.
110
110
  */
@@ -116,7 +116,7 @@ function main() {
116
116
  const project = projectRoot();
117
117
  if (isHomeDirectory({ project })) {
118
118
  console.error(
119
- `gf-kit init: refusing to run — the resolved project root is your home directory (${project}).\n` +
119
+ `frijoles-kit init: refusing to run — the resolved project root is your home directory (${project}).\n` +
120
120
  ' This usually means no Roadmap/ or .git was found between the current directory and $HOME,\n' +
121
121
  ' and $HOME itself is a git repo (a dotfiles checkout, for example). Run this from inside a\n' +
122
122
  ' real project — a directory with its own .git — or pass --root explicitly.'
@@ -124,7 +124,7 @@ function main() {
124
124
  return 2;
125
125
  }
126
126
  const { wrote, skipped } = initSkeleton({ project, log: (line) => console.log(line) });
127
- console.log(`gf-kit init: ${wrote.length} written, ${skipped.length} already present.`);
127
+ console.log(`frijoles-kit init: ${wrote.length} written, ${skipped.length} already present.`);
128
128
  return 0;
129
129
  }
130
130
 
@@ -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 (`gf-kit config set intent.reader 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.
@@ -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 \`gf-kit config set intent.reader on\`.`
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 \`gf-kit config set jev.egress true\` first.`;
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;
@@ -1,7 +1,7 @@
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 `gf doctor` reads it to say which modules are configured. A new
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
@@ -10,7 +10,7 @@
10
10
  // | 'first-high-risk-groom' | '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 (gf init), never here
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: gf login, then gf init), or later?',
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',
@@ -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 `gf doctor` (which skips never-yet
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',
@@ -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 `gf doctor` (which skips never-yet
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',
@@ -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 `gf` CLI imports the same rules instead of re-implementing them.
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[];
@@ -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 `gf-kit config` and, via `@golden-frijoles/kit/config`, by the `gf` CLI (D10).
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 `gf` CLI resolves the SAME directory (D10).
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 `gf doctor` PRINT, and a legacy file may still hold a literal token the write guard never saw
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) {
@@ -158,7 +158,7 @@ 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; `gf flags get <key>` must show production.'
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')
@@ -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 `gf init`'s own next-step line are **one surface**: "the three must say the same
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 `gf init` prints from `packages/cli/src/commands/init.ts`.
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
- // `gf init` writes exactly three names, and deliberately does NOT write `flag_sync` or an ingest
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 `gf keys create --type flag_sync`. Never both in one place.
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 = 'gf';
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 `gf init` writes and the app reads. ONE definition, on purpose (CLI epic D6):
51
- * the file and its reader are generated together in `gf init`, and this is that pair's address on
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 `gf init` writes, and refuses to write if it cannot get it into `.gitignore`. */
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
- * `gf flags ls --env production` and told the reader to look for the words *"never turned on"*. Two
99
- * separate errors: `gf flags ls` accepts only `--project` and prints all three environments as
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
@@ -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
- * `gf init` produces. Same rule, same reason, as the CLI's own `readEnvValue`.
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.
@@ -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` (`gf-kit --root` sets it), taken as given: a person named it,
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,
@@ -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 … gf-kit <x>` from npm's cache, far from the project.
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
  //
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ // one-pagers.mjs — three sheets a maker can consult in seconds, derived from the strategy files (coaches-v2 D10).
3
+ //
4
+ // node scripts/one-pagers.mjs render into Roadmap/00-strategy/one-pagers/ (.html and .md each)
5
+ // node scripts/one-pagers.mjs --out <dir> …somewhere else
6
+ //
7
+ // The sheets: a business model canvas, a value proposition sheet and a persona poster. The Strategy gate's Approve
8
+ // renders them, each coach renders them after its last write, and this command renders them on demand.
9
+ //
10
+ // ── Derived, never typed ──────────────────────────────────────────────────────────────────────────────────────────
11
+ // Every line on a sheet comes from a strategy file; nothing here holds strategy. A sheet typed by hand drifts from the
12
+ // narrative the day after it is printed, and then two documents disagree about the product (the 2026-10-05 reference
13
+ // sheets were transcribed, and needed a revision the next morning). A field the files don't fill says "Not written
14
+ // yet" rather than being guessed.
15
+ //
16
+ // ── Honest labels ─────────────────────────────────────────────────────────────────────────────────────────────────
17
+ // Each claim shows `true today`, `aspirational`, `agreed`, `sourced` or `hypothesis`, taken from the `(…)` label the
18
+ // coach wrote at the end of the line. A persona line with NO label renders as `hypothesis`: an unlabelled line is an
19
+ // unchecked one, and the poster must never present an anecdote as a researched frustration (dogfood F34). A sheet
20
+ // whose source file is still `status: draft` is watermarked "Draft".
21
+ //
22
+ // ── Licences ──────────────────────────────────────────────────────────────────────────────────────────────────────
23
+ // The Business Model Canvas is Strategyzer AG's, licensed CC BY-SA 3.0: usable with "Strategyzer.com" visible under
24
+ // it, which the canvas carries. The Value Proposition Canvas is NOT licensed for software, so the value proposition
25
+ // sheet is our own layout (the customer's outcome, motivation and gaps against the product's promise) and never uses
26
+ // its name or shape.
27
+ //
28
+ // Exit codes: 0 rendered, or nothing to render yet (said in one line) · 2 usage or could not write.
29
+ // Zero deps — Node 18+.
30
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
31
+ import { join, resolve } from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { projectRoot } from './lib/project-root.mjs';
34
+ import { ONE_PAGERS_DIR, STRATEGY_DIR, parseFrontmatter, section } from './lib/strategy-files.mjs';
35
+
36
+ /** The persona block's fields, in the order the narrative template lists them under `### Persona`. */
37
+ export const PERSONA_FIELDS = [
38
+ 'Role',
39
+ 'Stage',
40
+ 'Background',
41
+ 'Goals',
42
+ 'Frustrations',
43
+ 'Drivers',
44
+ 'Moments they reach for it',
45
+ 'Where to find them',
46
+ 'Words to use',
47
+ 'Words to avoid',
48
+ 'Not this persona',
49
+ ];
50
+
51
+ export const LABELS = ['true today', 'aspirational', 'agreed', 'sourced', 'hypothesis'];
52
+ export const NOT_WRITTEN = 'Not written yet';
53
+ export const BMC_CREDIT = 'The Business Model Canvas by Strategyzer.com, licensed CC BY-SA 3.0.';
54
+
55
+ /** Split a trailing `(label)` or `(sourced: <where>)` off a claim. An unknown or missing label is `null`. */
56
+ export function splitLabel(raw) {
57
+ const text = String(raw ?? '').trim();
58
+ const m = text.match(
59
+ // The source may hold a markdown link or a URL with its own parentheses; a trailing full stop is allowed.
60
+ /\s*\(\s*(true today|aspirational|agreed|hypothesis|sourced)(?:\s*:\s*((?:[^()]|\([^()]*\))*))?\s*\)\s*\.?\s*$/i
61
+ );
62
+ if (!m) return { text, label: null, source: null };
63
+ return { text: text.slice(0, m.index).trim(), label: m[1].toLowerCase(), source: m[2]?.trim() || null };
64
+ }
65
+
66
+ /** A value the coach has not written: missing, or still the template's `<…>` placeholder. */
67
+ const unwritten = (v) => !v || /^<[^>]*>(?:\s*\([^)]*\))?$/.test(v.trim()); // a placeholder, maybe with its label hint
68
+ /** A value with any `<…>` placeholder fragment removed: half-filled template text never reaches a sheet. */
69
+ const fill = (v) =>
70
+ String(v ?? '')
71
+ .replace(/<[^>]*>/g, '')
72
+ .replace(/\s{2,}/g, ' ')
73
+ .trim();
74
+
75
+ /** `**Label:** value` lines inside `body`, by label. */
76
+ export function labelledLines(body) {
77
+ const out = {};
78
+ const lines = String(body ?? '').split('\n');
79
+ const LABEL = /^\s*(?:[-*]\s+)?\*\*([^*]+?):\*\*\s*(.*)$/;
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const m = lines[i].match(LABEL);
82
+ if (!m) continue;
83
+ const parts = [m[2].trim()];
84
+ const items = [];
85
+ // A value may wrap onto following lines, or be a list of sub-bullets under an empty label: both are written
86
+ // content, and dropping them would print "Not written yet" over something the maker wrote (review of #316).
87
+ for (let k = i + 1; k < lines.length; k++) {
88
+ const next = lines[k];
89
+ if (!next.trim() || LABEL.test(next) || /^#{1,6} /.test(next)) break;
90
+ const bullet = next.match(/^\s*(?:[-*]|\d+\.)\s+(.+)$/);
91
+ if (bullet && (!parts[0] || /^\s/.test(next))) items.push(bullet[1].trim());
92
+ else if (!bullet && !items.length) parts.push(next.trim());
93
+ else break;
94
+ }
95
+ const raw = [parts.join(' ').trim(), ...items.filter((x) => !unwritten(x))].filter(
96
+ (x) => x && !unwritten(x)
97
+ );
98
+ const value = fill(raw.join(' · '));
99
+ if (!unwritten(value)) out[m[1].trim()] = value;
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** The `- ` bullets directly under a `**Label:**` line (Gaps:), or every top-level bullet when no label is given. */
105
+ export function bullets(body, after = null) {
106
+ const lines = String(body ?? '').split('\n');
107
+ let start = 0;
108
+ if (after) {
109
+ start = lines.findIndex((l) => new RegExp(`^\\s*\\*\\*${after}:\\*\\*`).test(l));
110
+ if (start === -1) return [];
111
+ start += 1;
112
+ }
113
+ const out = [];
114
+ for (const line of lines.slice(start)) {
115
+ if (after && /^\s*\*\*[^*]+:\*\*/.test(line)) break;
116
+ const m = line.match(/^\s*(?:[-*]|\d+\.)\s+(.+)$/);
117
+ // `- **Fast:** drafts next week` is a benefit too: keep it, as "Fast: drafts next week".
118
+ // Placeholder check on the RAW text, before fill(): `<benefit> (true today | aspirational)` must not survive as
119
+ // a benefit called "(true today | aspirational)" (review of #316, round 2).
120
+ if (!m || unwritten(m[1])) continue;
121
+ const item = fill(m[1].replace(/^\*\*([^*]+?):\*\*\s*/, '$1: '));
122
+ if (item && !unwritten(item)) out.push(item);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /** The body of `### <heading>` inside an h2 section's text. */
128
+ function subsection(body, heading) {
129
+ const lines = String(body ?? '').split('\n');
130
+ const start = lines.findIndex((l) => l.trim() === `### ${heading}`);
131
+ if (start === -1) return null;
132
+ const end = lines.findIndex((l, i) => i > start && /^#{2,3} /.test(l));
133
+ return lines.slice(start + 1, end === -1 ? undefined : end).join('\n');
134
+ }
135
+
136
+ /** A claim with its label; a persona line without one is a hypothesis (F34). */
137
+ function claim(raw, { defaultLabel = null } = {}) {
138
+ const c = splitLabel(raw);
139
+ return { ...c, label: c.label ?? defaultLabel };
140
+ }
141
+
142
+ /** Pure — everything the three sheets show, read from the three files' text (any may be null). */
143
+ export function readStrategy({ narrative = null, northStar = null, risk = null } = {}) {
144
+ const n = narrative ?? '';
145
+ const fm = parseFrontmatter(n);
146
+ const problem = section(n, 'Problem to solve') ?? '';
147
+ const audience = section(n, 'Target audience') ?? '';
148
+ const vp = section(n, 'Value proposition') ?? '';
149
+ const adv = section(n, 'Competitive advantage') ?? '';
150
+ const growth = section(n, 'Growth strategy') ?? '';
151
+ const bm = section(n, 'Business model') ?? '';
152
+ const personaBody = subsection(audience, 'Persona');
153
+ const personaLines = labelledLines(personaBody);
154
+ const p = labelledLines(problem);
155
+ const a = labelledLines(audience);
156
+ const v = labelledLines(vp);
157
+ const c = labelledLines(adv);
158
+ const g = labelledLines(growth);
159
+ const b = labelledLines(bm);
160
+
161
+ const ns = northStar ?? '';
162
+ const metricLine = (section(ns, 'North Star metric') ?? '')
163
+ .split('\n')
164
+ .find((l) => l.trim() && !l.startsWith('_'));
165
+ const metric = metricLine?.match(/^\*\*([^*]+)\*\*\s*:?\s*(.*)$/);
166
+
167
+ const r = risk ?? '';
168
+ const domino = labelledLines(section(r, 'Highest domino'));
169
+
170
+ // A claim the coach left unlabelled still shows what it is (review of #316): the maker approved it at the Strategy
171
+ // gate when the file is agreed; until then it is unchecked. Persona lines stay hypothesis either way (F34).
172
+ const N = { defaultLabel: fm.status === 'agreed' ? 'agreed' : 'hypothesis' };
173
+ const benefits = bullets(vp).map((x) => claim(x, N));
174
+ return {
175
+ product: (n.match(/^# .*?—\s*(.+)$/m)?.[1] ?? '').replace(/^<.*>$/, '').trim() || null,
176
+ status: {
177
+ narrative: narrative === null ? null : fm.status || 'draft',
178
+ northStar: northStar === null ? null : parseFrontmatter(ns).status || 'draft',
179
+ risk: risk === null ? null : parseFrontmatter(r).status || 'draft',
180
+ },
181
+ persona:
182
+ personaBody === null
183
+ ? null
184
+ : PERSONA_FIELDS.map((f) => ({
185
+ field: f,
186
+ ...(personaLines[f]
187
+ ? claim(personaLines[f], { defaultLabel: 'hypothesis' })
188
+ : { text: null, label: null }),
189
+ })),
190
+ outcome: p.Outcome ? claim(p.Outcome, N) : null,
191
+ motivation: p.Motivation ? claim(p.Motivation, N) : null,
192
+ gaps: bullets(problem, 'Gaps').map((x) => claim(x, N)),
193
+ segmentsNow: a.Now ? claim(a.Now, N) : null,
194
+ segmentsFuture: a.Future ? claim(a.Future, N) : null,
195
+ tagline: v.Tagline ? claim(v.Tagline, N) : null,
196
+ benefits,
197
+ shortTerm: c['Short term'] ? claim(c['Short term'], N) : null,
198
+ longTerm: c['Long term'] ? claim(c['Long term'], N) : null,
199
+ channelsNow: g.Now ? claim(g.Now, N) : null,
200
+ channelsLater: g.Later ? claim(g.Later, N) : null,
201
+ revenue: b.Revenue ? claim(b.Revenue, N) : null,
202
+ pricing: b.Pricing ? claim(b.Pricing, N) : null,
203
+ costs: b.Costs ? claim(b.Costs, N) : null,
204
+ partners: b['Key partners'] ? claim(b['Key partners'], N) : null,
205
+ activities: b['Key activities'] ? claim(b['Key activities'], N) : null,
206
+ resources: b['Key resources'] ? claim(b['Key resources'], N) : null,
207
+ relationships: b['Customer relationships'] ? claim(b['Customer relationships'], N) : null,
208
+ northStar:
209
+ metric && !unwritten(metric[1])
210
+ ? { name: metric[1].replace(/:\s*$/, '').trim(), definition: metric[2].trim() }
211
+ : null,
212
+ notClaimedYet: [
213
+ ...(domino.Hypothesis
214
+ ? [
215
+ {
216
+ text: domino.Hypothesis,
217
+ label: 'hypothesis',
218
+ source: null,
219
+ why: `riskiest assumption${domino.Dimension ? ` (${domino.Dimension})` : ''}`,
220
+ },
221
+ ]
222
+ : []),
223
+ ...benefits
224
+ .filter((x) => x.label === 'aspirational')
225
+ .map((x) => ({ ...x, why: 'aspirational benefit' })),
226
+ ],
227
+ };
228
+ }
229
+
230
+ // ── Rendering ─────────────────────────────────────────────────────────────────────────────────────────────────────
231
+
232
+ const esc = (s) =>
233
+ String(s).replace(
234
+ /[&<>"']/g,
235
+ (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch]
236
+ );
237
+
238
+ function chipHtml(c) {
239
+ if (!c?.label) return '';
240
+ const cls = c.label === 'hypothesis' || c.label === 'aspirational' ? 'hyp' : 'ok';
241
+ const title = c.source ? ` title="${esc(c.source)}"` : '';
242
+ return ` <span class="chip ${cls}"${title}>${esc(c.label)}</span>`;
243
+ }
244
+ const chipMd = (c) => (c?.label ? ` _(${c.label}${c.source ? `: ${c.source}` : ''})_` : '');
245
+
246
+ const lineHtml = (c) =>
247
+ c?.text ? `${esc(c.text)}${chipHtml(c)}` : `<span class="muted">${NOT_WRITTEN}</span>`;
248
+ const lineMd = (c) => (c?.text ? `${c.text}${chipMd(c)}` : `_${NOT_WRITTEN}_`);
249
+ const listHtml = (cs) =>
250
+ cs.length
251
+ ? `<ul>${cs.map((c) => `<li>${lineHtml(c)}</li>`).join('')}</ul>`
252
+ : `<p class="muted">${NOT_WRITTEN}</p>`;
253
+ const listMd = (cs) => (cs.length ? cs.map((c) => `- ${lineMd(c)}`).join('\n') : `_${NOT_WRITTEN}_`);
254
+
255
+ const CSS = `:root{--bg:#f3f3ef;--paper:#fff;--ink:#1a1916;--muted:#66625a;--line:#d8d5cc;--gold:#9c7000;--gold-bg:#fbf1d3;
256
+ --hyp:#7a4a00;--hyp-bg:#fff6e0;--ok:#2b6b3a;--ok-bg:#e3f2e6;color-scheme:light}
257
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#11100e;--paper:#1a1916;--ink:#ecebe5;--muted:#a29e94;
258
+ --line:#2f2d29;--gold:#e5b53d;--gold-bg:rgba(229,181,61,.14);--hyp:#f0b35a;--hyp-bg:rgba(240,179,90,.12);--ok:#86d196;--ok-bg:rgba(134,209,150,.12);color-scheme:dark}}
259
+ :root[data-theme="dark"]{--bg:#11100e;--paper:#1a1916;--ink:#ecebe5;--muted:#a29e94;--line:#2f2d29;--gold:#e5b53d;
260
+ --gold-bg:rgba(229,181,61,.14);--hyp:#f0b35a;--hyp-bg:rgba(240,179,90,.12);--ok:#86d196;--ok-bg:rgba(134,209,150,.12);color-scheme:dark}
261
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;padding:24px 16px}
262
+ .sheet{position:relative;max-width:1240px;margin:0 auto;background:var(--paper);border:1px solid var(--line);border-radius:6px;padding:24px 26px;overflow:hidden}
263
+ header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:6px 24px;border-bottom:2px solid var(--ink);padding-bottom:10px;margin-bottom:16px}
264
+ h1{font:800 28px/1.1 system-ui,sans-serif;margin:0}.kicker{display:block;font:600 11px system-ui;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}
265
+ h2{font:700 12px system-ui;text-transform:uppercase;letter-spacing:.1em;margin:0 0 8px}
266
+ ul{margin:0;padding-left:16px}li{margin:0 0 5px}p{margin:0 0 6px}.muted{color:var(--muted)}
267
+ .chip{display:inline-block;font:600 10.5px system-ui;letter-spacing:.04em;text-transform:uppercase;padding:2px 7px;border-radius:99px;vertical-align:1px}
268
+ .chip.ok{background:var(--ok-bg);color:var(--ok)}.chip.hyp{background:var(--hyp-bg);color:var(--hyp);border:1px dashed var(--hyp)}
269
+ .grid{display:grid;gap:12px}.box{border:1px solid var(--line);border-radius:6px;padding:12px 14px}
270
+ .bmc{grid-template-columns:repeat(5,1fr);grid-template-rows:auto auto auto}
271
+ .bmc .kp{grid-row:1/3}.bmc .vp{grid-row:1/3;grid-column:3}.bmc .cs{grid-row:1/3;grid-column:5}
272
+ .bmc .ka{grid-column:2}.bmc .kr{grid-column:2;grid-row:2}.bmc .cr{grid-column:4}.bmc .ch{grid-column:4;grid-row:2}
273
+ .bmc .co{grid-column:1/3;grid-row:3}.bmc .rs{grid-column:3/6;grid-row:3}
274
+ .two{grid-template-columns:1fr 1fr}.persona{grid-template-columns:repeat(3,1fr)}
275
+ .lead{font-size:18px;margin:0 0 12px}.ns{border:2px solid var(--gold);background:var(--gold-bg)}.ns b{color:var(--gold);font-size:18px;display:block}
276
+ footer{margin-top:16px;padding-top:8px;border-top:1px solid var(--line);font-size:11.5px;color:var(--muted);display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 18px}
277
+ .draft{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;font:900 140px system-ui;color:var(--hyp);opacity:.09;transform:rotate(-18deg)}
278
+ @media screen and (max-width:820px){.bmc,.two,.persona{grid-template-columns:1fr}.bmc>*{grid-column:auto!important;grid-row:auto!important}}
279
+ @media print{body{background:#fff;padding:0}.sheet{border:0;max-width:none;padding:8mm}@page{size:A4 landscape;margin:8mm}}`;
280
+
281
+ function page({ title, kicker, body, draft, sources, footer = '' }) {
282
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
283
+ <title>${esc(title)}</title><style>${CSS}</style></head><body><main class="sheet">${draft ? '<div class="draft" aria-hidden="true">Draft</div>' : ''}
284
+ <header><div><span class="kicker">${esc(kicker)}${draft ? ' · draft' : ''}</span><h1>${esc(title)}</h1></div>
285
+ <div class="muted">From ${esc(sources)}.</div></header>
286
+ ${body}
287
+ <footer>${footer}<span>Rendered from the strategy files by one-pagers.mjs. Never edit this sheet; edit the file and render again.</span></footer></main></body></html>
288
+ `;
289
+ }
290
+
291
+ const isDraft = (...statuses) => statuses.some((s) => s && s !== 'agreed');
292
+ const named = (s, kind) => `${s.product ? `${s.product}: ` : ''}${kind}`;
293
+
294
+ /** Pure — the business model canvas, as { html, md }. */
295
+ export function renderCanvas(s) {
296
+ const box = (cls, title, inner) => `<section class="box ${cls}"><h2>${title}</h2>${inner}</section>`;
297
+ const one = (c) => `<p>${lineHtml(c)}</p>`;
298
+ const segments = [
299
+ s.segmentsNow && { ...s.segmentsNow, text: `Now: ${s.segmentsNow.text}` },
300
+ s.segmentsFuture && { ...s.segmentsFuture, text: `Later: ${s.segmentsFuture.text}` },
301
+ ].filter(Boolean);
302
+ const channels = [
303
+ s.channelsNow && { ...s.channelsNow, text: `Now: ${s.channelsNow.text}` },
304
+ s.channelsLater && { ...s.channelsLater, text: `Later: ${s.channelsLater.text}` },
305
+ ].filter(Boolean);
306
+ const value = [s.tagline, ...s.benefits].filter(Boolean);
307
+ const revenue = [s.revenue, s.pricing].filter(Boolean);
308
+ const draft = isDraft(s.status.narrative);
309
+ const html = page({
310
+ title: named(s, 'Business model canvas'),
311
+ kicker: 'Strategy one-pager',
312
+ draft,
313
+ sources: 'pmf-narrative.md',
314
+ body: `<div class="grid bmc">${box('kp', 'Key partners', one(s.partners))}${box('ka', 'Key activities', one(s.activities))}${box('kr', 'Key resources', one(s.resources))}${box('vp', 'Value propositions', listHtml(value))}${box('cr', 'Customer relationships', one(s.relationships))}${box('ch', 'Channels', listHtml(channels))}${box('cs', 'Customer segments', listHtml(segments))}${box('co', 'Cost structure', one(s.costs))}${box('rs', 'Revenue streams', listHtml(revenue))}</div>`,
315
+ footer: `<span>${esc(BMC_CREDIT)}</span>`,
316
+ });
317
+ const md = [
318
+ `# ${named(s, 'Business model canvas')}${draft ? ' (draft)' : ''}`,
319
+ '',
320
+ ...[
321
+ ['Key partners', lineMd(s.partners)],
322
+ ['Key activities', lineMd(s.activities)],
323
+ ['Key resources', lineMd(s.resources)],
324
+ ['Value propositions', listMd(value)],
325
+ ['Customer relationships', lineMd(s.relationships)],
326
+ ['Channels', listMd(channels)],
327
+ ['Customer segments', listMd(segments)],
328
+ ['Cost structure', lineMd(s.costs)],
329
+ ['Revenue streams', listMd(revenue)],
330
+ ].flatMap(([h, v]) => [`## ${h}`, '', v, '']),
331
+ `_${BMC_CREDIT}_`,
332
+ '',
333
+ ].join('\n');
334
+ return { html, md };
335
+ }
336
+
337
+ /** Pure — the value proposition sheet (our own layout, never the reserved canvas), as { html, md }. */
338
+ export function renderValueSheet(s) {
339
+ const draft = isDraft(s.status.narrative, s.status.northStar, s.status.risk);
340
+ const ns = s.northStar
341
+ ? `<section class="box ns"><h2>North Star</h2><b>${esc(s.northStar.name)}</b><p>${esc(s.northStar.definition)}</p></section>`
342
+ : `<section class="box"><h2>North Star</h2><p class="muted">${NOT_WRITTEN}</p></section>`;
343
+ const html = page({
344
+ title: named(s, 'Value proposition sheet'),
345
+ kicker: 'Strategy one-pager',
346
+ draft,
347
+ sources: 'pmf-narrative.md · north-star.md · risk-validation.md',
348
+ body: `<p class="lead"><strong>${lineHtml(s.tagline)}</strong></p>
349
+ <div class="grid two"><section class="box"><h2>What the customer is after</h2><p><strong>Outcome:</strong> ${lineHtml(s.outcome)}</p><p><strong>Why:</strong> ${lineHtml(s.motivation)}</p><h2>What stands in the way</h2>${listHtml(s.gaps)}</section>
350
+ <section class="box"><h2>What the product promises</h2>${listHtml(s.benefits)}<h2>Why it wins</h2><p><strong>Today:</strong> ${lineHtml(s.shortTerm)}</p><p><strong>Over time:</strong> ${lineHtml(s.longTerm)}</p></section></div>
351
+ <div class="grid two" style="margin-top:12px">${ns}<section class="box"><h2>Not claimed yet</h2>${s.notClaimedYet.length ? `<ul>${s.notClaimedYet.map((c) => `<li>${esc(c.text)}${chipHtml(c)} <span class="muted">(${esc(c.why)})</span></li>`).join('')}</ul>` : '<p class="muted">Nothing marked as untested.</p>'}</section></div>`,
352
+ });
353
+ const md = [
354
+ `# ${named(s, 'Value proposition sheet')}${draft ? ' (draft)' : ''}`,
355
+ '',
356
+ `**${lineMd(s.tagline)}**`,
357
+ '',
358
+ '## What the customer is after',
359
+ '',
360
+ `- **Outcome:** ${lineMd(s.outcome)}`,
361
+ `- **Why:** ${lineMd(s.motivation)}`,
362
+ '',
363
+ '## What stands in the way',
364
+ '',
365
+ listMd(s.gaps),
366
+ '',
367
+ '## What the product promises',
368
+ '',
369
+ listMd(s.benefits),
370
+ '',
371
+ '## Why it wins',
372
+ '',
373
+ `- **Today:** ${lineMd(s.shortTerm)}`,
374
+ `- **Over time:** ${lineMd(s.longTerm)}`,
375
+ '',
376
+ '## North Star',
377
+ '',
378
+ s.northStar ? `**${s.northStar.name}**: ${s.northStar.definition}` : `_${NOT_WRITTEN}_`,
379
+ '',
380
+ '## Not claimed yet',
381
+ '',
382
+ s.notClaimedYet.length
383
+ ? s.notClaimedYet.map((c) => `- ${c.text}${chipMd(c)} (${c.why})`).join('\n')
384
+ : '_Nothing marked as untested._',
385
+ '',
386
+ ].join('\n');
387
+ return { html, md };
388
+ }
389
+
390
+ /** Pure — the persona poster, as { html, md }. Every line carries a label; an unlabelled one reads hypothesis. */
391
+ export function renderPersona(s) {
392
+ const draft = isDraft(s.status.narrative);
393
+ const rows = s.persona ?? PERSONA_FIELDS.map((field) => ({ field, text: null, label: null }));
394
+ const role = rows.find((r) => r.field === 'Role');
395
+ const html = page({
396
+ title: named(s, `Persona${role?.text ? `: ${role.text}` : ''}`),
397
+ kicker: 'Strategy one-pager',
398
+ draft,
399
+ sources: 'pmf-narrative.md (Target audience → Persona)',
400
+ body: `<div class="grid persona">${rows
401
+ .map((r) => `<section class="box"><h2>${esc(r.field)}</h2><p>${lineHtml(r)}</p></section>`)
402
+ .join('')}</div>`,
403
+ footer:
404
+ '<span><span class="chip ok">sourced</span> / <span class="chip ok">agreed</span> = checked or confirmed · <span class="chip hyp">hypothesis</span> = not checked yet</span>',
405
+ });
406
+ const md = [
407
+ `# ${named(s, `Persona${role?.text ? `: ${role.text}` : ''}`)}${draft ? ' (draft)' : ''}`,
408
+ '',
409
+ ...rows.flatMap((r) => [`## ${r.field}`, '', lineMd(r), '']),
410
+ ].join('\n');
411
+ return { html, md };
412
+ }
413
+
414
+ export const SHEETS = {
415
+ 'business-model-canvas': renderCanvas,
416
+ 'value-proposition-sheet': renderValueSheet,
417
+ 'persona-poster': renderPersona,
418
+ };
419
+
420
+ function readOrNull(path) {
421
+ try {
422
+ return readFileSync(path, 'utf8');
423
+ } catch {
424
+ return null;
425
+ }
426
+ }
427
+
428
+ function main(argv) {
429
+ const root = projectRoot();
430
+ const i = argv.indexOf('--out');
431
+ if (i !== -1 && !argv[i + 1]) {
432
+ process.stderr.write('usage: one-pagers.mjs [--out <dir>]\n');
433
+ return 2;
434
+ }
435
+ const out = i !== -1 ? resolve(argv[i + 1]) : join(root, ONE_PAGERS_DIR);
436
+ const file = (k) => readOrNull(join(root, STRATEGY_DIR, `${k}.md`));
437
+ const narrative = file('pmf-narrative');
438
+ if (narrative === null) {
439
+ process.stdout.write(
440
+ `one-pagers: nothing to render yet — the sheets come from ${STRATEGY_DIR}/pmf-narrative.md, which doesn't exist.\n`
441
+ );
442
+ return 0;
443
+ }
444
+ const s = readStrategy({ narrative, northStar: file('north-star'), risk: file('risk-validation') });
445
+ try {
446
+ mkdirSync(out, { recursive: true });
447
+ const written = [];
448
+ for (const [name, render] of Object.entries(SHEETS)) {
449
+ const { html, md } = render(s);
450
+ writeFileSync(join(out, `${name}.html`), html);
451
+ writeFileSync(join(out, `${name}.md`), md);
452
+ written.push(`${name}.html`);
453
+ }
454
+ const draft = isDraft(s.status.narrative, s.status.northStar, s.status.risk);
455
+ process.stdout.write(
456
+ `one-pagers: wrote ${written.join(', ')} (+ .md) to ${out}${draft ? ' — marked draft until the Strategy gate approves the files' : ''}.\n`
457
+ );
458
+ return 0;
459
+ } catch (e) {
460
+ process.stderr.write(`one-pagers: could not write to ${out}: ${e.message}\n`);
461
+ return 2;
462
+ }
463
+ }
464
+
465
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1])
466
+ process.exitCode = main(process.argv.slice(2));
@@ -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 `gf init`.
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 `gf init`. If you DO want it in CI, inject the same variables
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) — `gf flags get <key>` is the verb that answers that, per flag and per
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 `gf`, reading `.env.local`, the HTTP probe — happens in `main()` and
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: `gf` creates and kills
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 `gf init` fixes. A stale CLI is not one of them, so it does not drag the install
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 `gf` runnable, and what does it say its version 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 `gf init`.
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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@golden-frijoles/kit",
3
- "version": "0.42.0",
3
+ "version": "0.44.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": {
7
+ "frijoles-kit": "bin.mjs",
7
8
  "gf-kit": "bin.mjs"
8
9
  },
9
10
  "files": [