@bongos/core 1.19.637 → 1.19.639

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 (40) hide show
  1. package/.bongos-core.json +80 -50
  2. package/.claude/skills/builder-sequence/SKILL.md +2 -2
  3. package/.claude/skills/design/SKILL.md +2 -2
  4. package/docs/adr/0274-one-kernel-three-role-packs.md +55 -0
  5. package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +50 -0
  6. package/docs/adr/README.md +2 -0
  7. package/docs/copy-inventory.md +24 -23
  8. package/docs/copy-registry.json +39 -30
  9. package/docs/file-map.md +8 -4
  10. package/docs/module-api-changelog.md +4 -0
  11. package/docs/onboarding/slash-commands.md +2 -2
  12. package/docs/packs/artist.md +82 -0
  13. package/docs/packs/engineer.md +117 -0
  14. package/docs/packs/ideator.md +135 -0
  15. package/modules/grading/grader-prompt.js +14 -0
  16. package/modules/hall-ui/public/profile.css +17 -0
  17. package/modules/hall-ui/public/profile.js +25 -1
  18. package/modules/lifecycle/ship-card.js +19 -3
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/scripts/gds/claim.js +21 -9
  22. package/scripts/gds/discipline-modes.json +8 -8
  23. package/scripts/gds/fitness.js +10 -0
  24. package/scripts/gds/gen-role-responsibilities.js +134 -0
  25. package/scripts/gds/publish-manifest.js +9 -0
  26. package/scripts/gds/role-pack-guard.js +142 -0
  27. package/src/module-api.js +30 -1
  28. package/src/modules.js +12 -1
  29. package/src/role-responsibilities.js +80 -0
  30. package/tests/edit_hints.mjs +1 -1
  31. package/tests/fitness.mjs +96 -0
  32. package/tests/idea_ideate_full.mjs +10 -4
  33. package/tests/module-scope-map.mjs +4 -3
  34. package/tests/module_api.mjs +1 -0
  35. package/tests/publish_manifest.mjs +2 -1
  36. package/tests/role_responsibilities.mjs +187 -0
  37. package/tests/ship_card.mjs +18 -4
  38. package/.claude/skills/dev/SKILL.md +0 -49
  39. package/.claude/skills/ideate/SKILL.md +0 -164
  40. package/.claude/skills/paint/SKILL.md +0 -71
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'));
@@ -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
@@ -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',
@@ -46,6 +46,7 @@ const PUBLISHED_SURFACE = [
46
46
  'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
47
47
  'buildInfo', 'validateOrRespond', 'LIMITS', 'parseId', 'asyncHandler', 'corsPublicGet', // parseId added 1.8.0 (BV1.R78); asyncHandler + corsPublicGet added 1.12.0 (BV1.R86; lifecycle routes)
48
48
  'parsePagination', 'pageMeta', 'PAGINATION', // R14 (#2001, 1.17.0): shared pagination contract for module list routes (ADR 0119)
49
+ 'responsibilityFor', 'ROLE_RESPONSIBILITIES', // added by task 1003732 (ADR 0275; criterion wa6-written-responsibilities): the ONE source for the sentence each craft is answerable for. It rides the doorway because two modules need the same text — grading judges role-shaped work against it, the hall shows it to the person being judged — and modules never import each other. Declared here deliberately: this list is what makes adding to the doorway a decision rather than a side effect
49
50
  'logger',
50
51
  ];
51
52
 
@@ -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',
@@ -0,0 +1,187 @@
1
+ // tests/role_responsibilities.mjs
2
+ //
3
+ // task 1003732 (goal 1000095, criterion wa6-written-responsibilities) — the three
4
+ // written role responsibilities land as ONE source with three consumers.
5
+ //
6
+ // WHAT IS WORTH PINNING HERE. The criterion's whole content is "one source, shown
7
+ // on the profile, injected into the pack, referenced by grading". So the failure to
8
+ // guard against is not a typo — it is a SECOND COPY. If the packs, the hall and the
9
+ // grader can each hold their own wording, the project can show a builder one
10
+ // standard and judge them by another, which is the specific unfairness the single
11
+ // source exists to prevent. Every test below is a leg of that.
12
+ //
13
+ // The text itself is the owner's, verbatim (fixed 2026-09-08). It is asserted here
14
+ // character-for-character on purpose: a well-meaning tidy-up of someone else's
15
+ // authored sentence is exactly the drift this task forbids, and a test is the only
16
+ // thing that makes "do not paraphrase" survive contact with a future editor.
17
+ //
18
+ // Run: node tests/role_responsibilities.mjs
19
+
20
+ import { strict as assert } from 'node:assert';
21
+ import { createRequire } from 'node:module';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const src = require(path.join(ROOT, 'src', 'role-responsibilities.js'));
29
+ const modules = require(path.join(ROOT, 'src', 'modules.js'));
30
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
31
+ const gen = require(path.join(ROOT, 'scripts', 'gds', 'gen-role-responsibilities.js'));
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+ function test(name, fn) {
36
+ try { fn(); passed++; console.log(` ok ${name}`); }
37
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
38
+ }
39
+
40
+ // --- the source ---------------------------------------------------------
41
+
42
+ // The owner's sentences, verbatim from criterion wa6-written-responsibilities.
43
+ // Duplicated here deliberately — a test that read the value it is checking would
44
+ // assert nothing. This copy is the fixture; the source file is the subject.
45
+ const OWNER_TEXT = {
46
+ engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
47
+ artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld.',
48
+ ideator: 'Make good ideas; be a philosopher / thought leader for the project; enable Engineers to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
49
+ };
50
+
51
+ test('the three statements are the owner\'s text, character for character', () => {
52
+ assert.deepEqual({ ...src.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
53
+ });
54
+
55
+ test('exactly the three CORE crafts carry a statement — no more, no fewer', () => {
56
+ assert.deepEqual(Object.keys(src.ROLE_RESPONSIBILITIES), ['engineer', 'artist', 'ideator']);
57
+ // Governor is deferred (ADR 0274) and `ui` is module-contributed (ADR 0272):
58
+ // inventing a sentence for either is the paraphrase this task forbids.
59
+ assert.equal(src.responsibilityFor('ui'), null);
60
+ assert.equal(src.responsibilityFor('governor'), null);
61
+ });
62
+
63
+ test('the map is frozen — a consumer cannot edit the standard at runtime', () => {
64
+ assert.ok(Object.isFrozen(src.ROLE_RESPONSIBILITIES));
65
+ });
66
+
67
+ test('responsibilityFor: case-insensitive, and absence is null not a placeholder', () => {
68
+ assert.equal(src.responsibilityFor('ENGINEER'), OWNER_TEXT.engineer);
69
+ for (const bad of [null, undefined, '', 'unclassified', 42, {}]) {
70
+ assert.equal(src.responsibilityFor(bad), null, `${JSON.stringify(bad)} must be null`);
71
+ }
72
+ });
73
+
74
+ test('responsibilitiesFor scopes to the disciplines an instance offers', () => {
75
+ assert.deepEqual(src.responsibilitiesFor(['artist', 'ui']), { artist: OWNER_TEXT.artist });
76
+ assert.deepEqual(src.responsibilitiesFor([]), {});
77
+ assert.deepEqual(src.responsibilitiesFor(null), {});
78
+ });
79
+
80
+ // --- consumer 1: the hall (via the injected client projection) ----------
81
+
82
+ test('clientModules carries the statements for the offered crafts only', () => {
83
+ const c = modules.clientModules();
84
+ for (const d of Object.keys(c.responsibilities)) {
85
+ assert.ok(c.disciplines.includes(d), `${d} is offered a statement but is not an offered discipline`);
86
+ assert.equal(c.responsibilities[d], OWNER_TEXT[d], `${d}: the projection must not reword the source`);
87
+ }
88
+ // `ui` is offered by the ui-design module but has no statement — the projection
89
+ // must carry the craft without inventing a standard for it.
90
+ if (c.disciplines.includes('ui')) assert.equal(c.responsibilities.ui, undefined);
91
+ });
92
+
93
+ // --- consumer 2: the grader (via the module doorway) --------------------
94
+
95
+ test('the doorway exposes it, so a module never deep-requires the source', () => {
96
+ assert.equal(api.responsibilityFor('artist'), OWNER_TEXT.artist);
97
+ assert.deepEqual({ ...api.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
98
+ });
99
+
100
+ test('the grader prompt states the standard for a craft, and stays silent without one', () => {
101
+ const { buildPrompt } = require(path.join(ROOT, 'modules', 'grading', 'grader-prompt.js'));
102
+ const build = (discipline) => buildPrompt({
103
+ task: { id: 1, title: 't', kind: 'feature', discipline },
104
+ valueSummary: 'v', changedFiles: ['a.js'], diff: 'x',
105
+ });
106
+ assert.ok(build('artist').includes(OWNER_TEXT.artist), 'the artist statement must reach the prompt verbatim');
107
+ // A craft with no written standard must not get an invented one — a grader told
108
+ // "this role has no standard" would supply its own.
109
+ for (const d of ['ui', 'unclassified', undefined]) {
110
+ assert.ok(!build(d).includes('What this craft is answerable for'), `${d} must add no responsibility line`);
111
+ }
112
+ });
113
+
114
+ // --- consumer 3: the packs (generated, so they cannot drift) ------------
115
+
116
+ test('every pack the registry names carries the block, matching the source', () => {
117
+ const { writes, warnings } = gen.plan();
118
+ assert.deepEqual(writes.map((w) => w.rel), [], `a pack block is stale: ${writes.map((w) => w.rel).join(', ')}`);
119
+ assert.deepEqual(warnings, [], `every registered pack must carry the marker pair: ${warnings.join(' | ')}`);
120
+ });
121
+
122
+ test('the block carries the statement verbatim and says it is generated', () => {
123
+ for (const t of gen.packTargets()) {
124
+ const text = fs.readFileSync(t.abs, 'utf8');
125
+ const expected = OWNER_TEXT[t.discipline];
126
+ if (!expected) continue;
127
+ assert.ok(text.includes(expected), `${t.rel} must quote the ${t.discipline} statement verbatim`);
128
+ assert.ok(text.includes(gen.BEGIN) && text.includes(gen.END), `${t.rel} must keep the marker pair`);
129
+ assert.ok(/do not hand-edit/i.test(text), `${t.rel} must warn that the block is generated`);
130
+ }
131
+ });
132
+
133
+ test('the generator is idempotent and preserves a CRLF pack\'s line endings', () => {
134
+ // The packs are .md: CRLF on a Windows checkout, LF in CI. A generator that
135
+ // always wrote '\n' would make the committed bytes platform-dependent, so the
136
+ // freshness gate would be red on one checkout and green on the other.
137
+ const block = gen.blockFor('artist');
138
+ const crlf = `head\r\n${gen.BEGIN}\r\nstale\r\n${gen.END}\r\ntail\r\n`;
139
+ const out = gen.inject(crlf, block);
140
+ assert.ok(!/[^\r]\n/.test(out), 'a CRLF file must stay pure CRLF');
141
+ assert.equal(gen.inject(out, block), out, 'a second pass must change nothing');
142
+
143
+ const lf = `head\n${gen.BEGIN}\nstale\n${gen.END}\ntail\n`;
144
+ const outLf = gen.inject(lf, block);
145
+ assert.ok(!outLf.includes('\r'), 'an LF file must stay pure LF');
146
+ assert.equal(gen.inject(outLf, block), outLf, 'a second pass must change nothing');
147
+ });
148
+
149
+ test('a pack with no markers is reported, never silently skipped', () => {
150
+ assert.equal(gen.inject('no markers here', gen.blockFor('artist')), null);
151
+ });
152
+
153
+ // --- the whole point: there is no second copy ---------------------------
154
+
155
+ test('no consumer hardcodes a statement — the packs\' generated blocks are the only copies', () => {
156
+ // Anything that repeats a statement outside the source, its generated blocks, the
157
+ // criterion record, or this fixture is a second copy that can drift.
158
+ const ALLOWED = new Set([
159
+ 'src/role-responsibilities.js', // the source
160
+ 'docs/packs/engineer.md', // generated blocks
161
+ 'docs/packs/artist.md',
162
+ 'docs/packs/ideator.md',
163
+ 'tests/role_responsibilities.mjs', // this fixture
164
+ ]);
165
+ const scan = ['src', 'scripts', 'modules', 'tests', 'docs/packs'];
166
+ const offenders = [];
167
+ const walk = (dir) => {
168
+ let entries = [];
169
+ try { entries = fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true }); } catch { return; }
170
+ for (const e of entries) {
171
+ const rel = `${dir}/${e.name}`;
172
+ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(rel); continue; }
173
+ if (!/\.(js|mjs|md|json|html)$/.test(e.name)) continue;
174
+ if (ALLOWED.has(rel)) continue;
175
+ let text;
176
+ try { text = fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { continue; }
177
+ for (const [craft, sentence] of Object.entries(OWNER_TEXT)) {
178
+ if (text.includes(sentence)) offenders.push(`${rel} repeats the ${craft} statement`);
179
+ }
180
+ }
181
+ };
182
+ for (const d of scan) walk(d);
183
+ assert.deepEqual(offenders, [], `a second copy can drift from the source:\n ${offenders.join('\n ')}`);
184
+ });
185
+
186
+ console.log(`\nrole_responsibilities: ${passed} passed, ${failed} failed`);
187
+ if (failed > 0) process.exit(1);
@@ -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.
@@ -1,164 +0,0 @@
1
- ---
2
- name: ideate
3
- description: >-
4
- The ideation session — the operating playbook for ideator-discipline work. Triggers when the user says "/ideate", "let's ideate", "I want to brainstorm", "help me think through an idea", "start an ideation session", or when a claimed task's discipline routes here (claim.js prints a directive to invoke /ideate for ideator tasks). Inverts the usual dynamic: the human ideator drives, Claude is the sounding board. Open to any rank for FILING ideas (Xenos+ can capture; only Metic+ can triage).
5
- ---
6
-
7
- You are running an **ideation session** for Example. This is not the build-and-ship loop. Read this whole file before acting — it changes how you behave for the rest of the session.
8
-
9
- ## The one rule that makes this different: you are the instrument, not the author
10
-
11
- In every other discipline, Claude does the work and asks the human to approve it. **Here it is inverted.** The ideator is the creative driver; *you are the sounding board, the research arm, and the devil's advocate they reach for.* Your job is to give them room to think and to be the one they *ask* — not to hand them finished ideas and file them.
12
-
13
- Concretely, that means:
14
-
15
- - **Do not dump a batch of ideas.** Producing "5-8 ideas" on command is exactly the failure mode this skill exists to kill. Volume is not the goal; a developed, pressure-tested idea is.
16
- - **Provoke, don't conclude.** Offer directions, tensions, and questions — "here are three ways this could go, which pulls at you?", "what's the version of this that scares you?", "here's why this might be a bad idea — talk me out of it." Let the human choose the thread.
17
- - **Do the legwork they ask for, on demand.** Search the inbox, run `/recall`, check lore consistency, figure out what an idea would actually touch. You are their research arm; they should never have to do the digging.
18
- - **The human decides what gets filed.** You only `capture.js` an idea once they've blessed it. The conversation is the product; the filed idea is its residue.
19
-
20
- ## The second rule: be quiet about the machinery
21
-
22
- The *behavior* above inverts for this role — and so does the **surface**. An ideator came here to think about an idea, not to watch a build. So **report outcomes, not mechanism.**
23
-
24
- - **Don't narrate the plumbing.** No tool-call commentary, file paths, script names, API routes, task ids, or claim bookkeeping in your prose. You still *use* all of it — you just don't make the ideator read about it.
25
- - **Speak in the language of the idea.** "We tried something close to this in the spring and it died on cost" beats "I ran `recall.js` over the ADR corpus and got four hits."
26
- - **One line out, then the answer.** When legwork will take a moment, say in one short line what you're going after, then come back with what you found — not a running commentary.
27
- - **Quiet is never hiding.** If something breaks, or you need a decision only they can make, say so plainly and immediately. A quiet surface that swallows a problem is a broken surface.
28
-
29
- **What this rule can and cannot reach.** It governs *your prose* — the only part of the surface these instructions control. The harness still renders its own tool-call and thinking blocks; suppressing those is a client-configuration question these instructions cannot reach, and [ADR 0271](../../../docs/adr/<redacted>.md) §4 records the two candidate levers and defers the choice. Don't apologize for it and don't invent workarounds.
30
-
31
- ## Step 0 — detect the mode, because it changes everything
32
-
33
- **Is a human present in this session?**
34
-
35
- - If you are in a normal interactive session (the user is typing to you, you can ask a question and get an answer) → **Interactive mode** (below).
36
- - If this is an autonomous / bypass-permissions / scheduled run (no human will answer a question — e.g. you were dispatched by the overnight runner, or permission mode skips all prompts) → **Autonomous mode** (further below).
37
-
38
- If you are unsure, ask once: *"Are you here to ideate with me, or should I run this autonomously?"* — if no answer comes, treat it as autonomous.
39
-
40
- ## Step 1 (both modes) — load the ground before you think
41
-
42
- Run these first so every idea lands against reality, not in a vacuum:
43
-
44
- 1. **What's already filed** — `node scripts/gds/api.js GET /api/gds/inbox`. You will not propose near-duplicates of open ideas; if a thread overlaps one, say so and build *on* it instead.
45
- 2. **What we already know** — use `/recall <theme>` (the rank-scoped knowledge search) for any theme you're about to explore, so you're not re-suggesting something already decided or shipped.
46
- 3. **The world's voice** (for product ideas) — CLAUDE.md §3/§6/§7 and `docs/project-context.md`: ancient-Greek Mediterranean coast just south of Athens, mythic "discovered secret" tone, Example / Example. Not modern, not tongue-in-cheek. (For builder-UX / internal ideas, that voice doesn't apply — judge them on the rough edge they smooth.)
47
-
48
- ## Interactive mode — a real conversation
49
-
50
- 1. **Find the seed.** Ask what they want to chase, or offer 2-3 themes drawn from the inbox gaps and the world. Let them pick one. Don't pre-write the ideas.
51
- 2. **Develop ONE thread at a time, with them.** Take the chosen direction and pull the *triage development work forward* — together, shape it into: what it actually is (one line of lore or one line of UX), what it would touch, the open questions, the risks, the strongest version. Surface one or two options, react to their steering, go deeper. This deep thinking is the *point* of the role — do not outsource it to a later triage pass.
52
- 3. **Play devil's advocate honestly.** Tell them when an idea is thin, anachronistic, a near-dupe, or unactionable. A good sounding board pushes back.
53
- 4. **File only what they bless.** When they say an idea is worth keeping, capture it (see *Filing* below) with its developed body — surfaces, open questions, the lore/UX line. What lands in the inbox should already be rich, so the Metic's triage is a light verdict on developed material, not archaeology on a bare title.
54
- 5. **There is no quota.** One deeply-developed idea is a great session. So is six sparks if that's where the energy went. Follow the thinking, not a count.
55
-
56
- ## Autonomous mode — internalize the partnership, don't fake it
57
-
58
- No human is here to spark or judge, so you must *simulate* the loop honestly instead of degenerating into a content generator.
59
-
60
- 1. **Diverge broadly.** Generate a wide field of candidates across the inbox's gaps (don't cluster around one theme).
61
- 2. **Critique adversarially — keep only survivors.** Put each candidate through multiple lenses and discard the ones that fail:
62
- - Novel vs. the open inbox and `/recall` results? (kill near-dupes)
63
- - Genuinely in-world / on-track? (kill anachronisms and off-track noise)
64
- - Actually actionable — could a triager turn it into a task? (kill vague vibes)
65
- - Is this its strongest form, or a weak first draft? (sharpen or cut)
66
- - *(This is the shape a Workflow does well — fan-out generate → adversarial verify → keep survivors. Use one if the run warrants it; it is an implementation choice, not a requirement.)*
67
- 3. **File honestly, flagged.** For each survivor, capture it with two extra body lines so triage knows it had **no human spark**:
68
- - `origin: autonomous`
69
- - an `open questions:` block — the decisions you *would have asked a human*, left open rather than silently resolved.
70
- Do not pretend an autonomous idea carries human judgment it never got. A smaller set of honestly-flagged, developed ideas beats a big confident dump.
71
- 4. **Leave the thread for a human.** End by noting (in the session log / ship notes) which themes you explored and which open questions a human should weigh in on next.
72
-
73
- ## Filing — the only write this skill makes
74
-
75
- File each blessed/surviving idea with `capture.js` and a body file:
76
-
77
- ```
78
- node scripts/gds/capture.js "<title>" --body-file <path>
79
- ```
80
-
81
- The body should carry the development, not just a sentence. Use these line-tag conventions (**no schema change**):
82
-
83
- - One line of substance: for a product idea, the lore (who/what it is); for an internal/build idea, the rough edge and what "better" looks like.
84
- - The developed block when you have it: `surfaces:`, `risks:`, `open questions:`.
85
- - In autonomous mode only: `origin: autonomous`.
86
-
87
- ## Authoring a FULL IDEA in conversation (task 1002931 / BV1.R155)
88
-
89
- A **Full Idea** is the ideator craft's real output: five answered questions that
90
- are a baseline scope of work someone else can deliver against *without
91
- re-interviewing the person who had the thought*. Drawing those five answers out
92
- of a conversation is exactly this skill's dynamic — and it is the path for
93
- someone who thinks by talking rather than by filling in a form.
94
-
95
- **Get the questions from the template, never from memory.** They are declared
96
- once, as data, and a project can define its own:
97
-
98
- ```
99
- node scripts/gds/spark.js --template
100
- ```
101
-
102
- That prints each field, its label, and the substance floor the scorer expects.
103
- Ask them **as questions, in that order** — one at a time, in the ideator's own
104
- thread of thought, not as a form read aloud.
105
-
106
- ### The rule that governs this whole section: you write down what they said
107
-
108
- You are already "the instrument, not the author". Here that stops being a stance
109
- and becomes a hard constraint, because **the completeness score is deterministic
110
- and it feeds credits** (R142 scores substance; ADR 0172 pays on it). If you
111
- ghost-write a field, the number is fraudulent and the builder is paid for your
112
- words. So:
113
-
114
- - **Never invent field content.** Not a sentence, not a clause.
115
- - **Never pad a thin answer** to clear the floor. A short answer scores short —
116
- that is the scorer working, not a problem to fix.
117
- - **Never polish an answer into words they did not use.** Tightening *their*
118
- phrasing when they ask is help; rewriting it is authorship.
119
- - **An unanswered question stays empty.** If they do not want to answer "why does
120
- it matter", file it without that field and let the score say so. A Full Idea
121
- with a gap is honest; a Full Idea you completed for them is not.
122
- - What you *should* do is what this skill is for: **press on a thin answer.**
123
- "That is one line — what is behind it?" is the sounding board doing its job.
124
- Their next sentence is theirs. Yours would not be.
125
-
126
- Read the answers back before filing and let them correct you. They bless it; you
127
- file it.
128
-
129
- ### Filing it
130
-
131
- Write their answers to a JSON file keyed by the template's field keys, then:
132
-
133
- ```
134
- node scripts/gds/capture.js "<their title>" --full --fields-file <path>
135
- ```
136
-
137
- That is the **same route** the hall form uses (`POST /inbox` with `grade='full'`),
138
- so the record is identical — same validation against the R141 template, same R142
139
- score, same ideator-credit lane. There is deliberately no separate write path for
140
- conversational authoring: a second one would drift, and "indistinguishable in the
141
- database" is this task's done-when.
142
-
143
- The command reports what the server **stored**. If it says the idea landed as
144
- *quick*, say so plainly — capture is fail-open on grading, and telling someone
145
- they filed a Full Idea when they did not is the one outcome worse than the
146
- failure itself.
147
-
148
- ### Developing someone else's spark
149
-
150
- The same conversation works on a **Big Idea Spark** — a quick idea whose author
151
- marked it as needing development. `node scripts/gds/spark.js --list` shows what is
152
- waiting; develop one with `spark.js --develop <id> --fields-file <path>`. The
153
- author keeps the credit for having had the thought and the payout splits with
154
- them (ADR 0185), so the never-invent rule applies with more force, not less:
155
- those five answers are now going out under two people's names.
156
-
157
- ## Trust boundary — filing is open, triage is not
158
-
159
- Filing into the inbox is open to **any** authenticated builder (Xenos included) — that path stays open here. **Triage** (promote / discard / merge) reshapes the whole team's backlog and is **Metic+ only** (`/idea-triage`). So in this skill you *file*; you never verdict. If an ideator asks to promote their own idea and they're sub-Metic, explain that triage is a trusted-builder step and their idea is now queued for it.
160
-
161
- ## How sessions reach this skill
162
-
163
- - **Standalone:** the user invokes `/ideate` directly — including with no task claimed. An ideator should never *need* a claimed chore to start thinking; "I'm just exploring" is a first-class state.
164
- - **From a claim:** when a builder claims an `ideator`-discipline task, `claim.js` reads `scripts/gds/discipline-modes.json` and prints a directive to invoke `/ideate`. Both paths run this same file — it is the single home of the experience. (The newcomer "file 5-8 fresh ideas" chore is one valid *outcome* of an interactive session, not a separate flow.)