@bongos/core 1.20.41 → 1.20.43

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.
Files changed (59) hide show
  1. package/.bongos-core.json +94 -64
  2. package/.claude/skills/backlog-review/SKILL.md +1 -0
  3. package/.claude/skills/blocker-review/SKILL.md +1 -1
  4. package/.claude/skills/bug-triage/SKILL.md +1 -0
  5. package/.claude/skills/builder-backup/SKILL.md +1 -0
  6. package/.claude/skills/builder-claim/SKILL.md +1 -1
  7. package/.claude/skills/builder-cost/SKILL.md +1 -0
  8. package/.claude/skills/builder-exit/SKILL.md +1 -0
  9. package/.claude/skills/builder-key/SKILL.md +1 -1
  10. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  11. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  12. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  13. package/.claude/skills/builder-setup/SKILL.md +1 -1
  14. package/.claude/skills/collab-review/SKILL.md +1 -1
  15. package/.claude/skills/design/SKILL.md +1 -1
  16. package/.claude/skills/design-sync/SKILL.md +1 -0
  17. package/.claude/skills/feedback/SKILL.md +1 -1
  18. package/.claude/skills/figma-design-sync/SKILL.md +1 -0
  19. package/.claude/skills/goal-close/SKILL.md +184 -0
  20. package/.claude/skills/goal-create/SKILL.md +1 -1
  21. package/.claude/skills/goal-review/SKILL.md +5 -102
  22. package/.claude/skills/goal-uat/SKILL.md +5 -81
  23. package/.claude/skills/grade-audit/SKILL.md +6 -85
  24. package/.claude/skills/grade-recover/SKILL.md +1 -1
  25. package/.claude/skills/grade-sweep/SKILL.md +174 -0
  26. package/.claude/skills/grader-health/SKILL.md +5 -72
  27. package/.claude/skills/idea-triage/SKILL.md +1 -0
  28. package/.claude/skills/merge-mode/SKILL.md +1 -1
  29. package/.claude/skills/new-project/SKILL.md +1 -0
  30. package/.claude/skills/owner-review/SKILL.md +40 -0
  31. package/.claude/skills/planning-session/SKILL.md +1 -0
  32. package/.claude/skills/priority-session/SKILL.md +1 -0
  33. package/.claude/skills/read-session-export/SKILL.md +1 -1
  34. package/.claude/skills/recall/SKILL.md +1 -1
  35. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  36. package/.claude/skills/session-handoff/SKILL.md +1 -1
  37. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  38. package/docs/architecture.md +1 -1
  39. package/docs/file-map.md +12 -10
  40. package/docs/module-api-changelog.md +4 -0
  41. package/docs/onboarding/slash-commands.md +17 -11
  42. package/docs/packs/artist.md +1 -1
  43. package/docs/packs/engineer.md +4 -5
  44. package/modules/copy-desk/module.json +1 -1
  45. package/{.claude → modules/copy-desk}/skills/tweak/SKILL.md +1 -0
  46. package/modules/lifecycle/dependency-advisory.js +2 -2
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +12 -0
  50. package/scripts/gds/fitness-ratchets.js +2 -0
  51. package/scripts/gds/module-artifact.js +1 -1
  52. package/scripts/gds/module-assess-security.js +251 -0
  53. package/scripts/gds/skill-lint.js +32 -9
  54. package/src/module-api.js +1 -1
  55. package/tests/module_assess_security.mjs +216 -0
  56. package/tests/skill_grade_audit.mjs +34 -17
  57. package/tests/skill_grader_health.mjs +28 -10
  58. package/tests/skill_lint.mjs +37 -0
  59. package/tests/skill_menu_ruling.mjs +99 -0
@@ -0,0 +1,216 @@
1
+ // tests/module_assess_security.mjs — the Security assessment gate (task 1003792, ADR 0343 D2).
2
+ // No database and no network: the dependency audit is injected (and auditDependencies
3
+ // is driven with a fake npm), the store row goes to a fake pg client, and the tarball
4
+ // is a REAL packed + verified one. Pinned:
5
+ // - the outcome is only ever passed / failed / not_scored — never a number (D2 gate)
6
+ // - denied files, a non-registry dependency source and a high/critical advisory fail
7
+ // - an audit that cannot run is not_scored, never a pass (a gate that cannot run stays shut)
8
+ // - floating ranges and the maintenance posture are reported, never failed on
9
+ //
10
+ // Run: node tests/module_assess_security.mjs
11
+
12
+ import { test } from 'node:test';
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const { assessSecurity, auditDependencies, recordSecuritySignal, insertSecuritySignal, maintenancePosture, registrySpec } = require('../scripts/gds/module-assess-security.js');
21
+ const { packModule } = require('../scripts/gds/module-artifact.js');
22
+ const { relativeArtifactPath } = require('../src/bongos/module-store.js');
23
+
24
+ const file = (p, text = '') => ({ path: p, buf: Buffer.from(text) });
25
+ const MJ = (over = {}) => ({ key: 'weather', title: 'Weather', description: 'x', version: '1.2.0', coreVersion: '^1.0.0', contributes: {}, ...over });
26
+ const clean = async () => ({ ok: true, findings: [] });
27
+ const noAudit = async () => { throw new Error('the audit must not run for a module with no dependencies'); };
28
+
29
+ test('no dependencies and no denied files passes, with no number and no audit', async () => {
30
+ const s = await assessSecurity('weather', [file('module.json'), file('index.js')], MJ(), { audit: noAudit });
31
+ assert.equal(s.outcome, 'passed');
32
+ assert.equal(s.score, null, 'the gate never carries a score (core_265: security is pass/fail only)');
33
+ assert.equal(s.sample_size, 0);
34
+ });
35
+
36
+ test('a credential-named file fails the gate on the publish denylist', async () => {
37
+ const s = await assessSecurity('weather', [file('module.json'), file('.env', 'X=1')], MJ(), { audit: noAudit });
38
+ assert.equal(s.outcome, 'failed');
39
+ assert.deepEqual(s.detail.failures.map((f) => f.check), ['denylist']);
40
+ assert.deepEqual(s.detail.failures[0].files, ['modules/weather/.env']);
41
+ });
42
+
43
+ test('a dependency fetched from outside the registry fails, and is never handed to npm', async () => {
44
+ const specs = ['git+https://example.invalid/x.git', 'https://example.invalid/x.tgz', 'github:someone/thing', 'someone/thing',
45
+ 'npm:other@1.0.0', 'file:../../../../etc', 'link:../x', 'workspace:*', '1.0.0 || file:../x', 'git@github.com:a/b', 'rm -rf /', 'a#b'];
46
+ for (const spec of specs) {
47
+ const s = await assessSecurity('weather', [file('module.json')],
48
+ MJ({ dependencies: { lodash: '^4.17.21', thing: spec } }), { audit: noAudit });
49
+ assert.equal(s.outcome, 'failed', spec);
50
+ assert.equal(s.detail.failures[0].check, 'dependency_source', spec);
51
+ assert.deepEqual(s.detail.failures[0].dependencies.map((d) => d.package), ['thing'], spec);
52
+ }
53
+ const badName = await assessSecurity('weather', [file('module.json')], MJ({ dependencies: { '../evil': '1.0.0' } }), { audit: noAudit });
54
+ assert.equal(badName.outcome, 'failed');
55
+ });
56
+
57
+ test('registry specs: pinned, ranges and wildcards are allowed and told apart', () => {
58
+ const cases = [['1.2.3', 'pinned'], ['1.2.3-beta.1', 'pinned'], ['^14.26.4', 'floating range'], ['~1.2', 'floating range'],
59
+ ['>=1.0.0 <2.0.0', 'floating range'], ['1.x', 'floating range'], ['1.0.0 - 2.0.0', 'floating range'], ['^1 || ^2', 'floating range'],
60
+ ['*', 'wildcard'], ['latest', 'wildcard'], ['', 'wildcard']];
61
+ for (const [spec, kind] of cases) assert.deepEqual(registrySpec('lodash', spec), { ok: true, kind }, spec);
62
+ assert.equal(registrySpec('@scope/pkg', '^1.0.0').ok, true);
63
+ });
64
+
65
+ test('a wildcard is noted separately from a floating range', async () => {
66
+ const s = await assessSecurity('weather', [file('module.json')], MJ({ dependencies: { a: '*', b: '^1.0.0', c: '1.0.0' } }), { audit: clean });
67
+ assert.equal(s.outcome, 'passed');
68
+ assert.deepEqual(s.detail.notes.filter((n) => n.check.startsWith('dependency')).map((n) => [n.check, n.package]),
69
+ [['dependency_wildcard', 'a'], ['dependency_range', 'b']]);
70
+ });
71
+
72
+ test('a floating range is only noted', async () => {
73
+ const ok = await assessSecurity('weather', [file('module.json')], MJ({ dependencies: { lodash: '^4.17.21' } }), { audit: clean });
74
+ assert.equal(ok.outcome, 'passed');
75
+ assert.equal(ok.sample_size, 1);
76
+ assert.equal(ok.detail.notes.find((n) => n.check === 'dependency_range').package, 'lodash');
77
+ });
78
+
79
+ test('a high or critical advisory in a dependency fails the gate and names it', async () => {
80
+ const audit = async (deps) => {
81
+ assert.deepEqual(deps, { lodash: '^4.0.0' }, 'the audit sees exactly the declared dependencies');
82
+ return { ok: true, findings: [{ package: 'lodash', severity: 'critical', id: 'GHSA-x', summary: 'Prototype pollution', fix: 'lodash@4.17.21' }] };
83
+ };
84
+ const s = await assessSecurity('weather', [file('module.json')], MJ({ dependencies: { lodash: '^4.0.0' } }), { audit });
85
+ assert.equal(s.outcome, 'failed');
86
+ const v = s.detail.failures.find((f) => f.check === 'vulnerable_dependency');
87
+ assert.equal(v.advisories[0].id, 'GHSA-x');
88
+ });
89
+
90
+ test('an audit that cannot run is not_scored — never a pass', async () => {
91
+ const s = await assessSecurity('weather', [file('module.json')], MJ({ dependencies: { lodash: '^4.0.0' } }),
92
+ { audit: async () => ({ ok: false, reason: 'offline' }) });
93
+ assert.equal(s.outcome, 'not_scored');
94
+ assert.match(s.reason, /could not run/);
95
+ });
96
+
97
+ test('the maintenance posture is reported, never failed on', async () => {
98
+ assert.equal(maintenancePosture(MJ()), 'undeclared');
99
+ assert.equal(maintenancePosture(MJ({ maintenance: { status: 'orphaned' } })), 'orphaned');
100
+ for (const status of [undefined, 'deprecated', 'orphaned']) {
101
+ const mj = status ? MJ({ maintenance: { status, note: 'n' } }) : MJ();
102
+ const s = await assessSecurity('weather', [file('module.json')], mj, { audit: noAudit });
103
+ assert.equal(s.outcome, 'passed', String(status));
104
+ assert.equal(s.detail.notes.find((n) => n.check === 'maintenance').status, status || 'undeclared');
105
+ }
106
+ const kept = await assessSecurity('weather', [file('module.json')], MJ({ maintainer: 'a', maintenance: { status: 'maintained' } }), { audit: noAudit });
107
+ assert.equal(kept.detail.notes.length, 0, 'a maintained module has nothing to note');
108
+ });
109
+
110
+ // ---- auditDependencies: npm driven through a fake exec ----
111
+
112
+ function fakeNpm({ lockOk = true, auditOut }) {
113
+ const calls = [];
114
+ const exec = (cmd, argv, opts, cb) => {
115
+ const args = cmd === 'cmd.exe' ? argv.slice(argv.indexOf('npm') + 1) : argv;
116
+ calls.push({ args, pkg: JSON.parse(fs.readFileSync(path.join(opts.cwd, 'package.json'), 'utf8')) });
117
+ if (args[0] === 'install') {
118
+ if (lockOk) fs.writeFileSync(path.join(opts.cwd, 'package-lock.json'), '{}');
119
+ return cb(lockOk ? null : new Error('E404'), '', '');
120
+ }
121
+ return cb(new Error('exit 1'), auditOut, '');
122
+ };
123
+ return { calls, exec };
124
+ }
125
+
126
+ test('the audit resolves metadata only (no install scripts) and reads npm audit\'s JSON', async () => {
127
+ const auditOut = JSON.stringify({ vulnerabilities: { lodash: { severity: 'high', via: [{ source: 1, title: 'Bad', severity: 'high', url: 'u' }], fixAvailable: true }, tiny: { severity: 'low', via: [] } } });
128
+ const npm = fakeNpm({ auditOut });
129
+ const a = await auditDependencies({ lodash: '^4.0.0' }, { exec: npm.exec });
130
+ assert.equal(a.ok, true);
131
+ assert.deepEqual(a.findings.map((f) => f.package), ['lodash'], 'only high and critical count');
132
+ const install = npm.calls[0].args;
133
+ assert.equal(install[0], 'install');
134
+ assert.ok(install.includes('--package-lock-only') && install.includes('--ignore-scripts'), 'no package code is downloaded or run');
135
+ assert.deepEqual(npm.calls[0].pkg.dependencies, { lodash: '^4.0.0' });
136
+ });
137
+
138
+ test('an unresolvable dependency or an unreadable audit is a failed audit run, not a clean one', async () => {
139
+ assert.equal((await auditDependencies({ nope: '^1.0.0' }, { exec: fakeNpm({ lockOk: false }).exec })).ok, false);
140
+ assert.equal((await auditDependencies({ a: '^1.0.0' }, { exec: fakeNpm({ auditOut: 'not json' }).exec })).ok, false);
141
+ assert.equal((await auditDependencies({ a: '^1.0.0' }, { exec: fakeNpm({ auditOut: JSON.stringify({ error: { code: 'ENOAUDIT' } }) }).exec })).ok, false);
142
+ });
143
+
144
+ // ---- recordSecuritySignal: tarball in, one appended row out ----
145
+
146
+ function fakeDb(versionRow) {
147
+ const inserts = [];
148
+ return {
149
+ inserts,
150
+ async query(sql, params) {
151
+ if (/FROM store_module_versions/.test(sql)) return { rows: versionRow ? [versionRow] : [] };
152
+ if (/INSERT INTO module_assessment_signals/.test(sql)) {
153
+ assert.match(sql, /'security'/);
154
+ assert.doesNotMatch(sql, /UPDATE/i);
155
+ inserts.push(params);
156
+ return { rows: [{ id: 3, version_id: params[0], part: 'security', outcome: params[1] }] };
157
+ }
158
+ throw new Error(`unexpected query: ${sql}`);
159
+ },
160
+ };
161
+ }
162
+
163
+ function published(mj = MJ()) {
164
+ const src = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-sec-src-'));
165
+ fs.mkdirSync(path.join(src, 'weather'), { recursive: true });
166
+ fs.writeFileSync(path.join(src, 'weather', 'module.json'), JSON.stringify(mj));
167
+ fs.writeFileSync(path.join(src, 'weather', 'index.js'), 'module.exports = {};\n');
168
+ const { tgz } = packModule('weather', { modulesDir: src, now: () => new Date('2026-09-30T00:00:00Z'), modeOf: () => '644' });
169
+ const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-sec-store-'));
170
+ const final = path.join(storeDir, 'weather', 'weather-1.2.0.tgz');
171
+ fs.mkdirSync(path.dirname(final), { recursive: true });
172
+ fs.writeFileSync(final, tgz);
173
+ return { storeDir, final, row: { id: 42, module_key: 'weather', version: '1.2.0', artifact_path: relativeArtifactPath(final, { dir: storeDir }) } };
174
+ }
175
+
176
+ test('a published version is checked from its tarball and one security row is appended', async () => {
177
+ const { storeDir, row } = published(MJ({ dependencies: { lodash: '^4.17.21' } }));
178
+ const db = fakeDb(row);
179
+ let audited = null;
180
+ const res = await recordSecuritySignal(42, { db, storeDir, audit: async (d) => { audited = d; return { ok: true, findings: [] }; } });
181
+ assert.equal(res.ok, true);
182
+ assert.deepEqual(audited, { lodash: '^4.17.21' }, 'the dependencies come from the published module.json');
183
+ const [versionId, outcome, score, sample] = db.inserts[0];
184
+ assert.deepEqual([versionId, outcome, score, sample], [42, 'passed', null, 1]);
185
+ });
186
+
187
+ test('a missing or tampered tarball is not_scored, and an unknown version writes nothing', async () => {
188
+ const { storeDir, final, row } = published();
189
+ fs.writeFileSync(final, 'junk');
190
+ const db = fakeDb(row);
191
+ await recordSecuritySignal(42, { db, storeDir, audit: noAudit });
192
+ assert.equal(db.inserts[0][1], 'not_scored');
193
+ assert.doesNotMatch(db.inserts[0][4], /[\\/]/, 'no local path in the stored reason');
194
+
195
+ const none = fakeDb(null);
196
+ const res = await recordSecuritySignal(9, { db: none, audit: noAudit });
197
+ assert.equal(res.code, 'version_not_found');
198
+ assert.equal(none.inserts.length, 0);
199
+ });
200
+
201
+ test('insertSecuritySignal writes the security part with no score', async () => {
202
+ const seen = [];
203
+ await insertSecuritySignal(5, { outcome: 'failed', score: null, sample_size: 2, reason: 'r', detail: { failures: [] } },
204
+ { db: { async query(sql, params) { seen.push(params); return { rows: [{}] }; } } });
205
+ assert.deepEqual(seen[0], [5, 'failed', null, 2, 'r', '{"failures":[]}']);
206
+ });
207
+
208
+ test('a core script never requires a module file (the one-way rule)', () => {
209
+ const src = fs.readFileSync(new URL('../scripts/gds/module-assess-security.js', import.meta.url), 'utf8').replace(/\/\/.*$/gm, '');
210
+ assert.doesNotMatch(src, /require\([^)]*modules\//);
211
+ });
212
+
213
+ test('the gate never runs the module\'s own code', () => {
214
+ const src = fs.readFileSync(new URL('../scripts/gds/module-assess-security.js', import.meta.url), 'utf8').replace(/\/\/.*$/gm, '');
215
+ assert.doesNotMatch(src, /process\.execPath|require\(\s*path\.join|import\(/, 'no node process or require over module files');
216
+ });
@@ -1,9 +1,10 @@
1
- // tests/skill_grade_audit.mjs — the /grade-audit skill exists and covers the
2
- // accountability sweep from the grader-hardening evaluation §6 (task 1002657):
3
- // the override ledger, dropped blocker/major findings on passing grades (with
4
- // the advisory-Narc attribution filter), the false-pass spot-check, and the
5
- // outage roll-up handoff — describing and queueing ONLY (ADR 0158 §2: it
6
- // never confirms a task or flips status; ADR 0162: it adds no approval step to a ship).
1
+ // tests/skill_grade_audit.mjs — the grade AUDIT (once /grade-audit, now Part B of the
2
+ // merged /grade-sweep — task 1004471) covers the accountability sweep from the
3
+ // grader-hardening evaluation §6 (task 1002657): the override ledger, dropped
4
+ // blocker/major findings on passing grades (with the advisory-Narc attribution
5
+ // filter), the false-pass spot-check, and the outage roll-up handoff — describing and
6
+ // queueing ONLY (ADR 0158 §2: it never confirms a task or flips status; ADR 0162: it
7
+ // adds no approval step to a ship). /grade-audit stays typeable as a hidden alias.
7
8
  import assert from 'node:assert/strict';
8
9
  import { test } from 'node:test';
9
10
  import { readFileSync, existsSync } from 'node:fs';
@@ -11,16 +12,22 @@ import { fileURLToPath } from 'node:url';
11
12
  import path from 'node:path';
12
13
 
13
14
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
- const SKILL = path.join(ROOT, '.claude', 'skills', 'grade-audit', 'SKILL.md');
15
+ const SKILL = path.join(ROOT, '.claude', 'skills', 'grade-sweep', 'SKILL.md');
16
+ const ALIAS = path.join(ROOT, '.claude', 'skills', 'grade-audit', 'SKILL.md');
15
17
 
16
18
  // .claude/skills is INSTANCE-materialized; a neutral core checkout carries none
17
19
  // (task 1002470) — skip there, same guard shape as file_map.mjs.
18
20
  const skillsDirExists = existsSync(path.join(ROOT, '.claude', 'skills'));
19
21
 
20
- test('/grade-audit exists, covers the sweep legs, and stays describe-and-queue only', (t) => {
22
+ // Part B of the merged file: from its heading to the shared report section.
23
+ const partB = (md) => md.slice(md.indexOf('## Part B'), md.indexOf('## Report shape'));
24
+
25
+ test('the grade audit (grade-sweep Part B) covers the sweep legs, and stays describe-and-queue only', (t) => {
21
26
  if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
22
- assert.ok(existsSync(SKILL), '.claude/skills/grade-audit/SKILL.md must exist (task 1002657)');
23
- const md = readFileSync(SKILL, 'utf8');
27
+ assert.ok(existsSync(SKILL), '.claude/skills/grade-sweep/SKILL.md must exist (task 1004471, was grade-audit — task 1002657)');
28
+ const whole = readFileSync(SKILL, 'utf8');
29
+ const md = partB(whole);
30
+ assert.ok(md.length > 1000, 'Part B is present');
24
31
 
25
32
  // Data surfaces.
26
33
  assert.match(md, /\/api\/bongos\/public\/recent-shipped/, 'sweeps the recent-shipped window');
@@ -35,16 +42,26 @@ test('/grade-audit exists, covers the sweep legs, and stays describe-and-queue o
35
42
  assert.match(md, /blocker|major/, 'targets blocker/major findings on passing grades');
36
43
  assert.match(md, /Narc/i, 'carries the advisory-Narc attribution filter');
37
44
  assert.match(md, /capture\.js/, 'queues findings via capture.js');
38
- assert.match(md, /\[grade-audit\]/, 'uses the deterministic title marker for idempotency');
45
+ assert.match(md, /\[grade-audit\]/, 'keeps the deterministic title marker for idempotency (old rows carry it)');
39
46
 
40
47
  // Leg 3 — false-pass spot-check (the 2/8 runtime-blindness classes).
41
48
  assert.match(md, /runtime/i, 'spot-checks for runtime-behavior blindness');
42
49
 
43
- // Leg 4 — outage roll-up handoff.
44
- assert.match(md, /\/grader-health/, 'hands outage windows to /grader-health');
50
+ // Leg 4 — outage roll-up hands a streak to the health half, now in the same file.
51
+ assert.match(md, /Part A/, 'hands outage streaks to Part A (was /grader-health)');
52
+
53
+ // The hard constraint — stated once for the whole merged skill.
54
+ assert.match(whole, /never confirm/i, 'states it never confirms');
55
+ assert.match(whole, /ADR 0162/, 'anchors the no-approval-step-on-a-ship constraint');
56
+ assert.ok(!/curl[^\n]*\/confirm/.test(whole), 'carries no ready-to-paste confirm curl');
57
+ });
45
58
 
46
- // The hard constraint.
47
- assert.match(md, /never confirm/i, 'states it never confirms');
48
- assert.match(md, /ADR 0162/, 'anchors the no-approval-step-on-a-ship constraint');
49
- assert.ok(!/curl[^\n]*\/confirm/.test(md), 'carries no ready-to-paste confirm curl');
59
+ test('/grade-audit still works when typed: a hidden alias that runs grade-sweep Part B (task 1004471)', (t) => {
60
+ if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
61
+ const md = readFileSync(ALIAS, 'utf8');
62
+ assert.match(md, /^name: grade-audit$/m);
63
+ assert.match(md, /^disable-model-invocation: true$/m, 'hidden from the model\'s listing, typeable as /grade-audit');
64
+ assert.match(md, /grade-sweep\/SKILL\.md/, 'points at the merged skill by path');
65
+ assert.match(md, /Part B/, 'runs the audit half only');
66
+ assert.match(readFileSync(SKILL, 'utf8'), /^disable-model-invocation: true$/m, 'the merged grade skill is hidden too (owner ruling)');
50
67
  });
@@ -1,8 +1,10 @@
1
- // tests/skill_grader_health.mjs — the /grader-health skill exists and covers
2
- // the four health axes from the grader-hardening evaluation §6 (task 1002656):
3
- // outage runs, pass-rate drift, cost anomalies, and stale model pins — reading
4
- // LIVE data surfaces, emitting exact --regrade remediation lines, and staying
5
- // read-only (describe, never confirm/flip — ADR 0158 §2; no approval step on anyone's ship — ADR 0162).
1
+ // tests/skill_grader_health.mjs — the grader HEALTH check (once /grader-health, now
2
+ // Part A of the merged /grade-sweep — task 1004471) covers the four health axes from
3
+ // the grader-hardening evaluation §6 (task 1002656): outage runs, pass-rate drift,
4
+ // cost anomalies, and stale model pins — reading LIVE data surfaces, emitting exact
5
+ // --regrade remediation lines, and staying read-only (describe, never confirm/flip —
6
+ // ADR 0158 §2; no approval step on anyone's ship — ADR 0162). /grader-health stays
7
+ // typeable as a hidden alias.
6
8
  import assert from 'node:assert/strict';
7
9
  import { test } from 'node:test';
8
10
  import { readFileSync, existsSync } from 'node:fs';
@@ -10,16 +12,22 @@ import { fileURLToPath } from 'node:url';
10
12
  import path from 'node:path';
11
13
 
12
14
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
- const SKILL = path.join(ROOT, '.claude', 'skills', 'grader-health', 'SKILL.md');
15
+ const SKILL = path.join(ROOT, '.claude', 'skills', 'grade-sweep', 'SKILL.md');
16
+ const ALIAS = path.join(ROOT, '.claude', 'skills', 'grader-health', 'SKILL.md');
14
17
 
15
18
  // .claude/skills is INSTANCE-materialized; a neutral core checkout carries none
16
19
  // (task 1002470) — skip there, same guard shape as file_map.mjs.
17
20
  const skillsDirExists = existsSync(path.join(ROOT, '.claude', 'skills'));
18
21
 
19
- test('/grader-health exists and covers the four health axes on live data', (t) => {
22
+ // Part A of the merged file: from its heading to Part B.
23
+ const partA = (md) => md.slice(md.indexOf('## Part A'), md.indexOf('## Part B'));
24
+
25
+ test('the grader health check (grade-sweep Part A) covers the four health axes on live data', (t) => {
20
26
  if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
21
- assert.ok(existsSync(SKILL), '.claude/skills/grader-health/SKILL.md must exist (task 1002656)');
22
- const md = readFileSync(SKILL, 'utf8');
27
+ assert.ok(existsSync(SKILL), '.claude/skills/grade-sweep/SKILL.md must exist (task 1004471, was grader-health — task 1002656)');
28
+ const whole = readFileSync(SKILL, 'utf8');
29
+ const md = partA(whole);
30
+ assert.ok(md.length > 1000, 'Part A is present');
23
31
 
24
32
  // Live data surfaces, named concretely.
25
33
  assert.match(md, /\/api\/bongos\/public\/grades/, 'reads the public grade trend');
@@ -44,5 +52,15 @@ test('/grader-health exists and covers the four health axes on live data', (t) =
44
52
  assert.match(md, /--probe/, 'offers the optional live calibration probe');
45
53
  assert.match(md, /grade-smoke\.js/, 'names the probe script');
46
54
  assert.match(md, /read-only/i, 'states the read-only contract');
47
- assert.match(md, /ADR 0162/, 'anchors the no-approval-step-on-a-ship constraint');
55
+ assert.match(whole, /ADR 0162/, 'anchors the no-approval-step-on-a-ship constraint');
56
+ });
57
+
58
+ test('/grader-health still works when typed: a hidden alias that runs grade-sweep Part A (task 1004471)', (t) => {
59
+ if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
60
+ const md = readFileSync(ALIAS, 'utf8');
61
+ assert.match(md, /^name: grader-health$/m);
62
+ assert.match(md, /^disable-model-invocation: true$/m, 'hidden from the model\'s listing, typeable as /grader-health');
63
+ assert.match(md, /grade-sweep\/SKILL\.md/, 'points at the merged skill by path');
64
+ assert.match(md, /Part A/, 'runs the health half only');
65
+ assert.match(md, /--probe/, 'passes --probe through');
48
66
  });
@@ -138,6 +138,43 @@ test('the listing counts only RESIDENT skills: an off module\'s skill is linted
138
138
  fs.rmSync(root, { recursive: true, force: true });
139
139
  });
140
140
 
141
+ test('a HIDDEN skill (disable-model-invocation: true) is linted but not counted toward the listing budget, and is reported on its own (task 1004471)', () => {
142
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-lint-hidden-'));
143
+ const put = (rel, text) => { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); };
144
+ const skill = (n, extra = '') => `---\nname: ${n}\ndescription: >-\n ${'y'.repeat(100)}\n${extra}---\nbody\n`;
145
+ put('.claude/skills/shown/SKILL.md', skill('shown'));
146
+ put('.claude/skills/tucked/SKILL.md', skill('tucked', 'disable-model-invocation: true\n'));
147
+ put('.claude/skills/false-flag/SKILL.md', skill('false-flag', 'disable-model-invocation: false\n'));
148
+ const files = lint.listSkillFiles(root);
149
+ const report = lint.lintSkills(files, { resident: lint.residentSkillFiles(root, { files }) });
150
+ assert.equal(report.results.length, 3, 'all three are linted');
151
+ assert.equal(report.listedSkills, 2, 'only a literal true hides — false stays listed');
152
+ assert.equal(report.listingChars, ('shown'.length + 100) + ('false-flag'.length + 100));
153
+ assert.equal(report.hiddenSkills, 1);
154
+ assert.equal(report.hiddenChars, 'tucked'.length + 100);
155
+ assert.deepEqual(report.hiddenNames, ['tucked']);
156
+ const out = lint.format(report, { relTo: root });
157
+ assert.match(out, /listing 215 chars over 2 listed skill/);
158
+ assert.match(out, /1 hidden skill\(s\).*not counted\): 106 chars — tucked/);
159
+ fs.rmSync(root, { recursive: true, force: true });
160
+ });
161
+
162
+ test('a hidden skill is exempt from the long-description warning (it costs no listing) but not from the hard cap', () => {
163
+ const long = 'z'.repeat(lint.DESC_WARN_CHARS + 10);
164
+ assert.equal(run(fm(long, 'disable-model-invocation: true\n')).warnings.length, 0);
165
+ assert.equal(run(fm(long)).warnings.length, 1, 'the same text listed still warns');
166
+ assert.equal(run(fm('z'.repeat(lint.DESC_HARD_CHARS + 1), 'disable-model-invocation: true\n')).errors.length, 1);
167
+ assert.equal(lint.isHidden({ 'disable-model-invocation': 'TRUE ' }), true);
168
+ assert.equal(lint.isHidden({ 'disable-model-invocation': 'yes' }), false, 'anything but true leaves it listed');
169
+ });
170
+
171
+ test('the real default listing is under the ~8,000-char budget once hidden skills drop out (task 1004471)', () => {
172
+ const files = lint.listSkillFiles(ROOT);
173
+ const report = lint.lintSkills(files, { resident: lint.residentSkillFiles(ROOT, { files }) });
174
+ assert.equal(report.overBudget, false, `listing is ${report.listingChars} chars over ${report.listedSkills} listed skills`);
175
+ assert.ok(report.hiddenSkills >= 12, `the owner's hide list is in effect (${report.hiddenSkills} hidden)`);
176
+ });
177
+
141
178
  test('every committed skill parses (the ten that did not on 2026-09-03 must stay fixed)', () => {
142
179
  const report = lint.lintSkills(lint.listSkillFiles(ROOT));
143
180
  const broken = report.results.filter((r) => r.errors.length).map((r) => path.relative(ROOT, r.file) + ': ' + r.errors.join('; '));
@@ -0,0 +1,99 @@
1
+ // tests/skill_menu_ruling.mjs — the owner's skill-menu ruling (task 1004471, blocker
2
+ // 1000139) holds: which skills the model is shown, which are hidden but still typeable
3
+ // as /name, and the three merges (/owner-review, /goal-close, /grade-sweep) with every
4
+ // old command still answering. Pinned skill by skill, so a later edit that re-lists a
5
+ // hidden skill, hides a safety net, or breaks an old /name fails here by name.
6
+ import assert from 'node:assert/strict';
7
+ import { test } from 'node:test';
8
+ import { createRequire } from 'node:module';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const lint = require('../scripts/gds/skill-lint.js');
16
+
17
+ const byName = new Map(lint.listSkillFiles(ROOT).map((f) => [path.basename(path.dirname(f)), f]));
18
+ const read = (n) => fs.readFileSync(byName.get(n), 'utf8');
19
+ const info = (n) => lint.lintSkillFile(read(n), { file: byName.get(n) });
20
+
21
+ // The ruling, as written into the task.
22
+ const LISTED_TRIMMED = ['grade-recover', 'collab-review', 'session-handoff', 'builder-claim', 'builder-sequence', 'blocker-solve', 'design', 'goal-create', 'blocker-review', 'builder-reauth', 'read-session-export', 'recall', 'worktree-clean', 'merge-mode', 'builder-setup', 'builder-key', 'bongos-feedback', 'feedback', 'builder-redteam', 'scan-before-install', 'owner-review'];
23
+ const LISTED_AS_IS = ['strand-fix', 'builder-ship', 'builder-start', 'fix-task', 'ask-for-help', 'ship-check', 'status', 'builder-end', 'builder-release', 'builder-stage'];
24
+ const HIDDEN = ['planning-session', 'goal-close', 'grade-sweep', 'builder-exit', 'builder-backup', 'builder-cost', 'new-project', 'design-sync', 'figma-design-sync', 'tweak'];
25
+ const ALIASES = ['backlog-review', 'bug-triage', 'idea-triage', 'priority-session', 'goal-uat', 'goal-review', 'grade-audit', 'grader-health'];
26
+
27
+ test('every skill the ruling keeps listed is present, listed, and (where trimmed) at most 250 chars', () => {
28
+ for (const n of [...LISTED_TRIMMED, ...LISTED_AS_IS]) {
29
+ assert.ok(byName.has(n), `${n} exists`);
30
+ const r = info(n);
31
+ assert.equal(r.hidden, false, `${n} must stay in the model's listing`);
32
+ assert.deepEqual(r.errors, [], `${n} parses`);
33
+ }
34
+ const over = LISTED_TRIMMED.filter((n) => info(n).description.length > 250).map((n) => `${n} ${info(n).description.length}`);
35
+ assert.deepEqual(over, [], 'trimmed descriptions are <= 250 chars');
36
+ });
37
+
38
+ test('every skill the ruling hides — and every old name a merge replaced — is hidden but still a typeable skill', () => {
39
+ for (const n of [...HIDDEN, ...ALIASES]) {
40
+ assert.ok(byName.has(n), `/${n} still exists, so typing it still works`);
41
+ assert.equal(info(n).hidden, true, `${n} carries disable-model-invocation: true`);
42
+ }
43
+ });
44
+
45
+ test('the safety nets and owner-pinned triggers survive the trim', () => {
46
+ const d = (n) => info(n).description;
47
+ assert.match(d('builder-redteam'), /"I found a security issue"/);
48
+ assert.match(d('scan-before-install'), /"is this plugin safe to install"/);
49
+ assert.match(d('builder-reauth'), /"Bongos CLI session is no longer valid"/, 'fires on the CLI\'s own error string');
50
+ assert.match(d('builder-setup'), /"no Bongos session"/, 'fires on the CLI\'s own error string');
51
+ assert.match(d('builder-key'), /never accept a key pasted in chat/, 'keeps the hard rule in the listing');
52
+ assert.match(d('bongos-feedback'), /unasked/, 'keeps its unprompted trigger');
53
+ for (const n of [...LISTED_TRIMMED, ...LISTED_AS_IS, ...HIDDEN, ...ALIASES]) {
54
+ const fm = read(n).split('\n---')[0];
55
+ for (const k of ['plain', 'reach-for', 'cost']) assert.match(fm, new RegExp(`^${k}:`, 'm'), `${n} keeps its ${k}: line (the hall Commands page reads it)`);
56
+ }
57
+ });
58
+
59
+ test('/owner-review is the listed front door to backlog -> bugs -> ideas -> priorities, and plain words open each step', () => {
60
+ const md = read('owner-review');
61
+ const desc = info('owner-review').description;
62
+ for (const phrase of ['"review the backlog"', '"triage bugs"', '"triage ideas"', '"review the inbox"']) assert.ok(desc.includes(phrase), `listing routes ${phrase}`);
63
+ const order = ['backlog-review', 'bug-triage', 'idea-triage', 'priority-session'].map((s) => md.indexOf(`.claude/skills/${s}/SKILL.md`));
64
+ assert.ok(order.every((i) => i > 0), 'each step names its playbook by path');
65
+ assert.deepEqual([...order].sort((a, b) => a - b), order, 'in the ruling\'s order');
66
+ for (const step of ['backlog', 'bugs', 'ideas', 'priorities']) assert.match(md, new RegExp('`' + step + '`'), `/owner-review ${step} opens one step`);
67
+ assert.match(md, /Read that step's `SKILL\.md` with the Read tool/, 'the model reads a hidden step by path — the Skill tool cannot load it');
68
+ for (const s of ['backlog-review', 'bug-triage', 'idea-triage', 'priority-session']) assert.match(md, new RegExp('`/' + s + '`'), `/${s} is named as still typeable`);
69
+ });
70
+
71
+ test('the nightly idea-triage routine still finds the playbook it reads by path', () => {
72
+ const nightly = fs.readFileSync(path.join(ROOT, '.claude/scheduled-tasks/idea-triage-nightly/SKILL.md'), 'utf8');
73
+ assert.match(nightly, /\.claude\/skills\/idea-triage\/SKILL\.md/);
74
+ assert.ok(fs.existsSync(path.join(ROOT, '.claude/skills/idea-triage/SKILL.md')));
75
+ assert.ok(read('idea-triage').length > 5000, 'the full playbook, not an alias stub');
76
+ });
77
+
78
+ test('/goal-close carries both halves; /goal-uat and /goal-review run one each', () => {
79
+ const md = read('goal-close');
80
+ const p1 = md.indexOf('## Part 1'), p2 = md.indexOf('## Part 2');
81
+ assert.ok(p1 > 0 && p2 > p1, 'UAT part, then residue part');
82
+ assert.match(md.slice(p1, p2), /uat\.js --queue/, 'Part 1 reads the UAT queue');
83
+ assert.match(md.slice(p1, p2), /--recording/, 'Part 1 signs off with a recording');
84
+ assert.match(md.slice(p2), /done-when\/pending-review/, 'Part 2 reads the review queue');
85
+ assert.match(md.slice(p2), /override_reason/, 'Part 2 overrides only with a written reason');
86
+ assert.match(md, /xenos/, 'one Metic+ gate at the top');
87
+ assert.match(read('goal-uat'), /goal-close\/SKILL\.md[\s\S]*Part 1/);
88
+ assert.match(read('goal-review'), /goal-close\/SKILL\.md[\s\S]*Part 2/);
89
+ });
90
+
91
+ test('/tweak lives in the copy-desk module, which contributes it (default-on, so it stays installed)', () => {
92
+ assert.ok(fs.existsSync(path.join(ROOT, 'modules/copy-desk/skills/tweak/SKILL.md')));
93
+ assert.ok(!fs.existsSync(path.join(ROOT, '.claude/skills/tweak/SKILL.md')) || !require('node:child_process').execFileSync('git', ['ls-files', '.claude/skills/tweak'], { cwd: ROOT, encoding: 'utf8' }).trim(), 'no tracked core copy (an untracked materialised one is fine)');
94
+ const mj = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/copy-desk/module.json'), 'utf8'));
95
+ assert.deepEqual(mj.contributes.skills, ['tweak']);
96
+ assert.equal(mj.default, true);
97
+ const files = lint.listSkillFiles(ROOT);
98
+ assert.ok(lint.residentSkillFiles(ROOT, { files }).some((f) => path.basename(path.dirname(f)) === 'tweak'), 'resident on a default instance');
99
+ });