@bongos/core 1.17.1 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.bongos-core.json +244 -264
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/GOVERNANCE.md +1 -1
  15. package/README.md +1 -1
  16. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  17. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  18. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  19. package/docs/adr/0020-security-model-public-stub.md +1 -1
  20. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  21. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  22. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  23. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  24. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  25. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  26. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  27. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  28. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  29. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  31. package/docs/adr/0057-container-cost-ledger.md +3 -3
  32. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  33. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  34. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  35. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  36. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  37. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  38. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  39. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  40. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  41. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  42. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  43. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  44. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  45. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  46. package/docs/adr/README.md +9 -6
  47. package/docs/architecture.md +2 -2
  48. package/docs/branding-contract.md +5 -4
  49. package/docs/canonical-permissions.md +1 -1
  50. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  51. package/docs/file-map.md +18 -17
  52. package/docs/handoff-template.md +2 -2
  53. package/docs/modules-contract.md +6 -1
  54. package/docs/onboarding/diagrams/README.md +7 -7
  55. package/docs/onboarding/drachmae-and-karma.md +18 -18
  56. package/docs/onboarding/primer.md +9 -9
  57. package/docs/recipes/audit-replay.md +2 -2
  58. package/docs/recipes/builder-box-lifecycle.md +2 -2
  59. package/docs/recipes/claude-design-loop.md +1 -1
  60. package/docs/recipes/ops-gotchas.md +1 -1
  61. package/docs/recipes/private-npm-distribution.md +17 -0
  62. package/docs/recipes/staging-environment.md +1 -1
  63. package/docs/recipes/upgrading-the-core.md +55 -0
  64. package/docs/research/cursor-competitive-analysis.md +1 -1
  65. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  66. package/modules/discord/module.json +2 -1
  67. package/modules/economy/achievements.js +9 -1
  68. package/modules/economy/credits.js +10 -1
  69. package/modules/hall-ui/public/builders.js +71 -4
  70. package/modules/hall-ui/public/goal-inbox.js +191 -0
  71. package/modules/hall-ui/public/hall-widgets.js +14 -1
  72. package/modules/hall-ui/public/index.html +3 -0
  73. package/modules/hall-ui/public/new-project-wizard.js +1 -0
  74. package/modules/hall-ui/public/style.css +43 -0
  75. package/modules/provisioning/CLAUDE.md +15 -0
  76. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  77. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  78. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  79. package/modules/provisioning/module.json +17 -0
  80. package/modules/provisioning/onboard-plan.js +37 -0
  81. package/modules/provisioning/provisioning.js +539 -0
  82. package/modules/provisioning/rate-limit.js +35 -0
  83. package/modules/provisioning/routes/provisioning.js +607 -0
  84. package/modules/provisioning/tests/provisioning.mjs +232 -0
  85. package/modules/provisioning/ui/.gitkeep +0 -0
  86. package/modules/security/secret-scrub.js +10 -0
  87. package/package-lock.json +2 -2
  88. package/package.json +7 -2
  89. package/scripts/discord/README.md +1 -1
  90. package/scripts/gds/claim.js +98 -48
  91. package/scripts/gds/claude-materialize.js +30 -9
  92. package/scripts/gds/cutover-codemod.js +241 -0
  93. package/scripts/gds/fitness.js +54 -0
  94. package/scripts/gds/init.js +59 -3
  95. package/scripts/gds/lint-infra-exec.js +70 -0
  96. package/scripts/gds/lint-workflow-exec.js +63 -0
  97. package/scripts/gds/mirror-lineage-check.js +14 -5
  98. package/scripts/gds/mirror-redact.js +8 -0
  99. package/scripts/gds/onboard.js +4 -0
  100. package/scripts/gds/package-core.js +18 -3
  101. package/scripts/gds/publish-manifest.js +60 -2
  102. package/scripts/gds/run-routine.js +51 -13
  103. package/scripts/gds/run-unit-tests.js +4 -0
  104. package/scripts/gds/update-channel.js +186 -0
  105. package/scripts/gds/upgrade.js +116 -9
  106. package/src/bongos/auth.js +14 -1
  107. package/src/bongos/serve-internal.js +10 -1
  108. package/src/branding.js +1 -0
  109. package/src/module-api.js +12 -1
  110. package/src/module-loader/loader.js +36 -4
  111. package/src/module-loader/manifest-schema.js +16 -1
  112. package/tests/branding.mjs +11 -0
  113. package/tests/claim_error_surface.mjs +107 -0
  114. package/tests/claude_materialize.mjs +20 -0
  115. package/tests/consumer_layout_boot.mjs +302 -0
  116. package/tests/cutover_codemod.mjs +184 -0
  117. package/tests/device_flow_start.mjs +25 -2
  118. package/tests/hook_core_resolve.mjs +125 -0
  119. package/tests/infra_exec_paths.mjs +104 -0
  120. package/tests/init.mjs +47 -0
  121. package/tests/mirror_lineage_proof.mjs +14 -0
  122. package/tests/module_loader.mjs +42 -0
  123. package/tests/module_manifest.mjs +19 -0
  124. package/tests/onboard.mjs +13 -0
  125. package/tests/package_core.mjs +8 -0
  126. package/tests/provisioning_github_app_routes.mjs +15 -0
  127. package/tests/publish_manifest.mjs +52 -1
  128. package/tests/run_routine.mjs +26 -1
  129. package/tests/run_routine_two_root.mjs +77 -0
  130. package/tests/scrubber_corpus.mjs +17 -0
  131. package/tests/update_channel.mjs +186 -0
  132. package/tests/upgrade.mjs +131 -0
  133. package/tests/workflow_exec_paths.mjs +98 -0
  134. package/docs/design/container-exterior-tiles.md +0 -49
  135. package/docs/design/container-exterior.md +0 -81
  136. package/docs/design/container-interior.md +0 -77
  137. package/docs/design/world-map-scroll.md +0 -41
  138. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  139. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  140. package/docs/recipes/cloudbongos-standup.md +0 -111
  141. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  142. package/docs/recipes/parallel-load-test.md +0 -32
  143. package/docs/recipes/pixel-art-pipeline.md +0 -411
  144. package/docs/recipes/staging-data.md +0 -50
  145. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  146. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  147. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  148. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  149. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  150. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  151. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  152. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  153. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  154. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  155. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  156. package/scripts/gds/art-stage.js +0 -152
  157. package/server.js +0 -212
  158. package/tests/art_key.mjs +0 -121
  159. package/tests/day_cycle.mjs +0 -197
  160. package/tests/snapshot_picker.mjs +0 -233
  161. package/tests/sprite_facing.mjs +0 -35
  162. package/tests/terrain_picker.mjs +0 -298
  163. package/tests/turn_in_place.mjs +0 -280
  164. package/tests/walkability.mjs +0 -188
@@ -230,7 +230,20 @@ async function startDeviceFlow() {
230
230
  // device_code silently produced a broken flow / an opaque 500 (the 2026-07-08 demo
231
231
  // login failure). Mirror the token-exchange guard (data.error, above); task 2126.
232
232
  if (data.error) {
233
- throw new Error(`GitHub rejected device-flow start: ${data.error}${data.error_description ? ` — ${data.error_description}` : ''}`);
233
+ // device_flow_disabled is self-inflicted + common: GitHub defaults Device Flow OFF on new
234
+ // Apps and the one-click manifest can't enable it (task 2129). Make the CLI error self-
235
+ // service by naming the right settings surface. We hold only client_id here (not the app
236
+ // slug — that lives on the control plane), so we can't build /settings/apps/<slug>; but the
237
+ // id PREFIX distinguishes a GitHub App (Iv*) from an OAuth App (Ov*), which picks the right
238
+ // listing to open.
239
+ let hint = '';
240
+ if (data.error === 'device_flow_disabled') {
241
+ const where = /^Iv/i.test(creds.client_id || '')
242
+ ? 'https://github.com/settings/apps (org-owned apps: https://github.com/organizations/<org>/settings/apps)'
243
+ : 'https://github.com/settings/developers';
244
+ hint = ` — terminal sign-in needs Device Flow enabled on the app: open ${where}, open your app, check "Enable Device Flow", and save. Web sign-in is unaffected.`;
245
+ }
246
+ throw new Error(`GitHub rejected device-flow start: ${data.error}${data.error_description ? ` — ${data.error_description}` : ''}${hint}`);
234
247
  }
235
248
  if (!data.device_code) {
236
249
  throw new Error('GitHub device/code returned no device_code (unexpected response shape).');
@@ -318,7 +318,16 @@ function mountInternalSurfaces(app) {
318
318
  // confirmation can commit-PIN (deployedCommit === mergeCommit) rather than only
319
319
  // checking liveness via /healthz. Low-sensitivity (a public commit sha).
320
320
  app.get('/version', (_req, res) => {
321
- res.json(buildInfo());
321
+ // coreVersion — the @bongos/core doorway version this process is running
322
+ // (src/module-api CORE_VERSION), read lazily to avoid a load-time cycle
323
+ // (module-api requires build-info at load, so a top-level require here
324
+ // would be circular). Additive: lets fleet/upgrade tooling confirm which
325
+ // core an instance actually materialized after a `bongos upgrade`,
326
+ // alongside the deployed git commit below. (Added in 1.17.2, task 2148 —
327
+ // the throwaway `updateCanary` proof marker was removed in phase E.)
328
+ let coreVersion = null;
329
+ try { coreVersion = require('../module-api').CORE_VERSION; } catch { /* fail-soft: version unknown */ }
330
+ res.json({ ...buildInfo(), coreVersion });
322
331
  });
323
332
 
324
333
  // Generated typed client for classic <script> web pages (task 1996 / R09).
package/src/branding.js CHANGED
@@ -42,6 +42,7 @@ const ENV_OVERRIDES = [
42
42
  ['BUILDERS_ORIGIN', ['domains', 'buildersOrigin']],
43
43
  ['STATUS_ORIGIN', ['domains', 'statusOrigin']],
44
44
  ['OAUTH_ORIGIN', ['domains', 'oauthOrigin']],
45
+ ['PROVISIONING_ORIGIN', ['domains', 'provisioningOrigin']],
45
46
  ['COOKIE_DOMAIN', ['domains', 'cookieDomain']],
46
47
  ];
47
48
 
package/src/module-api.js CHANGED
@@ -188,8 +188,19 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
188
188
  // REQUIRED because 1.17.0 was already published to npm and task 2121 shipped
189
189
  // without one, so `bongos package-core --publish` on 1.17.0 would be rejected as a
190
190
  // re-publish. Every `^1.x` / `^1.17` module pin is satisfied by 1.17.1.
191
+ // 1.17.2 — task 2148 (canary upgrade-proof on demo.cloudbongos.com): a controlled
192
+ // PATCH bump used to round-trip the real `bongos upgrade` channel on the live
193
+ // demo instance with an OBSERVABLE code marker (the channel shipped in 1.16.0
194
+ // and was proven on emersonian-circles by task 1976, but never on demo with a
195
+ // served marker). No doorway export is added, changed, or removed — the exported
196
+ // surface is byte-identical to 1.17.1. What changed: the served /version payload
197
+ // now carries `coreVersion` (the running doorway version — a PERMANENT additive
198
+ // read so upgrade/fleet tooling can confirm which core an instance materialized),
199
+ // plus a THROWAWAY `updateCanary` marker removed in the phase-E follow-up. A
200
+ // backwards-compatible additive change with no API removal is exactly semver
201
+ // PATCH. Every `^1.x` / `^1.17` module pin is satisfied by 1.17.2.
191
202
  // ---------------------------------------------------------------------------
192
- const CORE_VERSION = '1.17.1'; // see the changelog above — 1.17.1 republishes the artifact with the task-2121 agent-doc stub (no doorway change) (task 2124)
203
+ const CORE_VERSION = '1.17.2'; // see the changelog above — 1.17.2 is the task-2148 canary upgrade-proof bump (adds /version coreVersion; no doorway export change)
193
204
 
194
205
  // A namespaced logger so a module's log lines are attributable + consistent.
195
206
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -19,11 +19,42 @@ const fs = require('node:fs');
19
19
  const path = require('node:path');
20
20
  const { validateManifest } = require('./manifest-schema');
21
21
  const { satisfies } = require('./semver');
22
- const { resolveCoreRoot } = require('../instance-config');
22
+ const { resolveCoreRoot, resolveInstanceRoot } = require('../instance-config');
23
23
 
24
- // ADR 0108 §1: modules/ ships WITH the core package. resolveCoreRoot() returns
25
- // the repo root today, so this is byte-identical for the live instance.
26
- const DEFAULT_ROOTS = [path.join(resolveCoreRoot(), 'modules')];
24
+ // Collapse roots that resolve to the SAME absolute path, preserving order.
25
+ // Normalizes via path.resolve so a cwd that arrives with a trailing '.'/'//'
26
+ // segment still dedupes against the core root. Returns the resolved (canonical)
27
+ // paths. (Exported for the loader tests.)
28
+ function dedupeRoots(roots) {
29
+ const out = [];
30
+ const seen = new Set();
31
+ for (const r of roots) {
32
+ const key = path.resolve(r);
33
+ if (seen.has(key)) continue;
34
+ seen.add(key);
35
+ out.push(key);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ // ADR 0108 §1 + task 2133 (R89-prereq): discovery must span TWO roots — the core
41
+ // PACKAGE's own modules/ (resolveCoreRoot()) AND the consumer/instance's own
42
+ // modules/ (resolveInstanceRoot()). Post-split the host modules (game /
43
+ // art-pipeline / provisioning) live at the instance root and are EXCLUDED from
44
+ // the @bongos/core package, so a core-only scan boots a split instance with none
45
+ // of its host modules.
46
+ //
47
+ // DEDUP BY RESOLVED PATH IS MANDATORY. In a single checkout resolveInstanceRoot()
48
+ // falls back to process.cwd(), which equals resolveCoreRoot() (the repo root), so
49
+ // the two candidates are the SAME directory. discoverModules() treats a repeated
50
+ // module KEY as a hard ERROR (not a skip), so scanning one root twice would turn
51
+ // every module into a "duplicate module key" error. dedupeRoots() collapses the
52
+ // coincident roots, so a single checkout scans ONE root (byte-identical to
53
+ // before) while a diverged split scans BOTH.
54
+ const DEFAULT_ROOTS = dedupeRoots([
55
+ path.join(resolveCoreRoot(), 'modules'),
56
+ path.join(resolveInstanceRoot(), 'modules'),
57
+ ]);
27
58
 
28
59
  // Lazy — avoids an eval-time require cycle (modules.js → loader → module-api →
29
60
  // auth → …). By call time the graph is loaded; falls back to the known initial
@@ -263,6 +294,7 @@ module.exports = {
263
294
  stopModulePollers,
264
295
  loaderSnapshot,
265
296
  coreVersion,
297
+ dedupeRoots,
266
298
  DEFAULT_ROOTS,
267
299
  _reset,
268
300
  };
@@ -30,7 +30,11 @@
30
30
  // },
31
31
  // "provides": ["reward"], // OPTIONAL. seam PORTS this module registers a provider for.
32
32
  // "consumes": ["grade"], // OPTIONAL. seam ports this module resolves (required capabilities).
33
- // "prerequisites": { "modules": ["economy"] } // OPTIONAL. other modules that must be enabled.
33
+ // "prerequisites": { "modules": ["economy"] }, // OPTIONAL. other modules that must be enabled.
34
+ // "dependencies": { "discord.js": "^14.26.4" } // OPTIONAL. external npm deps this module needs at
35
+ // // runtime. Core stays lean (express+pg, ADR 0108); a bundled
36
+ // // module that needs more declares it HERE, and the consumer
37
+ // // composes the deps of the modules it ENABLES (ADR 0138).
34
38
  // }
35
39
  //
36
40
  // validateManifest(obj) -> { valid, errors[], manifest } — pure, no I/O.
@@ -126,6 +130,17 @@ function validateManifest(obj) {
126
130
  }
127
131
  }
128
132
 
133
+ // --- optional runtime npm dependencies (ADR 0138 / task 2160) ---
134
+ // A bundled module's own external npm deps. @bongos/core stays lean (express+pg, ADR 0108);
135
+ // a module that needs more (e.g. discord.js) declares it here, and the consumer composes the
136
+ // deps of the modules it ENABLES (scripts/gds/init.js). Shape: { "<npm-name>": "<semver-range>" }.
137
+ if ('dependencies' in obj) {
138
+ const d = obj.dependencies;
139
+ if (!isObj(d) || !Object.entries(d).every(([k, v]) => isStr(k) && isStr(v))) {
140
+ E('dependencies: must be an object of { "<npm-name>": "<semver-range>" } string pairs');
141
+ }
142
+ }
143
+
129
144
  return { valid: errors.length === 0, errors, manifest: errors.length === 0 ? obj : null };
130
145
  }
131
146
 
@@ -52,6 +52,17 @@ test('env override is ignored when empty', () => {
52
52
  assert.equal(b.domains.publicOrigin, 'https://amazonprimea.com');
53
53
  });
54
54
 
55
+ test('provisioningOrigin: optional, passes through config, env-overridable (task 2101)', () => {
56
+ // Not required — unset resolves to undefined, and callers fall back to publicOrigin.
57
+ assert.equal(resolveBranding({ neutral: NEUTRAL, instance: OTB }).domains.provisioningOrigin, undefined);
58
+ // Set in the instance pack, it survives the deep-merge.
59
+ const inst = resolveBranding({ neutral: NEUTRAL, instance: { ...OTB, domains: { ...OTB.domains, provisioningOrigin: 'https://get.cloudbongos.com' } } });
60
+ assert.equal(inst.domains.provisioningOrigin, 'https://get.cloudbongos.com');
61
+ // And <PREFIX>_PROVISIONING_ORIGIN beats config (same precedence as the other origins).
62
+ const env = resolveBranding({ neutral: NEUTRAL, instance: OTB, env: { OTB_PROVISIONING_ORIGIN: 'https://get.cloudbongos.com' } });
63
+ assert.equal(env.domains.provisioningOrigin, 'https://get.cloudbongos.com');
64
+ });
65
+
55
66
  test('missing required field throws', () => {
56
67
  assert.throws(() => resolveBranding({ neutral: { identity: { productName: 'X' } } }), /missing required field/);
57
68
  });
@@ -0,0 +1,107 @@
1
+ // tests/claim_error_surface.mjs
2
+ //
3
+ // task 1476 — a failed /builder-claim must read as GUIDANCE, not a raw object
4
+ // dump. The API returns the R01 error envelope (ADR 0116):
5
+ // { error: { code, message, details } }
6
+ // so claim.js's old `r.data.error === 'CODE'` string branches all silently
7
+ // missed (error is now an object) and the refusal printed as `[object Object]`.
8
+ // This exercises the pure formatClaimFailure() surface DB-free, over BOTH the
9
+ // current envelope shape and the legacy flat shape.
10
+ //
11
+ // Run: node tests/claim_error_surface.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { formatClaimFailure } = require('../scripts/gds/claim.js');
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+ function t(name, fn) {
22
+ try { fn(); passed++; console.log(` PASS ${name}`); }
23
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
24
+ }
25
+
26
+ const envelope = (code, details = {}, message = code, status = 409) => ({
27
+ status,
28
+ data: { error: { code, message, details } },
29
+ });
30
+ const text = (r) => r.lines.join('\n');
31
+
32
+ // The regression guard: no refusal may ever render a raw object.
33
+ t('never dumps [object Object] for any known code', () => {
34
+ const codes = [
35
+ 'stage_required', 'session_already_has_active_claim', 'session_claim_check_failed',
36
+ 'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
37
+ 'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
38
+ 'some_unmapped_future_code',
39
+ ];
40
+ for (const c of codes) {
41
+ const out = text(formatClaimFailure(envelope(c, { active_task_id: 5 }, 'msg'), 12));
42
+ assert.ok(!out.includes('[object Object]'), `${c} produced a raw object dump`);
43
+ assert.ok(out.trim().length > 0, `${c} produced no output`);
44
+ }
45
+ });
46
+
47
+ // The core 1476 case: one-claim-per-session refusal is guidance + teaches parallel.
48
+ t('session_already_has_active_claim: names the in-flight task and teaches parallelism', () => {
49
+ const out = text(formatClaimFailure(
50
+ envelope('session_already_has_active_claim', {
51
+ active_task_id: 42, active_task_title: 'Foo', what_to_do: 'ship 42 first',
52
+ }), 99));
53
+ assert.match(out, /#42/, 'should name the in-flight task id');
54
+ assert.match(out, /Foo/, 'should name the in-flight task title');
55
+ assert.match(out, /ship 42 first/, 'should relay the server what_to_do');
56
+ assert.match(out, /PARALLEL/i, 'should teach how to parallelize');
57
+ assert.match(out, /worktree|bulk-claim/i, 'should point at separate worktree or bulk-claim');
58
+ });
59
+
60
+ t('session_claim_check_failed shares the same guidance branch', () => {
61
+ const out = text(formatClaimFailure(envelope('session_claim_check_failed', {}), 3));
62
+ assert.match(out, /active claim/i);
63
+ assert.match(out, /PARALLEL/i);
64
+ });
65
+
66
+ // Envelope + legacy flat shape must BOTH resolve the code + fields.
67
+ t('TASK_NOT_READY: envelope shape reads details.currentStatus + hint', () => {
68
+ const out = text(formatClaimFailure(
69
+ envelope('TASK_NOT_READY', { currentStatus: 'backlog', hint: 'promote it first' }), 7));
70
+ assert.match(out, /not ready/i);
71
+ assert.match(out, /backlog/);
72
+ assert.match(out, /promote it first/);
73
+ });
74
+
75
+ t('TASK_NOT_READY: legacy flat shape still resolves', () => {
76
+ const flat = { status: 409, data: { error: 'TASK_NOT_READY', currentStatus: 'blocked' } };
77
+ const out = text(formatClaimFailure(flat, 7));
78
+ assert.match(out, /not ready/i);
79
+ assert.match(out, /blocked/);
80
+ });
81
+
82
+ t('DEPS_NOT_SHIPPED: lists each blocking dependency', () => {
83
+ const out = text(formatClaimFailure(
84
+ envelope('DEPS_NOT_SHIPPED', { deps: [{ id: 10, status: 'ready', title: 'Dep A' }] }), 8));
85
+ assert.match(out, /#10/);
86
+ assert.match(out, /Dep A/);
87
+ });
88
+
89
+ t('MODULE_CONFLICT + TOUCHES_CONFLICT: list conflicting task titles', () => {
90
+ const mc = text(formatClaimFailure(
91
+ envelope('MODULE_CONFLICT', { module_key: 'economy', conflictTaskTitles: ['Bar'] }), 4));
92
+ assert.match(mc, /economy/);
93
+ assert.match(mc, /Bar/);
94
+ const tc = text(formatClaimFailure(
95
+ envelope('TOUCHES_CONFLICT', { conflictTaskTitles: ['Baz'] }), 4));
96
+ assert.match(tc, /Baz/);
97
+ });
98
+
99
+ t('unknown code: fallback carries code + message, exit 1', () => {
100
+ const r = formatClaimFailure(envelope('weird_thing', {}, 'boom', 500), 1);
101
+ assert.equal(r.exit, 1);
102
+ assert.match(text(r), /weird_thing/);
103
+ assert.match(text(r), /boom/);
104
+ });
105
+
106
+ console.log(`\nclaim_error_surface: ${passed} passed, ${failed} failed`);
107
+ process.exit(failed === 0 ? 0 : 1);
@@ -108,6 +108,26 @@ t('materializeClaude: copies + rewrites skills into a fresh instance dir (no lef
108
108
  rmSync(dir, { recursive: true, force: true });
109
109
  });
110
110
 
111
+ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companions keep _core-resolve (task 2138)', () => {
112
+ const dir = mkdtempSync(join(tmpdir(), 'w3-rt-'));
113
+ const res = m.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
114
+ assert.ok(res.scheduledTasks > 0, 'at least one scheduled-task file materialized');
115
+ const rt = join(dir, '.claude', 'scheduled-tasks');
116
+ assert.ok(existsSync(rt), '.claude/scheduled-tasks created in the instance');
117
+ // A prose/judgment body's core invocation is bongosified so a consumer-layout model can run it.
118
+ const stale = readFileSync(join(rt, 'stale-claim-release', 'SKILL.md'), 'utf8');
119
+ assert.ok(stale.includes('bongos exec scripts/gds/pulse.js'), 'body core-invocation rewritten to bongos exec');
120
+ assert.ok(!stale.includes('node scripts/gds/pulse.js'), 'no bare relative node scripts/gds left in the body');
121
+ // A deterministic routine's `.claude/...` companion-step command is NOT a core invocation → preserved.
122
+ const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
123
+ assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
124
+ // Companion .js resolves its core require()s via the materialized _core-resolve.js sibling (task 2139 pattern).
125
+ const check = readFileSync(join(rt, 'diagram-drift', 'check.js'), 'utf8');
126
+ assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
127
+ assert.ok(!check.includes("require('../../../scripts/gds/"), 'no bare ../../../scripts/gds require left in the companion');
128
+ rmSync(dir, { recursive: true, force: true });
129
+ });
130
+
111
131
  t('materializeClaude: SELF-COPY guard — target === core root is a no-op (byte-identical for the live instance)', () => {
112
132
  const res = m.materializeClaude({ coreRoot: ROOT, instanceDir: ROOT, dryRun: false });
113
133
  assert.equal(res.skipped, true, 'materializing onto the core root must skip');
@@ -0,0 +1,302 @@
1
+ // tests/consumer_layout_boot.mjs
2
+ //
3
+ // R89-prereq CI proof (task 2142): boot @bongos/core from a REAL consumer layout —
4
+ // the packaged .tgz installed into node_modules, with the instance's OWN content
5
+ // (config / modules / .claude) sitting at a DIFFERENT root — and prove the two-root
6
+ // module loader (task 2133) discovers modules from BOTH roots.
7
+ //
8
+ // WHY THIS EXISTS. Every existing gate runs the SOURCE tree with all deps present:
9
+ // - tests/platform_boot.mjs imports src/platform-server.js straight from the repo;
10
+ // - tests/fitness_two_root.mjs diverges resolveInstanceRoot() to a temp dir but
11
+ // keeps cwd/core = the real repo (resolveCoreRoot() is still __dirname/..).
12
+ // NONE installs the packaged core into a tree whose instance root is elsewhere —
13
+ // which is exactly why the pre-2133 single-root loader gap was invisible: a
14
+ // core-only scan boots a split instance with none of its host modules, and no test
15
+ // ever exercised that shape. This harness closes the blind spot end to end.
16
+ //
17
+ // WHAT IT PROVES (the two-root checks all go RED against the pre-2133 loader):
18
+ // 1. package — scripts/gds/package-core.js builds an installable .tgz.
19
+ // 2. install — `npm install <tgz>` lands it at node_modules/@bongos/core.
20
+ // 3. roots — resolveCoreRoot() is UNDER node_modules/@bongos/core while
21
+ // resolveInstanceRoot() is the consumer tree: two DIFFERENT dirs, so
22
+ // a green result can never be a same-directory coincidence.
23
+ // 4. boot — `node node_modules/@bongos/core/src/platform-server.js` serves
24
+ // /healthz 200 (the game-less core boots from the package).
25
+ // 5. two-root — discoverModules() loads a module resolved from the CORE modules
26
+ // dir AND the consumer-probe module resolved from the INSTANCE
27
+ // modules dir, with zero errors. A single-root loader physically
28
+ // cannot find consumer-probe → this IS the task-2133 done-when.
29
+ // 6. fitness — the packaged fitness.js discoveredModuleKeys() spans both roots.
30
+ // 7. gen-file-map — reads the INSTANCE root (lists the consumer's .claude/skills).
31
+ // 8. gen-repo-map — maps CORE code (scripts/gds/fitness.js) from node_modules.
32
+ //
33
+ // This is an INTEGRATION harness (a package build + `npm install` + a live boot), so
34
+ // it is EXCLUDED from the DB-free unit gate (scripts/gds/run-unit-tests.js INTEGRATION
35
+ // set) and driven by .github/workflows/consumer-layout-boot.yml. It needs NO Postgres
36
+ // (/healthz, the loader, fitness, and the generators are all DB-free). Re-runnable:
37
+ // node tests/consumer_layout_boot.mjs # builds a fresh core .tgz
38
+ // CORE_TGZ=/abs/path/bongos-core-x.y.z.tgz node tests/consumer_layout_boot.mjs # reuse a prebuilt one
39
+
40
+ import assert from 'node:assert/strict';
41
+ import http from 'node:http';
42
+ import net from 'node:net';
43
+ import { spawn, spawnSync } from 'node:child_process';
44
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from 'node:fs';
45
+ import { tmpdir } from 'node:os';
46
+ import { join, dirname } from 'node:path';
47
+ import { fileURLToPath } from 'node:url';
48
+
49
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
50
+ const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm';
51
+ const CORE_VERSION_RANGE = '^1.0.0'; // consumer-probe's coreVersion — satisfied by any 1.x core.
52
+
53
+ let passed = 0;
54
+ let failed = 0;
55
+ function check(name, fn) {
56
+ try { fn(); passed++; console.log(` PASS ${name}`); }
57
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.message ? e.message : e}`); }
58
+ }
59
+
60
+ // Run a node snippet in `cwd` and parse its stdout as JSON. Used to introspect the
61
+ // INSTALLED core (its loader / fitness / generators) exactly as a consumer process
62
+ // would resolve them — absolute paths are injected so the snippet never depends on
63
+ // the snippet file's own location (mirrors tests/fitness_two_root.mjs).
64
+ function runNodeJSON(snippet, cwd) {
65
+ const r = spawnSync(process.execPath, ['-e', snippet], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
66
+ if (r.status !== 0) {
67
+ throw new Error(`node snippet exited ${r.status}\n--- stdout ---\n${r.stdout}\n--- stderr ---\n${r.stderr}`);
68
+ }
69
+ try { return JSON.parse(r.stdout); }
70
+ catch { throw new Error(`snippet stdout was not JSON:\n${r.stdout}\n--- stderr ---\n${r.stderr}`); }
71
+ }
72
+
73
+ function freePort() {
74
+ return new Promise((resolve, reject) => {
75
+ const srv = net.createServer();
76
+ srv.on('error', reject);
77
+ srv.listen(0, '127.0.0.1', () => {
78
+ const { port } = srv.address();
79
+ srv.close(() => resolve(port));
80
+ });
81
+ });
82
+ }
83
+
84
+ function getHealthz(port) {
85
+ return new Promise((resolve) => {
86
+ const req = http.get({ host: '127.0.0.1', port, path: '/healthz', timeout: 2000 }, (res) => {
87
+ let body = '';
88
+ res.on('data', (c) => (body += c));
89
+ res.on('end', () => resolve({ status: res.statusCode, body }));
90
+ });
91
+ req.on('error', () => resolve(null));
92
+ req.on('timeout', () => { req.destroy(); resolve(null); });
93
+ });
94
+ }
95
+
96
+ async function waitForHealthz(port, timeoutMs = 20000) {
97
+ const deadline = Date.now() + timeoutMs;
98
+ while (Date.now() < deadline) {
99
+ const res = await getHealthz(port);
100
+ if (res) return res;
101
+ await new Promise((r) => setTimeout(r, 300));
102
+ }
103
+ return null;
104
+ }
105
+
106
+ async function main() {
107
+ console.log('=== consumer-layout boot proof (task 2142): install @bongos/core, boot it, assert two-root module load ===');
108
+
109
+ const workRoot = mkdtempSync(join(tmpdir(), 'consumer-layout-'));
110
+ const consumer = join(workRoot, 'consumer');
111
+ let child = null;
112
+
113
+ try {
114
+ // --- 1. package -----------------------------------------------------------
115
+ // Build the installable core .tgz from HEAD (the committed tree — so on a PR the
116
+ // job packages exactly the code under review). Reuse a prebuilt one via CORE_TGZ.
117
+ let tgz = process.env.CORE_TGZ || '';
118
+ check('package: scripts/gds/package-core.js builds an installable .tgz', () => {
119
+ if (tgz) { assert.ok(existsSync(tgz), `CORE_TGZ does not exist: ${tgz}`); return; }
120
+ const outDir = join(workRoot, 'dist');
121
+ const r = spawnSync(process.execPath, [join(REPO_ROOT, 'scripts/gds/package-core.js'), '--out-dir', outDir],
122
+ { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
123
+ assert.equal(r.status, 0, `package-core exited ${r.status}\n${r.stderr}`);
124
+ const pinLine = r.stdout.trim().split('\n').filter(Boolean).pop();
125
+ const pin = JSON.parse(pinLine);
126
+ tgz = join(outDir, pin.tarball.name);
127
+ assert.ok(existsSync(tgz), `packaged tarball missing: ${tgz}`);
128
+ });
129
+ if (!tgz || !existsSync(tgz)) { throw new Error('cannot continue without a core tarball'); }
130
+
131
+ // --- build the consumer tree: instance content at its OWN root -------------
132
+ // A minimal-but-real Cloud Bongos consumer: a bongos.config.json pointing the
133
+ // instance root at itself, a HOST module (consumer-probe) that is NOT part of
134
+ // @bongos/core, and an instance-owned .claude/skills dir. The host module is the
135
+ // crux: it can only be discovered via resolveInstanceRoot(), so finding it proves
136
+ // the second root is scanned.
137
+ mkdirSync(consumer, { recursive: true });
138
+ writeFileSync(join(consumer, 'package.json'), `${JSON.stringify({
139
+ name: 'consumer-layout-probe',
140
+ version: '0.0.0',
141
+ private: true,
142
+ description: 'Ephemeral @bongos/core consumer used by the R89 two-root CI proof (task 2142).',
143
+ dependencies: { '@bongos/core': `file:${tgz}` },
144
+ }, null, 2)}\n`);
145
+ writeFileSync(join(consumer, 'bongos.config.json'), `${JSON.stringify({ instanceRoot: '.' }, null, 2)}\n`);
146
+ // A tiny instance CLAUDE.md (the charter heading keeps fitness's budget check happy
147
+ // if anything reads it) — makes the tree a realistic instance, not load-bearing here.
148
+ writeFileSync(join(consumer, 'CLAUDE.md'),
149
+ '# Consumer probe instance\n\n## Working with this file (content charter)\n\nEphemeral fixture for the task-2142 two-root CI proof.\n');
150
+ mkdirSync(join(consumer, 'config'), { recursive: true });
151
+ writeFileSync(join(consumer, 'config', 'branding.json'), `${JSON.stringify({
152
+ configDir: 'cloudbongos', envPrefix: 'CLOUDBONGOS',
153
+ identity: { productName: 'Consumer Probe' },
154
+ }, null, 2)}\n`);
155
+ // The host module — a valid manifest, key === dir name, coreVersion any 1.x.
156
+ mkdirSync(join(consumer, 'modules', 'consumer-probe'), { recursive: true });
157
+ writeFileSync(join(consumer, 'modules', 'consumer-probe', 'module.json'), `${JSON.stringify({
158
+ key: 'consumer-probe',
159
+ title: 'Consumer Probe',
160
+ description: 'A host module that ships only in the instance repo, never in @bongos/core.',
161
+ coreVersion: CORE_VERSION_RANGE,
162
+ contributes: {},
163
+ }, null, 2)}\n`);
164
+ // Instance-owned .claude/skills (gen-file-map lists the instance root's skills).
165
+ mkdirSync(join(consumer, '.claude', 'skills', 'consumer-probe-skill'), { recursive: true });
166
+ writeFileSync(join(consumer, '.claude', 'skills', 'consumer-probe-skill', 'SKILL.md'),
167
+ '# consumer-probe-skill\n\nInstance-owned skill fixture for the task-2142 proof.\n');
168
+ mkdirSync(join(consumer, '.claude', 'scheduled-tasks'), { recursive: true });
169
+
170
+ // --- 2. install -----------------------------------------------------------
171
+ const coreDir = join(consumer, 'node_modules', '@bongos', 'core');
172
+ check('install: npm install lands the core at node_modules/@bongos/core', () => {
173
+ const r = spawnSync(NPM_BIN, ['install', '--no-audit', '--no-fund', '--loglevel=error'],
174
+ { cwd: consumer, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
175
+ assert.equal(r.status, 0, `npm install exited ${r.status}\n${r.stderr}`);
176
+ assert.ok(existsSync(join(coreDir, 'package.json')), 'node_modules/@bongos/core/package.json missing');
177
+ assert.ok(existsSync(join(coreDir, 'src', 'platform-server.js')), 'installed core has no src/platform-server.js');
178
+ assert.ok(existsSync(join(coreDir, 'modules')), 'installed core ships no modules/ dir');
179
+ });
180
+ if (!existsSync(coreDir)) { throw new Error('cannot continue without an installed core'); }
181
+
182
+ const A = (p) => JSON.stringify(join(coreDir, p)); // absolute path into the installed core, JSON-quoted
183
+
184
+ // --- 3. roots diverge -----------------------------------------------------
185
+ // The whole proof is meaningless if the two roots secretly coincide, so pin that
186
+ // down first: core resolves UNDER node_modules/@bongos/core, instance is the
187
+ // consumer tree, and they are different directories.
188
+ check('roots: resolveCoreRoot() (node_modules) and resolveInstanceRoot() (consumer) are distinct', () => {
189
+ const r = runNodeJSON(`
190
+ const ic = require(${A('src/instance-config.js')});
191
+ process.stdout.write(JSON.stringify({ core: ic.resolveCoreRoot(), inst: ic.resolveInstanceRoot() }));
192
+ `, consumer);
193
+ // Compare realpaths — the child's process.cwd() resolves symlinks (e.g. macOS
194
+ // /var → /private/var), so match that rather than the raw tmp path we created.
195
+ assert.ok(r.core.includes(join('node_modules', '@bongos', 'core')), `core root not under node_modules: ${r.core}`);
196
+ assert.equal(r.inst, realpathSync(consumer), `instance root should be the consumer tree, got ${r.inst}`);
197
+ assert.notEqual(r.core, r.inst, 'core root and instance root must be different directories');
198
+ });
199
+
200
+ // --- 4. boot + /healthz ---------------------------------------------------
201
+ const port = await freePort();
202
+ child = spawn(process.execPath, [join(coreDir, 'src', 'platform-server.js')], {
203
+ cwd: consumer,
204
+ env: { ...process.env, PORT: String(port), HOST: '127.0.0.1' },
205
+ stdio: ['ignore', 'pipe', 'pipe'],
206
+ });
207
+ let childErr = '';
208
+ child.stderr.on('data', (c) => { childErr += c; });
209
+ child.stdout.on('data', () => {});
210
+ const health = await waitForHealthz(port);
211
+ check('boot: node node_modules/@bongos/core/src/platform-server.js serves /healthz 200', () => {
212
+ assert.ok(health, `/healthz never came up on port ${port}\n--- server stderr ---\n${childErr.slice(-2000)}`);
213
+ assert.equal(health.status, 200, `/healthz status ${health.status}`);
214
+ assert.equal(health.body.trim(), 'ok', `/healthz body was "${health.body}"`);
215
+ });
216
+
217
+ // --- 5. two-root module load (the task-2133 done-when) --------------------
218
+ // The real default scan (DEFAULT_ROOTS) PLUS an explicit single-root scan that
219
+ // reproduces the pre-2133 loader shape — so the job doesn't just PASS today, it
220
+ // proves it would go RED if the loader regressed to one root ("done when: green
221
+ // AND would RED against the pre-B1 loader").
222
+ const disc = runNodeJSON(`
223
+ const path = require('node:path');
224
+ const loader = require(${A('src/module-loader/loader.js')});
225
+ const ic = require(${A('src/instance-config.js')});
226
+ const coreModulesDir = path.join(ic.resolveCoreRoot(), 'modules');
227
+ const instModulesDir = path.join(ic.resolveInstanceRoot(), 'modules');
228
+ const dflt = loader.discoverModules(); // real DEFAULT_ROOTS
229
+ const single = loader.discoverModules({ roots: [coreModulesDir] }); // pre-2133: core root only
230
+ const two = loader.discoverModules({ roots: [coreModulesDir, instModulesDir] });
231
+ const hasProbe = (r) => r.loaded.some((m) => m.key === 'consumer-probe');
232
+ process.stdout.write(JSON.stringify({
233
+ coreModulesDir, instModulesDir,
234
+ loaded: dflt.loaded.map((m) => ({ key: m.key, dir: m.dir })),
235
+ errors: dflt.errors,
236
+ singleRootHasProbe: hasProbe(single),
237
+ twoRootHasProbe: hasProbe(two),
238
+ }));
239
+ `, consumer);
240
+ check('two-root: discoverModules() loads modules from BOTH the core AND the instance root, zero errors', () => {
241
+ assert.deepEqual(disc.errors, [], `loader reported errors: ${JSON.stringify(disc.errors)}`);
242
+ const fromCore = disc.loaded.filter((m) => m.dir.startsWith(disc.coreModulesDir));
243
+ const fromInstance = disc.loaded.filter((m) => m.dir.startsWith(disc.instModulesDir));
244
+ assert.ok(fromCore.length > 0,
245
+ `no module resolved from the CORE root (${disc.coreModulesDir}) — the package shipped no modules?`);
246
+ const probe = fromInstance.find((m) => m.key === 'consumer-probe');
247
+ assert.ok(probe,
248
+ `consumer-probe was NOT loaded from the INSTANCE root (${disc.instModulesDir}) — a single-root (pre-2133) loader would miss it. loaded=${JSON.stringify(disc.loaded.map((m) => m.key))}`);
249
+ });
250
+ check('counterfactual: a single-root (pre-2133) scan MISSES consumer-probe — proves this job reds on regression', () => {
251
+ assert.equal(disc.twoRootHasProbe, true, 'two-root scan must find consumer-probe (sanity)');
252
+ assert.equal(disc.singleRootHasProbe, false,
253
+ 'a core-only scan found consumer-probe — the instance module must be invisible to a single root, or this job could not RED against the pre-B1 loader');
254
+ });
255
+
256
+ // --- 6. fitness spans both roots -----------------------------------------
257
+ check('fitness: discoveredModuleKeys() unions the core + instance module roots', () => {
258
+ const r = runNodeJSON(`
259
+ const f = require(${A('scripts/gds/fitness.js')});
260
+ process.stdout.write(JSON.stringify({ keys: f.discoveredModuleKeys() }));
261
+ `, consumer);
262
+ assert.ok(r.keys.includes('consumer-probe'),
263
+ `fitness.discoveredModuleKeys() missing the instance module 'consumer-probe': ${JSON.stringify(r.keys)}`);
264
+ // At least one KNOWN core module must also be present (core root scanned too).
265
+ const coreKeys = disc.loaded.filter((m) => m.dir.startsWith(disc.coreModulesDir)).map((m) => m.key);
266
+ assert.ok(coreKeys.some((k) => r.keys.includes(k)),
267
+ `fitness keys carry no core module (expected some of ${JSON.stringify(coreKeys)}): ${JSON.stringify(r.keys)}`);
268
+ });
269
+
270
+ // --- 7. gen-file-map reads the INSTANCE root ------------------------------
271
+ check('gen-file-map: buildModel() lists the INSTANCE root .claude/skills (consumer-probe-skill)', () => {
272
+ const model = runNodeJSON(`
273
+ const g = require(${A('scripts/gds/gen-file-map.js')});
274
+ process.stdout.write(JSON.stringify(g.buildModel()));
275
+ `, consumer);
276
+ const skills = model.find((s) => s.id === 'skills');
277
+ assert.ok(skills, 'gen-file-map produced no skills section');
278
+ const names = skills.entries.map((e) => e.name);
279
+ assert.deepEqual(names, ['consumer-probe-skill'],
280
+ `gen-file-map read the wrong .claude/skills (expected only the consumer's): ${JSON.stringify(names)}`);
281
+ });
282
+
283
+ // --- 8. gen-repo-map reads the CORE root ----------------------------------
284
+ check('gen-repo-map: buildModel() maps CORE code (scripts/gds/fitness.js) from node_modules', () => {
285
+ const model = runNodeJSON(`
286
+ const g = require(${A('scripts/gds/gen-repo-map.js')});
287
+ const m = g.buildModel();
288
+ process.stdout.write(JSON.stringify({ files: m.files.length, hasFitness: m.byFile.some((f) => f.rel === 'scripts/gds/fitness.js') }));
289
+ `, consumer);
290
+ assert.ok(model.files > 50, `core scan found too few files (${model.files}) — core root not resolved to the package?`);
291
+ assert.equal(model.hasFitness, true, 'gen-repo-map did not map the core file scripts/gds/fitness.js');
292
+ });
293
+ } finally {
294
+ if (child && child.exitCode === null) { try { child.kill('SIGTERM'); } catch { /* already gone */ } }
295
+ try { rmSync(workRoot, { recursive: true, force: true }); } catch { /* best-effort */ }
296
+ }
297
+
298
+ console.log(`\nconsumer_layout_boot: ${passed} passed, ${failed} failed`);
299
+ process.exit(failed ? 1 : 0);
300
+ }
301
+
302
+ main().catch((e) => { console.error(e); process.exit(1); });