@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.
- package/.bongos-core.json +80 -50
- package/.claude/skills/builder-sequence/SKILL.md +2 -2
- package/.claude/skills/design/SKILL.md +2 -2
- package/docs/adr/0274-one-kernel-three-role-packs.md +55 -0
- package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +50 -0
- package/docs/adr/README.md +2 -0
- package/docs/copy-inventory.md +24 -23
- package/docs/copy-registry.json +39 -30
- package/docs/file-map.md +8 -4
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/slash-commands.md +2 -2
- package/docs/packs/artist.md +82 -0
- package/docs/packs/engineer.md +117 -0
- package/docs/packs/ideator.md +135 -0
- package/modules/grading/grader-prompt.js +14 -0
- package/modules/hall-ui/public/profile.css +17 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/lifecycle/ship-card.js +19 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +21 -9
- package/scripts/gds/discipline-modes.json +8 -8
- package/scripts/gds/fitness.js +10 -0
- package/scripts/gds/gen-role-responsibilities.js +134 -0
- package/scripts/gds/publish-manifest.js +9 -0
- package/scripts/gds/role-pack-guard.js +142 -0
- package/src/module-api.js +30 -1
- package/src/modules.js +12 -1
- package/src/role-responsibilities.js +80 -0
- package/tests/edit_hints.mjs +1 -1
- package/tests/fitness.mjs +96 -0
- package/tests/idea_ideate_full.mjs +10 -4
- package/tests/module-scope-map.mjs +4 -3
- package/tests/module_api.mjs +1 -0
- package/tests/publish_manifest.mjs +2 -1
- package/tests/role_responsibilities.mjs +187 -0
- package/tests/ship_card.mjs +18 -4
- package/.claude/skills/dev/SKILL.md +0 -49
- package/.claude/skills/ideate/SKILL.md +0 -164
- package/.claude/skills/paint/SKILL.md +0 -71
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_doc": "
|
|
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
|
-
"
|
|
5
|
+
"pack": "docs/packs/engineer.md",
|
|
6
6
|
"label": "Engineering session",
|
|
7
|
-
"directive": "This is an ENGINEER task.
|
|
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
|
-
"
|
|
10
|
+
"pack": "docs/packs/ideator.md",
|
|
11
11
|
"label": "Ideation session",
|
|
12
|
-
"directive": "This is an IDEATOR task.
|
|
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
|
-
"
|
|
15
|
+
"pack": "docs/packs/artist.md",
|
|
16
16
|
"label": "Art session",
|
|
17
|
-
"directive": "This is an ARTIST task.
|
|
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
|
|
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
|
}
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
+
};
|
package/tests/edit_hints.mjs
CHANGED
|
@@ -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/
|
|
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
|
];
|