@golden-frijoles/kit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/config-registry.mjs +10 -0
- package/dist/lib/config.mjs +1 -1
- package/dist/routine-bootstrap.mjs +194 -0
- package/dist/routines/README.md +473 -0
- package/dist/routines/ops-nightly.prompt.md +125 -0
- package/dist/routines/pmo-report.prompt.md +52 -0
- package/dist/routines/pr-review.prompt.md +98 -0
- package/dist/routines/prod-smoke.prompt.md +140 -0
- package/dist/routines/roadmap-hygiene.prompt.md +97 -0
- package/dist/routines/smoke-triage.prompt.md +196 -0
- package/dist/routines/weekly-recap.prompt.md +95 -0
- package/package.json +1 -1
|
@@ -118,6 +118,16 @@ export const REGISTRY = Object.freeze([
|
|
|
118
118
|
default: null,
|
|
119
119
|
question: 'Where should standups and recaps go: Telegram (the scheduled reports post there), or print to the terminal? Slack works for test and ad-hoc messages only, not the scheduled reports. Secrets stay in .env.local.',
|
|
120
120
|
},
|
|
121
|
+
{
|
|
122
|
+
key: 'routines',
|
|
123
|
+
module: 'Operate',
|
|
124
|
+
// never-yet: declares the section so readSection accepts it, while `gf doctor` (which skips never-yet
|
|
125
|
+
// rows) never reports Operate unconfigured for it — its values are per-routine fill-ins that
|
|
126
|
+
// routine-bootstrap.mjs names itself when one is missing (#191 review).
|
|
127
|
+
askWhen: 'never-yet',
|
|
128
|
+
default: null,
|
|
129
|
+
question: 'Which project values should routine prompts fill before you schedule them?',
|
|
130
|
+
},
|
|
121
131
|
{
|
|
122
132
|
key: 'deploy.vercelProject',
|
|
123
133
|
module: 'Operate',
|
package/dist/lib/config.mjs
CHANGED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// routine-bootstrap.mjs — render one reviewed routine prompt with this project's declared fill-ins.
|
|
3
|
+
//
|
|
4
|
+
// The prompts deliberately mix project values with values a routine learns while it runs. Replacing every
|
|
5
|
+
// angle-bracket string would turn `<date>` or `<PR#>` into stale prose before the first run, so this table is
|
|
6
|
+
// the one authority for the distinction. A missing project value refuses before stdout: a partly-filled prompt
|
|
7
|
+
// pasted into /schedule is a cloud-side configuration bug that is hard to see and harder to unwind.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/routine-bootstrap.mjs <name>
|
|
10
|
+
// node scripts/routine-bootstrap.mjs --list
|
|
11
|
+
//
|
|
12
|
+
// Exit codes: 0 rendered, 1 missing fill-ins/configuration, 2 bad routine name or usage. Zero deps; Node 18+.
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { readSection } from './lib/config.mjs';
|
|
18
|
+
import { loadPromptBody } from './lib/cross-agent-cli.mjs';
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
export const ROUTINES_DIR = join(HERE, 'routines');
|
|
22
|
+
|
|
23
|
+
// Includes every token in the seven prompts and their runbook. `source: 'runbook'` records tokens that are
|
|
24
|
+
// documentation examples rather than substitutions in a rendered prompt; keeping them here makes a new token a
|
|
25
|
+
// visible classification decision. The three descriptive TEMPLATE FILL-IN markers have separate keys because a
|
|
26
|
+
// single generic replacement would leave unrelated framework, rules, and deploy advice in the wrong prompt.
|
|
27
|
+
export const ROUTINE_TOKENS = Object.freeze([
|
|
28
|
+
{ token: '<root-repo>', kind: 'fill-in', key: 'root-repo' },
|
|
29
|
+
{ token: '<app-repo>', kind: 'fill-in', key: 'app-repo' },
|
|
30
|
+
{ token: '<api-repo>', kind: 'fill-in', key: 'api-repo', source: 'runbook' },
|
|
31
|
+
{ token: '<your-org>', kind: 'fill-in', key: 'your-org' },
|
|
32
|
+
{ token: '<appDir>', kind: 'fill-in', key: 'appDir' },
|
|
33
|
+
{ token: '<PROD_URL>', kind: 'fill-in', key: 'PROD_URL' },
|
|
34
|
+
{ token: '<PROD_DOMAIN>', kind: 'fill-in', key: 'PROD_DOMAIN' },
|
|
35
|
+
{ token: '<vercelProject>', kind: 'fill-in', key: 'vercelProject' },
|
|
36
|
+
{ token: '<stalePreviewAgeDays>', kind: 'fill-in', key: 'stalePreviewAgeDays' },
|
|
37
|
+
{ token: '<rule one>', kind: 'fill-in', key: 'rule one' },
|
|
38
|
+
{ token: '<rule two>', kind: 'fill-in', key: 'rule two' },
|
|
39
|
+
{
|
|
40
|
+
token: "TEMPLATE FILL-IN: name your framework's async gotchas here, e.g. which request APIs are async",
|
|
41
|
+
kind: 'fill-in',
|
|
42
|
+
key: 'async-gotchas',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
token:
|
|
46
|
+
'TEMPLATE FILL-IN: list them here,\nnumbered, exactly as AGENTS.md states them, so this reviewer checks the same rules the builders follow.\nThe shape the origin project\'s list took, for reference: which system owns which data ("the commerce\nengine owns all orders and payments — never a second table for them"), which layer is never replaced\n("the auth provider is the auth layer — no custom auth pages"), which interfaces must stay accurate (an\nAPI/agent surface), and the copy/locale rule for user-visible text.',
|
|
47
|
+
kind: 'fill-in',
|
|
48
|
+
key: 'rules-context',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
token: "TEMPLATE FILL-IN: the frontend's deploy path, e.g. build → hosting service",
|
|
52
|
+
kind: 'fill-in',
|
|
53
|
+
key: 'deploy-path',
|
|
54
|
+
},
|
|
55
|
+
{ token: '<date>', kind: 'runtime' },
|
|
56
|
+
{ token: '<id>', kind: 'runtime' },
|
|
57
|
+
{ token: '<trigger-id>', kind: 'runtime' },
|
|
58
|
+
{ token: '<name>', kind: 'runtime' },
|
|
59
|
+
{ token: '<your-file>', kind: 'runtime' },
|
|
60
|
+
{ token: '<repo>', kind: 'runtime' },
|
|
61
|
+
{ token: '<PR#>', kind: 'runtime' },
|
|
62
|
+
{ token: '<PR>', kind: 'runtime' },
|
|
63
|
+
{ token: '<N>', kind: 'runtime' },
|
|
64
|
+
{ token: '<one-line reason>', kind: 'runtime' },
|
|
65
|
+
{ token: '<the files you touched>', kind: 'runtime' },
|
|
66
|
+
{ token: '<the spec>', kind: 'runtime' },
|
|
67
|
+
{ token: '<your message>', kind: 'runtime' },
|
|
68
|
+
{ token: '<[a-z-]*-repo>', kind: 'runtime', source: 'runbook' },
|
|
69
|
+
{ token: 'TEMPLATE FILL-IN', kind: 'runtime', source: 'runbook' },
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
export function routineNames(dir = ROUTINES_DIR, readDir = readdirSync) {
|
|
73
|
+
// A missing routines/ is "no routines", reported by the CLI's usage line — never a stack trace (agy on #191).
|
|
74
|
+
if (!existsSync(dir)) return [];
|
|
75
|
+
return readDir(dir)
|
|
76
|
+
.filter((name) => name.endsWith('.prompt.md'))
|
|
77
|
+
.map((name) => name.replace(/\.prompt\.md$/, ''))
|
|
78
|
+
.sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const present = (value) => (typeof value === 'string' ? value.trim().length > 0 : value != null);
|
|
82
|
+
|
|
83
|
+
export function fillInsForPrompt(body) {
|
|
84
|
+
return ROUTINE_TOKENS.filter((entry) => entry.kind === 'fill-in' && body.includes(entry.token));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderPrompt(body, values) {
|
|
88
|
+
let rendered = body;
|
|
89
|
+
for (const entry of fillInsForPrompt(body)) {
|
|
90
|
+
if (present(values?.[entry.key])) rendered = rendered.split(entry.token).join(String(values[entry.key]));
|
|
91
|
+
}
|
|
92
|
+
return rendered;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function missingFillIns(body, values) {
|
|
96
|
+
return fillInsForPrompt(body).filter((entry) => !present(values?.[entry.key]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Pure: placeholders the table does not know — `<TEMPLATE FILL-IN…`/`TEMPLATE FILL-IN:` markers, and `<token>`s. */
|
|
100
|
+
export function unclassifiedTokens(text) {
|
|
101
|
+
const known = new Set(ROUTINE_TOKENS.map((e) => e.token));
|
|
102
|
+
const found = new Set();
|
|
103
|
+
// Remove every CLASSIFIED marker first, so a new marker beside a known one is still seen (#191 review).
|
|
104
|
+
let rest = text;
|
|
105
|
+
for (const e of ROUTINE_TOKENS) if (e.kind === 'fill-in') rest = rest.split(e.token).join('');
|
|
106
|
+
if (/<TEMPLATE FILL-IN|TEMPLATE FILL-IN:/.test(rest)) found.add('TEMPLATE FILL-IN');
|
|
107
|
+
for (const m of text.matchAll(/<([A-Za-z][A-Za-z0-9_ #.-]{0,40})>/g)) if (!known.has(m[0])) found.add(m[0]);
|
|
108
|
+
return [...found];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function promptPath(name, dir = ROUTINES_DIR) {
|
|
112
|
+
return join(dir, `${name}.prompt.md`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** CLI seam: all I/O is injected so the refusal contract can be tested without a checkout. */
|
|
116
|
+
export function runRoutineBootstrap(
|
|
117
|
+
argv,
|
|
118
|
+
{
|
|
119
|
+
root,
|
|
120
|
+
routinesDir = ROUTINES_DIR,
|
|
121
|
+
names = routineNames(routinesDir),
|
|
122
|
+
readSectionFor = readSection,
|
|
123
|
+
loadBody = loadPromptBody,
|
|
124
|
+
out = (text) => process.stdout.write(text),
|
|
125
|
+
err = (text) => process.stderr.write(text),
|
|
126
|
+
} = {}
|
|
127
|
+
) {
|
|
128
|
+
if (argv.length === 1 && argv[0] === '--list') {
|
|
129
|
+
out(`${names.join('\n')}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (argv.length !== 1 || argv[0].startsWith('-')) {
|
|
133
|
+
err(`Usage: node scripts/routine-bootstrap.mjs <name> | --list\nValid routines: ${names.join(', ')}\n`);
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
const name = argv[0];
|
|
137
|
+
if (!names.includes(name)) {
|
|
138
|
+
err(`Unknown routine "${name}". Valid routines: ${names.join(', ')}\n`);
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// CRLF-safe: the multi-line fill-in markers are matched with \n (agy on #191).
|
|
143
|
+
const body = loadBody(promptPath(name, routinesDir)).replace(/\r\n/g, '\n');
|
|
144
|
+
let values;
|
|
145
|
+
try {
|
|
146
|
+
const section = readSectionFor('routines', root === undefined ? {} : { root });
|
|
147
|
+
values = section.raw ?? {};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
err(`routine-bootstrap: could not read routines from golden-frijoles.config.json: ${error.message}\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
const missing = missingFillIns(body, values);
|
|
153
|
+
if (missing.length) {
|
|
154
|
+
err(
|
|
155
|
+
'routine-bootstrap: refusing to print a partly-filled prompt. Set every missing golden-frijoles.config.json routines key:\n'
|
|
156
|
+
);
|
|
157
|
+
for (const entry of missing) err(` ${entry.key} (for ${entry.token})\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
const rendered = renderPrompt(body, values);
|
|
161
|
+
// Grep-to-zero, whatever the table knows (check-template-drift's syntax): a TEMPLATE FILL-IN marker, or an
|
|
162
|
+
// angle-bracket token nobody classified, refuses — a new token must be a visible decision, never a pass.
|
|
163
|
+
const unclassified = unclassifiedTokens(rendered);
|
|
164
|
+
if (unclassified.length) {
|
|
165
|
+
err(
|
|
166
|
+
`routine-bootstrap: refusing — unclassified placeholder(s) left in "${name}": ${unclassified.join(', ')}. ` +
|
|
167
|
+
'Classify each in ROUTINE_TOKENS (fill-in or runtime).\n'
|
|
168
|
+
);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
const leftovers = fillInsForPrompt(rendered);
|
|
172
|
+
if (leftovers.length) {
|
|
173
|
+
err(
|
|
174
|
+
`routine-bootstrap: refusing to print a prompt with unresolved fill-ins: ${leftovers.map((entry) => entry.token).join(', ')}\n`
|
|
175
|
+
);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
out(`${rendered}\n`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isMain = (() => {
|
|
183
|
+
if (!process.argv[1]) return false;
|
|
184
|
+
try {
|
|
185
|
+
// Symlinked kit entrypoints otherwise compare a link on one side to a real module path on the other and silently do nothing.
|
|
186
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
|
|
192
|
+
// No explicit root: readSection's default (projectRoot) honours GF_PROJECT_ROOT and walks up from a subdirectory,
|
|
193
|
+
// like every other kit script (#191 review).
|
|
194
|
+
if (isMain) process.exitCode = runRoutineBootstrap(process.argv.slice(2));
|