@bongos/core 1.19.315 → 1.19.317
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/.bongos-core.json +104 -29
- package/.claude/skills/merge-mode/SKILL.md +16 -0
- package/.claude/skills/planning-session/SKILL.md +10 -0
- package/modules/lifecycle/db-analytics.js +719 -0
- package/modules/lifecycle/db-claim-reads.js +165 -0
- package/modules/lifecycle/db-claims.js +934 -0
- package/modules/lifecycle/db-deps-criteria.js +441 -0
- package/modules/lifecycle/db-goals.js +763 -0
- package/modules/lifecycle/db-grade.js +233 -0
- package/modules/lifecycle/db-overrides.js +181 -0
- package/modules/lifecycle/db-rank-authz.js +446 -0
- package/modules/lifecycle/db-shared.js +115 -0
- package/modules/lifecycle/db-ship.js +994 -0
- package/modules/lifecycle/db-tasks.js +810 -0
- package/modules/lifecycle/db-versions.js +118 -0
- package/modules/lifecycle/db.js +167 -5564
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/coreB-carve-proof.sh +18 -9
- package/scripts/gds/fitness.js +35 -27
- package/scripts/gds/identity-strip.js +75 -0
- package/scripts/gds/preflight.js +25 -2
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/skill-preflight.js +336 -0
- package/src/module-api.js +5 -1
- package/tests/auto_satisfy_criteria.mjs +3 -2
- package/tests/dependencies_migration.mjs +3 -2
- package/tests/fitness.mjs +43 -0
- package/tests/goal_abuse_matrix.mjs +3 -2
- package/tests/goal_advisory.mjs +3 -2
- package/tests/government_claim_eligibility.mjs +3 -1
- package/tests/government_rank_sync.mjs +9 -2
- package/tests/helpers.mjs +19 -0
- package/tests/migration_allocation.mjs +3 -1
- package/tests/publish_reconciler.mjs +3 -1
- package/tests/rank_gate.mjs +3 -1
- package/tests/reward_gate.mjs +3 -1
- package/tests/skill_preflight.mjs +251 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/skill-preflight.js — an ops skill declares the machine it needs,
|
|
3
|
+
// and refuses early when this machine is not it (task 1003167 / G03, goal
|
|
4
|
+
// 1000077; criterion guards-skill-declares-machine).
|
|
5
|
+
//
|
|
6
|
+
// THE INCIDENT. /merge-mode states in its own prose that it assumes the Archon's
|
|
7
|
+
// Mac or the droplet. It was run on a dev box anyway. Worse, ship.js ALREADY knew:
|
|
8
|
+
// strandNextStep() tells a box "/merge-mode is NOT available here — it needs gh +
|
|
9
|
+
// a main checkout this machine doesn't have." So the knowledge existed in two
|
|
10
|
+
// places and neither stopped the run, because prose cannot refuse and the one
|
|
11
|
+
// executable copy was on a different code path. A capability a procedure needs
|
|
12
|
+
// must be a declaration the machine can CHECK, not a sentence a reader can skim.
|
|
13
|
+
//
|
|
14
|
+
// THREE PARTS, matching that diagnosis:
|
|
15
|
+
// 1. `requires:` in the skill's own YAML frontmatter — the declaration.
|
|
16
|
+
// 2. this preflight — the check, naming the MISSING capability and nothing else.
|
|
17
|
+
// 3. checkOpsSkillsDeclareMachine() in fitness.js — the assertion that a skill
|
|
18
|
+
// whose fenced commands touch main/deploy actually carries a declaration.
|
|
19
|
+
//
|
|
20
|
+
// TRI-STATE, deliberately. A probe returns true / false / null, and null means
|
|
21
|
+
// "could not tell" (the context-pack isMainCheckout precedent). We REFUSE on
|
|
22
|
+
// false and WARN on null: refusing on unknown would block legitimate machines,
|
|
23
|
+
// and passing on unknown silently would rebuild the hole this closes.
|
|
24
|
+
//
|
|
25
|
+
// The preflight NEVER connects to anything. Proving an SSH key reaches a host
|
|
26
|
+
// means using it, and a preflight with side effects is not a preflight.
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const { spawnSync } = require('node:child_process');
|
|
33
|
+
const { resolveWorktreeRoot } = require('./context-pack.js');
|
|
34
|
+
const { depsFinding } = require('./preflight.js');
|
|
35
|
+
|
|
36
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
37
|
+
const runGit = (args) => spawnSync('git', args, { cwd: process.cwd(), encoding: 'utf8' }).stdout || '';
|
|
38
|
+
|
|
39
|
+
// Memoized for the life of the process. merge-mode declares BOTH `gh` and
|
|
40
|
+
// `push-credential`, and push-credential's probe also consults gh — so without
|
|
41
|
+
// this, one preflight spawned `gh auth status` twice, each with a 15s timeout.
|
|
42
|
+
// A preflight slow enough to be worth skipping is a preflight nobody runs, which
|
|
43
|
+
// would defeat the entire point of this task.
|
|
44
|
+
let _ghAuthed = null;
|
|
45
|
+
function ghAuthed({ fresh = false } = {}) {
|
|
46
|
+
if (!fresh && _ghAuthed !== null) return _ghAuthed;
|
|
47
|
+
_ghAuthed = ghAuthedUncached();
|
|
48
|
+
return _ghAuthed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ghAuthedUncached() {
|
|
52
|
+
try {
|
|
53
|
+
const a = spawnSync('gh', ['auth', 'status'], {
|
|
54
|
+
stdio: 'ignore', shell: process.platform === 'win32', windowsHide: true, timeout: 15000,
|
|
55
|
+
});
|
|
56
|
+
return !!a && !a.error && a.status === 0;
|
|
57
|
+
} catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function binaryPresent(name) {
|
|
61
|
+
try {
|
|
62
|
+
const probe = process.platform === 'win32' ? ['where', [name]] : ['which', [name]];
|
|
63
|
+
const r = spawnSync(probe[0], probe[1], { stdio: 'ignore', shell: process.platform === 'win32', timeout: 10000 });
|
|
64
|
+
return !!r && !r.error && r.status === 0;
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The registry. `probe` returns true | false | null. `missing` is the sentence a
|
|
69
|
+
// builder reads when it comes back false — it must name the remedy, because the
|
|
70
|
+
// whole failure mode here was a reader who knew the rule and still had nowhere
|
|
71
|
+
// to go with it.
|
|
72
|
+
const CAPABILITIES = {
|
|
73
|
+
gh: {
|
|
74
|
+
what: 'an authenticated GitHub CLI',
|
|
75
|
+
probe: () => ghAuthed(),
|
|
76
|
+
missing: 'no authenticated `gh` on this machine. Run `gh auth login`, or use a machine that has it — '
|
|
77
|
+
+ 'a dev box normally does not, by design (it publishes through the server instead, ADR 0055).',
|
|
78
|
+
},
|
|
79
|
+
'main-checkout': {
|
|
80
|
+
what: 'the main checkout, not a linked worktree',
|
|
81
|
+
// Tri-state on purpose: resolveWorktreeRoot cannot always tell.
|
|
82
|
+
probe: () => resolveWorktreeRoot({ runGit, cwd: process.cwd() }).isMainCheckout,
|
|
83
|
+
missing: 'this is a linked worktree, not the main checkout. Procedures that mutate `main` must run in the '
|
|
84
|
+
+ 'main checkout — a worktree holding the branch has nowhere to merge INTO (task 1002572).',
|
|
85
|
+
},
|
|
86
|
+
'push-credential': {
|
|
87
|
+
// Mirrors ship.js pushVia(): the project's operative definition of "can push"
|
|
88
|
+
// is an authed gh (or an explicit token), and everything else routes through
|
|
89
|
+
// the server. Following that rather than inventing a stricter rule keeps one
|
|
90
|
+
// answer to the question instead of two that can disagree.
|
|
91
|
+
what: 'a credential that can push to origin',
|
|
92
|
+
probe: () => ghAuthed() || !!(process.env.GITHUB_TOKEN || process.env.GH_TOKEN),
|
|
93
|
+
missing: 'no push credential here. This machine lands work through the server-mediated publish '
|
|
94
|
+
+ '(`node scripts/gds/ship.js <id>` resumes it) — it cannot push `main` directly.',
|
|
95
|
+
},
|
|
96
|
+
'droplet-ssh': {
|
|
97
|
+
what: 'SSH access to the deploy host',
|
|
98
|
+
// false when there is no ssh at all; null when there is, because proving the
|
|
99
|
+
// key and host actually work would mean connecting.
|
|
100
|
+
probe: () => (binaryPresent('ssh') ? null : false),
|
|
101
|
+
missing: 'no `ssh` binary on this machine, so a step that reaches the deploy host cannot run. '
|
|
102
|
+
+ 'In `ci` deploy mode nothing should need it — check config/deploy.json before proceeding.',
|
|
103
|
+
},
|
|
104
|
+
tty: {
|
|
105
|
+
what: 'an interactive terminal',
|
|
106
|
+
probe: () => !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
107
|
+
missing: 'no TTY — this procedure prompts, and a prompt with nobody to answer it hangs. '
|
|
108
|
+
+ 'Use the non-interactive form (a --*-file flag or an explicit flag) instead.',
|
|
109
|
+
},
|
|
110
|
+
'node-modules': {
|
|
111
|
+
what: 'resolvable dependencies',
|
|
112
|
+
// DELEGATES to preflight.js rather than testing repoRoot/node_modules itself.
|
|
113
|
+
// Writing that test here is exactly the bug idea 1000788 reported against
|
|
114
|
+
// depsFinding: a worktree has no node_modules of its own, yet every require
|
|
115
|
+
// resolves fine because node walks up. Two copies of a probe are two chances
|
|
116
|
+
// to disagree, and this task exists because knowledge lived in two places.
|
|
117
|
+
probe: () => depsFinding().level === 'ok',
|
|
118
|
+
missing: 'dependencies are not resolvable from this tree — a step needing pg/express/pino will fail. '
|
|
119
|
+
+ 'Run `npm ci` in the repo root.',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ---- the declaration ------------------------------------------------------
|
|
124
|
+
// Read `requires:` out of a skill's YAML frontmatter. Accepts the inline form
|
|
125
|
+
// (`requires: [gh, main-checkout]`) and the dash-list form. Returns
|
|
126
|
+
// { requires, declared, error }: `declared` distinguishes "declared nothing"
|
|
127
|
+
// from "declared an empty list", which the fitness check needs to tell apart.
|
|
128
|
+
function parseRequires(md) {
|
|
129
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(String(md || ''));
|
|
130
|
+
if (!m) return { requires: [], declared: false, error: null };
|
|
131
|
+
const fm = m[1];
|
|
132
|
+
const inline = /^requires:[ \t]*\[(.*?)\][ \t]*$/m.exec(fm);
|
|
133
|
+
if (inline) {
|
|
134
|
+
const items = inline[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
135
|
+
return { requires: items, declared: true, error: null };
|
|
136
|
+
}
|
|
137
|
+
const block = /^requires:[ \t]*\n((?:[ \t]*-[ \t]*\S.*\n?)+)/m.exec(fm);
|
|
138
|
+
if (block) {
|
|
139
|
+
const items = block[1].split('\n')
|
|
140
|
+
.map((l) => /^[ \t]*-[ \t]*(.+?)[ \t]*$/.exec(l))
|
|
141
|
+
.filter(Boolean).map((x) => x[1].replace(/^['"]|['"]$/g, ''));
|
|
142
|
+
return { requires: items, declared: true, error: null };
|
|
143
|
+
}
|
|
144
|
+
if (/^requires:/m.test(fm)) {
|
|
145
|
+
// Present but unparseable — say so rather than reporting zero requirements,
|
|
146
|
+
// which would read as "declared, needs nothing" (the docs-entropy lesson: a
|
|
147
|
+
// scan that finds nothing must not be indistinguishable from a clean pass).
|
|
148
|
+
return { requires: [], declared: true, error: 'requires: is present but not a list ([a, b] or a dash list)' };
|
|
149
|
+
}
|
|
150
|
+
return { requires: [], declared: false, error: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- what a skill's fenced commands imply it needs -------------------------
|
|
154
|
+
// FENCED BLOCKS ONLY, and comments stripped. Prose that NAMES a command is
|
|
155
|
+
// discussion — dev/SKILL.md forbids `git push origin main`, and an earlier draft
|
|
156
|
+
// of this scan flagged it for saying so. A fenced line is an instruction to run.
|
|
157
|
+
const SIGNALS = [
|
|
158
|
+
[/(?:^|[\s|(])node\s+scripts\/gds\/push-main\.js\b/m, 'push-credential'],
|
|
159
|
+
[/(?:^|[\s|(])git\s+push\s+origin\s+main\b/m, 'push-credential'],
|
|
160
|
+
[/(?:^|[\s|(])node\s+scripts\/gds\/go-live\.js\b/m, 'push-credential'],
|
|
161
|
+
[/(?:^|[\s|(])ssh\s+(?:-o\s+\S+\s+)?\S+@/m, 'droplet-ssh'],
|
|
162
|
+
[/~\/deploy\.sh/m, 'droplet-ssh'],
|
|
163
|
+
[/(?:^|[\s|(])node\s+scripts\/gds\/merge-lock\.js\b/m, 'main-checkout'],
|
|
164
|
+
[/(?:^|[\s|(])bash\s+scripts\/gds\/smoke-write\.sh\b/m, 'main-checkout'],
|
|
165
|
+
[/(?:^|[\s|(])gh\s+(?:auth|pr|secret|api|run|repo|workflow)\b/m, 'gh'],
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
function fencedCode(md) {
|
|
169
|
+
const fences = String(md || '').match(/```[a-z]*\n[\s\S]*?```/g) || [];
|
|
170
|
+
return fences
|
|
171
|
+
.map((f) => f.replace(/^```[a-z]*\n/, '').replace(/```$/, ''))
|
|
172
|
+
.join('\n')
|
|
173
|
+
.split('\n')
|
|
174
|
+
.map((l) => l.replace(/(^|\s)#.*$/, '$1')) // a shell comment is discussion
|
|
175
|
+
.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function inferRequired(md) {
|
|
179
|
+
const code = fencedCode(md);
|
|
180
|
+
const out = new Set();
|
|
181
|
+
for (const [rx, cap] of SIGNALS) if (rx.test(code)) out.add(cap);
|
|
182
|
+
return [...out].sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- the check ------------------------------------------------------------
|
|
186
|
+
// Returns { ok, missing, unknown, checked }. `missing` is what came back FALSE —
|
|
187
|
+
// the only thing the CLI prints, because the task's own wording is that the
|
|
188
|
+
// preflight must name the MISSING capability, not recite the list.
|
|
189
|
+
function preflight(requires, caps = CAPABILITIES) {
|
|
190
|
+
const missing = [];
|
|
191
|
+
const unknown = [];
|
|
192
|
+
const checked = [];
|
|
193
|
+
for (const name of requires) {
|
|
194
|
+
const cap = caps[name];
|
|
195
|
+
if (!cap) {
|
|
196
|
+
missing.push({ name, why: `unknown capability "${name}" — not in the registry (typo in the skill's requires:?)` });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
let verdict;
|
|
200
|
+
try { verdict = cap.probe(); } catch (err) { verdict = null; }
|
|
201
|
+
checked.push(name);
|
|
202
|
+
if (verdict === false) missing.push({ name, why: cap.missing });
|
|
203
|
+
else if (verdict === null) unknown.push({ name, why: `could not confirm ${cap.what} — proceeding, but verify before a destructive step.` });
|
|
204
|
+
}
|
|
205
|
+
return { ok: missing.length === 0, missing, unknown, checked };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// A skill name is one path SEGMENT. Containment is checked rather than assumed:
|
|
209
|
+
// `skill-preflight.js ../../../etc` would otherwise read outside .claude/skills.
|
|
210
|
+
// Harmless today (a local, read-only CLI the caller could equally well `cat`), but
|
|
211
|
+
// this helper is exactly the kind that gets reused somewhere less trusting.
|
|
212
|
+
// Returns null for anything that is not a plain segment.
|
|
213
|
+
function skillPath(name) {
|
|
214
|
+
const n = String(name || '');
|
|
215
|
+
if (!n || n !== path.basename(n) || n === '.' || n === '..') return null;
|
|
216
|
+
const full = path.join(REPO_ROOT, '.claude', 'skills', n, 'SKILL.md');
|
|
217
|
+
const base = path.join(REPO_ROOT, '.claude', 'skills') + path.sep;
|
|
218
|
+
return full.startsWith(base) ? full : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function preflightSkill(name, caps = CAPABILITIES) {
|
|
222
|
+
const p = skillPath(name);
|
|
223
|
+
if (!p) {
|
|
224
|
+
return { ok: false, requires: [], missing: [{ name: 'skill', why: `"${name}" is not a skill name — it must be a single directory name under .claude/skills/.` }], unknown: [], checked: [] };
|
|
225
|
+
}
|
|
226
|
+
let md;
|
|
227
|
+
try { md = fs.readFileSync(p, 'utf8'); } catch {
|
|
228
|
+
return { ok: false, missing: [{ name: 'skill', why: `no skill named "${name}" at .claude/skills/${name}/SKILL.md` }], unknown: [], checked: [] };
|
|
229
|
+
}
|
|
230
|
+
const { requires, error } = parseRequires(md);
|
|
231
|
+
if (error) return { ok: false, missing: [{ name: 'requires', why: `${name}: ${error}` }], unknown: [], checked: [] };
|
|
232
|
+
return { ...preflight(requires, caps), requires };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- the assertion (fitness check 23) --------------------------------------
|
|
236
|
+
// A skill whose FENCED commands need a capability must DECLARE it. This is the
|
|
237
|
+
// part that stops the contract rotting: without it, a new ops step gets added to
|
|
238
|
+
// a skill years from now and the declaration silently falls behind the commands.
|
|
239
|
+
//
|
|
240
|
+
// It compares inferRequired() (what the fenced commands imply) against
|
|
241
|
+
// parseRequires() (what the frontmatter claims) and reds the build on a gap. It
|
|
242
|
+
// does NOT complain about a skill declaring MORE than its fences imply — a human
|
|
243
|
+
// knows things the scan cannot see, and punishing an over-declaration would train
|
|
244
|
+
// people to declare less.
|
|
245
|
+
const SKILLS_DIR = path.join(REPO_ROOT, '.claude', 'skills');
|
|
246
|
+
|
|
247
|
+
function checkOpsSkillsDeclareMachine({ dir = SKILLS_DIR, read = null, names: injected = null } = {}) {
|
|
248
|
+
const violations = [];
|
|
249
|
+
const warnings = [];
|
|
250
|
+
let names = injected;
|
|
251
|
+
if (!names) {
|
|
252
|
+
try {
|
|
253
|
+
names = fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
254
|
+
} catch { names = []; }
|
|
255
|
+
}
|
|
256
|
+
// A scan that enumerates nothing must fail, not pass (the docs-entropy lesson).
|
|
257
|
+
if (!names.length) {
|
|
258
|
+
return {
|
|
259
|
+
name: 'ops skills declare the machine they need (G03)',
|
|
260
|
+
ok: false, hardFail: true,
|
|
261
|
+
violations: ['scan defect — enumerated 0 skills under .claude/skills/.'],
|
|
262
|
+
warnings: [], note: 'static scan of skill frontmatter vs the commands each skill actually fences.',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const readOne = read || ((n) => {
|
|
266
|
+
try { return fs.readFileSync(path.join(dir, n, 'SKILL.md'), 'utf8'); } catch { return null; }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
let declaring = 0;
|
|
270
|
+
for (const n of names) {
|
|
271
|
+
const md = readOne(n);
|
|
272
|
+
if (md === null) continue;
|
|
273
|
+
const { requires, error } = parseRequires(md);
|
|
274
|
+
if (error) { violations.push(`${n}: ${error}`); continue; }
|
|
275
|
+
if (requires.length) declaring += 1;
|
|
276
|
+
for (const r of requires) {
|
|
277
|
+
if (!CAPABILITIES[r]) {
|
|
278
|
+
violations.push(`${n} declares an unknown capability "${r}" — it can never be probed, so the declaration is decorative. Registry: ${Object.keys(CAPABILITIES).join(', ')}.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// DECLARING is not ENFORCING. planning-session shipped a `requires:` with no
|
|
282
|
+
// preflight call, which is the same shape as the original bug: a contract
|
|
283
|
+
// stated where nothing checks it. If a skill names a machine, it must also
|
|
284
|
+
// run the check (the grader caught this one on the first pass).
|
|
285
|
+
if (requires.length && !/skill-preflight\.js\s+\S/.test(md)) {
|
|
286
|
+
violations.push(
|
|
287
|
+
`${n} declares requires: [${requires.join(', ')}] but never runs \`node scripts/gds/skill-preflight.js ${n}\`. `
|
|
288
|
+
+ 'A declaration nothing enforces is prose, and prose cannot refuse — the exact failure this check exists for.'
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const inferred = inferRequired(md);
|
|
292
|
+
const undeclared = inferred.filter((c) => !requires.includes(c));
|
|
293
|
+
if (undeclared.length) {
|
|
294
|
+
violations.push(
|
|
295
|
+
`${n} fences commands needing [${undeclared.join(', ')}] but does not declare them. `
|
|
296
|
+
+ 'Add them to `requires:` in the frontmatter so the preflight can refuse early — '
|
|
297
|
+
+ 'prose cannot refuse, which is how /merge-mode came to be run on a dev box (task 1003167).'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
name: 'ops skills declare the machine they need (G03)',
|
|
303
|
+
ok: violations.length === 0,
|
|
304
|
+
hardFail: violations.length > 0,
|
|
305
|
+
violations,
|
|
306
|
+
warnings,
|
|
307
|
+
note: `${names.length} skill(s) scanned; ${declaring} declare a machine contract. A skill whose FENCED commands need gh / a main checkout / a push credential / the deploy host must name it in \`requires:\`, so \`node scripts/gds/skill-preflight.js <skill>\` can refuse before the procedure starts. Prose in a skill body cannot refuse.`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function main() {
|
|
312
|
+
const name = process.argv[2];
|
|
313
|
+
if (!name || name.startsWith('--')) {
|
|
314
|
+
console.error('usage: node scripts/gds/skill-preflight.js <skill-name>');
|
|
315
|
+
console.error(' exits 1 naming the capability this machine is MISSING for that skill.');
|
|
316
|
+
process.exit(2);
|
|
317
|
+
}
|
|
318
|
+
const r = preflightSkill(name);
|
|
319
|
+
for (const u of r.unknown) console.error(`? ${u.name}: ${u.why}`);
|
|
320
|
+
if (r.ok) {
|
|
321
|
+
console.log(`preflight OK — ${name}: ${r.checked.length ? r.checked.join(', ') : 'no machine requirements declared'}.`);
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
console.error(`STOP — /${name} cannot run on this machine.`);
|
|
325
|
+
for (const m of r.missing) console.error(`✗ ${m.name}: ${m.why}`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (require.main === module) main();
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
CAPABILITIES, SIGNALS,
|
|
333
|
+
parseRequires, inferRequired, fencedCode,
|
|
334
|
+
preflight, preflightSkill, skillPath, checkOpsSkillsDeclareMachine,
|
|
335
|
+
ghAuthed, binaryPresent,
|
|
336
|
+
};
|
package/src/module-api.js
CHANGED
|
@@ -1057,8 +1057,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1057
1057
|
// landed since 1.19.313 with no explicit bump. run 32668599111. (task 1002620)
|
|
1058
1058
|
// 1.19.315 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1059
1059
|
// landed since 1.19.314 with no explicit bump. run 32668969037. (task 1002620)
|
|
1060
|
+
// 1.19.316 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1061
|
+
// landed since 1.19.315 with no explicit bump. run 32669347711. (task 1002620)
|
|
1062
|
+
// 1.19.317 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1063
|
+
// landed since 1.19.316 with no explicit bump. run 32669571610. (task 1002620)
|
|
1060
1064
|
// ---------------------------------------------------------------------------
|
|
1061
|
-
const CORE_VERSION = '1.19.
|
|
1065
|
+
const CORE_VERSION = '1.19.317'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1062
1066
|
|
|
1063
1067
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1064
1068
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { strict as assert } from 'node:assert';
|
|
35
35
|
import { createRequire } from 'node:module';
|
|
36
36
|
import { readFileSync } from 'node:fs';
|
|
37
|
-
import { makeRunner } from './helpers.mjs';
|
|
37
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
38
38
|
|
|
39
39
|
process.env.NODE_ENV = 'test';
|
|
40
40
|
const require = createRequire(import.meta.url);
|
|
@@ -113,7 +113,8 @@ await test('EXEC: every write goes through the caller-supplied exec (so a ship t
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
await test('EXEC: shipTask passes its transaction CLIENT, and the reconciler passes the pool', () => {
|
|
116
|
-
|
|
116
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
117
|
+
const db = lifecycleDbSource();
|
|
117
118
|
// db.js is over the fitness oversize threshold, so the hook is ONE line there
|
|
118
119
|
// and its body lives in done-when.closeCompletedWorkForShip. What must hold is
|
|
119
120
|
// that the thing handed in is the txn client, not a pool.
|
|
@@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert';
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
|
-
import { makeRunner } from './helpers.mjs';
|
|
16
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
17
17
|
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const repoRoot = join(__dirname, '..');
|
|
@@ -22,7 +22,8 @@ const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
|
22
22
|
const mig = read('migrations', '163_polymorphic_dependencies.sql');
|
|
23
23
|
const migN = mig.replace(/\s+/g, ' ');
|
|
24
24
|
// the task-dependency CRUD + the claim-gate slice carved to the lifecycle module at BV1.R86.
|
|
25
|
-
|
|
25
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
26
|
+
const dbSrc = lifecycleDbSource();
|
|
26
27
|
|
|
27
28
|
const { test, summary } = makeRunner();
|
|
28
29
|
|
package/tests/fitness.mjs
CHANGED
|
@@ -542,6 +542,49 @@ test('identity gate skips comments + instance-injected tokens (no false positive
|
|
|
542
542
|
assert.equal(scanPromptIdentity('x.html', '<title>{{worldName}} — the gate</title>').length, 0, 'a {{worldName}} token carries no literal');
|
|
543
543
|
});
|
|
544
544
|
|
|
545
|
+
// task 1002756 — the two holes that let migrate.sh's founder DB default survive:
|
|
546
|
+
// (1) the stripper could be OPENED by a `/*` inside a `//` line comment (a real
|
|
547
|
+
// `*/` hundreds of lines later then swallowed everything between — provision.js
|
|
548
|
+
// lost ~330 lines from the scanned text), and (2) whole file classes were never
|
|
549
|
+
// scanned. These lock the stripper; the root widening is pinned by the note test.
|
|
550
|
+
test('identity gate: a /* inside a line comment cannot open a phantom block (task 1002756)', () => {
|
|
551
|
+
const { scanPromptIdentity } = fitness;
|
|
552
|
+
const src = [
|
|
553
|
+
'// glob note: import <dir>/*.caddy', // the phantom opener shape
|
|
554
|
+
'const leak = "amazonprimea.example";', // must STILL be seen
|
|
555
|
+
'doThing();',
|
|
556
|
+
'/* a real block */ const ok = 1;',
|
|
557
|
+
].join('\n');
|
|
558
|
+
assert.equal(scanPromptIdentity('x.js', src).length, 1, 'the literal after a /*-bearing line comment must be caught');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('identity gate: a // inside a block comment does not truncate the block', () => {
|
|
562
|
+
const { scanPromptIdentity } = fitness;
|
|
563
|
+
const src = [
|
|
564
|
+
'/* rationale: see https://amazonprimea.com — historical // note',
|
|
565
|
+
'still inside the block, amazonprimea again',
|
|
566
|
+
'*/',
|
|
567
|
+
'const clean = 1;',
|
|
568
|
+
].join('\n');
|
|
569
|
+
assert.equal(scanPromptIdentity('x.js', src).length, 0, 'comment prose naming the brand must not trip the scan');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('identity gate: comment markers inside strings and regex literals are inert', () => {
|
|
573
|
+
const { scanPromptIdentity } = fitness;
|
|
574
|
+
// A string containing /* must not open a comment that hides a later literal…
|
|
575
|
+
assert.equal(scanPromptIdentity('x.js', 'const g = "src/*.js";\nconst leak = "amazonprimea";').length, 1);
|
|
576
|
+
// …a regex containing the brand is code and IS caught (the detectors carve out their own hits)…
|
|
577
|
+
assert.equal(scanPromptIdentity('x.js', 'const re = /amazonprimea/i;').length, 1);
|
|
578
|
+
// …and a division sign is not mistaken for a regex opener that swallows code.
|
|
579
|
+
assert.equal(scanPromptIdentity('x.js', 'const x = a / b / c;\nconst leak = "amazonprimea";').length, 1);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('identity gate scans the WHOLE scripts/ root, not just scripts/gds (task 1002756)', () => {
|
|
583
|
+
const r = fitness.checkKernelPromptsNoHardcodedIdentity();
|
|
584
|
+
assert.match(r.note, /scripts\/\*\* \(whole root/, 'note must document the widened root');
|
|
585
|
+
assert.equal(r.hardFail, false, 'live tree must be clean under the widened scan:\n' + r.violations.join('\n'));
|
|
586
|
+
});
|
|
587
|
+
|
|
545
588
|
// --- write-route validation build check (BV1.R12 / task 1999, ADR 0118) ---
|
|
546
589
|
|
|
547
590
|
test('write-route validation check: the live tree is clean (R11 sweep reached 100%)', () => {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { strict as assert } from 'node:assert';
|
|
47
47
|
import { createRequire } from 'node:module';
|
|
48
48
|
import { readFileSync } from 'node:fs';
|
|
49
|
-
import { makeRunner } from './helpers.mjs';
|
|
49
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
50
50
|
|
|
51
51
|
process.env.NODE_ENV = 'test';
|
|
52
52
|
|
|
@@ -201,7 +201,8 @@ await test('[Q6] non-member on a public PROTECTED-scope goal → action=apply (s
|
|
|
201
201
|
assert.equal(r.action, 'apply', 'a protected-scope goal is never offered as open-join');
|
|
202
202
|
});
|
|
203
203
|
await test('[Q7] source contract: BOTH INSERT INTO claims sites run the extracted gate (batch cannot bypass)', () => {
|
|
204
|
-
|
|
204
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
205
|
+
const dbSrc = lifecycleDbSource();
|
|
205
206
|
const insertSites = dbSrc.match(/INSERT INTO claims \(/g) || [];
|
|
206
207
|
assert.equal(insertSites.length, 2, 'exactly two claim-insert sites (claimTask + claimTasksBatch) — a third needs the gate too');
|
|
207
208
|
const gateCalls = dbSrc.match(/await goalMembershipFailures\(/g) || [];
|
package/tests/goal_advisory.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { strict as assert } from 'node:assert';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
19
|
import http from 'node:http';
|
|
20
|
-
import { makeRunner } from './helpers.mjs';
|
|
20
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
21
21
|
|
|
22
22
|
process.env.NODE_ENV = 'test';
|
|
23
23
|
|
|
@@ -25,7 +25,8 @@ const require = createRequire(import.meta.url);
|
|
|
25
25
|
const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
|
|
26
26
|
|
|
27
27
|
const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
28
|
-
|
|
28
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
29
|
+
const dbSrc = lifecycleDbSource();
|
|
29
30
|
const { test, summary } = makeRunner();
|
|
30
31
|
|
|
31
32
|
const V = 'BONGOS-V1';
|
|
@@ -37,6 +37,7 @@ const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
|
|
|
37
37
|
const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
|
|
38
38
|
const db = require(path.join(LIFECYCLE, 'db.js'));
|
|
39
39
|
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
40
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
40
41
|
|
|
41
42
|
const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
|
|
42
43
|
|
|
@@ -147,7 +148,8 @@ test('the permission predicate cannot see requires_rank at all (structural)', ()
|
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
test('every claim path in db.js still gates on requires_rank as its own check', () => {
|
|
150
|
-
|
|
151
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
152
|
+
const src = lifecycleDbSource();
|
|
151
153
|
// claimTask, claimTasksBatch, validateClaimBatch — three call sites, three
|
|
152
154
|
// separate rankAllowsTask conjuncts. If a refactor ever folds the floor into the
|
|
153
155
|
// permission predicate, this count drops and the test reds.
|
|
@@ -18,6 +18,7 @@ import { createRequire } from 'node:module';
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -189,8 +190,14 @@ test('the writer runs on the CALLER\'s transaction client when given one', () =>
|
|
|
189
190
|
});
|
|
190
191
|
|
|
191
192
|
test('both rank-change twins rotate the role inside their transaction', () => {
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
// task 1003204: the lifecycle twin moved out of db.js into db-rank-authz.js
|
|
194
|
+
// when the monolith was carved up. Read the whole lifecycle family so the
|
|
195
|
+
// assertion follows the code instead of a filename.
|
|
196
|
+
const twins = [
|
|
197
|
+
['src/bongos/db.js', fs.readFileSync(path.join(ROOT, 'src/bongos/db.js'), 'utf8')],
|
|
198
|
+
['modules/lifecycle/db-*.js', lifecycleDbSource()],
|
|
199
|
+
];
|
|
200
|
+
for (const [rel, src] of twins) {
|
|
194
201
|
const body = src.slice(src.indexOf('async function _applyRankChangeTx'));
|
|
195
202
|
const upd = body.indexOf('UPDATE builders SET rank');
|
|
196
203
|
const sync = body.indexOf('await syncGovernanceRankRole(client');
|
package/tests/helpers.mjs
CHANGED
|
@@ -123,3 +123,22 @@ export function requireHostModule(require, spec, note) {
|
|
|
123
123
|
throw err;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
// lifecycleDbSource — the LIFECYCLE data layer as one source string.
|
|
128
|
+
//
|
|
129
|
+
// task 1003204 (goal 1000079) split the 5,748-line modules/lifecycle/db.js into
|
|
130
|
+
// db.js (a pure re-export facade) + twelve cohesive db-*.js units. A pile of
|
|
131
|
+
// tests assert things about "the data layer" by regexing its source — that the
|
|
132
|
+
// claim paths gate on requires_rank, that a literal appears in exactly one
|
|
133
|
+
// place, that dependency CRUD points at the right table. Those assertions were
|
|
134
|
+
// always about the LAYER, not about one filename, so they read the whole family
|
|
135
|
+
// here. Reading the family (rather than repointing each test at whichever db-*
|
|
136
|
+
// unit happens to hold the code today) also means a future re-carve moves code
|
|
137
|
+
// between units without silently disarming the assertion.
|
|
138
|
+
export function lifecycleDbSource(modulesDir = path.join(ROOT, 'modules')) {
|
|
139
|
+
const dir = path.join(modulesDir, 'lifecycle');
|
|
140
|
+
const files = fs.readdirSync(dir)
|
|
141
|
+
.filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
|
|
142
|
+
.sort();
|
|
143
|
+
return files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
|
|
144
|
+
}
|
|
@@ -28,6 +28,7 @@ import { readFileSync } from 'node:fs';
|
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import { dirname, join } from 'node:path';
|
|
30
30
|
import { createRequire } from 'node:module';
|
|
31
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
31
32
|
|
|
32
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
34
|
const repoRoot = join(__dirname, '..');
|
|
@@ -49,7 +50,8 @@ async function test(name, fn) {
|
|
|
49
50
|
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
50
51
|
const migrationSql = read('migrations', '081_tasks_needs_migration.sql');
|
|
51
52
|
// the tasks db slice + the tasks route carved to modules/lifecycle/ at BV1.R86.
|
|
52
|
-
|
|
53
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
54
|
+
const dbSrc = lifecycleDbSource();
|
|
53
55
|
const routesSrc = read('modules', 'lifecycle', 'routes', 'tasks.js');
|
|
54
56
|
const captureSrc = read('scripts', 'gds', 'capture.js');
|
|
55
57
|
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const reconciler = require('../modules/lifecycle/publish-reconciler.js');
|
|
19
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
19
20
|
|
|
20
21
|
let passed = 0;
|
|
21
22
|
let failed = 0;
|
|
@@ -1647,7 +1648,8 @@ test('vector 2: shipped_at is stamped ONLY by shipTask, and shipTask requires co
|
|
|
1647
1648
|
const url = await import('node:url');
|
|
1648
1649
|
const path = await import('node:path');
|
|
1649
1650
|
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
1650
|
-
|
|
1651
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
1652
|
+
const dbSrc = lifecycleDbSource();
|
|
1651
1653
|
|
|
1652
1654
|
// Every statement that writes shipped_at, ignoring SELECT projections and comments.
|
|
1653
1655
|
const writes = [...dbSrc.matchAll(/shipped_at\s*=\s*now\(\)/g)];
|
package/tests/rank_gate.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const require = createRequire(import.meta.url);
|
|
|
26
26
|
const auth = require('../src/bongos/auth.js');
|
|
27
27
|
const db = require('../modules/lifecycle/db.js');
|
|
28
28
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
29
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
29
30
|
|
|
30
31
|
let passed = 0;
|
|
31
32
|
let failed = 0;
|
|
@@ -319,7 +320,8 @@ console.log('\nlistClaimableTasks SQL rank-tier CASE (regression: thetes drift):
|
|
|
319
320
|
|
|
320
321
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
321
322
|
// listClaimableTasks carved to the lifecycle module at BV1.R86 — read the SQL there.
|
|
322
|
-
|
|
323
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
324
|
+
const dbSrc = lifecycleDbSource();
|
|
323
325
|
|
|
324
326
|
t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value', () => {
|
|
325
327
|
// Isolate the listClaimableTasks function body so we don't match CASE
|
package/tests/reward_gate.mjs
CHANGED
|
@@ -25,6 +25,7 @@ const db = require('../modules/lifecycle/db.js');
|
|
|
25
25
|
// the SAME singleton object db.js captured, so mutating its .query stubs the INSERT
|
|
26
26
|
// createTask runs — no database needed.
|
|
27
27
|
const sharedPool = require('../src/module-api.js').pool;
|
|
28
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
28
29
|
|
|
29
30
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const root = join(__dir, '..');
|
|
@@ -132,7 +133,8 @@ t('null task is a no-op (no throw, no assignment)', () => {
|
|
|
132
133
|
console.log('\nenforcement points (source-wiring — AUTO-ASSIGN):');
|
|
133
134
|
|
|
134
135
|
const tasksRouteSrc = readFileSync(join(root, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
135
|
-
|
|
136
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
137
|
+
const dbSrc = lifecycleDbSource();
|
|
136
138
|
const migration178 = readFileSync(join(root, 'migrations/178_reward_gate_auto_assign_revert_autopromote.sql'), 'utf8');
|
|
137
139
|
|
|
138
140
|
t('promote route AUTO-ASSIGNS then proceeds (no 409 refusal for missing reward)', () => {
|