@bongos/core 1.17.1 → 1.17.3

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 (233) hide show
  1. package/.bongos-core.json +521 -281
  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/clients/bongos-client/README.md +1 -1
  17. package/clients/bongos-client/bongos-client.global.js +2 -0
  18. package/clients/bongos-client/index.cjs +2 -0
  19. package/clients/bongos-client/index.d.ts +4 -2
  20. package/clients/bongos-client/index.mjs +2 -0
  21. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  22. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  23. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  24. package/docs/adr/0020-security-model-public-stub.md +1 -1
  25. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  26. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  27. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  28. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  29. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  30. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  31. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  32. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  33. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  34. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  35. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  36. package/docs/adr/0057-container-cost-ledger.md +3 -3
  37. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  38. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  39. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  40. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  41. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  42. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  43. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  44. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  45. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  46. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  47. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  48. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  49. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  50. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  51. package/docs/adr/README.md +9 -6
  52. package/docs/api/openapi.json +65 -26
  53. package/docs/api-reference.md +3 -2
  54. package/docs/architecture.md +2 -2
  55. package/docs/branding-contract.md +5 -4
  56. package/docs/canonical-permissions.md +1 -1
  57. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  58. package/docs/file-map.md +18 -17
  59. package/docs/handoff-template.md +2 -2
  60. package/docs/modules-contract.md +6 -1
  61. package/docs/onboarding/diagrams/README.md +7 -7
  62. package/docs/onboarding/drachmae-and-karma.md +18 -18
  63. package/docs/onboarding/primer.md +9 -9
  64. package/docs/recipes/audit-replay.md +2 -2
  65. package/docs/recipes/builder-box-lifecycle.md +2 -2
  66. package/docs/recipes/claude-design-loop.md +1 -1
  67. package/docs/recipes/ops-gotchas.md +1 -1
  68. package/docs/recipes/private-npm-distribution.md +17 -0
  69. package/docs/recipes/staging-environment.md +1 -1
  70. package/docs/recipes/upgrading-the-core.md +55 -0
  71. package/docs/research/cursor-competitive-analysis.md +1 -1
  72. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  73. package/modules/discord/module.json +2 -1
  74. package/modules/economy/achievements.js +9 -1
  75. package/modules/economy/credits.js +10 -1
  76. package/modules/hall-ui/public/builders.js +137 -620
  77. package/modules/hall-ui/public/goal-inbox.js +232 -0
  78. package/modules/hall-ui/public/goals-page.js +4 -1
  79. package/modules/hall-ui/public/goals.html +8 -0
  80. package/modules/hall-ui/public/hall-kit.js +474 -0
  81. package/modules/hall-ui/public/hall-widgets.js +14 -5
  82. package/modules/hall-ui/public/index.html +3 -0
  83. package/modules/hall-ui/public/instances-panel.js +24 -3
  84. package/modules/hall-ui/public/new-project-wizard.js +7 -5
  85. package/modules/hall-ui/public/style.css +168 -0
  86. package/modules/hall-ui/public/work.css +194 -0
  87. package/modules/hall-ui/public/work.html +46 -374
  88. package/modules/hall-ui/public/work.js +137 -102
  89. package/modules/lifecycle/criterion-suggest.js +80 -0
  90. package/modules/lifecycle/db.js +42 -5
  91. package/modules/lifecycle/routes/goals.js +26 -0
  92. package/modules/lifecycle/routes/tasks.js +21 -0
  93. package/modules/provisioning/CLAUDE.md +15 -0
  94. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  95. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  96. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  97. package/modules/provisioning/module.json +17 -0
  98. package/modules/provisioning/onboard-plan.js +37 -0
  99. package/modules/provisioning/provisioning.js +539 -0
  100. package/modules/provisioning/rate-limit.js +35 -0
  101. package/modules/provisioning/routes/provisioning.js +607 -0
  102. package/modules/provisioning/tests/provisioning.mjs +232 -0
  103. package/modules/provisioning/ui/.gitkeep +0 -0
  104. package/modules/security/secret-scrub.js +10 -0
  105. package/package-lock.json +2 -2
  106. package/package.json +7 -2
  107. package/scripts/discord/README.md +1 -1
  108. package/scripts/gds/claim.js +98 -48
  109. package/scripts/gds/claude-materialize.js +30 -9
  110. package/scripts/gds/cutover-codemod.js +241 -0
  111. package/scripts/gds/fitness.js +54 -0
  112. package/scripts/gds/init.js +59 -3
  113. package/scripts/gds/lint-infra-exec.js +70 -0
  114. package/scripts/gds/lint-workflow-exec.js +63 -0
  115. package/scripts/gds/mirror-lineage-check.js +14 -5
  116. package/scripts/gds/mirror-redact.js +8 -0
  117. package/scripts/gds/onboard.js +4 -0
  118. package/scripts/gds/package-core.js +18 -3
  119. package/scripts/gds/publish-manifest.js +60 -2
  120. package/scripts/gds/run-routine.js +51 -13
  121. package/scripts/gds/run-unit-tests.js +4 -0
  122. package/scripts/gds/update-channel.js +186 -0
  123. package/scripts/gds/upgrade.js +116 -9
  124. package/scripts/hall-preview/README.md +103 -0
  125. package/scripts/hall-preview/fixtures/access-requests.json +1 -0
  126. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  127. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  128. package/scripts/hall-preview/fixtures/audit-log.json +1 -0
  129. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
  130. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  131. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  132. package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
  133. package/scripts/hall-preview/fixtures/goals.json +502 -0
  134. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  135. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  136. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  137. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  138. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  139. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  140. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  141. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  142. package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
  143. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  144. package/scripts/hall-preview/fixtures/instance.json +124 -0
  145. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  146. package/scripts/hall-preview/fixtures/me.json +476 -0
  147. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  148. package/scripts/hall-preview/fixtures/override-requests.json +1 -0
  149. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  150. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  151. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  152. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  153. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  154. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  155. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  156. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  157. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  158. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  159. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  160. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  161. package/scripts/hall-preview/fixtures/security__docs.json +1 -0
  162. package/scripts/hall-preview/fixtures/security__reports.json +1 -0
  163. package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
  164. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  165. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  166. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  167. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  168. package/scripts/hall-preview/fixtures/versions.json +89 -0
  169. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  170. package/scripts/hall-preview/server.js +178 -0
  171. package/scripts/migrate.sh +29 -4
  172. package/src/bongos/auth.js +14 -1
  173. package/src/bongos/optimizer.js +37 -2
  174. package/src/bongos/serve-internal.js +10 -1
  175. package/src/branding.js +1 -0
  176. package/src/module-api.js +12 -1
  177. package/src/module-loader/loader.js +36 -4
  178. package/src/module-loader/manifest-schema.js +16 -1
  179. package/tests/branding.mjs +11 -0
  180. package/tests/claim_error_surface.mjs +107 -0
  181. package/tests/claude_materialize.mjs +20 -0
  182. package/tests/consumer_layout_boot.mjs +302 -0
  183. package/tests/criterion_suggest.mjs +106 -0
  184. package/tests/cutover_codemod.mjs +184 -0
  185. package/tests/device_flow_start.mjs +25 -2
  186. package/tests/hook_core_resolve.mjs +125 -0
  187. package/tests/infra_exec_paths.mjs +104 -0
  188. package/tests/init.mjs +47 -0
  189. package/tests/migrate_root_resolution.mjs +97 -0
  190. package/tests/mirror_lineage_proof.mjs +14 -0
  191. package/tests/module_loader.mjs +42 -0
  192. package/tests/module_manifest.mjs +19 -0
  193. package/tests/onboard.mjs +13 -0
  194. package/tests/package_core.mjs +8 -0
  195. package/tests/provisioning_github_app_routes.mjs +15 -0
  196. package/tests/publish_manifest.mjs +52 -1
  197. package/tests/run_routine.mjs +26 -1
  198. package/tests/run_routine_two_root.mjs +77 -0
  199. package/tests/scrubber_corpus.mjs +17 -0
  200. package/tests/update_channel.mjs +186 -0
  201. package/tests/upgrade.mjs +131 -0
  202. package/tests/workflow_exec_paths.mjs +98 -0
  203. package/docs/design/container-exterior-tiles.md +0 -49
  204. package/docs/design/container-exterior.md +0 -81
  205. package/docs/design/container-interior.md +0 -77
  206. package/docs/design/world-map-scroll.md +0 -41
  207. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  208. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  209. package/docs/recipes/cloudbongos-standup.md +0 -111
  210. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  211. package/docs/recipes/parallel-load-test.md +0 -32
  212. package/docs/recipes/pixel-art-pipeline.md +0 -411
  213. package/docs/recipes/staging-data.md +0 -50
  214. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  215. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  216. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  217. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  218. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  219. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  220. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  221. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  222. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  223. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  224. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  225. package/scripts/gds/art-stage.js +0 -152
  226. package/server.js +0 -212
  227. package/tests/art_key.mjs +0 -121
  228. package/tests/day_cycle.mjs +0 -197
  229. package/tests/snapshot_picker.mjs +0 -233
  230. package/tests/sprite_facing.mjs +0 -35
  231. package/tests/terrain_picker.mjs +0 -298
  232. package/tests/turn_in_place.mjs +0 -280
  233. package/tests/walkability.mjs +0 -188
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ // scripts/hall-preview/server.js — the hall preview harness (task 2167).
3
+ //
4
+ // The dev-box sandbox previews the GAME only (/builders 404s there, idea 490),
5
+ // so hall-ui changes are reviewed HERE: a tiny express app that serves the real
6
+ // modules/hall-ui/public files through the REAL serving seams (boot-time ?v=
7
+ // stamping, {{token}} replacement, __BRANDING__/__MODULES__ injection, theme
8
+ // markup) with the /api/gds|bongos reads answered from checked-in fixtures.
9
+ // Boots DB-less (pg pools connect lazily). Proven recipe from tasks 1723/1742,
10
+ // finally checked in.
11
+ //
12
+ // node scripts/hall-preview/server.js [--port 4600] [--brand <file>]
13
+ // [--modules-off dev-box,provisioning] [--empty]
14
+ //
15
+ // --port listen port (default 4600)
16
+ // --brand branding pack file — default config/branding.neutral.json
17
+ // (vanilla); pass config/branding.json for the instance pack
18
+ // --modules-off comma list of module keys to report disabled in
19
+ // window.__MODULES__ (verifies moduleOn() gating)
20
+ // --empty serve every fixture with its arrays emptied — the
21
+ // empty-state screenshot pass
22
+ //
23
+ // Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
24
+ // and the `builders.` host prefix exercises the real subdomain serving path.
25
+ // Refresh fixtures from live data: node scripts/hall-preview/refresh-fixtures.js
26
+ 'use strict';
27
+
28
+ const path = require('path');
29
+ const fs = require('fs');
30
+
31
+ const ROOT = path.resolve(__dirname, '..', '..');
32
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
33
+ const FIXTURES = path.join(__dirname, 'fixtures');
34
+
35
+ // ---- flags (before any src/ require: --brand must win the branding resolve) --
36
+ function arg(name) {
37
+ const i = process.argv.indexOf(name);
38
+ return i > -1 ? process.argv[i + 1] : null;
39
+ }
40
+ const PORT = Number(arg('--port')) || 4600;
41
+ const EMPTY = process.argv.includes('--empty');
42
+ const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
43
+ // --modules-on: force-report modules as enabled — a dev box's local config
44
+ // often lacks prod's instance modules (provisioning, dev-box), and their
45
+ // widgets can't be reviewed without it (task 2169).
46
+ const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.trim()).filter(Boolean);
47
+ const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
48
+ process.env.GDS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
49
+
50
+ /* eslint-disable import/no-dynamic-require */
51
+ // express comes from this tree's node_modules when installed, else node's
52
+ // normal upward walk finds the main checkout's copy (a git worktree under
53
+ // .claude/worktrees/ has no node_modules of its own). The src/ seams below are
54
+ // ALWAYS taken from THIS tree so a worktree's edits are what gets previewed.
55
+ let express;
56
+ try { express = require(path.join(ROOT, 'node_modules', 'express')); }
57
+ catch (_) { express = require('express'); }
58
+ const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
59
+ const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
60
+ const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
61
+
62
+ // ---- fixtures ---------------------------------------------------------------
63
+ // File naming: the API path with slashes as "__" — /me/recent-ships →
64
+ // me__recent-ships.json. Query strings are ignored when matching. Id-bearing
65
+ // paths fall back to the generic names below so one fixture serves any id.
66
+ const FIXTURE_FALLBACKS = [
67
+ [/^goals\/\d+\/conflicts$/, 'goals__conflicts'],
68
+ [/^goals\/\d+\/requests$/, 'goals__requests'],
69
+ [/^goals\/\d+$/, 'goals__detail'],
70
+ [/^tasks\/\d+$/, 'tasks__detail'],
71
+ [/^analytics\/builder\/\d+$/, 'analytics__builder'],
72
+ [/^builders\/\d+\/active-claims$/, 'builders__active-claims'],
73
+ ];
74
+
75
+ function loadFixture(name) {
76
+ try {
77
+ return JSON.parse(fs.readFileSync(path.join(FIXTURES, `${name}.json`), 'utf8'));
78
+ } catch (_) { return undefined; }
79
+ }
80
+
81
+ // --empty: the same pages, with nothing in them — every array drains so each
82
+ // page's empty states render. Scalars/strings survive (identity stays real).
83
+ function emptied(v) {
84
+ if (Array.isArray(v)) return [];
85
+ if (v && typeof v === 'object') {
86
+ const out = {};
87
+ for (const [k, val] of Object.entries(v)) out[k] = emptied(val);
88
+ return out;
89
+ }
90
+ return v;
91
+ }
92
+
93
+ function fixtureFor(apiPath) {
94
+ const exact = loadFixture(apiPath.replace(/\//g, '__'));
95
+ if (exact !== undefined) return exact;
96
+ for (const [re, name] of FIXTURE_FALLBACKS) {
97
+ if (re.test(apiPath)) {
98
+ const hit = loadFixture(name);
99
+ if (hit !== undefined) return hit;
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ // ---- app ----------------------------------------------------------------------
106
+ const app = express();
107
+
108
+ // The API stub — intercepts every /api/gds/*, /api/bongos/*, /api/bongos/v1/*
109
+ // request BEFORE mountInternalSurfaces can reach the real routes (which would
110
+ // touch Postgres — the harness is DB-less by design). A plain function
111
+ // middleware, not a path mount: regex-mount semantics differ across express
112
+ // majors and a silent non-match here means a pg crash.
113
+ app.use((req, res, next) => {
114
+ const m = req.path.match(/^\/api\/(?:gds|bongos)(?:\/v1)?\/(.+)$/);
115
+ if (!m) return next();
116
+ const apiPath = m[1].replace(/\/$/, '');
117
+
118
+ // SSE live channel: emit one nudge (so "live refresh" paths run once), then
119
+ // keep-alive comments. Pages treat this exactly like prod's stream.
120
+ if (apiPath === 'live') {
121
+ res.set({ 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
122
+ res.flushHeaders();
123
+ setTimeout(() => res.write('event: nudge\ndata: {}\n\n'), 1200);
124
+ const tick = setInterval(() => res.write(': keep-alive\n\n'), 25000);
125
+ req.on('close', () => clearInterval(tick));
126
+ return;
127
+ }
128
+
129
+ if (req.method !== 'GET') {
130
+ return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
131
+ }
132
+ const fix = fixtureFor(apiPath);
133
+ if (fix === undefined) {
134
+ console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
135
+ return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
136
+ }
137
+ res.json(EMPTY ? emptied(fix) : fix);
138
+ });
139
+
140
+ // The hall pages, pre-auth-gate, through the real seams: boot-time stamped
141
+ // (?v= content hashes), {{token}}-replaced, __BRANDING__/__MODULES__ injected,
142
+ // theme markup appended — byte-for-byte what prod serves after its gates.
143
+ const stamped = buildStampedHtml(PUB, '/builders/');
144
+ const mods = JSON.parse(JSON.stringify(clientModules()));
145
+ for (const key of MODULES_OFF) {
146
+ if (mods.enabled && key in mods.enabled) mods.enabled[key] = false;
147
+ else (mods.enabled = mods.enabled || {})[key] = false;
148
+ }
149
+ for (const key of MODULES_ON) {
150
+ (mods.enabled = mods.enabled || {})[key] = true;
151
+ }
152
+
153
+ function sendPage(res, name) {
154
+ const html = stamped[name];
155
+ if (!html) return false;
156
+ res.type('html').send(injectBrandingGlobal(html, undefined, mods));
157
+ return true;
158
+ }
159
+
160
+ app.get('/', (req, res, next) => { if (!sendPage(res, 'index.html')) next(); });
161
+ app.get(/^\/task\/\d+$/, (req, res, next) => { if (!sendPage(res, 'task.html')) next(); });
162
+ app.get(/^\/([a-z-]+)$/, (req, res, next) => {
163
+ if (!sendPage(res, `${req.params[0]}.html`)) next();
164
+ });
165
+
166
+ // Everything else — /builders/*.css|js assets, /lib/bongos-client.js, diagram
167
+ // PNGs — falls through to the real internal-surface mounting. Our handlers
168
+ // above registered first, so the hall's auth gates in there never fire for
169
+ // the page routes we already served.
170
+ mountInternalSurfaces(app);
171
+
172
+ // Loopback only — this is a pre-auth preview of hall pages with fixture data;
173
+ // *.localhost resolves to 127.0.0.1 so nothing else is needed.
174
+ app.listen(PORT, '127.0.0.1', () => {
175
+ console.log(`hall-preview: http://builders.localhost:${PORT}/`);
176
+ console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
177
+ console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
178
+ });
@@ -12,18 +12,43 @@ set -euo pipefail
12
12
  # alongside the instance, so both resolve to the SAME dir and this is byte-identical to the
13
13
  # old `cd "$(dirname "$0")/.."`. Once the core installs from node_modules, resolveCoreRoot()
14
14
  # points into the package (core migrations + src/) while resolveInstanceRoot() stays the host
15
- # repo (env → bongos.config → cwd). We resolve from the SCRIPT-relative dir so the result does
16
- # not depend on the caller's cwd (preserving the old script-relative behavior), and fail open to
17
- # that dir if node is unavailable so a deploy never breaks.
15
+ # repo (env → bongos.config → cwd).
16
+ #
17
+ # CORE root is resolved SCRIPT-relative (the package's own files sit next to this script), so it
18
+ # never depends on the caller's cwd. The INSTANCE root is the OPPOSITE: it MUST come from where
19
+ # migrate was INVOKED. `npm run migrate` from the instance sets INIT_CWD (and $PWD) to the
20
+ # instance dir, where bongos.config.json lives. Resolving the instance root script-relative would
21
+ # `cd` into the core PACKAGE first — whose dir has no bongos.config.json — so with no
22
+ # OTB_INSTANCE_ROOT env it falls back to the package dir as the "instance" and reads the core's
23
+ # VANILLA module config, SILENTLY skipping every host + flag-gated module migration (R89 rehearsal
24
+ # F6, task 2180: literal `npm run migrate` applied 115 rows and created no game schema; from the
25
+ # instance cwd it applied 121). So resolve the instance root from the invocation cwd, preferring an
26
+ # EXPLICIT pointer (OTB_INSTANCE_ROOT env, else the invocation cwd's bongos.config.json).
27
+ # resolveInstanceRootExplicit() returns '' (NOT process.cwd()) when neither is present, so we fall
28
+ # back to the script-relative DEFAULT_ROOT — the monolith case (cwd==core==instance), byte-identical
29
+ # to the old behavior, and a stray cwd can never masquerade as the instance. Fail open to
30
+ # DEFAULT_ROOT if node is unavailable so a deploy never breaks.
31
+ INVOKE_CWD="${INIT_CWD:-$PWD}"
18
32
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
33
  DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
20
34
  CORE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveCoreRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
21
- INSTANCE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveInstanceRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
35
+ INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require('$DEFAULT_ROOT/src/instance-config').resolveInstanceRootExplicit() || '')" 2>/dev/null || true)"
36
+ INSTANCE_ROOT="${INSTANCE_ROOT:-$DEFAULT_ROOT}"
22
37
  # Run from the instance root so instance-relative paths resolve (the post-deploy art check below)
23
38
  # AND so the enabledModules() read finds the instance's config/modules.json (resolveInstanceRoot
24
39
  # falls back to cwd).
25
40
  cd "$INSTANCE_ROOT"
26
41
 
42
+ # Test/ops seam (task 2180): print the resolved roots and exit BEFORE any DB work, so a DB-free
43
+ # unit test (tests/migrate_root_resolution.mjs) can assert the INSTANCE root resolves from the
44
+ # invocation cwd (a consumer's bongos.config.json), not script-relative into the core package —
45
+ # the F6 regression. Also handy in the field: `npm run migrate -- --print-roots` shows what
46
+ # migrate sees.
47
+ if [ "${1:-}" = "--print-roots" ]; then
48
+ printf 'INVOKE_CWD=%s\nCORE_ROOT=%s\nINSTANCE_ROOT=%s\n' "$INVOKE_CWD" "$CORE_ROOT" "$INSTANCE_ROOT"
49
+ exit 0
50
+ fi
51
+
27
52
  DB="${PGDATABASE:-amazonprimea}"
28
53
 
29
54
  # --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
@@ -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).');
@@ -53,10 +53,45 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
53
53
  // tasks_session_fit (derived fits_session_modes[] + credits_per_minute).
54
54
  // Also exclude tasks this builder already has an active claim on, mirroring
55
55
  // listClaimableTasks in db.js.
56
+ // task 2181 (incident, blocker 74): the view bodies (migration core_190) are
57
+ // inlined so this query takes only ACCESS SHARE on base tables — a detached
58
+ // backend queued an ACCESS EXCLUSIVE DDL on the views and parked every view
59
+ // SELECT behind it. Plan-identical (Postgres inlines simple views); change in
60
+ // lockstep with the views / listClaimableTasks if a migration alters them.
56
61
  const { rows } = await pool.query(
57
62
  `SELECT ct.*, sf.fits_session_modes, sf.credits_per_minute
58
- FROM claimable_tasks ct
59
- JOIN tasks_session_fit sf ON sf.id = ct.id
63
+ FROM (
64
+ SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
65
+ FROM tasks t
66
+ WHERE t.status = 'ready'
67
+ AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
68
+ AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
69
+ AND NOT EXISTS (
70
+ SELECT 1 FROM claims c
71
+ JOIN tasks t2 ON t2.id = c.task_id
72
+ WHERE c.released_at IS NULL
73
+ AND t2.touches && t.touches
74
+ )
75
+ ) ct
76
+ JOIN (
77
+ SELECT t.id,
78
+ CASE
79
+ WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
80
+ AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
81
+ THEN ARRAY['autonomous','check-in','active']
82
+ WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
83
+ THEN ARRAY['active']
84
+ ELSE ARRAY['check-in','active']
85
+ END AS fits_session_modes,
86
+ CASE
87
+ WHEN t.credits_reward > 0
88
+ AND pms_calibrated_minutes(t.est_minutes, t.kind) IS NOT NULL
89
+ AND pms_calibrated_minutes(t.est_minutes, t.kind) > 0
90
+ THEN ROUND(t.credits_reward::numeric / pms_calibrated_minutes(t.est_minutes, t.kind), 3)
91
+ ELSE NULL
92
+ END AS credits_per_minute
93
+ FROM tasks t
94
+ ) sf ON sf.id = ct.id
60
95
  WHERE $1 = ANY(sf.fits_session_modes)
61
96
  AND NOT EXISTS (
62
97
  SELECT 1 FROM claims c
@@ -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');