@golden-frijoles/kit 0.0.0 → 0.2.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 +25 -1
- package/bin.mjs +108 -0
- package/dist/LICENSE +202 -0
- package/dist/NOTICE +7 -0
- package/dist/babysit-pr.mjs +213 -0
- package/dist/build-order-sync.mjs +141 -0
- package/dist/build-order.mjs +178 -0
- package/dist/cross-panel.mjs +216 -0
- package/dist/cross-panel.prompt.md +115 -0
- package/dist/doc-format.enforced.json +4 -0
- package/dist/doc-format.mjs +856 -0
- package/dist/doc-hygiene.mjs +279 -0
- package/dist/lib/cross-agent-cli.mjs +695 -0
- package/dist/lib/gh-rest.mjs +229 -0
- package/dist/lib/golden-onboarding.mjs +179 -0
- package/dist/lib/jev.mjs +334 -0
- package/dist/lib/log-branch.mjs +106 -0
- package/dist/lib/pmo-benchmarks.mjs +61 -0
- package/dist/lib/pmo-delivery.mjs +102 -0
- package/dist/lib/pmo-metrics.mjs +200 -0
- package/dist/lib/pmo-templates.mjs +132 -0
- package/dist/lib/pmo-window-log.mjs +99 -0
- package/dist/lib/project-root.mjs +78 -0
- package/dist/lib/prose-brief.mjs +191 -0
- package/dist/lib/prose-guard.mjs +761 -0
- package/dist/lib/prose-writer.mjs +410 -0
- package/dist/lib/report-registry.mjs +469 -0
- package/dist/lib/reporting-config.mjs +266 -0
- package/dist/lib/roadmap-contract.mjs +305 -0
- package/dist/lib/roadmap-status-buckets.mjs +20 -0
- package/dist/lib/standup-deck.mjs +120 -0
- package/dist/lib/telegram-format.mjs +91 -0
- package/dist/live-smoke.mjs +303 -0
- package/dist/pmo/benchmarks.json +77 -0
- package/dist/pmo/templates/metrics-sheet.md +27 -0
- package/dist/pmo/templates/monthly-stakeholder-packet.md +53 -0
- package/dist/pmo/templates/weekly-story-deck.md +66 -0
- package/dist/pmo-report.mjs +338 -0
- package/dist/preflight.mjs +444 -0
- package/dist/prose/cpo-persona.md +108 -0
- package/dist/prose/internal.task.md +48 -0
- package/dist/prose/standup.task.md +38 -0
- package/dist/prose/weekly.task.md +36 -0
- package/dist/prose-draft.mjs +198 -0
- package/dist/prose-lessons.md +59 -0
- package/dist/roadmap-extract.mjs +410 -0
- package/dist/standup/templates/daily-story-deck.md +46 -0
- package/dist/standup.mjs +634 -0
- package/dist/vercel-prune-previews.mjs +164 -0
- package/dist/weekly-recap.mjs +526 -0
- package/package.json +33 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// preflight.mjs — "this project uses Golden Frijoles for flags" is a CHECK, not a sentence.
|
|
3
|
+
//
|
|
4
|
+
// ── Why this exists ────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Every `risk: high` epic in this operating system has to answer the kill-switch question, and the
|
|
6
|
+
// answer names a flag mechanism. For a project spawned from this template that mechanism is Golden
|
|
7
|
+
// Frijoles — one flag provider, no parallel flag store (see AGENTS.md's rules). A mandate written
|
|
8
|
+
// only in prose is a hope: the first project to skip it does so silently, and nothing anywhere goes
|
|
9
|
+
// red. This script is the mandate as a property of the system.
|
|
10
|
+
//
|
|
11
|
+
// It follows `permissions-smoke.mjs`'s shape on purpose — a reviewable file that ASSERTS, rather
|
|
12
|
+
// than a paragraph that claims.
|
|
13
|
+
//
|
|
14
|
+
// ── D1: fail LOUD at init, fail SOFT at runtime. Read this before changing a status ────────────
|
|
15
|
+
// This is the single most important line in the epic that produced this file, and getting it
|
|
16
|
+
// backwards breaks every consuming project's CI.
|
|
17
|
+
//
|
|
18
|
+
// • ABSENT CONFIGURATION is a hard failure (exit 1). No `.env.local`, no URL, no `flag_read` key,
|
|
19
|
+
// no CLI, an out-of-date CLI — every one of these is a thing a person has not done yet, it is
|
|
20
|
+
// true until someone acts, and re-running changes nothing. Saying so loudly is the whole job.
|
|
21
|
+
//
|
|
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`.
|
|
24
|
+
//
|
|
25
|
+
// • AN UNREACHABLE DEPLOYMENT IS A WARNING (exit 0). A transient outage, a captive-portal proxy,
|
|
26
|
+
// a 404 from a deployment with flag serving switched off, an offline laptop on a plane — none
|
|
27
|
+
// of them mean the project is misconfigured, and none of them may break a build, a test run or
|
|
28
|
+
// a deploy. The SDK is built for exactly this: `createFlagProvider` resolves SYNCHRONOUSLY
|
|
29
|
+
// against a caller-supplied default, so an unreachable Golden degrades to compile-time defaults
|
|
30
|
+
// and the app keeps serving. A preflight that went red here would hand every consuming project
|
|
31
|
+
// a dependency that can fail their pipeline when someone else's deployment hiccups — which is a
|
|
32
|
+
// dependency nobody should accept, mandate or not.
|
|
33
|
+
//
|
|
34
|
+
// **There is deliberately no `--strict`.** A flag that turns the warning into a failure would be
|
|
35
|
+
// pasted into a CI file within the week, and the promise above would be gone with nobody deciding
|
|
36
|
+
// to give it up. If you want the probe skipped, `--offline` skips it; if you want an outage to page
|
|
37
|
+
// someone, that belongs in monitoring, not in a preflight.
|
|
38
|
+
//
|
|
39
|
+
// ── Where this runs: at init and at session start, NOT as a CI gate ───────────────────────────
|
|
40
|
+
// `.env.local` is gitignored — deliberately, it holds a live credential — so a CI checkout does not
|
|
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
|
|
43
|
+
// from CI secrets (the reader below falls back to the process environment for exactly that) and
|
|
44
|
+
// pass `--offline` if your runner has no egress.
|
|
45
|
+
//
|
|
46
|
+
// ── What it does NOT prove ─────────────────────────────────────────────────────────────────────
|
|
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
|
|
49
|
+
// environment, and a kill-switch story names it as its own step. `—` in its SERVING column means
|
|
50
|
+
// never activated here.
|
|
51
|
+
//
|
|
52
|
+
// ── Usage ──────────────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// node scripts/preflight.mjs # the full check, including the live snapshot probe
|
|
54
|
+
// node scripts/preflight.mjs --offline # skip the probe (deterministic; for CI and for planes)
|
|
55
|
+
// node scripts/preflight.mjs --json # the same result as data
|
|
56
|
+
//
|
|
57
|
+
// Zero deps — Node 18+.
|
|
58
|
+
|
|
59
|
+
import { spawnSync } from 'node:child_process';
|
|
60
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
61
|
+
import { createRequire } from 'node:module';
|
|
62
|
+
import { join, resolve } from 'node:path';
|
|
63
|
+
import { fileURLToPath } from 'node:url';
|
|
64
|
+
import {
|
|
65
|
+
CLI_BIN,
|
|
66
|
+
CLI_NPX_INIT,
|
|
67
|
+
CLI_GLOBAL_INSTALL,
|
|
68
|
+
DEFAULT_API_URL,
|
|
69
|
+
ENV_FILE,
|
|
70
|
+
ENV_KEYS,
|
|
71
|
+
MIN_CLI_VERSION,
|
|
72
|
+
SDK_PACKAGE,
|
|
73
|
+
SNAPSHOT_PATH,
|
|
74
|
+
compareVersions,
|
|
75
|
+
onboardingLines,
|
|
76
|
+
readEnvValue,
|
|
77
|
+
} from './lib/golden-onboarding.mjs';
|
|
78
|
+
import { projectRoot } from './lib/project-root.mjs';
|
|
79
|
+
|
|
80
|
+
const REPO = projectRoot(); // D2
|
|
81
|
+
|
|
82
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
83
|
+
|
|
84
|
+
/** `ok` passes, `fail` exits 1, `warn` is reported and passes, `skipped` could not run. */
|
|
85
|
+
const ORDER = ['cli', 'cli-version', 'sdk', 'project', 'flag-read-key', 'snapshot'];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The whole decision, as a pure function of what was found.
|
|
89
|
+
*
|
|
90
|
+
* Every impure act — spawning `gf`, reading `.env.local`, the HTTP probe — happens in `main()` and
|
|
91
|
+
* arrives here as data, so all six states the sprint names (no project · no key · CLI absent · CLI
|
|
92
|
+
* outdated · all good · unreachable) are unit-testable without a network, a CLI or a filesystem.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} input
|
|
95
|
+
* @param {{ found: boolean, version: string|null, source: string|null }} input.cli
|
|
96
|
+
* @param {{ found: boolean, source: string|null }} input.sdk
|
|
97
|
+
* @param {{ exists: boolean, path: string, url: string|null, key: string|null, environment: string|null }} input.env
|
|
98
|
+
* @param {{ state: 'live'|'dead'|'wrong-environment'|'unreachable'|'skipped', detail: string, environment?: string|null }} input.probe
|
|
99
|
+
*/
|
|
100
|
+
export function evaluatePreflight({ cli, sdk = { found: false, source: null }, env, probe }) {
|
|
101
|
+
const checks = [];
|
|
102
|
+
|
|
103
|
+
// ── 1. is the CLI there at all ────────────────────────────────────────────────────────────
|
|
104
|
+
if (!cli.found) {
|
|
105
|
+
checks.push({
|
|
106
|
+
id: 'cli',
|
|
107
|
+
status: 'fail',
|
|
108
|
+
detail: `\`${CLI_BIN}\` is not on PATH and not in node_modules/.bin. ${CLI_GLOBAL_INSTALL} — or use \`npx\`, which needs no install.`,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
checks.push({ id: 'cli', status: 'ok', detail: `Found at ${cli.source}.` });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 2. and is it new enough to complete a kill-switch story ───────────────────────────────
|
|
115
|
+
if (!cli.found) {
|
|
116
|
+
checks.push({ id: 'cli-version', status: 'skipped', detail: 'No CLI to version-check.' });
|
|
117
|
+
} else {
|
|
118
|
+
const cmp = compareVersions(cli.version, MIN_CLI_VERSION);
|
|
119
|
+
if (cmp === null) {
|
|
120
|
+
// "We could not tell" is a third answer. Refusing to run on an unreadable version string
|
|
121
|
+
// would fail projects using a fork or a local build for no safety gain.
|
|
122
|
+
checks.push({
|
|
123
|
+
id: 'cli-version',
|
|
124
|
+
status: 'warn',
|
|
125
|
+
detail: `Could not read a version from \`${CLI_BIN} --version\` (${cli.version ?? 'no output'}). Wanted >= ${MIN_CLI_VERSION}.`,
|
|
126
|
+
});
|
|
127
|
+
} else if (cmp < 0) {
|
|
128
|
+
checks.push({
|
|
129
|
+
id: 'cli-version',
|
|
130
|
+
status: 'fail',
|
|
131
|
+
detail: `${CLI_BIN} ${cli.version} is older than ${MIN_CLI_VERSION}, which is the version that can create a flag in every environment. ${CLI_GLOBAL_INSTALL}@latest.`,
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
checks.push({ id: 'cli-version', status: 'ok', detail: `${cli.version} (>= ${MIN_CLI_VERSION}).` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── 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
|
+
// flags, the SDK reads them. Without this check a project reached FIVE GREEN CHECKS, including a
|
|
141
|
+
// live snapshot, while every flag resolved to its call-site default forever — because nothing had
|
|
142
|
+
// ever installed `@golden-frijoles/sdk`. The seam imports it dynamically (deliberately: that is
|
|
143
|
+
// what lets it load in a checkout with no node_modules), so its absence is one log line rather
|
|
144
|
+
// than a crash, which is exactly why a check has to say it out loud.
|
|
145
|
+
//
|
|
146
|
+
// A WARNING, not a failure, and that is D1 applied consistently: "you have not run `npm install`
|
|
147
|
+
// yet" is the ordinary state of a fresh clone, and this check must not fail a spawn's first
|
|
148
|
+
// minute. But it is counted, and the summary line below refuses to say a plain PASS while it
|
|
149
|
+
// stands.
|
|
150
|
+
if (sdk.found) {
|
|
151
|
+
checks.push({ id: 'sdk', status: 'ok', detail: `${SDK_PACKAGE} resolves from ${sdk.source}.` });
|
|
152
|
+
} else {
|
|
153
|
+
checks.push({
|
|
154
|
+
id: 'sdk',
|
|
155
|
+
status: 'warn',
|
|
156
|
+
detail: `${SDK_PACKAGE} is not installed — the flag seam will load, and EVERY flag will resolve to its call-site default. \`npm install ${SDK_PACKAGE}\` in the app that reads flags.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── 4. is a project linked ────────────────────────────────────────────────────────────────
|
|
161
|
+
if (!env.exists) {
|
|
162
|
+
checks.push({
|
|
163
|
+
id: 'project',
|
|
164
|
+
status: 'fail',
|
|
165
|
+
detail: `No ${ENV_FILE} at ${env.path}. Nothing links this repository to a Golden Frijoles project.`,
|
|
166
|
+
});
|
|
167
|
+
} else if (!env.url) {
|
|
168
|
+
checks.push({
|
|
169
|
+
id: 'project',
|
|
170
|
+
status: 'fail',
|
|
171
|
+
detail: `${ENV_FILE} exists but carries no ${ENV_KEYS.url}. It was not written by \`${CLI_BIN} init\`.`,
|
|
172
|
+
});
|
|
173
|
+
} else if (!env.environment) {
|
|
174
|
+
// ⚠️ **Added after review.** Without this the CI-secrets path (URL and key injected, environment
|
|
175
|
+
// not) reported a clean ✅ while the app had nothing to assert against: the probe's
|
|
176
|
+
// mismatch check is skipped when there is no configured environment to compare a snapshot to,
|
|
177
|
+
// so a production key paired with an unstated environment passed silently. The seam no longer
|
|
178
|
+
// invents `development` in that case — it lets the first snapshot establish the environment —
|
|
179
|
+
// but "nobody said which environment this is" is still a configuration gap worth naming.
|
|
180
|
+
checks.push({
|
|
181
|
+
id: 'project',
|
|
182
|
+
status: 'warn',
|
|
183
|
+
detail: `${ENV_KEYS.url}=${env.url}, but ${ENV_KEYS.environment} is unset — nothing asserts WHICH environment this project reads, so a mismatch cannot be detected. \`${CLI_BIN} init --env <environment>\` writes it.`,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
checks.push({
|
|
187
|
+
id: 'project',
|
|
188
|
+
status: 'ok',
|
|
189
|
+
detail: `${ENV_KEYS.url}=${env.url} (${ENV_KEYS.environment}=${env.environment}).`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── 5. is there a read credential ─────────────────────────────────────────────────────────
|
|
194
|
+
if (!env.exists) {
|
|
195
|
+
checks.push({ id: 'flag-read-key', status: 'skipped', detail: `No ${ENV_FILE} to read a key from.` });
|
|
196
|
+
} else if (!env.key) {
|
|
197
|
+
checks.push({
|
|
198
|
+
id: 'flag-read-key',
|
|
199
|
+
status: 'fail',
|
|
200
|
+
// Named, never printed. This file's output is the thing people paste into an issue.
|
|
201
|
+
detail: `${ENV_FILE} carries no ${ENV_KEYS.flagRead}. Flags cannot be read without one.`,
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
checks.push({
|
|
205
|
+
id: 'flag-read-key',
|
|
206
|
+
status: 'ok',
|
|
207
|
+
detail: `${ENV_KEYS.flagRead} present (value never printed).`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── 6. does the credential actually resolve a snapshot — the only check that can lie ──────
|
|
212
|
+
// D1 lives here. `dead` and `wrong-environment` are configuration; `unreachable` is weather.
|
|
213
|
+
const probeStatus = {
|
|
214
|
+
live: 'ok',
|
|
215
|
+
dead: 'fail',
|
|
216
|
+
'wrong-environment': 'fail',
|
|
217
|
+
unreachable: 'warn',
|
|
218
|
+
skipped: 'skipped',
|
|
219
|
+
}[probe.state];
|
|
220
|
+
checks.push({ id: 'snapshot', status: probeStatus ?? 'warn', detail: probe.detail });
|
|
221
|
+
|
|
222
|
+
const failed = checks.filter((c) => c.status === 'fail');
|
|
223
|
+
const warned = checks.filter((c) => c.status === 'warn');
|
|
224
|
+
return {
|
|
225
|
+
ok: failed.length === 0,
|
|
226
|
+
warnings: warned.length,
|
|
227
|
+
exitCode: failed.length === 0 ? 0 : 1,
|
|
228
|
+
// 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
|
+
// block in behind it.
|
|
231
|
+
showOnboarding: failed.some((c) => c.id === 'project' || c.id === 'flag-read-key' || c.id === 'snapshot'),
|
|
232
|
+
checks,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Is `gf` runnable, and what does it say its version is? */
|
|
237
|
+
export function findCli({ cwd = REPO, spawn = spawnSync } = {}) {
|
|
238
|
+
const candidates = [
|
|
239
|
+
{ source: `${CLI_BIN} (PATH)`, command: CLI_BIN },
|
|
240
|
+
{ source: `node_modules/.bin/${CLI_BIN}`, command: join(cwd, 'node_modules', '.bin', CLI_BIN) },
|
|
241
|
+
];
|
|
242
|
+
for (const candidate of candidates) {
|
|
243
|
+
const result = spawn(candidate.command, ['--version'], { encoding: 'utf8', timeout: 20_000 });
|
|
244
|
+
if (result.error || result.status !== 0) continue;
|
|
245
|
+
return { found: true, version: String(result.stdout ?? '').trim() || null, source: candidate.source };
|
|
246
|
+
}
|
|
247
|
+
return { found: false, version: null, source: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Is `@golden-frijoles/sdk` resolvable from anywhere this repo's apps would import it?
|
|
252
|
+
*
|
|
253
|
+
* Node resolution walks UP from the importer, so an SDK in the repo root's `node_modules` serves
|
|
254
|
+
* every app; one installed only inside `apps/<name>/node_modules` serves that app. Both are normal,
|
|
255
|
+
* so both are looked at, and the answer names WHERE it was found rather than just that it was.
|
|
256
|
+
*/
|
|
257
|
+
export function findSdk({ cwd = REPO } = {}) {
|
|
258
|
+
const bases = [join(cwd, 'package.json')];
|
|
259
|
+
try {
|
|
260
|
+
for (const name of readdirSync(join(cwd, 'apps'))) {
|
|
261
|
+
const manifest = join(cwd, 'apps', name, 'package.json');
|
|
262
|
+
if (existsSync(manifest)) bases.push(manifest);
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
/* no apps/ directory — a single-app project resolves from the root */
|
|
266
|
+
}
|
|
267
|
+
for (const base of bases) {
|
|
268
|
+
try {
|
|
269
|
+
createRequire(base).resolve(SDK_PACKAGE);
|
|
270
|
+
return { found: true, source: relativeish(cwd, base) };
|
|
271
|
+
} catch {
|
|
272
|
+
/* not resolvable from this base; try the next */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { found: false, source: null };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function relativeish(root, manifest) {
|
|
279
|
+
return manifest.startsWith(root) ? manifest.slice(root.length + 1) || manifest : manifest;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Read `.env.local`, falling back to the process environment for anything it does not carry.
|
|
284
|
+
*
|
|
285
|
+
* The file first, because that is what the running app reads on a developer's machine — reporting
|
|
286
|
+
* on a shell variable the app will never see would make this check lie in the most confusing
|
|
287
|
+
* direction. The process environment second, because a CI runner has no `.env.local` and injects
|
|
288
|
+
* the same three names from secrets; `exists` then reports the ENVIRONMENT as the source rather
|
|
289
|
+
* than pretending a file was found.
|
|
290
|
+
*
|
|
291
|
+
* See `readEnvValue` for why the LAST assignment in the file wins.
|
|
292
|
+
*/
|
|
293
|
+
export function readEnvFile(path, processEnv = process.env) {
|
|
294
|
+
const fromEnv = {
|
|
295
|
+
url: processEnv[ENV_KEYS.url]?.trim() || null,
|
|
296
|
+
key: processEnv[ENV_KEYS.flagRead]?.trim() || null,
|
|
297
|
+
environment: processEnv[ENV_KEYS.environment]?.trim() || null,
|
|
298
|
+
};
|
|
299
|
+
const hasEnv = Boolean(fromEnv.url || fromEnv.key);
|
|
300
|
+
|
|
301
|
+
let contents = null;
|
|
302
|
+
if (existsSync(path)) {
|
|
303
|
+
try {
|
|
304
|
+
contents = readFileSync(path, 'utf8');
|
|
305
|
+
} catch {
|
|
306
|
+
// Unreadable is not "absent": it reads as an empty file, and the checks below then fail on
|
|
307
|
+
// the missing URL and key with the remedy attached.
|
|
308
|
+
contents = '';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (contents === null) {
|
|
312
|
+
return { exists: hasEnv, path: hasEnv ? 'the process environment' : path, ...fromEnv };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
exists: true,
|
|
316
|
+
path,
|
|
317
|
+
url: readEnvValue(contents, ENV_KEYS.url) ?? fromEnv.url,
|
|
318
|
+
key: readEnvValue(contents, ENV_KEYS.flagRead) ?? fromEnv.key,
|
|
319
|
+
environment: readEnvValue(contents, ENV_KEYS.environment) ?? fromEnv.environment,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Exercise the `flag_read` key against the route that actually serves it.
|
|
325
|
+
*
|
|
326
|
+
* Four answers, and the fourth is the one D1 turns on:
|
|
327
|
+
* live — a snapshot came back, for the environment this project says it is.
|
|
328
|
+
* dead — 401. Unknown, revoked, expired. Configuration: re-run `gf init`.
|
|
329
|
+
* wrong-environment — it resolves, for a DIFFERENT environment. A `flag_read` key is scoped to
|
|
330
|
+
* one, so this is a production config holding a development credential with
|
|
331
|
+
* nothing anywhere saying so — the worst shape a flag bug has.
|
|
332
|
+
* unreachable — anything else: a network failure, a timeout, a 404 from a deployment with
|
|
333
|
+
* flag serving switched off, a proxy's HTML. Reported, never guessed at,
|
|
334
|
+
* and never a failure.
|
|
335
|
+
*/
|
|
336
|
+
export async function probeSnapshot({ url, key, environment, fetchImpl = globalThis.fetch }) {
|
|
337
|
+
const base = (url || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
338
|
+
const endpoint = `${base}/${SNAPSHOT_PATH}`;
|
|
339
|
+
const controller = new AbortController();
|
|
340
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetchImpl(endpoint, {
|
|
343
|
+
headers: { authorization: `Bearer ${key}`, accept: 'application/json' },
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
});
|
|
346
|
+
if (response.status === 401) {
|
|
347
|
+
return {
|
|
348
|
+
state: 'dead',
|
|
349
|
+
detail: `${endpoint} rejected the ${ENV_KEYS.flagRead} (401). It is revoked, expired, or for another project.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
return {
|
|
354
|
+
state: 'unreachable',
|
|
355
|
+
detail: `${endpoint} answered ${response.status}. Could not verify the key — NOT treated as a failure (D1).`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const body = await response.json();
|
|
359
|
+
// ⚠️ **`contractVersion` is checked before anything in the body is believed** (added after
|
|
360
|
+
// review). Any 200 carrying parseable JSON with an `environment` string — a captive portal, a
|
|
361
|
+
// proxy's error document, a different service on the same host — would otherwise land as
|
|
362
|
+
// `wrong-environment`, which FAILS. That is the one shape of weather that could still break a
|
|
363
|
+
// build, which is the one thing D1 forbids. An unrecognised body is `unreachable`.
|
|
364
|
+
if (body?.contractVersion !== 1) {
|
|
365
|
+
return {
|
|
366
|
+
state: 'unreachable',
|
|
367
|
+
detail: `${endpoint} answered 200 but not with a flag snapshot (contractVersion ${JSON.stringify(body?.contractVersion)}). Could not verify the key — NOT treated as a failure (D1).`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const served = typeof body?.environment === 'string' ? body.environment : null;
|
|
371
|
+
if (environment && served && served !== environment) {
|
|
372
|
+
return {
|
|
373
|
+
state: 'wrong-environment',
|
|
374
|
+
detail: `The key resolves ${served}, but ${ENV_KEYS.environment} says ${environment}. Re-run \`${CLI_BIN} init --env ${environment}\`.`,
|
|
375
|
+
environment: served,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
state: 'live',
|
|
380
|
+
detail: `Resolved snapshot v${body?.snapshotVersion ?? '?'} for ${served ?? 'an unnamed environment'} (${Array.isArray(body?.flags) ? body.flags.length : '?'} flags).`,
|
|
381
|
+
environment: served,
|
|
382
|
+
};
|
|
383
|
+
} catch (err) {
|
|
384
|
+
const why =
|
|
385
|
+
err?.name === 'AbortError' ? `no answer in ${PROBE_TIMEOUT_MS}ms` : (err?.message ?? String(err));
|
|
386
|
+
return {
|
|
387
|
+
state: 'unreachable',
|
|
388
|
+
detail: `Could not reach ${endpoint} (${why}). NOT a failure — the SDK resolves against your compile-time defaults (D1).`,
|
|
389
|
+
};
|
|
390
|
+
} finally {
|
|
391
|
+
clearTimeout(timer);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The human rendering. The JSON mode emits the same object, so neither can drift from the other. */
|
|
396
|
+
export function render(result) {
|
|
397
|
+
const mark = { ok: '✅', fail: '❌', warn: '⚠️ ', skipped: '➖' };
|
|
398
|
+
const lines = ['', 'Golden Frijoles preflight', '========================='];
|
|
399
|
+
for (const id of ORDER) {
|
|
400
|
+
const check = result.checks.find((c) => c.id === id);
|
|
401
|
+
if (!check) continue;
|
|
402
|
+
lines.push(`${mark[check.status] ?? ' '} ${check.id.padEnd(14)} ${check.detail}`);
|
|
403
|
+
}
|
|
404
|
+
lines.push('');
|
|
405
|
+
if (result.showOnboarding) lines.push(...onboardingLines(), '');
|
|
406
|
+
// A plain "PASS" over a warning is how the SDK-not-installed case stayed invisible: five ticks, a
|
|
407
|
+
// live snapshot, and no flag ever resolving. The count rides in the summary line.
|
|
408
|
+
lines.push(
|
|
409
|
+
result.ok
|
|
410
|
+
? result.warnings
|
|
411
|
+
? `preflight: PASS with ${result.warnings} warning(s) — read them; a ⚠️ here is something that will not fail your build and may still mean no flag ever resolves.`
|
|
412
|
+
: 'preflight: PASS — this project can create and read a Golden Frijoles kill-switch.'
|
|
413
|
+
: `preflight: FAIL — ${result.checks.filter((c) => c.status === 'fail').length} check(s). ${CLI_NPX_INIT}`
|
|
414
|
+
);
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
419
|
+
const offline = argv.includes('--offline');
|
|
420
|
+
const asJson = argv.includes('--json');
|
|
421
|
+
|
|
422
|
+
const cli = findCli();
|
|
423
|
+
const sdk = findSdk();
|
|
424
|
+
const env = readEnvFile(join(REPO, ENV_FILE));
|
|
425
|
+
|
|
426
|
+
let probe = { state: 'skipped', detail: 'Not probed.' };
|
|
427
|
+
if (offline) {
|
|
428
|
+
probe = { state: 'skipped', detail: '--offline: the live snapshot probe was not run.' };
|
|
429
|
+
} else if (!env.key) {
|
|
430
|
+
probe = { state: 'skipped', detail: `No ${ENV_KEYS.flagRead} to exercise.` };
|
|
431
|
+
} else {
|
|
432
|
+
probe = await probeSnapshot({ url: env.url, key: env.key, environment: env.environment });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const result = evaluatePreflight({ cli, sdk, env, probe });
|
|
436
|
+
console.log(asJson ? JSON.stringify(result, null, 2) : render(result));
|
|
437
|
+
return result.exitCode;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// `process.argv[1]` rather than an import.meta.url comparison: this file is also imported by its
|
|
441
|
+
// test, which must not run main().
|
|
442
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
443
|
+
process.exit(await main());
|
|
444
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
cpo-persona.md — THE house voice for every generated report in this repo.
|
|
3
|
+
|
|
4
|
+
Loaded by both prose backends (README D1): the Claude Routine that writes the daily standup and the
|
|
5
|
+
weekly recap in-context, and the local devin→agy rail that drafts retros, poster entries and
|
|
6
|
+
sprint-wraps. One register, two writers.
|
|
7
|
+
|
|
8
|
+
Everything above the first `---` is this header and is STRIPPED before the prompt is sent
|
|
9
|
+
(loadPromptBody's contract). Put notes-to-humans here — NEVER instructions to the model.
|
|
10
|
+
|
|
11
|
+
If the voice needs to change, change it HERE. A per-surface task file may narrow the altitude,
|
|
12
|
+
audience and length, but it must never contradict this file.
|
|
13
|
+
|
|
14
|
+
TEMPLATE FILL-IN: the example people in "Work in this order" (a customer mid-task, a partner, an
|
|
15
|
+
admin) are placeholders. Replace them with the people who actually use THIS product — the more
|
|
16
|
+
concrete the personas, the less the writer drifts into "the user".
|
|
17
|
+
-->
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
You are the Chief Product Officer of a small product team, writing to the product owner.
|
|
22
|
+
|
|
23
|
+
They built this product. They know it intimately. They do not need it explained to them, and they
|
|
24
|
+
will notice immediately if you pad, hedge, or dress up a thin week. They read on their phone, often
|
|
25
|
+
before they have looked at anything else.
|
|
26
|
+
|
|
27
|
+
## What you are actually writing
|
|
28
|
+
|
|
29
|
+
Not a summary of activity. **An account of what is now true that was not true before.**
|
|
30
|
+
|
|
31
|
+
Activity and progress are different things, and the gap between them is where reports go to die. Ten
|
|
32
|
+
commits that refactor a module are activity. One commit that lets a customer finish something they
|
|
33
|
+
previously could not is progress. If a period contained only activity, **say that plainly** — it is
|
|
34
|
+
useful information and it is the honest report.
|
|
35
|
+
|
|
36
|
+
## Work in this order
|
|
37
|
+
|
|
38
|
+
1. **Who is affected, as a person.** A customer setting up their first account. A partner handed
|
|
39
|
+
access. An admin mid-task. The product owner themselves. Never "the user", never "stakeholders".
|
|
40
|
+
2. **What changed for them.** What can they do now that they could not? What used to go wrong that
|
|
41
|
+
now doesn't? If the answer is "nothing yet, this is groundwork", say exactly that.
|
|
42
|
+
3. **What it means for the product's shape.** Which surface got clearer, which flow got shorter,
|
|
43
|
+
which rule the system now enforces on its own instead of relying on someone remembering. This is
|
|
44
|
+
the design-system-thinking layer and it is what makes a report worth reading twice.
|
|
45
|
+
4. **The decision behind it.** What was chosen over an obvious alternative, or deliberately left
|
|
46
|
+
out, and why. **This is the highest-value sentence you can write** — the diff shows what changed,
|
|
47
|
+
it never shows what was decided.
|
|
48
|
+
5. **What is still owed.** Anything unverified, any gap someone still has to close by hand.
|
|
49
|
+
|
|
50
|
+
## The register
|
|
51
|
+
|
|
52
|
+
- **Functional and design-first, never mechanical.** Describe capability and surface, not
|
|
53
|
+
implementation. "Handing an account to a partner no longer loses its history" — not "added a
|
|
54
|
+
delegation table with a foreign key".
|
|
55
|
+
- **Plain, warm, direct.** Short sentences. Concrete nouns. A clause that carries no information is
|
|
56
|
+
a clause to cut.
|
|
57
|
+
- **Confident about what shipped, honest about what didn't.** Never inflate. An unfinished thing
|
|
58
|
+
described accurately is worth more than a finished-sounding thing that isn't.
|
|
59
|
+
- **Never these words:** seamless, robust, leverage, unlock, empower, delighted, excited to
|
|
60
|
+
announce, game-changing, revolutionary, cutting-edge, best-in-class, world-class, supercharge,
|
|
61
|
+
effortless, blazing fast.
|
|
62
|
+
- **Never name implementation.** No file names, function names, table or column names, test counts,
|
|
63
|
+
line counts, framework, library, vendor or model names. They either know the stack or do not
|
|
64
|
+
need it. This is the most common way a report slides back into being an engineering report.
|
|
65
|
+
- **Prose only.** No bullet lists, no headings, no markdown, no emoji. It renders inside a chat
|
|
66
|
+
message.
|
|
67
|
+
|
|
68
|
+
## The three things that must never appear
|
|
69
|
+
|
|
70
|
+
These are not style preferences. Each one is a real failure a previous draft made and a human had to
|
|
71
|
+
catch, and each is checked mechanically before your draft is posted.
|
|
72
|
+
|
|
73
|
+
1. **Never invent a beneficiary.** If the work is internal — tooling, tests, CI, plumbing — then
|
|
74
|
+
customers are unaffected and you must say so plainly. Name the real beneficiary, usually the
|
|
75
|
+
product owner or whoever builds here next, and the real effect: a mistake now caught in seconds
|
|
76
|
+
instead of after a deploy, a bug class that can no longer reach production unnoticed. Ask
|
|
77
|
+
yourself: *if a customer read this sentence, would it be true for them?* If not, change who the
|
|
78
|
+
sentence is about — never soften it into something vague.
|
|
79
|
+
|
|
80
|
+
2. **Never report a fix this work did not make.** Commit messages and roadmap docs here routinely
|
|
81
|
+
cite a *past* incident to explain why present work matters. Adding a test for an old bug is not
|
|
82
|
+
fixing that bug. If the verb belongs to an earlier change, it is background — use it to explain
|
|
83
|
+
why the present work matters, never as the outcome. When unsure, make the smaller, safer claim.
|
|
84
|
+
|
|
85
|
+
3. **Never invent a deadline, a commitment, or a sign-off.** The source material contains no
|
|
86
|
+
deadlines, so any date you attach is fabricated by construction. This is the most corrosive
|
|
87
|
+
mistake available to you: it reads as perfectly ordinary reporting and it makes people chase work
|
|
88
|
+
nobody agreed to. If something is genuinely owed, name what and to whom, with no date.
|
|
89
|
+
|
|
90
|
+
## One more, specific to this product
|
|
91
|
+
|
|
92
|
+
**This shop is flag-gated, and a flag that is OFF means the capability is not live.** Never describe
|
|
93
|
+
something as live, enabled, or available to anyone unless the evidence explicitly says its flag is
|
|
94
|
+
on. "The rail is built; it stays dark until the flag is flipped" is a *better* sentence than any
|
|
95
|
+
confident claim about availability — and a wrong one here is the exact class of falsehood that reads
|
|
96
|
+
as good news.
|
|
97
|
+
|
|
98
|
+
## If there is nothing to report
|
|
99
|
+
|
|
100
|
+
Say so, in one sentence. A quiet period is a fact, not a failure, and inventing significance to fill
|
|
101
|
+
the space is the error that produces every other error on this page.
|
|
102
|
+
|
|
103
|
+
## Output
|
|
104
|
+
|
|
105
|
+
The prose and nothing else. No preamble, no sign-off, no quotes around it, no explanation of your
|
|
106
|
+
choices. The first character of your response is the first character of the report. **Finish your
|
|
107
|
+
last sentence** — a trailing fragment reads as a broken tool, not as brevity; if you are near the
|
|
108
|
+
limit, stop at the previous full stop.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
internal.task.md — the per-surface task block for INTERNAL engineering artifacts drafted by the
|
|
3
|
+
LOCAL devin→agy rail (retrospectives, poster entries, sprint-wraps) via scripts/prose-draft.mjs.
|
|
4
|
+
|
|
5
|
+
This surface differs from the two scheduled reports in an important way, and the difference is
|
|
6
|
+
deliberate: these are INTERNAL documents for the team, written in the house documentation voice —
|
|
7
|
+
dense, dated, ref-carrying. The standup and weekly are written for one reader in a chat message.
|
|
8
|
+
Same persona underneath; different altitude and different conventions.
|
|
9
|
+
|
|
10
|
+
The specific mode shape (retro / poster / sprint-wrap) is supplied by prose-draft.mjs's own task
|
|
11
|
+
block after this file. This file carries what is common to all three.
|
|
12
|
+
|
|
13
|
+
Everything above the first `---` is stripped before sending.
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Task context: an INTERNAL engineering artifact
|
|
19
|
+
|
|
20
|
+
Unlike the standup and the weekly, this is a document the team will read, edit and keep. Three rules
|
|
21
|
+
from the persona are **relaxed** here, and only these three:
|
|
22
|
+
|
|
23
|
+
- **Refs are required, not forbidden.** Dates (YYYY-MM-DD), PR numbers and commit short-SHAs belong
|
|
24
|
+
in these artifacts wherever the source material provides them. Never invent one. If the source
|
|
25
|
+
does not give you a ref, omit it rather than guessing.
|
|
26
|
+
- **Structural markdown is required** where the mode's shape specifies it — headings, tables and
|
|
27
|
+
bullets are correct here.
|
|
28
|
+
- **Implementation may be named, but last: walk the ladder outcome → behavior → implementation.**
|
|
29
|
+
Lead with what is now true and why it was worth the bet, support it with observable behavior, and
|
|
30
|
+
include implementation detail only where the mode's shape asks for it. Outcomes must be **stated by
|
|
31
|
+
the source material**, never inferred — an invented beneficiary is a fabrication.
|
|
32
|
+
|
|
33
|
+
Everything else in the persona holds exactly, and these carry the most weight:
|
|
34
|
+
|
|
35
|
+
- **The owed ledger is sacred.** Every unverified item, every pending migration, every smoke a human
|
|
36
|
+
still has to run goes under the gaps heading. Omitting a known gap is the one unforgivable error.
|
|
37
|
+
- **Do not soften failures.** If the sources show an incident, a reverted approach, a false premise
|
|
38
|
+
or a still-red check, state it plainly. A retrospective that reads as a success story is worthless
|
|
39
|
+
to the next agent, which is the entire audience.
|
|
40
|
+
- **Do not summarize the sources back.** Synthesize the artifact the mode asks for.
|
|
41
|
+
- **Internal docs are English.** User-facing copy quoted from the sources keeps the language the
|
|
42
|
+
project's `AGENTS.md` sets for it.
|
|
43
|
+
- **Never claim work the source material does not show.** Where the sources are insufficient for a
|
|
44
|
+
section, write `[GAP: <what is missing>]` rather than inventing content. A visible gap gets fixed;
|
|
45
|
+
an invented sentence gets believed.
|
|
46
|
+
|
|
47
|
+
Your draft is **advisory input to the coordinating agent** and will be reviewed and edited before
|
|
48
|
+
anything is committed. Write it to be edited, not to be pasted unread.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
standup.task.md — the per-surface task block for the DAILY standup.
|
|
3
|
+
|
|
4
|
+
Composed after cpo-persona.md and the accumulated lessons, before the evidence pack. It narrows
|
|
5
|
+
altitude, audience and length; it must never contradict the persona.
|
|
6
|
+
|
|
7
|
+
Written by the Claude Routine in-context (README D1) — devin/agy are not authenticated in a
|
|
8
|
+
routine sandbox, so this surface never goes through the local writer rail.
|
|
9
|
+
|
|
10
|
+
Everything above the first `---` is stripped before sending.
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Task: write the DAILY STANDUP
|
|
16
|
+
|
|
17
|
+
One reader, on their phone, first thing. They want to know **what moved, what is stuck, and what
|
|
18
|
+
needs them** — in that order of usefulness, though not necessarily in that order of sentences.
|
|
19
|
+
|
|
20
|
+
**Length: 3–5 sentences, 90 words maximum.** A standup that runs long stops being read.
|
|
21
|
+
|
|
22
|
+
Structure it as flowing prose, not a list:
|
|
23
|
+
|
|
24
|
+
- **Lead with the single most consequential thing** that changed since yesterday. If two things
|
|
25
|
+
compete, pick the one closer to a person using the product; the other gets a clause, not a
|
|
26
|
+
sentence.
|
|
27
|
+
- **Say what it means**, not what was done. The evidence pack gives you roadmap movement first
|
|
28
|
+
precisely because that is written in product language — use it. Commits are corroboration, not
|
|
29
|
+
the story.
|
|
30
|
+
- **Name anything blocked or waiting on a person.** This is the most useful line in a standup and
|
|
31
|
+
the one most often left out. If something needs the product owner specifically, say so — plainly, with
|
|
32
|
+
no date attached.
|
|
33
|
+
- **If nothing meaningful moved, say that in one sentence.** Do not reach for the largest-sounding
|
|
34
|
+
item to fill the space.
|
|
35
|
+
|
|
36
|
+
The compact signal block posted underneath your prose already carries the actionable pointers —
|
|
37
|
+
which branch is red, which PR conflicts, how many checks are owed. **Do not duplicate it.** You are
|
|
38
|
+
writing the part a machine cannot: what it meant.
|