@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
@@ -1,26 +1,26 @@
1
1
  {
2
- "_doc": "Discipline -> operating-playbook map (task 1267). A discipline whose work is best done as a *mode of working* rather than the default claim->build->ship loop names the skill that IS its playbook here. claim.js reads this after a successful claim: if the claimed task's `discipline` has an entry, it prints the entry's `directive` so the session opens the right playbook. There is NO classification step at claim time — `discipline` is already a column on the task. This is the whole discipline-fork mechanism: one carrier (the claim the builder already runs), one data lookup, no hooks, the Conductor untouched. Absence of an entry is the default: engineer and artist are intentionally NOT listed, so their claim output is unchanged and they stay in the standard build-and-ship loop. To give a discipline its own experience later, add an entry here and write the skill no code change in claim.js. A missing or invalid version of this file must never break a claim (claim.js fails open). A mode entry may also set `\"materializeModuleSkills\": true` to have the claim land that mode's module-owned skills into this checkout's .claude/skills/ (task 1003486) — opt-in per discipline, core-checkout only, and fail-open like the hint itself, so a discipline that omits it claims exactly as before.",
2
+ "_doc": "The ROLE REGISTRY: discipline -> the operating playbook that craft works from (task 1267; reshaped by task 1002990). Since the kernel/pack split, a CORE craft names a `pack` a markdown role pack under docs/packs/ that carries everything the root CLAUDE.md kernel deliberately does not, because it is true for one craft only. A module-contributed discipline may instead name a `skill` (a slash-command playbook under .claude/skills/), which is how `ui` -> /design still works (ADR 0272 keeps ui-design's contribution unchanged). An entry may carry both; it must carry at least one. claim.js reads this after a successful claim and prints the entry's `directive` plus its pack path, so the session opens the right playbook. There is NO classification step at claim time — `discipline` is already a column on the task. One carrier (the claim the builder already runs), one data lookup, no hooks, the Conductor untouched. A missing or invalid version of this file must never break a claim (claim.js fails open) but fitness.js DOES hard-fail CI when an entry names a pack that is not in the tree, so the fail-open runtime cannot hide a broken registry from the build. GOVERNOR IS DEFERRED: three packs, not four, until the owner decides (criterion wa6-kernel-and-packs). A mode entry may also set \"materializeModuleSkills\": true to have the claim land that mode's module-owned skills into this checkout's .claude/skills/ (task 1003486) — opt-in per discipline, core-checkout only, and fail-open like the hint itself.",
3
3
  "modes": {
4
4
  "engineer": {
5
- "skill": "dev",
5
+ "pack": "docs/packs/engineer.md",
6
6
  "label": "Engineering session",
7
- "directive": "This is an ENGINEER task. /dev is its operating playbook the standard build loop, written down: understand the code before editing -> make the smallest correct, scoped change -> VERIFY it (smoke tests + the preview/verification workflow when observable) -> /builder-ship with real notes. Engineering is already the default way you'd work, so this is a reminder of the standard (and the autonomous-mode guardrails: be conservative, verify hard, don't wander), not a redirect. Open /dev if you want the checklist or you're running autonomously."
7
+ "directive": "This is an ENGINEER task. Read the Engineer pack now, before working — it is the operating playbook for this craft and the only place the standard build loop is written down: understand the code before editing -> make the smallest correct, scoped change -> VERIFY it (smoke tests + the preview/verification workflow when observable) -> /builder-ship with real notes. Engineering is already the default way you'd work, so the pack is the standard made explicit (plus the autonomous-mode guardrails: be conservative, verify hard, don't wander), not a redirect."
8
8
  },
9
9
  "ideator": {
10
- "skill": "ideate",
10
+ "pack": "docs/packs/ideator.md",
11
11
  "label": "Ideation session",
12
- "directive": "This is an IDEATOR task. To work it, invoke the /ideate skillthat skill is the operating playbook for ideation, and it is the only place the experience is defined. It inverts the usual dynamic: you (the human ideator) are the creative driver and Claude is your sounding board / research arm / devil's advocate, not a content generator filing ideas on command. If no human is present (autonomous / bypass-permissions), /ideate runs a self-adversarial divergence-and-critique loop instead of dumping a fixed batch. Open /ideate now rather than working this task the default way. THE SURFACE, not just the behavior (ADR 0271): an ideator session is QUIET about its own machinery. Report outcomes, not mechanism — do not narrate tool calls, file paths, script names, API routes or task/claim bookkeeping unless they ask or something actually failed. Speak in the language of the idea, not the plumbing. If a step breaks, say plainly what broke and what you need — quiet never means hiding a problem."
12
+ "directive": "This is an IDEATOR task. Read the Ideator pack now and work from it it is the operating playbook for ideation and the only place the experience is defined. It inverts the usual dynamic: you (the human ideator) are the creative driver and Claude is your sounding board / research arm / devil's advocate, not a content generator filing ideas on command. Quick ideas stay ONE LINE, and after one is filed Claude asks whether to develop it into a Full Idea — five questions, drawn out of the conversation in the ideator's own words and scored on completeness. If no human is present (autonomous / bypass-permissions), the pack runs a self-adversarial divergence-and-critique loop instead of dumping a fixed batch. THE SURFACE, not just the behavior (ADR 0271): an ideator session is QUIET about its own machinery. Report outcomes, not mechanism — do not narrate tool calls, file paths, script names, API routes or task/claim bookkeeping unless they ask or something actually failed. Speak in the language of the idea, not the plumbing. If a step breaks, say plainly what broke and what you need — quiet never means hiding a problem."
13
13
  },
14
14
  "artist": {
15
- "skill": "paint",
15
+ "pack": "docs/packs/artist.md",
16
16
  "label": "Art session",
17
- "directive": "This is an ARTIST task. To work it, invoke the /paint skillthat skill is the operating playbook for art sessions, and it is the only place the experience is defined. It is show-first / low-text: Claude communicates in pictures (display the generated tile, the /art gallery at sandbox-<login>.<instance-apex>/art, and the in-world sandbox), not paragraphs about what the pipeline did. It drives the existing pixel-art pipeline (otb-tile-generate / otb-design-review / otb-feedback-capture / art-stage.js) and keeps the locked palette + rubric sacred. If no human is present (autonomous / bypass-permissions), /paint generates, rubric-gates hard, stages passing assets to the gallery, and parks the visuals for human review. Open /paint now rather than working this task the default way; the gallery is at sandbox-<login>.<instance-apex>/art."
17
+ "directive": "This is an ARTIST task. Read the Artist pack now and work from it it is the operating playbook for art sessions and the only place the experience is defined. It is show-first and low-text: communicate in pictures and rendered surfaces, not paragraphs about what ran. Interpret as little as possible — take the brief literally, you are the artist's hands and not their art director — and arrive precalculated: load the look, have the options already rendered, never open with a form of intake questions. The artist's subject is how the project LOOKS AND READS, which is core (ADR 0272); a pixel-art or asset pipeline is a host module the pack drives only when the instance ships one. If no human is present (autonomous / bypass-permissions), make it, gate it hard, stage the passes and park the visuals for human review."
18
18
  },
19
19
  "ui": {
20
20
  "skill": "design",
21
21
  "materializeModuleSkills": true,
22
22
  "label": "UI design session",
23
- "directive": "This is a UI task — interface DESIGN work (layout, wireframes, design systems, Figma / Claude-Design), split out of the pixel-art `artist` lane (ADR 0079). To work it, invoke the /design skill — the operating playbook for UI-design sessions, the only place the experience is defined. Like /paint it is show-first: communicate in mock-ups and rendered screens, not paragraphs about layout decisions — stage the change on your sandbox preview (/builder-stage) and look at the real interface before shipping. Keep the locked branding (config/branding.json) and the existing component conventions of the surface you touch (modules/status-ui, modules/hall-ui, public/game/ui). If no human is present (autonomous / bypass-permissions), produce the design, stage it for review, and park the screens rather than shipping interface changes blind. Open /design now rather than working this task the default way."
23
+ "directive": "This is a UI task — interface DESIGN work (layout, wireframes, design systems, Figma / Claude-Design), split out of the pixel-art `artist` lane (ADR 0079) and kept as a module-contributed discipline by ADR 0272. To work it, invoke the /design skill — the operating playbook for UI-design sessions, the only place the experience is defined. Like the Artist pack it is show-first: communicate in mock-ups and rendered screens, not paragraphs about layout decisions — stage the change on your sandbox preview (/builder-stage) and look at the real interface before shipping. Keep the locked branding (config/branding.json) and the existing component conventions of the surface you touch (modules/status-ui, modules/hall-ui, public/game/ui). If no human is present (autonomous / bypass-permissions), produce the design, stage it for review, and park the screens rather than shipping interface changes blind. Open /design now rather than working this task the default way."
24
24
  }
25
25
  }
26
26
  }
@@ -188,6 +188,8 @@ function checkGeneratedArtifactsFresh() {
188
188
  } catch {
189
189
  violations.push('docs/api/openapi.json + docs/api-reference.md + docs/routes-permissions.md are STALE (or the spec is not valid OpenAPI 3.1) — run `node scripts/gds/gen-api-docs.js` and commit (task 1918).');
190
190
  }
191
+ // NB the role packs' responsibility blocks are generated too, but their freshness
192
+ // is asserted by role-pack-guard.js — one check owns "the packs are correct".
191
193
  return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus docs/file-map.md skills/scheduled-tasks sections and docs/api/openapi.json + api-reference.md (API). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
192
194
  }
193
195
 
@@ -455,6 +457,7 @@ const KERNEL_FILES = [
455
457
  'src/module-api.js', // the published doorway
456
458
  'src/modules.js', // the registry/resolver facade
457
459
  'src/module-seams.js', // the seam registry (ports/events/contributions)
460
+ 'src/role-responsibilities.js', // the owner's written role statements (task 1003732) — modules.js reads it; imports nothing
458
461
  'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
459
462
  'src/module-loader/manifest-schema.js', // the manifest validator
460
463
  'src/module-loader/semver.js', // coreVersion compatibility
@@ -562,6 +565,7 @@ const KERNEL_NAMES_MODULES_OK = new Set([
562
565
  'src/bongos/auth-discord.js', // the Discord account-link flow itself
563
566
  'src/bongos/permission-path-check.js', // names module path subtrees to PROTECT them
564
567
  'src/bongos/route-rank-check.js', // names module route dirs to rank-gate them
568
+ 'src/role-responsibilities.js', // the ideator's VERBATIM statement reads "Make good ideas" — the English word, colliding with the `ideas` key. Owner text; unparaphrasable.
565
569
  ]);
566
570
 
567
571
  // The discovered module keys = the directory names under modules/ (a manifest's
@@ -1342,6 +1346,12 @@ function checkSkillFrontmatter() {
1342
1346
  const CHECKS = [
1343
1347
  checkCoreHostBoundary,
1344
1348
  checkClaudeMdBudget,
1349
+ // Check 32 — task 1002990 / criterion wa6-kernel-and-packs: the root CLAUDE.md is
1350
+ // now a role-neutral KERNEL and each craft's playbook lives in a pack under
1351
+ // docs/packs/. claim.js resolves the pack through the role registry and fails
1352
+ // OPEN, so a pack path that points at nothing is invisible at runtime — CI is the
1353
+ // only place a hollowed-out role can be caught. Rationale in that file.
1354
+ require('./role-pack-guard.js').checkRolePacks,
1345
1355
  checkSkillFrontmatter,
1346
1356
  checkNestedDocsPresent,
1347
1357
  checkRankGuards,
@@ -0,0 +1,134 @@
1
+ // scripts/gds/gen-role-responsibilities.js
2
+ //
3
+ // WHAT. Writes each craft's written responsibility into its role pack under
4
+ // docs/packs/, between markers, from the ONE source at src/role-responsibilities.js.
5
+ //
6
+ // WHY A GENERATOR AND NOT A PARAGRAPH SOMEONE TYPES. Criterion
7
+ // wa6-written-responsibilities (goal 1000095) says there is one source "shown on
8
+ // the profile, injected into the pack, referenced by grading". The profile and the
9
+ // grader read the source at runtime, so they cannot drift. A markdown pack cannot
10
+ // require a JS module, so its copy is the only one that CAN drift — and the drift
11
+ // would be invisible and consequential: the pack is what a session is actually
12
+ // instructed by, so a stale sentence there means the project teaches one standard,
13
+ // shows a second on the profile, and grades against a third. This is the
14
+ // gen-diagrams / gen-session-index pattern (ADR 0062 §8): generate the block, gate
15
+ // its freshness in CI, never hand-edit it.
16
+ //
17
+ // USAGE
18
+ // node scripts/gds/gen-role-responsibilities.js # write the blocks
19
+ // node scripts/gds/gen-role-responsibilities.js --check # exit 2 if any block drifted (the CI gate)
20
+ //
21
+ // A pack with no markers is a WARNING, not a failure — a host instance may carry a
22
+ // pack this core does not know about. A pack whose marker block is STALE is the
23
+ // hard failure, because that is the drift the whole file exists to prevent.
24
+ 'use strict';
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const { ROLE_RESPONSIBILITIES, RESPONSIBILITIES_SOURCE } = require('../../src/role-responsibilities');
30
+ const { resolveCoreRoot } = require('../../src/instance-config');
31
+
32
+ // The packs are CORE content (they ship in the package alongside the registry that
33
+ // names them), so resolve them against the core root the same way the role-pack
34
+ // guard does — never the instance root.
35
+ const CORE_ROOT = resolveCoreRoot();
36
+ const REGISTRY_REL = 'scripts/gds/discipline-modes.json';
37
+
38
+ const BEGIN = '<!-- BEGIN GENERATED ROLE RESPONSIBILITY (scripts/gds/gen-role-responsibilities.js — do not hand-edit) -->';
39
+ const END = '<!-- END GENERATED ROLE RESPONSIBILITY -->';
40
+
41
+ // The block a pack carries. Deliberately short: the pack teaches the craft, this
42
+ // states the standard, and the provenance line is there so a reader knows the
43
+ // sentence is the owner's and not the platform's opinion.
44
+ function blockFor(discipline) {
45
+ const text = ROLE_RESPONSIBILITIES[discipline];
46
+ if (!text) return null;
47
+ const s = RESPONSIBILITIES_SOURCE;
48
+ return [
49
+ BEGIN,
50
+ '## What this craft is answerable for',
51
+ '',
52
+ `> ${text}`,
53
+ '',
54
+ `*The ${discipline}'s written responsibility — one source (\`src/role-responsibilities.js\`), shown on the hall profile, taught here, and referenced by the grader. Fixed by ${s.authoredBy} on ${s.authoredOn}, criterion \`${s.criterion}\`. Generated: do not hand-edit, and do not paraphrase it elsewhere.*`,
55
+ END,
56
+ ].join('\n');
57
+ }
58
+
59
+ // Replace the marked region, or report that the pack has no markers. Mirrors
60
+ // gen-session-index.injectBlock: bounded by the two literal markers, so a pack may
61
+ // put the block anywhere and move it later without touching this file.
62
+ //
63
+ // THE BLOCK TAKES THE FILE'S OWN LINE ENDING. These packs are `.md` and check out
64
+ // CRLF on Windows while CI checks out LF. A generator that always wrote '\n' would
65
+ // splice LF lines into a CRLF file, so the committed bytes and the bytes `--check`
66
+ // recomputes would differ by platform — the check would be red on one checkout and
67
+ // green on the other, for a file nobody touched. Detecting the file's ending makes
68
+ // the generator idempotent on both.
69
+ function inject(text, block) {
70
+ const i = text.indexOf(BEGIN);
71
+ const j = text.indexOf(END);
72
+ if (i === -1 || j === -1 || j < i) return null;
73
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
74
+ return text.slice(0, i) + block.replace(/\n/g, eol) + text.slice(j + END.length);
75
+ }
76
+
77
+ // Every discipline the role registry binds to a pack. Reading the registry rather
78
+ // than hardcoding three filenames means a pack that is renamed, or a fourth craft
79
+ // that is added later (Governor, when the owner decides), is picked up here with no
80
+ // edit — the same single-declaration rule the registry already enforces for claim.js.
81
+ function packTargets() {
82
+ const abs = path.join(CORE_ROOT, REGISTRY_REL);
83
+ const modes = (JSON.parse(fs.readFileSync(abs, 'utf8')) || {}).modes || {};
84
+ const out = [];
85
+ for (const [discipline, mode] of Object.entries(modes)) {
86
+ if (!mode || !mode.pack) continue;
87
+ out.push({ discipline, rel: mode.pack, abs: path.join(CORE_ROOT, mode.pack) });
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function plan() {
93
+ const writes = [];
94
+ const warnings = [];
95
+ for (const t of packTargets()) {
96
+ const block = blockFor(t.discipline);
97
+ if (!block) {
98
+ // A pack whose craft has no statement is legitimate (a module-contributed
99
+ // craft that grew a pack). Say so; never write an empty quote into it.
100
+ warnings.push(`${t.rel}: no written responsibility for '${t.discipline}' — leaving the pack alone.`);
101
+ continue;
102
+ }
103
+ let cur;
104
+ try { cur = fs.readFileSync(t.abs, 'utf8'); } catch {
105
+ warnings.push(`${t.rel}: named by the role registry but not readable — the role-pack guard is the check that fails on this.`);
106
+ continue;
107
+ }
108
+ const next = inject(cur, block);
109
+ if (next === null) { warnings.push(`${t.rel}: no marker block — add the BEGIN/END pair to carry the statement.`); continue; }
110
+ if (next !== cur) writes.push({ abs: t.abs, rel: t.rel, content: next });
111
+ }
112
+ return { writes, warnings };
113
+ }
114
+
115
+ function main() {
116
+ const check = process.argv.includes('--check');
117
+ const { writes, warnings } = plan();
118
+ for (const w of warnings) console.error(`gen-role-responsibilities: WARN ${w}`);
119
+ if (check) {
120
+ if (writes.length) {
121
+ for (const w of writes) console.error(`gen-role-responsibilities: STALE ${w.rel}`);
122
+ console.error('Run `node scripts/gds/gen-role-responsibilities.js` and commit.');
123
+ process.exit(2);
124
+ }
125
+ console.log('gen-role-responsibilities --check: up to date.');
126
+ return;
127
+ }
128
+ for (const w of writes) fs.writeFileSync(w.abs, w.content);
129
+ console.log(writes.length ? `gen-role-responsibilities: wrote ${writes.length} pack(s): ${writes.map((w) => w.rel).join(', ')}` : 'gen-role-responsibilities: already up to date.');
130
+ }
131
+
132
+ if (require.main === module) main();
133
+
134
+ module.exports = { BEGIN, END, blockFor, inject, packTargets, plan };
@@ -124,6 +124,15 @@ const PUBLISH_ALLOWLIST = [
124
124
  'docs/file-map.md',
125
125
  'docs/repo-map.md',
126
126
  'docs/handoff-template.md', // the portable session-handoff format (methodology)
127
+ // The ROLE PACKS (task 1002990). docs/ is default-deny, and these are not
128
+ // documentation ABOUT the methodology — they ARE it: since the kernel/pack split
129
+ // the root CLAUDE.md is role-neutral and everything a craft actually works from
130
+ // lives here. scripts/gds/discipline-modes.json ships (scripts/ is allowlisted)
131
+ // and names these paths, so omitting them would release a core whose every claim
132
+ // prints a pack path that resolves to nothing on the instance — the exact silent
133
+ // failure scripts/gds/role-pack-guard.js exists to make loud. The registry and
134
+ // this line are only ever added or removed together.
135
+ 'docs/packs/',
127
136
  'docs/canonical-permissions.md', // the rank-ladder reference IS the methodology (sanitized)
128
137
  'docs/project-context.template.md', // the neutral scaffold `cloudbongos init` fills (NOT the OTB-filled one)
129
138
  '.claude/skills/', // the methodology slash-commands (hooks/settings are denied below)
@@ -0,0 +1,142 @@
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
+ // Each pack's WRITTEN-RESPONSIBILITY block must match src/role-responsibilities.js
105
+ // (task 1003732). It lives with the other pack assertions rather than in
106
+ // checkGeneratedArtifactsFresh so one check owns "the packs are correct" — and the
107
+ // drift matters for the same reason the missing-pack case does: the profile and
108
+ // the grader read the source at runtime and cannot drift, but the pack is markdown,
109
+ // so its copy is the only one that can — and it is the copy a SESSION is instructed
110
+ // by. A stale block means the project teaches one standard, shows a second on the
111
+ // profile, and grades against a third. Skipped when the fs is stubbed (the negative
112
+ // tests inject a registry that names packs this generator would not find).
113
+ if (fsImpl === fs) {
114
+ try {
115
+ const { plan } = require('./gen-role-responsibilities.js');
116
+ const { writes } = plan();
117
+ for (const w of writes) {
118
+ violations.push(`${w.rel}: the written-responsibility block is STALE against src/role-responsibilities.js — run \`node scripts/gds/gen-role-responsibilities.js\` and commit (task 1003732).`);
119
+ }
120
+ } catch (e) {
121
+ violations.push(`could not check the packs' written-responsibility blocks (${e && e.message}) — run \`node scripts/gds/gen-role-responsibilities.js --check\`.`);
122
+ }
123
+ }
124
+
125
+ // The split's own shape, asserted so a later edit cannot quietly drop a craft
126
+ // back into the kernel: three core packs, and Governor deliberately absent
127
+ // (criterion wa6-kernel-and-packs — "three packs, not four, until the owner decides").
128
+ if (packCount < 3) {
129
+ 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.`);
130
+ }
131
+
132
+ return {
133
+ name: 'role registry packs resolve + fit their budget',
134
+ ok: violations.length === 0,
135
+ hardFail: violations.length > 0,
136
+ violations,
137
+ warnings,
138
+ 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.`,
139
+ };
140
+ }
141
+
142
+ module.exports = { checkRolePacks, PACK_CHAR_HARD_MAX, PACK_CHAR_TARGET, REGISTRY_REL };
package/src/module-api.js CHANGED
@@ -41,6 +41,22 @@ const instanceConfig = require('./instance-config');
41
41
  const staleTimer = require('./stale-timer');
42
42
  const seams = require('./module-seams');
43
43
  const { buildInfo } = require('./build-info');
44
+ // Eager and DESTRUCTURED on purpose, and inside the rule above rather than an
45
+ // exception to it: role-responsibilities.js requires NOTHING (80 lines of the
46
+ // owner's text plus three pure lookups), so it costs one file read and drags no
47
+ // server file into a CLI subcommand's require-closure — the same footing as
48
+ // `branding` and `instanceConfig` here.
49
+ //
50
+ // It has to be a NAMED require rather than `require(...).x` read inside a getter,
51
+ // or CI's dead-code ratchet counts these exports as dead and no PR touching them
52
+ // can merge — task 1003732 stranded on exactly that. `responsibilityFor`'s only
53
+ // consumer is modules/grading/grader-prompt.js reaching through this doorway, and
54
+ // modules/ sits outside knip's `project` globs by design, so the import HERE is
55
+ // the only place that use can be seen. knip credits a destructured binding and
56
+ // not a member access on a namespace object, which is why the sibling exports
57
+ // (`responsibilitiesFor` via src/modules.js, `ROLE_RESPONSIBILITIES` via
58
+ // scripts/gds/gen-role-responsibilities.js) were never flagged and this one was.
59
+ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibilities');
44
60
 
45
61
  // ---------------------------------------------------------------------------
46
62
  // CORE_VERSION — the version of THIS published surface (semver).
@@ -55,7 +71,7 @@ const { buildInfo } = require('./build-info');
55
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
72
  // the entry to that file. Look for a version's history there, not here.
57
73
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.637'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.639'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
75
 
60
76
  // A namespaced logger so a module's log lines are attributable + consistent.
61
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -405,6 +421,19 @@ module.exports = {
405
421
  get pageMeta() { return require('./bongos/routes/_helpers').pageMeta; },
406
422
  get PAGINATION() { return require('./bongos/routes/_helpers').PAGINATION; },
407
423
 
424
+ // --- the written role responsibilities (task 1003732, criterion
425
+ // wa6-written-responsibilities). ONE source for the sentence each craft is
426
+ // answerable for; it reaches modules through the doorway because more than
427
+ // one module needs the same text — grading references it when judging
428
+ // role-shaped work, and the hall shows it to the person being judged — and
429
+ // modules never import each other. `responsibilityFor(discipline)` returns
430
+ // null for a craft with no statement (a module-contributed one, or an
431
+ // unclassified task); that is a legitimate answer, not an error.
432
+ // Plain entries rather than getters: the module is loaded eagerly at the
433
+ // top of this file (it costs nothing), so there is nothing left to defer.
434
+ responsibilityFor,
435
+ ROLE_RESPONSIBILITIES,
436
+
408
437
  // --- logging
409
438
  logger,
410
439
  };
package/src/modules.js CHANGED
@@ -28,6 +28,7 @@ const path = require('node:path');
28
28
  // until a module is moved into modules/ — so this is a no-op on the current tree.
29
29
  const loader = require('./module-loader/loader');
30
30
  const { resolveCoreRoot, resolveInstanceRoot } = require('./instance-config');
31
+ const { responsibilitiesFor } = require('./role-responsibilities');
31
32
 
32
33
  // ADR 0108 §1: the neutral starter ships WITH the core package; the instance
33
34
  // pack is host content. Both resolvers return the repo root today, so this is
@@ -250,10 +251,20 @@ function enabledDisciplines(set = modules()) {
250
251
 
251
252
  // CLIENT-SAFE projection — module flags are just booleans, all safe to expose.
252
253
  // The hall/status front-ends read this to hide UI sections for off modules.
254
+ //
255
+ // `responsibilities` rides here (task 1003732) because this projection is already
256
+ // injected into every served page as a global (serve-internal.injectBrandingGlobal),
257
+ // and it already carries the discipline roster the statements are keyed by — so the
258
+ // profile gets the owner's text with no new route, no fetch, and no second copy to
259
+ // drift. Scoped to the disciplines this instance actually offers: a craft the
260
+ // instance does not have must not appear in its UI, and a module-contributed craft
261
+ // has no statement by design (src/role-responsibilities.js says why).
253
262
  function clientModules(set = modules()) {
263
+ const disciplines = enabledDisciplines(set);
254
264
  return {
255
265
  enabled: knownModules().reduce((o, k) => { o[k] = set[k] === true; return o; }, {}),
256
- disciplines: enabledDisciplines(set),
266
+ disciplines,
267
+ responsibilities: responsibilitiesFor(disciplines),
257
268
  };
258
269
  }
259
270
 
@@ -0,0 +1,80 @@
1
+ // src/role-responsibilities.js
2
+ //
3
+ // WHAT. The one source of each craft's written responsibility — the sentence that
4
+ // says what a person holding that role is actually answerable for. Three consumers
5
+ // read this file and nothing else: the hall profile shows it to the person, the
6
+ // role pack under docs/packs/ teaches a session from it, and the grader references
7
+ // it when it judges role-shaped work.
8
+ //
9
+ // WHY IT IS ONE FILE. Criterion wa6-written-responsibilities (goal 1000095) is
10
+ // explicit that there is ONE source — "shown on the profile, injected into the
11
+ // pack, referenced by grading" — and the reason is not tidiness. The promise made
12
+ // to a builder on their profile and the text their session is actually held to
13
+ // must be the same text; two copies that drift mean the project judged someone
14
+ // against a standard it never showed them. The pack half is enforced rather than
15
+ // promised: the packs carry a GENERATED block written by
16
+ // scripts/gds/gen-role-responsibilities.js, and fitness.js fails CI when it drifts
17
+ // from this file (the gen-diagrams / gen-session-index pattern, ADR 0062 §8).
18
+ //
19
+ // THE TEXT IS THE OWNER'S, VERBATIM. Fixed by the area owner on 2026-09-08 and
20
+ // recorded in the criterion. Do not paraphrase, tighten, or "improve" these
21
+ // sentences — a role's responsibility is the owner's to write, and the whole point
22
+ // of a single source is that it says what they said. Per-project sub-specialities
23
+ // (idea 1000733) build on this later and are deliberately not modelled here.
24
+ //
25
+ // KERNEL FILE. On the fitness.js KERNEL_FILES roster: three different modules read
26
+ // it (hall-ui, grading) plus the core's own client projection, and modules never
27
+ // import each other, so shared vocabulary has to sit in the kernel. It imports
28
+ // nothing, which is what makes that placement free.
29
+ 'use strict';
30
+
31
+ // discipline key (tasks.discipline / builders.preferred_disciplines) → the owner's
32
+ // sentence. Keys mirror CORE_DISCIPLINES order (engineer | artist | ideator), the
33
+ // migration 176 enum order. A module-contributed discipline (ui-design → 'ui') has
34
+ // no statement here on purpose: a craft a module brings is that module's to
35
+ // describe, and inventing one for it would be exactly the paraphrase this file
36
+ // forbids. Consumers must therefore treat "no statement" as normal, not as an error.
37
+ const ROLE_RESPONSIBILITIES = Object.freeze({
38
+ engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
39
+ 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.',
40
+ 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.',
41
+ });
42
+
43
+ // Provenance, carried with the text so every rendering can say where it came from
44
+ // rather than looking like a platform opinion someone typed.
45
+ const RESPONSIBILITIES_SOURCE = Object.freeze({
46
+ criterion: 'wa6-written-responsibilities',
47
+ goalId: '1000095',
48
+ authoredOn: '2026-09-08',
49
+ authoredBy: 'the area owner',
50
+ });
51
+
52
+ // The statement for one discipline, or null when the craft has none (a
53
+ // module-contributed discipline, an unclassified task, a stray value from an older
54
+ // row). Null is a legitimate answer every consumer must render as absence — never
55
+ // as a blank quote or a placeholder sentence.
56
+ function responsibilityFor(discipline) {
57
+ if (typeof discipline !== 'string') return null;
58
+ return ROLE_RESPONSIBILITIES[discipline.toLowerCase()] || null;
59
+ }
60
+
61
+ // The subset of statements an instance actually offers, so a client projection
62
+ // never ships a craft the instance does not have. Takes the discipline roster the
63
+ // caller already resolved (modules.enabledDisciplines()) rather than reaching for
64
+ // the module registry itself — that would make this file import domain and cost it
65
+ // its place on the kernel roster.
66
+ function responsibilitiesFor(disciplines) {
67
+ const out = {};
68
+ for (const d of Array.isArray(disciplines) ? disciplines : []) {
69
+ const text = responsibilityFor(d);
70
+ if (text) out[d] = text;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ module.exports = {
76
+ ROLE_RESPONSIBILITIES,
77
+ RESPONSIBILITIES_SOURCE,
78
+ responsibilityFor,
79
+ responsibilitiesFor,
80
+ };
@@ -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
  ];