@bongos/core 1.19.483 → 1.19.484

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.483",
6
- "core_contract": "1.19.483",
7
- "source_commit": "c26693cacb1d002d315e791fae45610e5f58d173",
5
+ "core_version": "1.19.484",
6
+ "core_contract": "1.19.484",
7
+ "source_commit": "5f998b05487ff6dac9dc1e6686c1f570537ed4dd",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-02T19:40:27.740Z",
9
+ "built_at": "2026-09-02T19:45:20.749Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 421,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2288,
20
- "tree_sha256": "c396255616cb72c04bb08391f634addfc62353497eaeb4159a3a25de2f900215",
20
+ "tree_sha256": "b7d00c3aefcc1320f168d63aba78e046d1378d513d1b5d24da3247163d92fce1",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -92,7 +92,7 @@
92
92
  {
93
93
  "path": ".claude/skills/builder-setup/SKILL.md",
94
94
  "mode": "0000644",
95
- "sha256": "dd7ea117475f9beb5ff854d90f18a884f3a62ae8a17cc623f182bc70b1a9cc1d"
95
+ "sha256": "ebe8b4b4109d4c6547bb4141e8c2d724a428390523ac94f14ab275a3dfdc913b"
96
96
  },
97
97
  {
98
98
  "path": ".claude/skills/builder-ship/SKILL.md",
@@ -127,7 +127,7 @@
127
127
  {
128
128
  "path": ".claude/skills/design/SKILL.md",
129
129
  "mode": "0000644",
130
- "sha256": "c9beec980b3abf853b1b6f5d2eaeda42366aeaa77214e90c4a77b26a032e7b35"
130
+ "sha256": "7c159eef705ce081724fb21613f71f44679332022dcdca509a718cd39fa85265"
131
131
  },
132
132
  {
133
133
  "path": ".claude/skills/dev/SKILL.md",
@@ -2157,7 +2157,7 @@
2157
2157
  {
2158
2158
  "path": "docs/module-api-changelog.md",
2159
2159
  "mode": "0000644",
2160
- "sha256": "2887e62895b9c062025048b048cfbb3244a2723ed7c51f06614fc7447438f059"
2160
+ "sha256": "45f8f6be3350501d2dd5ba92994768724b6a8e40ddafc5ba2a1018e3e9e15361"
2161
2161
  },
2162
2162
  {
2163
2163
  "path": "docs/modules-contract.md",
@@ -6752,12 +6752,12 @@
6752
6752
  {
6753
6753
  "path": "package-lock.json",
6754
6754
  "mode": "0000644",
6755
- "sha256": "5718d105f66e0618daf8927c0fd17dd7f40f98d9afad3a80250516f0b261eac7"
6755
+ "sha256": "faf10c9622d2ee995150d1773c5e7f1dce88835ba40976bc575b7de0becb628f"
6756
6756
  },
6757
6757
  {
6758
6758
  "path": "package.json",
6759
6759
  "mode": "0000644",
6760
- "sha256": "50f98b1003715a84d22b845f1c59f0b122a746e11778963338d5083900ec7a75"
6760
+ "sha256": "b79d9b6a1c531fc84e21f1077608c444a124514ddf260df94d5674d850b7b0f7"
6761
6761
  },
6762
6762
  {
6763
6763
  "path": "public-docs/index.html",
@@ -6982,7 +6982,7 @@
6982
6982
  {
6983
6983
  "path": "scripts/gds/claim.js",
6984
6984
  "mode": "0000644",
6985
- "sha256": "d2710556d771f5dcc7298806c91f3a33c7ad16b9f7738eef7edd14b7868d143b"
6985
+ "sha256": "7c81b6c9e85aab5f230f72a4638402061fa516d7bb9fb7997b1151bef5b117e6"
6986
6986
  },
6987
6987
  {
6988
6988
  "path": "scripts/gds/classify-discipline.js",
@@ -6992,7 +6992,7 @@
6992
6992
  {
6993
6993
  "path": "scripts/gds/claude-materialize.js",
6994
6994
  "mode": "0000644",
6995
- "sha256": "450c1a12c5e8c125db6794d74bb2c25251804d8613d83fa50a463e4007a46ddd"
6995
+ "sha256": "0dec071ca3a6624b45c51b9429463edc1ef70d83b0ce28104c95ea298eca64ad"
6996
6996
  },
6997
6997
  {
6998
6998
  "path": "scripts/gds/claude-md-template.js",
@@ -7107,7 +7107,7 @@
7107
7107
  {
7108
7108
  "path": "scripts/gds/discipline-modes.json",
7109
7109
  "mode": "0000644",
7110
- "sha256": "ae93a1068fa02259bc68372ce374e00db99df048730e3b3734619ec7f20da2dc"
7110
+ "sha256": "6b740035b0d793dd2063de32b660a202da518b920742892a09058943c54e60c5"
7111
7111
  },
7112
7112
  {
7113
7113
  "path": "scripts/gds/discord-channels.js",
@@ -8412,7 +8412,7 @@
8412
8412
  {
8413
8413
  "path": "src/module-api.js",
8414
8414
  "mode": "0000644",
8415
- "sha256": "6582fb440c524389c8c6bb3d11559e41a5f1bfc420c275f24bff258db863757b"
8415
+ "sha256": "3c086bb8bf6cbdead414ae11a969d0e2aba7abd7f7962eda123f951671f93c54"
8416
8416
  },
8417
8417
  {
8418
8418
  "path": "src/module-loader/catalog.js",
@@ -8932,7 +8932,7 @@
8932
8932
  {
8933
8933
  "path": "tests/claude_materialize.mjs",
8934
8934
  "mode": "0000644",
8935
- "sha256": "6599b89948d3289d869f94fd87072f26a37d10a1e67727faf6c05b5052a035c8"
8935
+ "sha256": "4be4d2a4791a7f0ed57f49332eafa938be9afe7ee0b011da0a496962c41ee901"
8936
8936
  },
8937
8937
  {
8938
8938
  "path": "tests/clawback_confirmed_backlog.mjs",
@@ -48,6 +48,8 @@ Runs `node scripts/gds/setup.js`. The script:
48
48
 
49
49
  6. **Suggest next step.** The Xenos path is action-only (#766) — no primer to read first. `setup.js` prints a "Newcomer path" footer (1. choose thy craft · 2. `/builder-start` · 3. `/builder-claim N` → `/builder-ship` · 4. ship three works and you rise to Thetes); relay it verbatim instead of paraphrasing. For a returning builder (rank ≥ Metic), `setup.js` prints no newcomer footer and `/builder-start` is the right immediate next step.
50
50
 
51
+ 7. **If they will do UI work, name the one extra step.** The design skills `/design` sends a session to use (`impeccable`, the taste and look skills) are module-owned and are not in a fresh core checkout: claiming a `ui` task lands them automatically, or `node scripts/gds/claude-materialize.js --module-skills-only` puts them there now (untracked copies, `--clean` removes them; ADR 0224).
52
+
51
53
  ## Constraints
52
54
 
53
55
  - **Never write the session token to chat or any file other than `~/.config/otb/gds-session.json`.** It's a bearer token equivalent to a GitHub OAuth grant for this user.
@@ -5,8 +5,9 @@ description: The UI design session — the operating playbook for ui-discipline
5
5
 
6
6
  You are running a **UI design session**. Two things make it different from `/dev`: the work is judged **by looking at it** (so every step ends in a rendered screen, not a paragraph), and it is done **inside a world that is not yours** — the instance's, which you read first and never override. The `ui-design` module's [CLAUDE.md](../../../modules/ui-design/CLAUDE.md) is the reference; this file is the loop.
7
7
 
8
- ## Step 0 — detect the mode
8
+ ## Step 0 — the tools, then the mode
9
9
 
10
+ - **The skills named below have to be ON THE MACHINE.** Claiming a `ui` task lands them as part of the claim (`scripts/gds/claim.js`, the `materializeModuleSkills` flag in `scripts/gds/discipline-modes.json`); opening `/design` without one does not. If `.claude/skills/` has no `impeccable`, run `node scripts/gds/claude-materialize.js --module-skills-only` — it copies every enabled module's `skills/` into this checkout as untracked, self-ignored dirs ([ADR 0224](../../../docs/adr/<redacted>.md)), so `git status` stays clean and `--clean` takes them away again. A scaffolded instance already received them at `bongos init`. A skill of the same name in your own `~/.claude/skills` is **not** the module's — it carries no platform preamble and was never scanned ([ADR 0220](../../../docs/adr/<redacted>.md)).
10
11
  - **Interactive** (someone is here to look and react): show, let them steer, iterate. One or two lines per step, then a screen.
11
12
  - **Autonomous** (no one will look): build inside the contract, prove it in both modes, stage or park the screens for review, and **never ship an interface change blind** — a task that cannot be verified visually gets a release with a reason, not a guess.
12
13
 
@@ -1403,5 +1403,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1403
1403
  landed since 1.19.481 with no explicit bump. run 33670786915. (task 1002620)
1404
1404
  1.19.483 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1405
1405
  landed since 1.19.482 with no explicit bump. run 33674657478. (task 1002620)
1406
+ 1.19.484 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1407
+ landed since 1.19.483 with no explicit bump. run 33675143968. (task 1002620)
1406
1408
  ---------------------------------------------------------------------------
1407
1409
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.483",
3
+ "version": "1.19.484",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.483",
9
+ "version": "1.19.484",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.483",
3
+ "version": "1.19.484",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -67,18 +67,49 @@ async function printRelevantLearnings(task) {
67
67
  // invalid map, or a discipline with no entry (engineer/artist today), prints
68
68
  // nothing and never breaks a successful claim.
69
69
  function printDisciplineMode(task) {
70
+ let mode = null;
70
71
  try {
71
72
  const discipline = task && task.discipline;
72
73
  if (!discipline) return;
73
74
  const raw = fs.readFileSync(path.join(__dirname, 'discipline-modes.json'), 'utf8');
74
75
  const modes = (JSON.parse(raw) || {}).modes || {};
75
- const mode = modes[discipline];
76
+ mode = modes[discipline];
76
77
  if (!mode || !mode.directive) return;
77
78
  console.log('');
78
79
  console.log(`▶ ${mode.label || 'Working mode'} — /${mode.skill}`);
79
80
  console.log(` ${mode.directive}`);
80
81
  } catch (_) {
81
82
  // never let the mode hint break a successful claim
83
+ return;
84
+ }
85
+ materializeModeSkills(mode);
86
+ }
87
+
88
+ // A task brings its own tools (task 1003486). A mode whose entry opts in with
89
+ // `materializeModuleSkills` lands the module-owned skills its playbook names — for
90
+ // `ui`, the ui-design taste kit /design sends the session to use — into THIS
91
+ // checkout's .claude/skills/, as the untracked, self-ignored copies ADR 0224 defined.
92
+ // Without it a builder is routed to /design, reads that impeccable and minimalist-ui
93
+ // are the tools, and finds none of them on the machine with no error saying why.
94
+ // Carried by the claim rather than a hook or a session start because a workbench is
95
+ // not re-provisioned every session (owner decision 2026-09-02): it runs only when
96
+ // someone actually claims work of that discipline.
97
+ //
98
+ // Fail-open in every branch, like the hint above it: the server already granted the
99
+ // claim, so nothing here may throw past this point — a failure prints the one command
100
+ // that does it by hand. The require is lazy so a claim of any other discipline never
101
+ // pays for loading the materialiser (and a broken materialiser cannot stop a claim).
102
+ function materializeModeSkills(mode) {
103
+ if (!mode || mode.materializeModuleSkills !== true) return;
104
+ try {
105
+ const { materializeModuleSkillsIntoCore, isCoreCheckoutRoot } = require('./claude-materialize');
106
+ const coreRoot = path.resolve(__dirname, '..', '..');
107
+ if (!isCoreCheckoutRoot(coreRoot)) return; // a scaffolded instance already has them from `bongos init`
108
+ const s = materializeModuleSkillsIntoCore({ coreRoot });
109
+ const n = s.materialized.length;
110
+ if (n) console.log(` ✓ ${n} module-owned skill(s) for this mode are in .claude/skills/ (untracked copies — ADR 0224).`);
111
+ } catch (e) {
112
+ console.log(` ⚠ could not land this mode's module-owned skills — run \`node scripts/gds/claude-materialize.js --module-skills-only\` (${e && e.message}).`);
82
113
  }
83
114
  }
84
115
 
@@ -638,4 +669,4 @@ if (require.main === module) {
638
669
  });
639
670
  }
640
671
 
641
- module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan };
672
+ module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills };
@@ -28,6 +28,7 @@ const path = require('node:path');
28
28
  const { spawnSync } = require('node:child_process');
29
29
  const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
30
30
  const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
31
+ const { isCorePackageName } = require('../../src/module-loader/catalog'); // one definition of "this checkout IS the core"
31
32
  const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
32
33
 
33
34
  // script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
@@ -469,6 +470,24 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
469
470
  // core's own tracked .claude/skills/ carries is left to the core. Idempotent: every run first
470
471
  // removes what the last manifest named (so a skill a module stopped declaring does not
471
472
  // linger), then lands the current set; `clean` stops after the removal.
473
+ // Is `root` the core CHECKOUT — a workbench a builder edits — rather than the core
474
+ // package installed under a scaffolded instance's node_modules? Only a checkout's own
475
+ // .claude/skills/ is the directory Claude Code reads for that project, so only there
476
+ // does materialising the module-owned skills put them in front of anyone; an instance
477
+ // already received them at ITS root from `bongos init`, and a second copy under
478
+ // node_modules/@bongos/core would be invisible (task 1003486). Fail-soft by
479
+ // construction: an unreadable package.json answers "not the core checkout", which is
480
+ // the conservative no-op rather than a copy into a tree we could not identify.
481
+ function isCoreCheckoutRoot(root) {
482
+ try {
483
+ const abs = path.resolve(root);
484
+ if (abs.split(/[\\/]+/).includes('node_modules')) return false;
485
+ return isCorePackageName(JSON.parse(fs.readFileSync(path.join(abs, 'package.json'), 'utf8')).name);
486
+ } catch (_) {
487
+ return false;
488
+ }
489
+ }
490
+
472
491
  const MODULE_SKILLS_MANIFEST = '.module-skills.json';
473
492
  function readModuleSkillsManifest(skillsDir) {
474
493
  const m = readJsonSafe(path.join(skillsDir, MODULE_SKILLS_MANIFEST));
@@ -535,4 +554,4 @@ if (require.main === module) {
535
554
  process.exit(s.refused.length ? 1 : 0);
536
555
  }
537
556
 
538
- module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
557
+ module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
@@ -1,5 +1,5 @@
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).",
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.",
3
3
  "modes": {
4
4
  "engineer": {
5
5
  "skill": "dev",
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "ui": {
20
20
  "skill": "design",
21
+ "materializeModuleSkills": true,
21
22
  "label": "UI design session",
22
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
24
  }
package/src/module-api.js CHANGED
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
34
34
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
35
35
  // the entry to that file. Look for a version's history there, not here.
36
36
  // ---------------------------------------------------------------------------
37
- const CORE_VERSION = '1.19.483'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
+ const CORE_VERSION = '1.19.484'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
38
38
 
39
39
  // A namespaced logger so a module's log lines are attributable + consistent.
40
40
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -9,6 +9,9 @@
9
9
  // 3. transformSettings — rewrites only the scripts/gds hook commands, keeps .claude/hooks/ ones.
10
10
  // 4. materializeClaude — copies + transforms skills into a target dir (no leftover node scripts/gds),
11
11
  // writes settings.json, copies hooks; and NO-OPS when the target IS the core root (self-copy guard).
12
+ // 5. task 1003486 — a UI task brings its own tools: the `materializeModuleSkills` opt-in on a
13
+ // discipline-modes entry, the core-checkout probe that keeps it out of a scaffolded instance,
14
+ // and the fail-open contract (a materialise error never breaks a claim the server already granted).
12
15
  //
13
16
  // Run: node tests/claude_materialize.mjs
14
17
 
@@ -22,6 +25,7 @@ import { fileURLToPath } from 'node:url';
22
25
  const require = createRequire(import.meta.url);
23
26
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
24
27
  const m = require('../scripts/gds/claude-materialize.js');
28
+ const claim = require('../scripts/gds/claim.js');
25
29
 
26
30
  let passed = 0;
27
31
  let failed = 0;
@@ -484,5 +488,77 @@ t('--module-skills-only: the CLI entry refuses without the flag (usage, exit 2)
484
488
  rmSync(dir, { recursive: true, force: true });
485
489
  });
486
490
 
491
+ // ---- task 1003486: claiming a ui task lands /design's tools in the checkout ----
492
+
493
+ t('the materialise-at-claim trigger is DATA: the ui mode opts in, every other discipline claims exactly as before', () => {
494
+ const modes = JSON.parse(readFileSync(join(ROOT, 'scripts', 'gds', 'discipline-modes.json'), 'utf8')).modes;
495
+ assert.equal(modes.ui.materializeModuleSkills, true, 'a ui claim brings the skills /design names');
496
+ assert.equal(modes.ui.skill, 'design', 'and still routes to /design');
497
+ for (const d of ['engineer', 'ideator', 'artist']) {
498
+ assert.ok(!modes[d].materializeModuleSkills, `${d} did not opt in, so its claim output is unchanged`);
499
+ }
500
+ });
501
+
502
+ t('isCoreCheckoutRoot: true for a real core checkout, false for the core installed into an instance, false for anything unreadable', () => {
503
+ assert.equal(m.isCoreCheckoutRoot(ROOT), true, 'this checkout names itself the core and is not under node_modules');
504
+ assert.equal(m.isCoreCheckoutRoot(join(ROOT, 'node_modules', '@bongos', 'core')), false,
505
+ 'a scaffolded instance runs claim.js from node_modules — materialising there would be invisible');
506
+ const other = mkdtempSync(join(tmpdir(), 'cm-not-core-'));
507
+ writeFileSync(join(other, 'package.json'), JSON.stringify({ name: 'some-instance' }));
508
+ assert.equal(m.isCoreCheckoutRoot(other), false, 'another checkout is that instance, not the core');
509
+ assert.equal(m.isCoreCheckoutRoot(join(other, 'nope')), false, 'an unreadable tree answers no — the conservative no-op');
510
+ rmSync(other, { recursive: true, force: true });
511
+ });
512
+
513
+ t('materializeModeSkills: a discipline that did not opt in does nothing, and a materialise failure prints one hint instead of breaking the claim', () => {
514
+ const lines = [];
515
+ const realLog = console.log;
516
+ console.log = (...a) => lines.push(a.join(' '));
517
+ const mPath = require.resolve('../scripts/gds/claude-materialize.js');
518
+ const realExports = require.cache[mPath].exports;
519
+ try {
520
+ for (const mode of [null, undefined, {}, { skill: 'dev' }, { materializeModuleSkills: false }, { materializeModuleSkills: 'yes' }]) {
521
+ claim.materializeModeSkills(mode);
522
+ }
523
+ assert.deepEqual(lines, [], 'no opt-in => nothing printed and nothing copied');
524
+
525
+ // forced failure in the materialise step: swallowed, one line, the manual command named
526
+ require.cache[mPath].exports = {
527
+ isCoreCheckoutRoot: () => true,
528
+ materializeModuleSkillsIntoCore: () => { throw new Error('disk on fire'); },
529
+ };
530
+ claim.materializeModeSkills({ materializeModuleSkills: true });
531
+ assert.equal(lines.length, 1, 'exactly one line, not a wall');
532
+ assert.match(lines[0], /claude-materialize\.js --module-skills-only/, 'the hint names the command that does it by hand');
533
+ assert.match(lines[0], /disk on fire/, 'and why it could not');
534
+
535
+ // a tree that is not the core checkout is a no-op even with the flag on
536
+ lines.length = 0;
537
+ require.cache[mPath].exports = {
538
+ isCoreCheckoutRoot: () => false,
539
+ materializeModuleSkillsIntoCore: () => { throw new Error('must never be called in an instance'); },
540
+ };
541
+ claim.materializeModeSkills({ materializeModuleSkills: true });
542
+ assert.deepEqual(lines, [], 'in a scaffolded instance the claim is unchanged — init already landed them');
543
+ } finally {
544
+ require.cache[mPath].exports = realExports;
545
+ console.log = realLog;
546
+ }
547
+ });
548
+
549
+ t('materializeModeSkills lands the real module-owned skills, self-ignored, and is byte-identical on a re-claim', () => {
550
+ const core = fakeCore();
551
+ // the real function resolves its own coreRoot, so drive the library the way it does
552
+ const first = m.materializeModuleSkillsIntoCore({ coreRoot: core });
553
+ assert.deepEqual(first.materialized, ['alpha']);
554
+ const manifest1 = readFileSync(join(core, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8');
555
+ const ignore = readFileSync(join(core, '.claude', 'skills', '.gitignore'), 'utf8');
556
+ assert.ok(ignore.split('\n').includes('/alpha/'), 'a re-claim leaves git status clean: the copy ignores itself');
557
+ const second = m.materializeModuleSkillsIntoCore({ coreRoot: core });
558
+ assert.deepEqual(second.materialized, ['alpha'], 'idempotent: the re-claim lands the same set');
559
+ assert.equal(readFileSync(join(core, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8'), manifest1, 'byte-identical manifest');
560
+ rmSync(core, { recursive: true, force: true });
561
+ });
562
+
487
563
  console.log(`\nclaude_materialize: ${passed} passed, ${failed} failed`);
488
564
  process.exit(failed ? 1 : 0);