@bongos/core 1.19.636 → 1.19.638

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.
@@ -0,0 +1,121 @@
1
+ // scripts/gds/role-pack-guard.js
2
+ //
3
+ // WHAT. The fitness check behind the kernel/pack split (task 1002990, criterion
4
+ // wa6-kernel-and-packs): every entry in the role registry that names a `pack` must
5
+ // point at a file that exists, and every pack must stay inside a context budget.
6
+ //
7
+ // WHY IT IS A HARD FAIL, when claim.js's own read is fail-open. The registry
8
+ // (scripts/gds/discipline-modes.json) is read at claim time by code that must never
9
+ // break a granted claim, so a pack path that points at nothing degrades to silence:
10
+ // the builder is told their craft's label and nothing else, and the playbook that
11
+ // was supposed to carry everything the role-neutral kernel dropped never loads. That
12
+ // failure is invisible at runtime by design. CI is therefore the only place it can
13
+ // be caught, which is exactly the split the criterion asks for — "CI budgets each
14
+ // pack" — and why a renamed or deleted pack must red the build rather than quietly
15
+ // hollow out a role.
16
+ //
17
+ // WHY A BUDGET AT ALL. A pack is loaded in full whenever someone works that craft,
18
+ // so it costs the same per-session tokens the root CLAUDE.md does (ADR 0061, the
19
+ // context-rot argument). The kernel got a budget when it was the only always-loaded
20
+ // file; the packs inherit the same discipline now that they carry half its content.
21
+ // The ceilings are advisory-then-hard in the same shape checkClaudeMdBudget uses:
22
+ // a target that WARNS, a ceiling that FAILS, both ratcheted down as detail relocates
23
+ // to ADRs and recipes.
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const { CORE_ROOT } = require('./fitness-lib.js');
29
+
30
+ // The registry lives beside the CLI that reads it, in the CORE package — the same
31
+ // resolution claim.js uses (path.join(__dirname, 'discipline-modes.json')), so this
32
+ // check and the runtime always read the same file even from a scaffolded instance
33
+ // whose repo root is somewhere else entirely (ADR 0108).
34
+ const REGISTRY_REL = 'scripts/gds/discipline-modes.json';
35
+
36
+ // ~4 chars/token for markdown prose, the estimator checkClaudeMdBudget uses.
37
+ const estTokens = (n) => Math.round(n / 4);
38
+
39
+ // Ratcheted just above the packs as authored by task 1002990 (engineer 12,223,
40
+ // ideator 10,218, artist 6,982 chars). Lower both as pack detail relocates to
41
+ // ADRs and recipes — the same ratchet contract ROOT_CHAR_HARD_MAX carries.
42
+ const PACK_CHAR_HARD_MAX = 16000; // ~4K tokens — a role pack must never balloon past this.
43
+ const PACK_CHAR_TARGET = 13000; // ~3.25K tokens — aspirational.
44
+
45
+ function checkRolePacks(fsImpl = fs) {
46
+ const violations = [];
47
+ const warnings = [];
48
+ const abs = path.join(CORE_ROOT, REGISTRY_REL);
49
+
50
+ let modes;
51
+ try {
52
+ modes = (JSON.parse(fsImpl.readFileSync(abs, 'utf8')) || {}).modes || {};
53
+ } catch (e) {
54
+ // Unlike claim.js this does NOT fail open. An unreadable registry is the one
55
+ // state that makes every other assertion here vacuous, so it is the failure.
56
+ return {
57
+ name: 'role registry packs resolve + fit their budget',
58
+ ok: false,
59
+ hardFail: true,
60
+ violations: [`${REGISTRY_REL} could not be read or parsed (${e && e.message}) — claim.js reads this file on every claim and fails open, so a broken registry is silent at runtime and can only be caught here.`],
61
+ warnings: [],
62
+ note: 'the role registry could not be read.',
63
+ };
64
+ }
65
+
66
+ const entries = Object.entries(modes);
67
+ let packCount = 0;
68
+ const sizes = [];
69
+
70
+ for (const [discipline, mode] of entries) {
71
+ if (!mode || typeof mode !== 'object') {
72
+ violations.push(`role registry entry '${discipline}' is not an object — every entry must carry a directive plus a pack or a skill.`);
73
+ continue;
74
+ }
75
+ // An entry with neither pointer routes a claim nowhere: claim.js prints the
76
+ // label and the session is left to guess its own playbook.
77
+ if (!mode.pack && !mode.skill) {
78
+ violations.push(`role registry entry '${discipline}' names neither a 'pack' (docs/packs/<craft>.md) nor a 'skill' — a claim of this discipline would be routed to no playbook at all.`);
79
+ }
80
+ if (!mode.pack) continue;
81
+
82
+ packCount += 1;
83
+ const packAbs = path.join(CORE_ROOT, mode.pack);
84
+ if (!fsImpl.existsSync(packAbs)) {
85
+ violations.push(`role registry entry '${discipline}' points at '${mode.pack}', which is not in the tree — claim.js would print the path and the session would find nothing there.`);
86
+ continue;
87
+ }
88
+ let text = '';
89
+ try {
90
+ text = fsImpl.readFileSync(packAbs, 'utf8');
91
+ } catch (e) {
92
+ violations.push(`role registry entry '${discipline}' points at '${mode.pack}', which exists but could not be read (${e && e.message}).`);
93
+ continue;
94
+ }
95
+ const chars = text.length;
96
+ sizes.push(`${discipline} ${chars}`);
97
+ if (chars > PACK_CHAR_HARD_MAX) {
98
+ violations.push(`${mode.pack} is ${chars} chars (~${estTokens(chars)} tokens) — over the ${PACK_CHAR_HARD_MAX}-char (~${estTokens(PACK_CHAR_HARD_MAX)}-token) hard ceiling. A pack loads in full for every session of its craft; relocate detail to an ADR or a recipe (ADR 0061).`);
99
+ } else if (chars > PACK_CHAR_TARGET) {
100
+ warnings.push(`${mode.pack} is ${chars} chars (~${estTokens(chars)} tokens) — over the ~${PACK_CHAR_TARGET}-char (~${estTokens(PACK_CHAR_TARGET)}-token) target. Trim prose, then lower PACK_CHAR_TARGET.`);
101
+ }
102
+ }
103
+
104
+ // The split's own shape, asserted so a later edit cannot quietly drop a craft
105
+ // back into the kernel: three core packs, and Governor deliberately absent
106
+ // (criterion wa6-kernel-and-packs — "three packs, not four, until the owner decides").
107
+ if (packCount < 3) {
108
+ violations.push(`only ${packCount} craft(s) name a pack — the split is one kernel plus three packs (Engineer, Artist, Ideator). A craft with no pack has nowhere to keep what the role-neutral kernel does not.`);
109
+ }
110
+
111
+ return {
112
+ name: 'role registry packs resolve + fit their budget',
113
+ ok: violations.length === 0,
114
+ hardFail: violations.length > 0,
115
+ violations,
116
+ warnings,
117
+ note: `${entries.length} registry entr(ies), ${packCount} with a pack (${sizes.join(', ') || 'none'} chars) — ≤ ${PACK_CHAR_HARD_MAX} chars each, target ~${PACK_CHAR_TARGET}. A pack path that resolves to nothing is silent at claim time, so CI is the only gate.`,
118
+ };
119
+ }
120
+
121
+ module.exports = { checkRolePacks, PACK_CHAR_HARD_MAX, PACK_CHAR_TARGET, REGISTRY_REL };
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.636'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.638'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -25,7 +25,7 @@ test('each generator input maps to its generator; unrelated paths map to nothing
25
25
  ['modules/status-ui/public/index.html', 'copy-inventory.js'],
26
26
  ['src/bongos/auth.js', 'gen-repo-map.js'],
27
27
  ['modules/lifecycle/routes/claims.js', 'gen-api-docs.js'],
28
- ['.claude/skills/dev/SKILL.md', 'gen-file-map.js'],
28
+ ['.claude/skills/status/SKILL.md', 'gen-file-map.js'],
29
29
  ['docs/session-logs/2026-09-03-x.md', 'gen-session-index.js'],
30
30
  ['docs/adr/0236-something.md', 'fitness.js'],
31
31
  ];
package/tests/fitness.mjs CHANGED
@@ -57,6 +57,102 @@ test('CLAUDE.md char budget is ratcheted and ordered (target < ceiling, live fil
57
57
  assert.equal(r.hardFail, false, 'the live CLAUDE.md must be under the char ceiling (ratchet locked in)');
58
58
  });
59
59
 
60
+ // --- the role packs (task 1002990 / ADR 0274) --------------------------------
61
+ //
62
+ // This guard is the ONLY place a broken pack path can surface: claim.js reads the
63
+ // role registry fail-open so it can never break a granted claim, which means an
64
+ // entry pointing at a missing file degrades to silence — the craft's label prints
65
+ // and its playbook never loads. The ADR rests the whole hard-fail argument on that,
66
+ // so an untested guard would undermine its own rationale. Hence both legs, the same
67
+ // pair every sibling guard here carries: the live tree is clean, AND the check
68
+ // actually catches the failure rather than being a no-op.
69
+ const rolePacks = require(path.join(ROOT, 'scripts', 'gds', 'role-pack-guard.js'));
70
+
71
+ test('role packs: the live registry resolves and every pack is under budget', () => {
72
+ const r = rolePacks.checkRolePacks();
73
+ assert.equal(r.hardFail, false, r.violations.join('\n'));
74
+ });
75
+
76
+ test('role pack budget is ratcheted and ordered (target < ceiling)', () => {
77
+ assert.ok(rolePacks.PACK_CHAR_TARGET < rolePacks.PACK_CHAR_HARD_MAX, 'target must sit below the hard ceiling');
78
+ });
79
+
80
+ test('role pack guard actually CATCHES a missing pack, a pointerless entry, and a short roster (not a no-op)', () => {
81
+ const real = fs;
82
+ const withRegistry = (modes) => ({
83
+ readFileSync: (p, e) => (String(p).includes('discipline-modes.json') ? JSON.stringify({ modes }) : real.readFileSync(p, e)),
84
+ existsSync: (p) => (String(p).includes('__gone__') ? false : real.existsSync(p)),
85
+ });
86
+
87
+ // (1) the silent runtime failure: a pack path that resolves to nothing.
88
+ const missing = rolePacks.checkRolePacks(withRegistry({
89
+ engineer: { pack: 'docs/packs/__gone__.md', directive: 'x' },
90
+ artist: { pack: 'docs/packs/artist.md', directive: 'x' },
91
+ ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
92
+ }));
93
+ assert.equal(missing.hardFail, true, 'a pack that is not in the tree must hard-fail');
94
+ assert.ok(missing.violations.some((v) => /__gone__/.test(v) && /not in the tree/.test(v)), 'the violation must name the unresolvable path');
95
+
96
+ // (2) an entry routing a claim to no playbook at all.
97
+ const pointerless = rolePacks.checkRolePacks(withRegistry({
98
+ engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
99
+ artist: { pack: 'docs/packs/artist.md', directive: 'x' },
100
+ ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
101
+ ui: { directive: 'x' },
102
+ }));
103
+ assert.equal(pointerless.hardFail, true, 'an entry with neither pack nor skill must hard-fail');
104
+ assert.ok(pointerless.violations.some((v) => /'ui'/.test(v) && /neither/.test(v)));
105
+
106
+ // (3) a craft quietly dropped back into the kernel — the split's own shape.
107
+ const short = rolePacks.checkRolePacks(withRegistry({
108
+ engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
109
+ artist: { pack: 'docs/packs/artist.md', directive: 'x' },
110
+ ideator: { skill: 'ideate', directive: 'x' },
111
+ }));
112
+ assert.equal(short.hardFail, true, 'fewer than three packs must hard-fail');
113
+ assert.ok(short.violations.some((v) => /only 2 craft/.test(v)));
114
+
115
+ // (4) an over-budget pack — the "CI budgets each pack" half of the criterion.
116
+ const fat = rolePacks.checkRolePacks({
117
+ readFileSync: (p, e) => {
118
+ if (String(p).includes('discipline-modes.json')) {
119
+ return JSON.stringify({ modes: {
120
+ engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
121
+ artist: { pack: 'docs/packs/artist.md', directive: 'x' },
122
+ ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
123
+ } });
124
+ }
125
+ return 'x'.repeat(rolePacks.PACK_CHAR_HARD_MAX + 1);
126
+ },
127
+ existsSync: () => true,
128
+ });
129
+ assert.equal(fat.hardFail, true, 'a pack over the char ceiling must hard-fail');
130
+ assert.ok(fat.violations.some((v) => /hard ceiling/.test(v)));
131
+
132
+ // …and the guard is not simply always-red: the real tree passes (leg 1 above),
133
+ // and an unreadable registry is itself the failure rather than a free pass.
134
+ const unreadable = rolePacks.checkRolePacks({
135
+ readFileSync: () => { throw new Error('boom'); },
136
+ existsSync: () => true,
137
+ });
138
+ assert.equal(unreadable.hardFail, true, 'an unreadable registry must fail, not fail open like claim.js');
139
+ });
140
+
141
+ // The ship card mirrors the registry by hand — it cannot require a core script
142
+ // across the module boundary, which is why claim.js and ship-card.js have always
143
+ // carried the map twice (task 1003487 was that duplication drifting). A test is the
144
+ // shared source they cannot have: rename a pack and this fails instead of the hall
145
+ // shipping a button that opens nothing.
146
+ test('ship-card routing agrees with the role registry, entry for entry', () => {
147
+ const registry = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'discipline-modes.json'), 'utf8')).modes;
148
+ const card = require(path.join(ROOT, 'modules', 'lifecycle', 'ship-card.js'));
149
+ for (const [discipline, mode] of Object.entries(registry)) {
150
+ const cmd = card.buildCardModel({ stage: 'claimed', taskId: 1, discipline, brand: { currency: 'credits' } }).action.cmd;
151
+ if (mode.pack) assert.ok(cmd.includes(mode.pack), `${discipline}: card says "${cmd}" but the registry names ${mode.pack}`);
152
+ else if (mode.skill) assert.equal(cmd, `/${mode.skill}`, `${discipline}: card and registry disagree on the skill`);
153
+ }
154
+ });
155
+
60
156
  test('nested CLAUDE.md present for every high-traffic dir', () => {
61
157
  const r = fitness.checkNestedDocsPresent();
62
158
  assert.equal(r.hardFail, false, r.violations.join('\n'));
@@ -172,14 +172,36 @@ test('every glob added since R101 carries a documented reason', () => {
172
172
  }
173
173
  });
174
174
 
175
- test('every surface floors at metic the ADR 0043 floor, unchanged by the move', () => {
175
+ // Metic is a FLOOR, not a fixed value (task 1003191). Pinning every surface to
176
+ // exactly 'metic' made the registry's own contract untestable and, worse,
177
+ // illegal: governance owns this data and raising one surface to 'archon' is a
178
+ // policy edit the enforcers are supposed to honour. What must never happen is a
179
+ // surface dropping BELOW Metic — that is ADR 0043 being weakened by a data edit.
180
+ // So: no surface under Metic, and the derived minimum stays exactly Metic.
181
+ test('no surface floors below Metic — the ADR 0043 floor (a raise above it is legal policy)', () => {
176
182
  for (const s of registry.SURFACES) {
177
- assert.equal(s.floor, 'metic', `${s.glob} must keep the Metic floor`);
183
+ assert.ok(ppc.RANK_TIER[s.floor] >= ppc.RANK_TIER.metic,
184
+ `${s.glob} floors at '${s.floor}', below the Metic floor ADR 0043 sets. Governance may RAISE a floor; it may never lower one under Metic.`);
178
185
  }
179
- assert.equal(registry.FLOOR, 'metic');
186
+ assert.equal(registry.FLOOR, 'metic', 'the lowest declared floor is the Metic floor itself');
180
187
  assert.equal(ppc.MIN_TIER, ppc.RANK_TIER.metic, 'the kernel floor must DERIVE to metic');
181
188
  });
182
189
 
190
+ test('a floor raise flows through to the enforcers, not just the registry', () => {
191
+ // The registry promises a re-mapped floor reaches the substrate. Each layer
192
+ // must read the hit's OWN floor: the cooperative ones through
193
+ // checkPermissionPaths, the authoritative post-push audit through its per-hit
194
+ // comparison (task 1003191 — it used to test the global minimum and silently
195
+ // downgraded every raise).
196
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'main-audit.js'), 'utf8');
197
+ assert.ok(!/\bppc\.isBelowFloor\s*\(/.test(src),
198
+ 'main-audit.js must not decide floors with isBelowFloor — that tests the registry '
199
+ + 'GLOBAL MINIMUM, so a governance floor raise is silently downgraded in the one '
200
+ + 'layer that catches a raw `git push origin main` (task 1003191).');
201
+ assert.ok(/RANK_TIER\[h\.floor\]/.test(src),
202
+ 'main-audit.js must compare each hit against its own registry floor');
203
+ });
204
+
183
205
  // ---------- the system-permission mapping flows from the registry ----------
184
206
 
185
207
  test('every entry maps to a permission the catalog defines as system:true', () => {
@@ -1,9 +1,15 @@
1
1
  // tests/idea_ideate_full.mjs
2
2
  //
3
- // task 1002931 (BV1.R155, goal 1000062) — /ideate authors a Full Idea
4
- // conversationally.
3
+ // task 1002931 (BV1.R155, goal 1000062) — the ideator's playbook authors a Full
4
+ // Idea conversationally.
5
5
  //
6
- // A skill is a prompt, so the thing to pin is the PROMPT'S CONTRACT. Two
6
+ // WHERE THAT PLAYBOOK LIVES (task 1002990). It used to be the /ideate skill. The
7
+ // kernel/pack split moved it into the Ideator role pack and deleted the skill, so
8
+ // this suite now reads docs/packs/ideator.md. Every contract below is unchanged —
9
+ // the file moved, the promises did not — and pointing the suite at the new home is
10
+ // what stops the split from silently dropping the guard with the skill.
11
+ //
12
+ // A pack is a prompt, so the thing to pin is the PROMPT'S CONTRACT. Two
7
13
  // done-when clauses, and both are checkable:
8
14
  //
9
15
  // 1. "indistinguishable in the database from one authored in the hall" — so the
@@ -30,7 +36,7 @@ import { fileURLToPath } from 'node:url';
30
36
 
31
37
  const require = createRequire(import.meta.url);
32
38
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
- const SKILL = fs.readFileSync(path.join(ROOT, '.claude/skills/ideate/SKILL.md'), 'utf8');
39
+ const SKILL = fs.readFileSync(path.join(ROOT, 'docs/packs/ideator.md'), 'utf8');
34
40
  // Prose assertions run against a WHITESPACE-NORMALISED copy. Markdown is hard-
35
41
  // wrapped, so any phrase long enough to be worth asserting will eventually
36
42
  // straddle a newline — and a regex that does not allow for that fails on a
@@ -134,6 +134,101 @@ t('#863-followup (finding #6): a sub-Metic author NAME still resolves and BLOCKS
134
134
  assert.equal(r.findings[0].rank, 'thetes');
135
135
  });
136
136
 
137
+ // -- per-hit registry floors (task 1003191) --------------------------------
138
+ //
139
+ // The authoritative layer used to decide floors with ppc.isBelowFloor(rank),
140
+ // which tests the registry's GLOBAL MINIMUM floor. Every live surface floors at
141
+ // 'metic', so that was indistinguishable from a per-hit comparison — until
142
+ // governance RAISES one surface, at which point layer 3 silently downgraded the
143
+ // raise back to the minimum while the pre-push hook and the grader (both on
144
+ // checkPermissionPaths, which compares per-hit) still blocked it. Layer 3 is the
145
+ // only one that catches a raw `git push origin main`, so the authoritative net
146
+ // was the loosest of the three.
147
+ //
148
+ // A uniform-floor fixture cannot tell the two implementations apart. This is the
149
+ // shape that can: one surface raised above the minimum, one left at it.
150
+ const ppc = require('../src/bongos/permission-path-check.js');
151
+
152
+ function mixedFloorRegistry() {
153
+ const SURFACES = [
154
+ { glob: 'infra/', floor: 'archon' }, // RAISED above the global minimum
155
+ { glob: 'src/bongos/db.js', floor: 'metic' }, // still at the minimum
156
+ ];
157
+ return {
158
+ RANK_TIER: ppc.RANK_TIER,
159
+ tierOf: (rank) => ppc.RANK_TIER[rank] ?? null,
160
+ matchProtected: (files) => (files || [])
161
+ .map((f) => {
162
+ const s = SURFACES.find((x) => (x.glob.endsWith('/') ? f.startsWith(x.glob) : f === x.glob));
163
+ return s ? { file: f, glob: s.glob, floor: s.floor, permission: 'path.protected.modify' } : null;
164
+ })
165
+ .filter(Boolean),
166
+ };
167
+ }
168
+
169
+ t('a RAISED surface floor is enforced: Metic touching an archon-floored path → BLOCKER', () => {
170
+ const r = analyzeCommits([{
171
+ sha: 'r01', subject: 'touch the substrate', authorName: 'SomeMetic', authorEmail: 'm@x',
172
+ isMerge: false, files: ['infra/deploy.sh'],
173
+ }], RANKS, { registry: mixedFloorRegistry() });
174
+ assert.equal(r.hardFail, true, 'a governance floor raise must reach the authoritative layer');
175
+ assert.equal(r.findings.length, 1);
176
+ assert.deepEqual(r.findings[0].floors, ['archon']);
177
+ assert.match(r.findings[0].summary, /Archon\+ builder/);
178
+ });
179
+
180
+ t('the finding reports only the paths the author actually fails to clear', () => {
181
+ // db.js floors at the minimum this Metic DOES clear; infra/ does not. A
182
+ // per-hit comparison names the second and stays silent about the first.
183
+ const r = analyzeCommits([{
184
+ sha: 'r02', subject: 'mixed', authorName: 'SomeMetic', authorEmail: 'm@x',
185
+ isMerge: false, files: ['src/bongos/db.js', 'infra/deploy.sh'],
186
+ }], RANKS, { registry: mixedFloorRegistry() });
187
+ assert.equal(r.hardFail, true);
188
+ assert.deepEqual(r.findings[0].files, ['infra/deploy.sh']);
189
+ assert.equal(r.findings[0].summary.includes('src/bongos/db.js'), false,
190
+ 'a path the author clears must not be reported as a violation');
191
+ });
192
+
193
+ t('a raised floor does not over-block: Archon touching the same path → no finding', () => {
194
+ const r = analyzeCommits([{
195
+ sha: 'r03', subject: 'x', authorName: 'somearchon', authorEmail: 'l@x',
196
+ isMerge: false, files: ['infra/deploy.sh'],
197
+ }], RANKS, { registry: mixedFloorRegistry() });
198
+ assert.equal(r.hardFail, false);
199
+ assert.equal(r.findings.length, 0);
200
+ });
201
+
202
+ t('a Metic still clears a surface left at the minimum floor', () => {
203
+ const r = analyzeCommits([{
204
+ sha: 'r04', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
205
+ isMerge: false, files: ['src/bongos/db.js'],
206
+ }], RANKS, { registry: mixedFloorRegistry() });
207
+ assert.equal(r.hardFail, false);
208
+ assert.equal(r.findings.length, 0);
209
+ });
210
+
211
+ t('an unresolved author is told the floor that actually governs, not a hardcoded Metic', () => {
212
+ const r = analyzeCommits([{
213
+ sha: 'r05', subject: 'x', authorName: 'Nobody Known', authorEmail: 'n@x',
214
+ isMerge: false, files: ['infra/deploy.sh'],
215
+ }], RANKS, { registry: mixedFloorRegistry() });
216
+ assert.equal(r.findings.length, 1);
217
+ assert.equal(r.findings[0].severity, 'review');
218
+ assert.match(r.findings[0].summary, /Archon\+ work/);
219
+ });
220
+
221
+ t('the default registry is the real one — no injection needed in production', () => {
222
+ // The seam must not change what the CLI does: infra/ floors at metic today,
223
+ // so a Metic touching it is legal on the live registry.
224
+ const r = analyzeCommits([{
225
+ sha: 'r06', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
226
+ isMerge: false, files: ['infra/deploy.sh'],
227
+ }], RANKS);
228
+ assert.equal(r.hardFail, false);
229
+ assert.equal(ppc.floorFor('infra/deploy.sh'), 'metic');
230
+ });
231
+
137
232
  // -- the default range window (task 1002566) -------------------------------
138
233
  //
139
234
  // Everything above tests the pure ANALYZER. Nothing tested what FEEDS it, which
@@ -231,9 +231,10 @@ test('methodology joined the roster (task 1002989) — the instruction surfaces
231
231
  const covers = (touch) => gsc.scopeSubset(['methodology'], [touch]).ok;
232
232
  for (const touch of [
233
233
  'CLAUDE.md', // the kernel half of the split (1002990)
234
- '.claude/skills/dev/SKILL.md', // one of the three discipline skills it deletes
235
- '.claude/skills/ideate/SKILL.md',
236
- '.claude/skills/paint/SKILL.md',
234
+ 'docs/packs/engineer.md', // the three role packs the split authored (1002990)
235
+ 'docs/packs/artist.md',
236
+ 'docs/packs/ideator.md',
237
+ '.claude/skills/status/SKILL.md', // the skills floor no module claims
237
238
  '.claude/rules/content-charter.md', // pre-carved for task 1003687
238
239
  'docs/adr/0264-the-ten-working-areas.md', // the ADR library
239
240
  'docs/handoff-template.md',
@@ -77,7 +77,8 @@ const PUBLISH = [
77
77
  'docs/handoff-template.md', // portable session-handoff format
78
78
  'docs/project-context.template.md', // the neutral scaffold (NOT the OTB-filled one)
79
79
  'config/design-tokens.schema.json', // generic contract schema
80
- '.claude/skills/dev/SKILL.md',
80
+ '.claude/skills/status/SKILL.md',
81
+ 'docs/packs/engineer.md', // the role packs the kernel/pack split moved the methodology into (task 1002990)
81
82
  'package.json',
82
83
  'Dockerfile',
83
84
  '.env.local.example',
@@ -201,14 +201,28 @@ test('claimed: neutral tone, scope rows, discipline action, brief in dropdown',
201
201
  assert.equal(row(m, 'Estimate'), '~2h');
202
202
  assert.equal(row(m, 'Touches'), '4 files');
203
203
  assert.equal(row(m, 'Value'), undefined); // no value summary pre-work
204
- assert.equal(m.action.cmd, '/dev');
204
+ assert.equal(m.action.cmd, 'Read docs/packs/engineer.md and start the work');
205
205
  const html = card.renderCardHtml(m);
206
206
  assert.match(html, /Extend the card to every stage\./); // brief in dropdown
207
207
  });
208
208
 
209
- test('claimed action routes by discipline', () => {
210
- assert.equal(card.buildCardModel({ stage: 'claimed', taskId: 1, discipline: 'artist', brand: BRAND }).action.cmd, '/paint');
211
- assert.equal(card.buildCardModel({ stage: 'claimed', taskId: 1, discipline: 'ideator', brand: BRAND }).action.cmd, '/ideate');
209
+ // The card must open the SAME playbook the claim just named, or it reintroduces the
210
+ // 1003487 divergence. Since task 1002990 the three core crafts have no slash command
211
+ // left to open — /dev, /paint and /ideate were deleted with the kernel/pack split —
212
+ // so the card points at the pack file; a module-contributed discipline still routes
213
+ // to its skill. A '/' prefix on a core craft here means a dead command in the hall.
214
+ test('claimed action routes by discipline — core crafts to their pack, modules to their skill', () => {
215
+ const cmdFor = (discipline) => card.buildCardModel({ stage: 'claimed', taskId: 1, discipline, brand: BRAND }).action.cmd;
216
+ assert.equal(cmdFor('artist'), 'Read docs/packs/artist.md and start the work');
217
+ assert.equal(cmdFor('ideator'), 'Read docs/packs/ideator.md and start the work');
218
+ assert.equal(cmdFor('ui'), '/design');
219
+ // An unknown or unset discipline falls back to the Engineer pack, exactly as it
220
+ // used to fall back to /dev — engineering is the project's default loop.
221
+ assert.equal(cmdFor('unclassified'), 'Read docs/packs/engineer.md and start the work');
222
+ assert.equal(cmdFor(undefined), 'Read docs/packs/engineer.md and start the work');
223
+ for (const d of ['engineer', 'artist', 'ideator']) {
224
+ assert.ok(!cmdFor(d).startsWith('/'), `${d} must not be routed to a slash command — /dev, /paint and /ideate no longer exist`);
225
+ }
212
226
  });
213
227
 
214
228
  // ---- review ----
@@ -1,49 +0,0 @@
1
- ---
2
- name: dev
3
- description: The engineering session — the operating playbook for engineer-discipline work (code, infra, Bongos itself, bugs, performance). Triggers when the user says "/dev", "let's build this", "work on this bug/feature", "start a dev session", or when a claimed task's discipline routes here (claim.js prints a directive to invoke /dev for engineer tasks). This is the project's DEFAULT build loop, written down — the baseline the other discipline modes (/ideate, /paint) invert from.
4
- ---
5
-
6
- You are running an **engineering session** for Example. Unlike `/ideate` (which flips *who drives*) and `/paint` (which flips *the medium*), this skill introduces no new dynamic — engineering **is** the default text-and-code loop. Its job is to make the standard explicit and keep you on it. Read it before working; lean on it most when you're new here or running autonomously.
7
-
8
- ## The loop: understand → scoped change → verify → ship
9
-
10
- 1. **You hold a claim — keep the work inside it.** Everything you change should serve the claimed task. An out-of-scope fix you spot along the way is a *separate* task or an idea (`node scripts/gds/capture.js`), not a quiet addition to this diff. Scope creep is the most common way an engineering session goes wrong.
11
-
12
- 2. **Understand before you edit.** Read the code you're about to change and the layer around it first:
13
- - The nested `CLAUDE.md` in the directory you're working in (loads on demand — it has the gotchas).
14
- - `docs/repo-map.md` (the symbol skeleton) to navigate without reading every file — it's gitignored + regenerated at deploy/box-fetch (ADR 0110), so if it's absent in your tree run `node scripts/gds/gen-repo-map.js`. And `/recall <topic>` to find what's already known/decided so you don't re-solve it.
15
- - Match the surrounding code: its naming, its idioms, its comment density. Code you write should read like the code already there.
16
-
17
- 3. **Make the smallest correct change.** Prefer the change that solves the task and nothing more. Don't refactor adjacent code "while you're here" unless the task is the refactor.
18
-
19
- 4. **Verify it — never report done on faith.** This is the step engineers most often skip:
20
- - Run the smoke tests (`/builder-ship` runs them, but run them yourself while iterating).
21
- - If the change is observable in the running app, use the **preview / verification workflow** (preview_start → reload → check console/snapshot → screenshot proof) rather than asking a human to check manually. Verify, then share the proof.
22
- - If it's not browser-observable (a different runtime, types, tooling), say so and verify the way that *does* exercise it.
23
-
24
- 5. **Ship with a real handoff.** `/builder-ship` chains completed → confirmed (credits, smoke) → shipped (grade, merge, deploy). Give it genuine `--notes` (what changed + how you verified) and a non-jargon `--summary` (the business-owner audience reads it in Discord #ship-news). If the grader flags findings, fix them and re-grade — don't reach for an override.
25
-
26
- ## Supporting discipline (the things that keep the ledger honest)
27
-
28
- - **Every change is backed by the claimed task** — no admin/tiny-fix/scaffolding loophole (CLAUDE.md §8). Direct DB writes are work too.
29
- - **Non-obvious decision → an ADR** in `docs/adr/`, linked from the shipping task's notes.
30
- - **A discovery that took >10 min to diagnose → a learning** (`node scripts/gds/learning-capture.js`); a multi-page how-to → a recipe under `docs/recipes/`.
31
- - **Blocked on something only Lars can decide/provide → a blocker** (`POST /api/gds/blockers`), attached to the task.
32
- - **Touched anything the onboarding diagrams / repo-map describe** → they regenerate on deploy; never hand-edit generated artifacts — edit the source/template and run the generator (`gen-diagrams.js`, `gen-repo-map.js`).
33
- - **Trust boundary:** a sub-Metic builder (Xenos/Thetes) stays out of the permission/pipeline core (rank/authz machinery, the ship/grade/deploy pipeline, `migrations/`, `infra/`, trust-boundary docs) and never runs raw `git push origin main` / `ssh` / `~/deploy.sh` — landing code is Bongos's job (`/builder-ship`). Metic+ is unaffected.
34
-
35
- ## Step 0 — detect the mode
36
-
37
- **Is a human present?**
38
-
39
- - **Interactive** (someone is here to decide): work collaboratively — propose the approach for anything non-obvious, let the human steer the design calls, then execute. You can think out loud; engineers are fine with text.
40
- - **Autonomous / bypass-permissions** (no human will answer): tighten up.
41
- - Be **conservative** — the smallest correct change, the lowest-risk path. No human is here to catch a wandering edit.
42
- - **Verify harder**, not less — smoke + preview proof before you call it done, because nobody else will look first.
43
- - **Don't invent scope or wander** into adjacent work, refactors, or "improvements" the task didn't ask for. If you spot them, capture them as ideas/tasks and move on.
44
- - If the task is **ambiguous or you hit a real blocker**, leave a clear note (blocker / release with a reason) rather than guessing at something large and irreversible.
45
-
46
- ## How sessions reach this skill
47
-
48
- - **Standalone:** invoke `/dev` directly when you want the engineering checklist in front of you.
49
- - **From a claim:** when a builder claims an `engineer`-discipline task, `claim.js` reads `scripts/gds/discipline-modes.json` and notes that `/dev` is this task's playbook. Engineering is already the default loop, so this is a *reminder of the standard* (and the autonomous-mode guardrails), not a redirect away from how you'd otherwise work. All three disciplines route uniformly through the same map.