@bongos/core 1.19.817 → 1.19.819
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 +48 -28
- package/.claude/skills/strand-fix/SKILL.md +11 -2
- package/.gitattributes +9 -0
- package/docs/adr/0298-the-reader-lens-is-advisory-and-waits-on-a-voice-guide.md +121 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/gds-db-backup.md +46 -0
- package/modules/discord/bug-attachments.js +36 -1
- package/modules/lifecycle/cascade.js +18 -5
- package/modules/lifecycle/tests/cascade.mjs +11 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/bug-attachment-audit.js +143 -0
- package/scripts/gds/capture.js +19 -0
- package/scripts/gds/db-backup-nightly.sh +37 -1
- package/scripts/gds/linkify-refs.js +9 -1
- package/scripts/gds/run-unit-tests.js +8 -1
- package/scripts/gds/skill-preflight.js +28 -4
- package/src/module-api.js +1 -1
- package/tests/bug_attachment_audit.mjs +122 -0
- package/tests/cli_surface.mjs +31 -2
- package/tests/linkify_refs.mjs +17 -0
- package/tests/module_api_lazy.mjs +32 -1
- package/tests/routine_capture_kind.mjs +147 -0
- package/tests/skill_preflight.mjs +45 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// tests/routine_capture_kind.mjs — a routine that files its findings must name a
|
|
2
|
+
// kind the server accepts (task 1003694).
|
|
3
|
+
//
|
|
4
|
+
// THE FAILURE THIS PINS. .claude/scheduled-tasks/dep-audit-nightly/SKILL.md filed
|
|
5
|
+
// with `--kind security`. That is not one of the kinds — captureIdea throws BAD_KIND
|
|
6
|
+
// and the route 400s — so the capture step of that nightly routine had NEVER filed
|
|
7
|
+
// anything.
|
|
8
|
+
//
|
|
9
|
+
// It is quiet in the worst way. The routine still runs, still does the audit, and
|
|
10
|
+
// only the step that turns a finding into a durable row fails. There is no
|
|
11
|
+
// accumulating queue whose emptiness would look wrong, so nothing ever looked wrong.
|
|
12
|
+
// A scheduled routine cannot notice its own silence, which is exactly why this is a
|
|
13
|
+
// test and not a runtime check.
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/routine_capture_kind.mjs
|
|
16
|
+
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { VALID_KINDS } = require('../modules/ideas/inbox.js');
|
|
26
|
+
|
|
27
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
|
|
29
|
+
// Every SKILL.md that could carry a capture command: the scheduled routines and the
|
|
30
|
+
// skills. A routine is the dangerous case (nobody reads its output), but a skill with
|
|
31
|
+
// a bad kind wastes a builder's time the same way.
|
|
32
|
+
function skillFiles() {
|
|
33
|
+
const out = [];
|
|
34
|
+
const roots = [
|
|
35
|
+
path.join(REPO_ROOT, '.claude', 'scheduled-tasks'),
|
|
36
|
+
path.join(REPO_ROOT, '.claude', 'skills'),
|
|
37
|
+
];
|
|
38
|
+
for (const root of roots) {
|
|
39
|
+
if (!existsSync(root)) continue;
|
|
40
|
+
for (const d of readdirSync(root, { withFileTypes: true })) {
|
|
41
|
+
if (!d.isDirectory()) continue;
|
|
42
|
+
const p = path.join(root, d.name, 'SKILL.md');
|
|
43
|
+
if (existsSync(p)) out.push(p);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// `--kind foo` — a LITERAL kind, on a CAPTURE command only.
|
|
50
|
+
//
|
|
51
|
+
// Both narrowings are load-bearing, and I got each wrong first:
|
|
52
|
+
//
|
|
53
|
+
// - CAPTURE ONLY. `--kind` is not unique to capture.js. recall.js takes its own
|
|
54
|
+
// `--kind doc|db`, and a matcher that reads every `--kind` in the tree reports
|
|
55
|
+
// three recall invocations as broken. A guard whose first run is mostly noise
|
|
56
|
+
// is a guard people learn to suppress.
|
|
57
|
+
// - LITERAL ONLY. A placeholder (`--kind <bug|cleanup>`, `--kind cleanup|refactor`)
|
|
58
|
+
// documents a CHOICE rather than passing a value; overnight-code-review and
|
|
59
|
+
// repo-simplify-sweep both use that form deliberately.
|
|
60
|
+
const CAPTURE_LINE_RE = /capture\.js/;
|
|
61
|
+
const KIND_RE = /--kind\s+([a-z][a-z0-9-]*)(?=[\s`'"]|$)/g;
|
|
62
|
+
|
|
63
|
+
// A capture command can wrap across lines with a trailing backslash, so the unit of
|
|
64
|
+
// inspection is the command, not the line: a run of lines joined while the previous
|
|
65
|
+
// one ends in `\`.
|
|
66
|
+
function captureCommands(text) {
|
|
67
|
+
const lines = String(text || '').split('\n');
|
|
68
|
+
const out = [];
|
|
69
|
+
let buf = null;
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const continues = /\\\s*$/.test(line);
|
|
72
|
+
if (buf !== null) {
|
|
73
|
+
buf += ' ' + line;
|
|
74
|
+
if (!continues) { out.push(buf); buf = null; }
|
|
75
|
+
} else if (CAPTURE_LINE_RE.test(line)) {
|
|
76
|
+
if (continues) buf = line;
|
|
77
|
+
else out.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (buf !== null) out.push(buf);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function literalKinds(text) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const cmd of captureCommands(text)) {
|
|
87
|
+
for (const m of cmd.matchAll(KIND_RE)) {
|
|
88
|
+
const after = cmd.slice(m.index + m[0].length, m.index + m[0].length + 1);
|
|
89
|
+
if (after === '|') continue; // `--kind cleanup|refactor` — a menu, not a value
|
|
90
|
+
out.push(m[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('every literal --kind in a routine or skill is a kind the server accepts', () => {
|
|
97
|
+
const files = skillFiles();
|
|
98
|
+
// The scan must not pass by finding nothing: these files exist, and at least one of
|
|
99
|
+
// them documents a capture command.
|
|
100
|
+
assert.ok(files.length > 0, 'found no SKILL.md files at all — the scan is broken, not clean');
|
|
101
|
+
|
|
102
|
+
const bad = [];
|
|
103
|
+
let seen = 0;
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
for (const kind of literalKinds(readFileSync(f, 'utf8'))) {
|
|
106
|
+
seen += 1;
|
|
107
|
+
if (!VALID_KINDS.includes(kind)) bad.push(`${path.relative(REPO_ROOT, f)}: --kind ${kind}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
assert.ok(seen > 0, 'no literal --kind found anywhere — the pattern has drifted, and this test would pass an empty repo');
|
|
111
|
+
assert.deepEqual(bad, [],
|
|
112
|
+
`these file a kind the server rejects with a 400 — for a SCHEDULED routine that means its findings are silently never filed:\n ${bad.join('\n ')}\n`
|
|
113
|
+
+ `Valid kinds: ${VALID_KINDS.join(', ')}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('the matcher reads a literal kind on a capture command', () => {
|
|
117
|
+
assert.deepEqual(literalKinds('node scripts/gds/capture.js "t" --kind infra --body-file x'), ['infra']);
|
|
118
|
+
assert.deepEqual(literalKinds('node scripts/gds/capture.js "t" --kind security'), ['security'],
|
|
119
|
+
'the real bug is still caught');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a DIFFERENT tool\'s --kind is not this guard\'s business', () => {
|
|
123
|
+
// recall.js takes --kind doc|db. Reading every --kind in the tree reported three
|
|
124
|
+
// recall invocations as broken on the first run of this guard.
|
|
125
|
+
assert.deepEqual(literalKinds('node scripts/gds/recall.js nightly backup --kind doc'), []);
|
|
126
|
+
assert.deepEqual(literalKinds('add `--kind doc` for ADRs/recipes'), []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('a documented menu is not a value', () => {
|
|
130
|
+
assert.deepEqual(literalKinds('node scripts/gds/capture.js --kind <bug|cleanup|refactor>'), []);
|
|
131
|
+
assert.deepEqual(literalKinds('node scripts/gds/capture.js --kind cleanup|refactor \\'), []);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('a capture command wrapped over lines is still read', () => {
|
|
135
|
+
const text = ['node scripts/gds/capture.js "t" \\', ' --kind security \\', ' --body-file /tmp/x'].join('\n');
|
|
136
|
+
assert.deepEqual(literalKinds(text), ['security'],
|
|
137
|
+
'dep-audit-nightly writes exactly this shape — a line-at-a-time matcher misses it');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('dep-audit-nightly specifically now files with an accepted kind (task 1003694)', () => {
|
|
141
|
+
const p = path.join(REPO_ROOT, '.claude', 'scheduled-tasks', 'dep-audit-nightly', 'SKILL.md');
|
|
142
|
+
const src = readFileSync(p, 'utf8');
|
|
143
|
+
assert.ok(!/--kind\s+security\b/.test(src), 'security is not a kind and never was');
|
|
144
|
+
const kinds = literalKinds(src);
|
|
145
|
+
assert.ok(kinds.length > 0, 'the routine must still file something');
|
|
146
|
+
for (const k of kinds) assert.ok(VALID_KINDS.includes(k), `${k} is not a valid kind`);
|
|
147
|
+
});
|
|
@@ -140,6 +140,51 @@ test('the live tree passes the declare-check', () => {
|
|
|
140
140
|
assert.equal(r.hardFail, false, `unexpected:\n ${r.violations.join('\n ')}`);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
// ---- CRLF (task 1003754) --------------------------------------------------
|
|
144
|
+
//
|
|
145
|
+
// Every regex in skill-preflight is anchored on \n, starting with the frontmatter
|
|
146
|
+
// fence /^---\n/. SKILL.md files are CRLF in a Windows checkout — .gitattributes
|
|
147
|
+
// pins *.js, *.json, *.sh and *.sql, but Markdown falls through `* text=auto` — so
|
|
148
|
+
// the frontmatter never matched and parseRequires reported `declared: false` for
|
|
149
|
+
// EVERY skill.
|
|
150
|
+
//
|
|
151
|
+
// The visible symptom was one red test below. The real damage was quieter: fitness
|
|
152
|
+
// check G03 compares a skill's fenced commands against its declaration, and with
|
|
153
|
+
// nothing parsed as declared it had nothing to compare — '0 declare a machine
|
|
154
|
+
// contract'. The guard against a skill's ops commands outrunning its declaration was
|
|
155
|
+
// INERT on Windows, which is a false green, not a false red.
|
|
156
|
+
|
|
157
|
+
test('parseRequires reads a CRLF frontmatter identically to an LF one (task 1003754)', () => {
|
|
158
|
+
const lf = '---\nname: x\nrequires: [gh, main-checkout]\n---\nbody\n';
|
|
159
|
+
const crlf = lf.replace(/\n/g, '\r\n');
|
|
160
|
+
assert.deepEqual(sp.parseRequires(crlf), sp.parseRequires(lf),
|
|
161
|
+
'a checkout\'s line endings must not change what a skill is understood to require');
|
|
162
|
+
assert.deepEqual(sp.parseRequires(crlf).requires, ['gh', 'main-checkout']);
|
|
163
|
+
assert.equal(sp.parseRequires(crlf).declared, true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('the dash-list form survives CRLF too', () => {
|
|
167
|
+
const crlf = '---\r\nname: x\r\nrequires:\r\n - gh\r\n - droplet-ssh\r\n---\r\nbody\r\n';
|
|
168
|
+
assert.deepEqual(sp.parseRequires(crlf).requires, ['gh', 'droplet-ssh']);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('inferRequired reads CRLF fenced commands (task 1003754)', () => {
|
|
172
|
+
// The half that would silently stop CATCHING an undeclared ops command.
|
|
173
|
+
const crlf = '---\r\nname: x\r\n---\r\n```bash\r\ngit push origin main\r\n```\r\n';
|
|
174
|
+
assert.ok(sp.inferRequired(crlf).includes('push-credential'),
|
|
175
|
+
'a main-push in a CRLF skill must still imply a push credential, or G03 cannot catch it');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the live corpus is NOT empty — G03 has something to compare (task 1003754)', () => {
|
|
179
|
+
// The check\'s own note read "0 declare a machine contract" on Windows. A guard
|
|
180
|
+
// with an empty corpus passes whatever the skills do.
|
|
181
|
+
const r = sp.checkOpsSkillsDeclareMachine();
|
|
182
|
+
const n = Number(/(\d+) declare a machine contract/.exec(r.note || '')?.[1] ?? 0);
|
|
183
|
+
assert.ok(n > 0,
|
|
184
|
+
`no skill parsed as declaring a machine contract, so check G03 has nothing to compare and `
|
|
185
|
+
+ `cannot fail. Note was: ${r.note}`);
|
|
186
|
+
});
|
|
187
|
+
|
|
143
188
|
test('merge-mode really declares the machine it needs', () => {
|
|
144
189
|
// The skill at the centre of the incident.
|
|
145
190
|
const r = sp.preflightSkill('merge-mode', caps);
|