@bongos/core 1.19.636 → 1.19.638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +56 -46
- 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/README.md +1 -0
- package/docs/file-map.md +5 -4
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/slash-commands.md +2 -2
- package/docs/packs/artist.md +78 -0
- package/docs/packs/engineer.md +107 -0
- package/docs/packs/ideator.md +125 -0
- 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 +6 -0
- package/scripts/gds/main-audit.js +41 -6
- package/scripts/gds/publish-manifest.js +9 -0
- package/scripts/gds/role-pack-guard.js +121 -0
- package/src/module-api.js +1 -1
- package/tests/edit_hints.mjs +1 -1
- package/tests/fitness.mjs +96 -0
- package/tests/government_protected_surfaces.mjs +25 -3
- package/tests/idea_ideate_full.mjs +10 -4
- package/tests/main_audit.mjs +95 -0
- package/tests/module-scope-map.mjs +4 -3
- package/tests/publish_manifest.mjs +2 -1
- 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// scripts/gds/role-pack-guard.js
|
|
2
|
+
//
|
|
3
|
+
// WHAT. The fitness check behind the kernel/pack split (task 1002990, criterion
|
|
4
|
+
// wa6-kernel-and-packs): every entry in the role registry that names a `pack` must
|
|
5
|
+
// point at a file that exists, and every pack must stay inside a context budget.
|
|
6
|
+
//
|
|
7
|
+
// WHY IT IS A HARD FAIL, when claim.js's own read is fail-open. The registry
|
|
8
|
+
// (scripts/gds/discipline-modes.json) is read at claim time by code that must never
|
|
9
|
+
// break a granted claim, so a pack path that points at nothing degrades to silence:
|
|
10
|
+
// the builder is told their craft's label and nothing else, and the playbook that
|
|
11
|
+
// was supposed to carry everything the role-neutral kernel dropped never loads. That
|
|
12
|
+
// failure is invisible at runtime by design. CI is therefore the only place it can
|
|
13
|
+
// be caught, which is exactly the split the criterion asks for — "CI budgets each
|
|
14
|
+
// pack" — and why a renamed or deleted pack must red the build rather than quietly
|
|
15
|
+
// hollow out a role.
|
|
16
|
+
//
|
|
17
|
+
// WHY A BUDGET AT ALL. A pack is loaded in full whenever someone works that craft,
|
|
18
|
+
// so it costs the same per-session tokens the root CLAUDE.md does (ADR 0061, the
|
|
19
|
+
// context-rot argument). The kernel got a budget when it was the only always-loaded
|
|
20
|
+
// file; the packs inherit the same discipline now that they carry half its content.
|
|
21
|
+
// The ceilings are advisory-then-hard in the same shape checkClaudeMdBudget uses:
|
|
22
|
+
// a target that WARNS, a ceiling that FAILS, both ratcheted down as detail relocates
|
|
23
|
+
// to ADRs and recipes.
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { CORE_ROOT } = require('./fitness-lib.js');
|
|
29
|
+
|
|
30
|
+
// The registry lives beside the CLI that reads it, in the CORE package — the same
|
|
31
|
+
// resolution claim.js uses (path.join(__dirname, 'discipline-modes.json')), so this
|
|
32
|
+
// check and the runtime always read the same file even from a scaffolded instance
|
|
33
|
+
// whose repo root is somewhere else entirely (ADR 0108).
|
|
34
|
+
const REGISTRY_REL = 'scripts/gds/discipline-modes.json';
|
|
35
|
+
|
|
36
|
+
// ~4 chars/token for markdown prose, the estimator checkClaudeMdBudget uses.
|
|
37
|
+
const estTokens = (n) => Math.round(n / 4);
|
|
38
|
+
|
|
39
|
+
// Ratcheted just above the packs as authored by task 1002990 (engineer 12,223,
|
|
40
|
+
// ideator 10,218, artist 6,982 chars). Lower both as pack detail relocates to
|
|
41
|
+
// ADRs and recipes — the same ratchet contract ROOT_CHAR_HARD_MAX carries.
|
|
42
|
+
const PACK_CHAR_HARD_MAX = 16000; // ~4K tokens — a role pack must never balloon past this.
|
|
43
|
+
const PACK_CHAR_TARGET = 13000; // ~3.25K tokens — aspirational.
|
|
44
|
+
|
|
45
|
+
function checkRolePacks(fsImpl = fs) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const abs = path.join(CORE_ROOT, REGISTRY_REL);
|
|
49
|
+
|
|
50
|
+
let modes;
|
|
51
|
+
try {
|
|
52
|
+
modes = (JSON.parse(fsImpl.readFileSync(abs, 'utf8')) || {}).modes || {};
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Unlike claim.js this does NOT fail open. An unreadable registry is the one
|
|
55
|
+
// state that makes every other assertion here vacuous, so it is the failure.
|
|
56
|
+
return {
|
|
57
|
+
name: 'role registry packs resolve + fit their budget',
|
|
58
|
+
ok: false,
|
|
59
|
+
hardFail: true,
|
|
60
|
+
violations: [`${REGISTRY_REL} could not be read or parsed (${e && e.message}) — claim.js reads this file on every claim and fails open, so a broken registry is silent at runtime and can only be caught here.`],
|
|
61
|
+
warnings: [],
|
|
62
|
+
note: 'the role registry could not be read.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entries = Object.entries(modes);
|
|
67
|
+
let packCount = 0;
|
|
68
|
+
const sizes = [];
|
|
69
|
+
|
|
70
|
+
for (const [discipline, mode] of entries) {
|
|
71
|
+
if (!mode || typeof mode !== 'object') {
|
|
72
|
+
violations.push(`role registry entry '${discipline}' is not an object — every entry must carry a directive plus a pack or a skill.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// An entry with neither pointer routes a claim nowhere: claim.js prints the
|
|
76
|
+
// label and the session is left to guess its own playbook.
|
|
77
|
+
if (!mode.pack && !mode.skill) {
|
|
78
|
+
violations.push(`role registry entry '${discipline}' names neither a 'pack' (docs/packs/<craft>.md) nor a 'skill' — a claim of this discipline would be routed to no playbook at all.`);
|
|
79
|
+
}
|
|
80
|
+
if (!mode.pack) continue;
|
|
81
|
+
|
|
82
|
+
packCount += 1;
|
|
83
|
+
const packAbs = path.join(CORE_ROOT, mode.pack);
|
|
84
|
+
if (!fsImpl.existsSync(packAbs)) {
|
|
85
|
+
violations.push(`role registry entry '${discipline}' points at '${mode.pack}', which is not in the tree — claim.js would print the path and the session would find nothing there.`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let text = '';
|
|
89
|
+
try {
|
|
90
|
+
text = fsImpl.readFileSync(packAbs, 'utf8');
|
|
91
|
+
} catch (e) {
|
|
92
|
+
violations.push(`role registry entry '${discipline}' points at '${mode.pack}', which exists but could not be read (${e && e.message}).`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const chars = text.length;
|
|
96
|
+
sizes.push(`${discipline} ${chars}`);
|
|
97
|
+
if (chars > PACK_CHAR_HARD_MAX) {
|
|
98
|
+
violations.push(`${mode.pack} is ${chars} chars (~${estTokens(chars)} tokens) — over the ${PACK_CHAR_HARD_MAX}-char (~${estTokens(PACK_CHAR_HARD_MAX)}-token) hard ceiling. A pack loads in full for every session of its craft; relocate detail to an ADR or a recipe (ADR 0061).`);
|
|
99
|
+
} else if (chars > PACK_CHAR_TARGET) {
|
|
100
|
+
warnings.push(`${mode.pack} is ${chars} chars (~${estTokens(chars)} tokens) — over the ~${PACK_CHAR_TARGET}-char (~${estTokens(PACK_CHAR_TARGET)}-token) target. Trim prose, then lower PACK_CHAR_TARGET.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The split's own shape, asserted so a later edit cannot quietly drop a craft
|
|
105
|
+
// back into the kernel: three core packs, and Governor deliberately absent
|
|
106
|
+
// (criterion wa6-kernel-and-packs — "three packs, not four, until the owner decides").
|
|
107
|
+
if (packCount < 3) {
|
|
108
|
+
violations.push(`only ${packCount} craft(s) name a pack — the split is one kernel plus three packs (Engineer, Artist, Ideator). A craft with no pack has nowhere to keep what the role-neutral kernel does not.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
name: 'role registry packs resolve + fit their budget',
|
|
113
|
+
ok: violations.length === 0,
|
|
114
|
+
hardFail: violations.length > 0,
|
|
115
|
+
violations,
|
|
116
|
+
warnings,
|
|
117
|
+
note: `${entries.length} registry entr(ies), ${packCount} with a pack (${sizes.join(', ') || 'none'} chars) — ≤ ${PACK_CHAR_HARD_MAX} chars each, target ~${PACK_CHAR_TARGET}. A pack path that resolves to nothing is silent at claim time, so CI is the only gate.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { checkRolePacks, PACK_CHAR_HARD_MAX, PACK_CHAR_TARGET, REGISTRY_REL };
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.638'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
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
|
];
|
package/tests/fitness.mjs
CHANGED
|
@@ -57,6 +57,102 @@ test('CLAUDE.md char budget is ratcheted and ordered (target < ceiling, live fil
|
|
|
57
57
|
assert.equal(r.hardFail, false, 'the live CLAUDE.md must be under the char ceiling (ratchet locked in)');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// --- the role packs (task 1002990 / ADR 0274) --------------------------------
|
|
61
|
+
//
|
|
62
|
+
// This guard is the ONLY place a broken pack path can surface: claim.js reads the
|
|
63
|
+
// role registry fail-open so it can never break a granted claim, which means an
|
|
64
|
+
// entry pointing at a missing file degrades to silence — the craft's label prints
|
|
65
|
+
// and its playbook never loads. The ADR rests the whole hard-fail argument on that,
|
|
66
|
+
// so an untested guard would undermine its own rationale. Hence both legs, the same
|
|
67
|
+
// pair every sibling guard here carries: the live tree is clean, AND the check
|
|
68
|
+
// actually catches the failure rather than being a no-op.
|
|
69
|
+
const rolePacks = require(path.join(ROOT, 'scripts', 'gds', 'role-pack-guard.js'));
|
|
70
|
+
|
|
71
|
+
test('role packs: the live registry resolves and every pack is under budget', () => {
|
|
72
|
+
const r = rolePacks.checkRolePacks();
|
|
73
|
+
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('role pack budget is ratcheted and ordered (target < ceiling)', () => {
|
|
77
|
+
assert.ok(rolePacks.PACK_CHAR_TARGET < rolePacks.PACK_CHAR_HARD_MAX, 'target must sit below the hard ceiling');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('role pack guard actually CATCHES a missing pack, a pointerless entry, and a short roster (not a no-op)', () => {
|
|
81
|
+
const real = fs;
|
|
82
|
+
const withRegistry = (modes) => ({
|
|
83
|
+
readFileSync: (p, e) => (String(p).includes('discipline-modes.json') ? JSON.stringify({ modes }) : real.readFileSync(p, e)),
|
|
84
|
+
existsSync: (p) => (String(p).includes('__gone__') ? false : real.existsSync(p)),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// (1) the silent runtime failure: a pack path that resolves to nothing.
|
|
88
|
+
const missing = rolePacks.checkRolePacks(withRegistry({
|
|
89
|
+
engineer: { pack: 'docs/packs/__gone__.md', directive: 'x' },
|
|
90
|
+
artist: { pack: 'docs/packs/artist.md', directive: 'x' },
|
|
91
|
+
ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
|
|
92
|
+
}));
|
|
93
|
+
assert.equal(missing.hardFail, true, 'a pack that is not in the tree must hard-fail');
|
|
94
|
+
assert.ok(missing.violations.some((v) => /__gone__/.test(v) && /not in the tree/.test(v)), 'the violation must name the unresolvable path');
|
|
95
|
+
|
|
96
|
+
// (2) an entry routing a claim to no playbook at all.
|
|
97
|
+
const pointerless = rolePacks.checkRolePacks(withRegistry({
|
|
98
|
+
engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
|
|
99
|
+
artist: { pack: 'docs/packs/artist.md', directive: 'x' },
|
|
100
|
+
ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
|
|
101
|
+
ui: { directive: 'x' },
|
|
102
|
+
}));
|
|
103
|
+
assert.equal(pointerless.hardFail, true, 'an entry with neither pack nor skill must hard-fail');
|
|
104
|
+
assert.ok(pointerless.violations.some((v) => /'ui'/.test(v) && /neither/.test(v)));
|
|
105
|
+
|
|
106
|
+
// (3) a craft quietly dropped back into the kernel — the split's own shape.
|
|
107
|
+
const short = rolePacks.checkRolePacks(withRegistry({
|
|
108
|
+
engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
|
|
109
|
+
artist: { pack: 'docs/packs/artist.md', directive: 'x' },
|
|
110
|
+
ideator: { skill: 'ideate', directive: 'x' },
|
|
111
|
+
}));
|
|
112
|
+
assert.equal(short.hardFail, true, 'fewer than three packs must hard-fail');
|
|
113
|
+
assert.ok(short.violations.some((v) => /only 2 craft/.test(v)));
|
|
114
|
+
|
|
115
|
+
// (4) an over-budget pack — the "CI budgets each pack" half of the criterion.
|
|
116
|
+
const fat = rolePacks.checkRolePacks({
|
|
117
|
+
readFileSync: (p, e) => {
|
|
118
|
+
if (String(p).includes('discipline-modes.json')) {
|
|
119
|
+
return JSON.stringify({ modes: {
|
|
120
|
+
engineer: { pack: 'docs/packs/engineer.md', directive: 'x' },
|
|
121
|
+
artist: { pack: 'docs/packs/artist.md', directive: 'x' },
|
|
122
|
+
ideator: { pack: 'docs/packs/ideator.md', directive: 'x' },
|
|
123
|
+
} });
|
|
124
|
+
}
|
|
125
|
+
return 'x'.repeat(rolePacks.PACK_CHAR_HARD_MAX + 1);
|
|
126
|
+
},
|
|
127
|
+
existsSync: () => true,
|
|
128
|
+
});
|
|
129
|
+
assert.equal(fat.hardFail, true, 'a pack over the char ceiling must hard-fail');
|
|
130
|
+
assert.ok(fat.violations.some((v) => /hard ceiling/.test(v)));
|
|
131
|
+
|
|
132
|
+
// …and the guard is not simply always-red: the real tree passes (leg 1 above),
|
|
133
|
+
// and an unreadable registry is itself the failure rather than a free pass.
|
|
134
|
+
const unreadable = rolePacks.checkRolePacks({
|
|
135
|
+
readFileSync: () => { throw new Error('boom'); },
|
|
136
|
+
existsSync: () => true,
|
|
137
|
+
});
|
|
138
|
+
assert.equal(unreadable.hardFail, true, 'an unreadable registry must fail, not fail open like claim.js');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// The ship card mirrors the registry by hand — it cannot require a core script
|
|
142
|
+
// across the module boundary, which is why claim.js and ship-card.js have always
|
|
143
|
+
// carried the map twice (task 1003487 was that duplication drifting). A test is the
|
|
144
|
+
// shared source they cannot have: rename a pack and this fails instead of the hall
|
|
145
|
+
// shipping a button that opens nothing.
|
|
146
|
+
test('ship-card routing agrees with the role registry, entry for entry', () => {
|
|
147
|
+
const registry = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'discipline-modes.json'), 'utf8')).modes;
|
|
148
|
+
const card = require(path.join(ROOT, 'modules', 'lifecycle', 'ship-card.js'));
|
|
149
|
+
for (const [discipline, mode] of Object.entries(registry)) {
|
|
150
|
+
const cmd = card.buildCardModel({ stage: 'claimed', taskId: 1, discipline, brand: { currency: 'credits' } }).action.cmd;
|
|
151
|
+
if (mode.pack) assert.ok(cmd.includes(mode.pack), `${discipline}: card says "${cmd}" but the registry names ${mode.pack}`);
|
|
152
|
+
else if (mode.skill) assert.equal(cmd, `/${mode.skill}`, `${discipline}: card and registry disagree on the skill`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
60
156
|
test('nested CLAUDE.md present for every high-traffic dir', () => {
|
|
61
157
|
const r = fitness.checkNestedDocsPresent();
|
|
62
158
|
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
@@ -172,14 +172,36 @@ test('every glob added since R101 carries a documented reason', () => {
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
// Metic is a FLOOR, not a fixed value (task 1003191). Pinning every surface to
|
|
176
|
+
// exactly 'metic' made the registry's own contract untestable and, worse,
|
|
177
|
+
// illegal: governance owns this data and raising one surface to 'archon' is a
|
|
178
|
+
// policy edit the enforcers are supposed to honour. What must never happen is a
|
|
179
|
+
// surface dropping BELOW Metic — that is ADR 0043 being weakened by a data edit.
|
|
180
|
+
// So: no surface under Metic, and the derived minimum stays exactly Metic.
|
|
181
|
+
test('no surface floors below Metic — the ADR 0043 floor (a raise above it is legal policy)', () => {
|
|
176
182
|
for (const s of registry.SURFACES) {
|
|
177
|
-
assert.
|
|
183
|
+
assert.ok(ppc.RANK_TIER[s.floor] >= ppc.RANK_TIER.metic,
|
|
184
|
+
`${s.glob} floors at '${s.floor}', below the Metic floor ADR 0043 sets. Governance may RAISE a floor; it may never lower one under Metic.`);
|
|
178
185
|
}
|
|
179
|
-
assert.equal(registry.FLOOR, 'metic');
|
|
186
|
+
assert.equal(registry.FLOOR, 'metic', 'the lowest declared floor is the Metic floor itself');
|
|
180
187
|
assert.equal(ppc.MIN_TIER, ppc.RANK_TIER.metic, 'the kernel floor must DERIVE to metic');
|
|
181
188
|
});
|
|
182
189
|
|
|
190
|
+
test('a floor raise flows through to the enforcers, not just the registry', () => {
|
|
191
|
+
// The registry promises a re-mapped floor reaches the substrate. Each layer
|
|
192
|
+
// must read the hit's OWN floor: the cooperative ones through
|
|
193
|
+
// checkPermissionPaths, the authoritative post-push audit through its per-hit
|
|
194
|
+
// comparison (task 1003191 — it used to test the global minimum and silently
|
|
195
|
+
// downgraded every raise).
|
|
196
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'main-audit.js'), 'utf8');
|
|
197
|
+
assert.ok(!/\bppc\.isBelowFloor\s*\(/.test(src),
|
|
198
|
+
'main-audit.js must not decide floors with isBelowFloor — that tests the registry '
|
|
199
|
+
+ 'GLOBAL MINIMUM, so a governance floor raise is silently downgraded in the one '
|
|
200
|
+
+ 'layer that catches a raw `git push origin main` (task 1003191).');
|
|
201
|
+
assert.ok(/RANK_TIER\[h\.floor\]/.test(src),
|
|
202
|
+
'main-audit.js must compare each hit against its own registry floor');
|
|
203
|
+
});
|
|
204
|
+
|
|
183
205
|
// ---------- the system-permission mapping flows from the registry ----------
|
|
184
206
|
|
|
185
207
|
test('every entry maps to a permission the catalog defines as system:true', () => {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// tests/idea_ideate_full.mjs
|
|
2
2
|
//
|
|
3
|
-
// task 1002931 (BV1.R155, goal 1000062) —
|
|
4
|
-
// conversationally.
|
|
3
|
+
// task 1002931 (BV1.R155, goal 1000062) — the ideator's playbook authors a Full
|
|
4
|
+
// Idea conversationally.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
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, '
|
|
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
|
package/tests/main_audit.mjs
CHANGED
|
@@ -134,6 +134,101 @@ t('#863-followup (finding #6): a sub-Metic author NAME still resolves and BLOCKS
|
|
|
134
134
|
assert.equal(r.findings[0].rank, 'thetes');
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// -- per-hit registry floors (task 1003191) --------------------------------
|
|
138
|
+
//
|
|
139
|
+
// The authoritative layer used to decide floors with ppc.isBelowFloor(rank),
|
|
140
|
+
// which tests the registry's GLOBAL MINIMUM floor. Every live surface floors at
|
|
141
|
+
// 'metic', so that was indistinguishable from a per-hit comparison — until
|
|
142
|
+
// governance RAISES one surface, at which point layer 3 silently downgraded the
|
|
143
|
+
// raise back to the minimum while the pre-push hook and the grader (both on
|
|
144
|
+
// checkPermissionPaths, which compares per-hit) still blocked it. Layer 3 is the
|
|
145
|
+
// only one that catches a raw `git push origin main`, so the authoritative net
|
|
146
|
+
// was the loosest of the three.
|
|
147
|
+
//
|
|
148
|
+
// A uniform-floor fixture cannot tell the two implementations apart. This is the
|
|
149
|
+
// shape that can: one surface raised above the minimum, one left at it.
|
|
150
|
+
const ppc = require('../src/bongos/permission-path-check.js');
|
|
151
|
+
|
|
152
|
+
function mixedFloorRegistry() {
|
|
153
|
+
const SURFACES = [
|
|
154
|
+
{ glob: 'infra/', floor: 'archon' }, // RAISED above the global minimum
|
|
155
|
+
{ glob: 'src/bongos/db.js', floor: 'metic' }, // still at the minimum
|
|
156
|
+
];
|
|
157
|
+
return {
|
|
158
|
+
RANK_TIER: ppc.RANK_TIER,
|
|
159
|
+
tierOf: (rank) => ppc.RANK_TIER[rank] ?? null,
|
|
160
|
+
matchProtected: (files) => (files || [])
|
|
161
|
+
.map((f) => {
|
|
162
|
+
const s = SURFACES.find((x) => (x.glob.endsWith('/') ? f.startsWith(x.glob) : f === x.glob));
|
|
163
|
+
return s ? { file: f, glob: s.glob, floor: s.floor, permission: 'path.protected.modify' } : null;
|
|
164
|
+
})
|
|
165
|
+
.filter(Boolean),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
t('a RAISED surface floor is enforced: Metic touching an archon-floored path → BLOCKER', () => {
|
|
170
|
+
const r = analyzeCommits([{
|
|
171
|
+
sha: 'r01', subject: 'touch the substrate', authorName: 'SomeMetic', authorEmail: 'm@x',
|
|
172
|
+
isMerge: false, files: ['infra/deploy.sh'],
|
|
173
|
+
}], RANKS, { registry: mixedFloorRegistry() });
|
|
174
|
+
assert.equal(r.hardFail, true, 'a governance floor raise must reach the authoritative layer');
|
|
175
|
+
assert.equal(r.findings.length, 1);
|
|
176
|
+
assert.deepEqual(r.findings[0].floors, ['archon']);
|
|
177
|
+
assert.match(r.findings[0].summary, /Archon\+ builder/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
t('the finding reports only the paths the author actually fails to clear', () => {
|
|
181
|
+
// db.js floors at the minimum this Metic DOES clear; infra/ does not. A
|
|
182
|
+
// per-hit comparison names the second and stays silent about the first.
|
|
183
|
+
const r = analyzeCommits([{
|
|
184
|
+
sha: 'r02', subject: 'mixed', authorName: 'SomeMetic', authorEmail: 'm@x',
|
|
185
|
+
isMerge: false, files: ['src/bongos/db.js', 'infra/deploy.sh'],
|
|
186
|
+
}], RANKS, { registry: mixedFloorRegistry() });
|
|
187
|
+
assert.equal(r.hardFail, true);
|
|
188
|
+
assert.deepEqual(r.findings[0].files, ['infra/deploy.sh']);
|
|
189
|
+
assert.equal(r.findings[0].summary.includes('src/bongos/db.js'), false,
|
|
190
|
+
'a path the author clears must not be reported as a violation');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
t('a raised floor does not over-block: Archon touching the same path → no finding', () => {
|
|
194
|
+
const r = analyzeCommits([{
|
|
195
|
+
sha: 'r03', subject: 'x', authorName: 'somearchon', authorEmail: 'l@x',
|
|
196
|
+
isMerge: false, files: ['infra/deploy.sh'],
|
|
197
|
+
}], RANKS, { registry: mixedFloorRegistry() });
|
|
198
|
+
assert.equal(r.hardFail, false);
|
|
199
|
+
assert.equal(r.findings.length, 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('a Metic still clears a surface left at the minimum floor', () => {
|
|
203
|
+
const r = analyzeCommits([{
|
|
204
|
+
sha: 'r04', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
|
|
205
|
+
isMerge: false, files: ['src/bongos/db.js'],
|
|
206
|
+
}], RANKS, { registry: mixedFloorRegistry() });
|
|
207
|
+
assert.equal(r.hardFail, false);
|
|
208
|
+
assert.equal(r.findings.length, 0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
t('an unresolved author is told the floor that actually governs, not a hardcoded Metic', () => {
|
|
212
|
+
const r = analyzeCommits([{
|
|
213
|
+
sha: 'r05', subject: 'x', authorName: 'Nobody Known', authorEmail: 'n@x',
|
|
214
|
+
isMerge: false, files: ['infra/deploy.sh'],
|
|
215
|
+
}], RANKS, { registry: mixedFloorRegistry() });
|
|
216
|
+
assert.equal(r.findings.length, 1);
|
|
217
|
+
assert.equal(r.findings[0].severity, 'review');
|
|
218
|
+
assert.match(r.findings[0].summary, /Archon\+ work/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
t('the default registry is the real one — no injection needed in production', () => {
|
|
222
|
+
// The seam must not change what the CLI does: infra/ floors at metic today,
|
|
223
|
+
// so a Metic touching it is legal on the live registry.
|
|
224
|
+
const r = analyzeCommits([{
|
|
225
|
+
sha: 'r06', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
|
|
226
|
+
isMerge: false, files: ['infra/deploy.sh'],
|
|
227
|
+
}], RANKS);
|
|
228
|
+
assert.equal(r.hardFail, false);
|
|
229
|
+
assert.equal(ppc.floorFor('infra/deploy.sh'), 'metic');
|
|
230
|
+
});
|
|
231
|
+
|
|
137
232
|
// -- the default range window (task 1002566) -------------------------------
|
|
138
233
|
//
|
|
139
234
|
// Everything above tests the pure ANALYZER. Nothing tested what FEEDS it, which
|
|
@@ -231,9 +231,10 @@ test('methodology joined the roster (task 1002989) — the instruction surfaces
|
|
|
231
231
|
const covers = (touch) => gsc.scopeSubset(['methodology'], [touch]).ok;
|
|
232
232
|
for (const touch of [
|
|
233
233
|
'CLAUDE.md', // the kernel half of the split (1002990)
|
|
234
|
-
'
|
|
235
|
-
'
|
|
236
|
-
'
|
|
234
|
+
'docs/packs/engineer.md', // the three role packs the split authored (1002990)
|
|
235
|
+
'docs/packs/artist.md',
|
|
236
|
+
'docs/packs/ideator.md',
|
|
237
|
+
'.claude/skills/status/SKILL.md', // the skills floor no module claims
|
|
237
238
|
'.claude/rules/content-charter.md', // pre-carved for task 1003687
|
|
238
239
|
'docs/adr/0264-the-ten-working-areas.md', // the ADR library
|
|
239
240
|
'docs/handoff-template.md',
|
|
@@ -77,7 +77,8 @@ const PUBLISH = [
|
|
|
77
77
|
'docs/handoff-template.md', // portable session-handoff format
|
|
78
78
|
'docs/project-context.template.md', // the neutral scaffold (NOT the OTB-filled one)
|
|
79
79
|
'config/design-tokens.schema.json', // generic contract schema
|
|
80
|
-
'.claude/skills/
|
|
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',
|
package/tests/ship_card.mjs
CHANGED
|
@@ -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, '/
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|