@bongos/core 1.19.1074 → 1.19.1076

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 (179) hide show
  1. package/.bongos-core.json +196 -211
  2. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/bongos.js +1 -3
  5. package/docs/adr/0031-cloud-dev-environments-for-builders.md +1 -1
  6. package/docs/adr/0035-builder-onboarding-three-paths.md +1 -1
  7. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  8. package/docs/adr/0045-devbox-desktop-app.md +1 -1
  9. package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
  10. package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
  11. package/docs/adr/0053-scoped-dev-box-session.md +1 -1
  12. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  13. package/docs/adr/0057-container-cost-ledger.md +1 -1
  14. package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +1 -1
  15. package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +1 -1
  16. package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +1 -1
  17. package/docs/adr/0072-dev-box-code-staleness-visibility.md +1 -1
  18. package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +1 -1
  19. package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
  20. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  21. package/docs/adr/0145-devbox-app-branding-driven-module.md +1 -1
  22. package/docs/adr/0148-task-scoped-box-source-access.md +2 -2
  23. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +1 -1
  24. package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
  25. package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
  26. package/docs/adr/0346-dev-boxes-are-retired.md +69 -0
  27. package/docs/adr/README.md +1 -0
  28. package/docs/api/openapi.json +4 -4
  29. package/docs/architecture.md +5 -6
  30. package/docs/branding-contract.md +2 -2
  31. package/docs/canonical-permissions.md +10 -18
  32. package/docs/copy-inventory.md +3 -3
  33. package/docs/copy-registry.json +3 -3
  34. package/docs/design/gate-navigation-direction.md +1 -1
  35. package/docs/design/hall-direction-v2.md +3 -3
  36. package/docs/design/modular-architecture/00-research-report.md +1 -1
  37. package/docs/design/modular-architecture/01-architecture.md +3 -3
  38. package/docs/design/modular-architecture/02-module-map.md +1 -2
  39. package/docs/design/modular-architecture/03-builder-flows.md +22 -20
  40. package/docs/design/modular-architecture/04-module-lifecycle.md +1 -1
  41. package/docs/design/modular-architecture/README.md +4 -2
  42. package/docs/design/reviews/hall-v2/README.md +2 -2
  43. package/docs/design/vanilla-hall-ui-redesign-scope.md +4 -4
  44. package/docs/file-map.md +5 -5
  45. package/docs/handoff-template.md +1 -1
  46. package/docs/module-api-changelog.md +8 -0
  47. package/docs/modules-contract.md +11 -14
  48. package/docs/page-readings.json +63 -63
  49. package/docs/recipes/bongos-cli-release.md +1 -2
  50. package/docs/recipes/multi-builder-merge.md +1 -1
  51. package/docs/recipes/ops-gotchas.md +0 -9
  52. package/docs/recipes/self-host.md +1 -1
  53. package/docs/recipes/windows-builders.md +1 -1
  54. package/migrations/core_258_drop_dev_box_tables.sql +3 -0
  55. package/modules/copy-desk/routes/copy-desk.js +1 -1
  56. package/modules/discord/ship-broadcast.js +3 -3
  57. package/modules/economy/credits.js +2 -2
  58. package/modules/hall-ui/public/palette.js +143 -3
  59. package/modules/ideas/projection.js +1 -1
  60. package/modules/lifecycle/db-ship.js +4 -4
  61. package/modules/lifecycle/github-push.js +12 -14
  62. package/modules/lifecycle/publish-reconciler.js +10 -10
  63. package/modules/lifecycle/routes/tasks.js +8 -8
  64. package/modules/lifecycle/ship-card.js +1 -1
  65. package/modules/lifecycle/task-visuals.js +2 -2
  66. package/modules/memory/routes/memory.js +2 -2
  67. package/modules/platform-identity/routes/sso.js +3 -2
  68. package/modules/provisioning/capacity.js +1 -1
  69. package/modules/provisioning/provisioning.js +4 -3
  70. package/modules/provisioning/routes/provisioning.js +4 -4
  71. package/modules/sessions/db.js +1 -1
  72. package/package-lock.json +2 -2
  73. package/package.json +1 -1
  74. package/release-notes.json +28 -0
  75. package/scripts/gds/artifact-staleness.js +2 -2
  76. package/scripts/gds/backfill-task-rewards.js +5 -5
  77. package/scripts/gds/claude-materialize.js +2 -2
  78. package/scripts/gds/cli-lib.js +7 -7
  79. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  80. package/scripts/gds/context-pack.js +5 -7
  81. package/scripts/gds/dev-box-guard.js +165 -0
  82. package/scripts/gds/doctor.js +3 -3
  83. package/scripts/gds/fitness-checks-packaging.js +4 -4
  84. package/scripts/gds/fitness-lib.js +10 -0
  85. package/scripts/gds/fitness-ratchets.js +1 -1
  86. package/scripts/gds/fitness.js +2 -10
  87. package/scripts/gds/gen-api-client.js +4 -4
  88. package/scripts/gds/gen-api-docs.js +1 -1
  89. package/scripts/gds/gen-repo-map.js +13 -14
  90. package/scripts/gds/gen-session-index.js +8 -8
  91. package/scripts/gds/leak-scan-allowlist.js +1 -1
  92. package/scripts/gds/plain-cards.js +1 -1
  93. package/scripts/gds/push-path-brief.js +10 -10
  94. package/scripts/gds/rename-history-check.js +1 -1
  95. package/scripts/gds/run-unit-tests.js +4 -0
  96. package/scripts/gds/session-digest-build.js +2 -2
  97. package/scripts/gds/ship-deploy-target.js +13 -13
  98. package/scripts/gds/ship-flow.js +1 -1
  99. package/scripts/gds/ship-land.js +6 -6
  100. package/scripts/gds/ship-merge.js +4 -4
  101. package/scripts/gds/ship-preflight-steps.js +4 -4
  102. package/scripts/gds/ship-regen.js +9 -12
  103. package/scripts/gds/ship.js +9 -9
  104. package/scripts/gds/skill-preflight.js +9 -8
  105. package/src/bongos/api-prefix.js +4 -4
  106. package/src/bongos/module-scope-map.js +1 -1
  107. package/src/bongos/platform-visibility-gate.js +1 -1
  108. package/src/bongos/routes/me.js +1 -1
  109. package/src/bongos/routes/security.js +3 -3
  110. package/src/bongos/routes.js +2 -2
  111. package/src/bongos/serve-internal.js +5 -6
  112. package/src/bongos-downloads.js +1 -6
  113. package/src/branding.js +2 -2
  114. package/src/build-info.js +4 -6
  115. package/src/instance-config.js +2 -2
  116. package/src/module-api.js +3 -3
  117. package/src/module-loader/manifest-schema.js +1 -1
  118. package/tests/api_alias_caller_ratchet.mjs +1 -1
  119. package/tests/api_alias_exceptions.mjs +1 -1
  120. package/tests/api_client.mjs +1 -1
  121. package/tests/api_path_404.mjs +1 -1
  122. package/tests/backfill_task_rewards.mjs +1 -1
  123. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  124. package/tests/cli_sessions.mjs +1 -1
  125. package/tests/context_pack.mjs +13 -13
  126. package/tests/copy_inventory.mjs +7 -7
  127. package/tests/core_258_box_session_claims_db.mjs +180 -0
  128. package/tests/credit_grant.mjs +2 -2
  129. package/tests/criterion_suggest.mjs +1 -1
  130. package/tests/design_tokens_sync.mjs +3 -2
  131. package/tests/fitness.mjs +89 -0
  132. package/tests/fitness_ratchets.mjs +2 -2
  133. package/tests/github_push_land_proof.mjs +1 -1
  134. package/tests/goal_advisory.mjs +3 -3
  135. package/tests/goal_suggest.mjs +3 -3
  136. package/tests/government_abuse_matrix.mjs +4 -3
  137. package/tests/hall_audit.mjs +1 -1
  138. package/tests/hall_palette.mjs +224 -11
  139. package/tests/helpers.mjs +1 -1
  140. package/tests/host_topology_skips.mjs +2 -2
  141. package/tests/migration_namespace.mjs +6 -6
  142. package/tests/module_api.mjs +2 -2
  143. package/tests/module_cli.mjs +5 -5
  144. package/tests/module_manifest.mjs +13 -13
  145. package/tests/module_route_rank.mjs +2 -2
  146. package/tests/modules_route_wiring.mjs +1 -1
  147. package/tests/onboarding_route_signals.mjs +1 -1
  148. package/tests/paste_token_session_store.mjs +1 -1
  149. package/tests/platform_visibility_gate.mjs +1 -1
  150. package/tests/project_modules_ui.mjs +17 -17
  151. package/tests/projects_hub_module_picker.mjs +75 -74
  152. package/tests/provisioning_capacity.mjs +1 -1
  153. package/tests/provisioning_recommendations.mjs +3 -2
  154. package/tests/publish_branch_route.mjs +1 -1
  155. package/tests/publish_manifest.mjs +1 -1
  156. package/tests/publish_reconciler.mjs +8 -8
  157. package/tests/push_path_brief.mjs +10 -9
  158. package/tests/regrade_eligibility.mjs +1 -1
  159. package/tests/rename_history_restraint.mjs +1 -1
  160. package/tests/repo_map.mjs +9 -9
  161. package/tests/seam_wiring_guard.mjs +11 -11
  162. package/tests/search_isolation.mjs +3 -3
  163. package/tests/session_records.mjs +1 -1
  164. package/tests/session_rename_fallback.mjs +1 -1
  165. package/tests/session_start_freshness.mjs +3 -3
  166. package/tests/session_token_hash_db.mjs +1 -1
  167. package/tests/ship_card.mjs +1 -1
  168. package/tests/ship_ci_deploy.mjs +17 -17
  169. package/tests/ship_premerge.mjs +1 -1
  170. package/tests/ship_resume.mjs +1 -1
  171. package/tests/skill_preflight.mjs +2 -2
  172. package/tests/skip_is_not_pass.mjs +1 -1
  173. package/tests/terms_acceptance.mjs +1 -1
  174. package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
  175. package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
  176. package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
  177. package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
  178. package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
  179. package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
package/src/branding.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/branding.js — the branding contract (ADR 0062 §3, task 1192 / V4.R53).
2
2
  //
3
3
  // The single source for every instance-identity string the platform renders:
4
- // product/world name, company, domains/origins, cookie + OAuth + dev-box
4
+ // product/world name, company, domains/origins, cookie + OAuth
5
5
  // hosts, currency label, the track set, theme, and the project name injected
6
- // into LLM prompts. Generalizes devbox-app/src/brand.js to the whole platform.
6
+ // into LLM prompts.
7
7
  //
8
8
  // Resolution precedence (highest wins):
9
9
  // env override -> instance config (config/branding.json) -> neutral
package/src/build-info.js CHANGED
@@ -41,12 +41,10 @@ function readGitSha() {
41
41
  }
42
42
  }
43
43
 
44
- // The deployed commit's date (ISO 8601), for the dev-box staleness comparison
45
- // (boxes.computeCodeStaleness, idea 332). A box reports its own HEAD commit date;
46
- // if it's > 2 days behind THIS, the box is flagged stale. Best-effort: after a
47
- // `git reset --hard origin/main` deploy, HEAD is the deployed commit, so its
48
- // committer date is the right reference. Null when git is unavailable (then the
49
- // "behind" check is skipped and only never-reported boxes flag).
44
+ // The deployed commit's date (ISO 8601), served on /version beside the commit.
45
+ // Best-effort: after a `git reset --hard origin/main` deploy, HEAD is the
46
+ // deployed commit, so its committer date is the right reference. Null when git
47
+ // is unavailable.
50
48
  function readGitCommitDate() {
51
49
  try {
52
50
  return execSync('git show -s --format=%cI HEAD', {
@@ -40,7 +40,7 @@ const LEGACY_DIRS = ['otb'];
40
40
  //
41
41
  // WHY THESE MUST OUTLIVE THIS TASK: these names are set OUTSIDE the repo —
42
42
  // /etc/cloudbongos/web.env on the droplet, systemd units, CI secrets, shipped
43
- // Dev Box binaries, live dev boxes. Deleting a spelling here is an outage the
43
+ // CLI binaries, builders' own machines. Deleting a spelling here is an outage the
44
44
  // next time one of those restarts. Task 1003706 (C3d) retires them deliberately,
45
45
  // after evidence — and asserts their ABSENCE, which is why the fallback needs a
46
46
  // test that it FIRES rather than one that merely tolerates it.
@@ -140,7 +140,7 @@ function configPath(name, b = safeBrand()) {
140
140
  // Lives here, not in cli-lib.js which owns the session STORE, because this module
141
141
  // is the lowest one that has to read the file (resolveApiBase below) and cli-lib
142
142
  // imports this — so this is the only place both can share. A rename APPENDS a
143
- // name; dropping one signs out every builder machine and every live dev box at
143
+ // name; dropping one signs out every builder machine at
144
144
  // once. Part 4 (task 1003706) retires the tail. (task 1003704)
145
145
  const SESSION_FILENAMES = Object.freeze([
146
146
  'bongos-session.json',
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.19.1074'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.19.1076'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -137,8 +137,8 @@ module.exports = {
137
137
  get requireBfgPrincipal() { return require('./bongos/auth').requireBfgPrincipal; },
138
138
  // allowBoxScope — DEPRECATED no-op, kept so the doorway loses no export inside
139
139
  // 1.x (removing one is a MAJOR, and every module declares ^1). It used to opt
140
- // a route in to the narrowing box-scoped session; that scope and its tokens
141
- // were retired (task 1003894), so an undeclared session source is now denied
140
+ // a route in to the narrowing machine-baked session scope; that scope and its
141
+ // tokens were retired (goal 1000120, task 1003894), so an undeclared session source is now denied
142
142
  // on every route whatever a route mounts. Passing it changes nothing. Drop it
143
143
  // at the next MAJOR.
144
144
  get allowBoxScope() { return deprecatedNoopMiddleware; },
@@ -10,7 +10,7 @@
10
10
  // THE SCHEMA (module.json):
11
11
  // {
12
12
  // "key": "art-pipeline", // REQUIRED. ^[a-z][a-z0-9-]*$ ; must match the module's dir name.
13
- // "title": "Dev boxes", // REQUIRED. Human label.
13
+ // "title": "Art pipeline", // REQUIRED. Human label.
14
14
  // "description": "…", // REQUIRED. One sentence.
15
15
  // "version": "1.4.2", // REQUIRED. The module's OWN exact version "X.Y.Z" (task 1003781) —
16
16
  // // what update, rollout channels and re-assessment name.
@@ -66,7 +66,7 @@ test('the exclusions are real and each one is reachable', () => {
66
66
  'scripts/gds/fitness-ratchets.js',
67
67
  'scripts/gds/api-alias-caller-scan.js',
68
68
  'tests/api_alias_exceptions.mjs', // 5 tests
69
- 'modules/dev-box/app/test/thing.mjs',
69
+ 'modules/agents/app/test/thing.mjs',
70
70
  'docs/adr/0016-permissions.md', // 6 the FROZEN RECORD under docs/ (narrowed, task 1004113)
71
71
  'docs/session-logs/2026-09-16-x.md',
72
72
  'docs/audits/2026-08-01-x.md',
@@ -27,7 +27,7 @@ const ALIAS = '/api/gds';
27
27
  test('the alias is still mounted — everything below depends on it', () => {
28
28
  const { API_PREFIX, LEGACY_API_PREFIXES, ALL_API_PREFIXES } = require(path.join(ROOT, 'src', 'bongos', 'api-prefix.js'));
29
29
  assert.equal(API_PREFIX, '/api/bongos', 'the canonical prefix is what in-repo callers now ask for');
30
- assert.ok(LEGACY_API_PREFIXES.includes(ALIAS), 'shipped Dev Box binaries and live dev boxes call the alias');
30
+ assert.ok(LEGACY_API_PREFIXES.includes(ALIAS), 'shipped binaries and live instances call the alias');
31
31
  assert.ok(ALL_API_PREFIXES.includes(ALIAS), 'and the router still mounts it');
32
32
  });
33
33
 
@@ -208,7 +208,7 @@ test('generated .d.ts types success responses (task 2061)', () => {
208
208
  });
209
209
 
210
210
  // The CJS + browser-global builds are pure transforms of the ESM source (task
211
- // 1996 / R09). devbox (require) and classic <script> web pages consume them, so
211
+ // 1996 / R09). CommonJS callers (require) and classic <script> web pages consume them, so
212
212
  // guard that they load and shape requests IDENTICALLY to the ESM build.
213
213
  test('CJS build behaves identically to the ESM build', async () => {
214
214
  const cjs = require('../clients/bongos-client/index.cjs');
@@ -38,7 +38,7 @@ t('API-shaped: /api/*, /gds/*, and bare GDS roots (/tasks, /me, …) match', ()
38
38
 
39
39
  t('NOT API-shaped: game/hall/static paths fall through to HTML/static', () => {
40
40
  for (const p of ['/', '/index.html', '/builders', '/builders/work', '/atlas-ab12.png',
41
- '/vendor/phaser.min.js', '/downloads/devbox/mac', '/game', '/healthz', '/version']) {
41
+ '/vendor/phaser.min.js', '/downloads/bongos/latest.json', '/game', '/healthz', '/version']) {
42
42
  assert.equal(isApiShapedPath(p), false, `${p} should NOT be API-shaped`);
43
43
  }
44
44
  });
@@ -77,7 +77,7 @@ await test('computeTaskReward: 0 raw → 0 effective (a 0-mapping pays nothing)'
77
77
 
78
78
  await test('netForBuilder: would-be minus already-paid', () => {
79
79
  assert.equal(netForBuilder(100, 30), 70);
80
- assert.equal(netForBuilder(100, 0), 100); // cost-plus paid nothing (dev-box/CI case)
80
+ assert.equal(netForBuilder(100, 0), 100); // cost-plus paid nothing (the CI case)
81
81
  });
82
82
 
83
83
  await test('netForBuilder: cost-plus over-paid → floored at 0 (never claws back)', () => {
@@ -1,7 +1,7 @@
1
- // tests/box_ship_permissions.mjs
1
+ // tests/cli_allowlist_permissions.mjs
2
2
  //
3
3
  // task 1051 (D): the blessed methodology CLI scripts must stay pre-allowed in the
4
- // project .claude/settings.json so a dev-box Claude session (auto permission mode)
4
+ // project .claude/settings.json so a Claude session in auto permission mode
5
5
  // does not hard-DENY the very command it exists to run. The #1027 nightmare
6
6
  // dead-ended on `node scripts/gds/ship.js` at the auto classifier; this guards
7
7
  // against that allow-rule being silently dropped.
@@ -12,7 +12,7 @@
12
12
  // on every routine read. The list of tools is `.claude/hooks/shell-tools.js`, the same
13
13
  // one the guards read: a rule may only exist for a tool git-guard and secret-gate see.
14
14
  //
15
- // Run: node tests/box_ship_permissions.mjs
15
+ // Run: node tests/cli_allowlist_permissions.mjs
16
16
 
17
17
  import { strict as assert } from 'node:assert';
18
18
  import fs from 'node:fs';
@@ -62,7 +62,7 @@ perTool('permissions.allow pre-allows the generic GDS API channel api.js (auto-m
62
62
  // idea-triage promote, create-task, blocker-resolve, criterion ratify, goal
63
63
  // create, cost-via-API, security reports — rides the generic caller
64
64
  // `node scripts/gds/api.js <METHOD> /api/gds/...`. If it is not pre-allowed, a
65
- // dev-box (or laptop) session in AUTO permission mode has the classifier
65
+ // session in AUTO permission mode has the classifier
66
66
  // hard-DENY those writes as "[External System Writes] ... beyond the user's
67
67
  // request" — mistaking a sanctioned idea-triage session for scope creep. The
68
68
  // GDS server enforces builders.rank on every request (ADR 0016), so this rule
@@ -309,5 +309,5 @@ test('the instance starter mirrors this list in verb form (claude-materialize.js
309
309
  });
310
310
 
311
311
  console.log('');
312
- console.log(`box_ship_permissions: ${passed} passed, ${failed} failed`);
312
+ console.log(`cli_allowlist_permissions: ${passed} passed, ${failed} failed`);
313
313
  process.exit(failed === 0 ? 0 : 1);
@@ -201,7 +201,7 @@ test('signing into a second instance PRESERVES the first — including one writt
201
201
  });
202
202
 
203
203
  test('the ACTIVE pointer still lands exactly where every existing reader looks', () => {
204
- // In-repo skills, hooks and the dev box all read the active session file in the configured dir. The
204
+ // In-repo skills and hooks all read the active session file in the configured dir. The
205
205
  // store is additive; if this moved, every one of them would silently stop finding a session.
206
206
  const { out } = inSandbox(`
207
207
  await lib.saveSession({ token:'T', api_base:'https://a.example.com', builder:{github_login:'x'} });
@@ -339,14 +339,14 @@ t('formatContextPackCompact: two lines, worktree + prefix, no undefined', () =>
339
339
  assert.ok(!/undefined/.test(out));
340
340
  });
341
341
 
342
- // ---- the task-scoped box with no claim (task 1003085) ----------------------
342
+ // ---- a checkout with no modules/lifecycle on disk (task 1003085) ------------
343
343
  //
344
- // A box scoped to a claim materializes modules/<key> only for the modules that
345
- // claim put in scope (ADR 0148). With NO active claim, modules/lifecycle is not
346
- // on disk — and this file's hard `require` of task-classifier threw
347
- // MODULE_NOT_FOUND at IMPORT time, which killed /builder-start and
348
- // /builder-claim: the two commands needed to escape the state, because a claim
349
- // is what fetches the module. The live escape was POST /claims by curl.
344
+ // When modules/lifecycle is not on disk (it was found on a checkout that held only
345
+ // the modules an active claim put in scope, so with no claim it was absent), this
346
+ // file's hard `require` of task-classifier threw MODULE_NOT_FOUND at IMPORT time,
347
+ // which killed /builder-start and /builder-claim: the two commands needed to escape
348
+ // the state, because a claim is what fetches the module. The live escape was
349
+ // POST /claims by curl.
350
350
  //
351
351
  // This reproduces the condition for real rather than asserting on source text:
352
352
  // a temp tree holding context-pack.js and the one other thing it imports, with
@@ -357,7 +357,7 @@ const os = require('node:os');
357
357
  const path = require('node:path');
358
358
  const url = require('node:url');
359
359
 
360
- function boxWithoutLifecycle() {
360
+ function checkoutWithoutLifecycle() {
361
361
  const here = path.dirname(url.fileURLToPath(import.meta.url));
362
362
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ctxpack-nolifecycle-'));
363
363
  fs.mkdirSync(path.join(root, 'scripts', 'gds'), { recursive: true });
@@ -369,13 +369,13 @@ function boxWithoutLifecycle() {
369
369
  // coupling has to be looked at — which is the point.
370
370
  fs.copyFileSync(path.join(here, '..', 'src', 'bongos', 'api-prefix.js'),
371
371
  path.join(root, 'src', 'bongos', 'api-prefix.js'));
372
- // No modules/ — that IS the box condition.
372
+ // No modules/ — that IS the condition.
373
373
  assert.ok(!fs.existsSync(path.join(root, 'modules')), 'the fixture must not carry modules/');
374
374
  return { root, mod: require(path.join(root, 'scripts', 'gds', 'context-pack.js')) };
375
375
  }
376
376
 
377
- t('a box with no modules/lifecycle can still LOAD the pack (the deadlock)', () => {
378
- const { mod } = boxWithoutLifecycle();
377
+ t('a checkout with no modules/lifecycle can still LOAD the pack (the deadlock)', () => {
378
+ const { mod } = checkoutWithoutLifecycle();
379
379
  const api = mod.apiContract();
380
380
  assert.equal(api.enums_available, false, 'it must say so, not pretend');
381
381
  assert.equal(api.api_prefix, '/api/bongos', 'everything not behind the module still works');
@@ -386,8 +386,8 @@ t('a box with no modules/lifecycle can still LOAD the pack (the deadlock)', () =
386
386
  assert.equal(api.manual_degree, null);
387
387
  });
388
388
 
389
- t('both formatters render on that box instead of throwing', () => {
390
- const { mod } = boxWithoutLifecycle();
389
+ t('both formatters render on that checkout instead of throwing', () => {
390
+ const { mod } = checkoutWithoutLifecycle();
391
391
  const pack = mod.buildContextPack({
392
392
  activeClaims: [], cwd: '/fallback', platform: 'linux',
393
393
  runGit: () => '/work/tree\n', runWhich: () => '',
@@ -29,11 +29,11 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
29
  const inv = require(path.join(ROOT, 'scripts', 'gds', 'copy-inventory.js'));
30
30
 
31
31
  test('stable ids survive a move and change with the text', () => {
32
- const a = inv.stableId('builders-hall', 'Close your box');
33
- assert.equal(a, inv.stableId('builders-hall', 'Close your box'), 'same surface + text → same id, whatever the file/line');
34
- assert.equal(a, inv.stableId('builders-hall', ' Close your box '), 'whitespace is normalized before hashing');
35
- assert.notEqual(a, inv.stableId('builders-hall', 'Close your dev box'), 'a text change must break the id — that is the signal a flag is stale');
36
- assert.notEqual(a, inv.stableId('status-dashboard', 'Close your box'), 'the same words on another surface are a different piece of copy');
32
+ const a = inv.stableId('builders-hall', 'Close your tab');
33
+ assert.equal(a, inv.stableId('builders-hall', 'Close your tab'), 'same surface + text → same id, whatever the file/line');
34
+ assert.equal(a, inv.stableId('builders-hall', ' Close your tab '), 'whitespace is normalized before hashing');
35
+ assert.notEqual(a, inv.stableId('builders-hall', 'Close your browser tab'), 'a text change must break the id — that is the signal a flag is stale');
36
+ assert.notEqual(a, inv.stableId('status-dashboard', 'Close your tab'), 'the same words on another surface are a different piece of copy');
37
37
  });
38
38
 
39
39
  test('the JS extractor refuses code — the 2144-row failure mode cannot come back', () => {
@@ -55,14 +55,14 @@ test('the JS extractor keeps copy, and names how a person sees it', () => {
55
55
  const src = [
56
56
  "toast('Priority order saved.');",
57
57
  "el.textContent = 'How the whole build is doing.';",
58
- "box.innerHTML = `<p class=\"setup__box-msg\">Closing your box… (a few seconds)</p>`;",
58
+ "panel.innerHTML = `<p class=\"setup__msg\">Closing your session… (a few seconds)</p>`;",
59
59
  "row.innerHTML = `<span class=\"pill\">Release task ${id}</span>`;",
60
60
  ].join('\n');
61
61
  const rows = inv.fromJs({ source: src, rel: 'x.js' });
62
62
  const byOrigin = Object.fromEntries(rows.map((r) => [r.origin, r.text]));
63
63
  assert.equal(byOrigin['js-notice'], 'Priority order saved.');
64
64
  assert.equal(byOrigin['js-text-assign'], 'How the whole build is doing.');
65
- assert.ok(rows.some((r) => r.origin === 'js-markup' && r.text === 'Closing your box… (a few seconds)'),
65
+ assert.ok(rows.some((r) => r.origin === 'js-markup' && r.text === 'Closing your session… (a few seconds)'),
66
66
  'text inside script-built markup is copy; the tags around it are not');
67
67
  assert.ok(rows.some((r) => r.text === 'Release task {…}'),
68
68
  'a template interpolation becomes a placeholder so the sentence stays readable and the hole stays visible');
@@ -0,0 +1,180 @@
1
+ // tests/core_258_box_session_claims_db.mjs
2
+ //
3
+ // task 1004362 — core_258 must apply on a database holding a claim created from a
4
+ // dev-box session.
5
+ //
6
+ // WHAT BROKE. core_258 (task 1003895) deletes every builder_sessions row with source
7
+ // 'box'. claims.creator_session_id (migration 013) references builder_sessions with no
8
+ // ON DELETE, so on the live database, which had one such claim, the DELETE failed the
9
+ // foreign key, the whole migration rolled back, and so did every core upgrade carrying
10
+ // it. The fix nulls the link first; NULL is what a web claim already carries.
11
+ //
12
+ // HOW THIS PROVES IT. The lane database is already migrated, so core_258 has run and the
13
+ // source CHECK no longer admits 'box'. Inside ONE transaction that is ALWAYS rolled back,
14
+ // the test widens the CHECK back the way migration 090 had it, seeds a box session and a
15
+ // claim created from it, then runs core_258's own statements (its BEGIN and COMMIT
16
+ // stripped so they join this transaction). Every table core_258 drops is already gone,
17
+ // and each DROP is IF EXISTS, so they are no-ops here. Nothing is committed or truncated.
18
+ //
19
+ // Real-DB (self-skips without Postgres; belongs in the INTEGRATION set).
20
+ //
21
+ // Run: DATABASE_URL=postgres://... node tests/core_258_box_session_claims_db.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { createRequire } from 'node:module';
25
+ import { readFileSync } from 'node:fs';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { dirname, join } from 'node:path';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const { Pool } = require('pg');
31
+
32
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
33
+ const migrationText = readFileSync(join(root, 'migrations/core_258_drop_dev_box_tables.sql'), 'utf8');
34
+
35
+ // The migration's own statements, minus its transaction wrapper. The count check keeps
36
+ // the strip honest: if the file stopped using BEGIN/COMMIT this would silently be a
37
+ // different migration, so say so instead.
38
+ const wrapper = /^\s*(BEGIN|COMMIT);\s*$/gm;
39
+ assert.equal((migrationText.match(wrapper) || []).length, 2, 'core_258 is expected to carry exactly one BEGIN and one COMMIT');
40
+ const migrationBody = migrationText.replace(wrapper, '');
41
+ // The pre-fix body: the same statements without the unlinking UPDATE, so the bug stays
42
+ // executable rather than described.
43
+ const preFixBody = migrationBody.replace(/UPDATE claims SET creator_session_id = NULL[\s\S]*?;/, '');
44
+ assert.notEqual(preFixBody, migrationBody, 'core_258 no longer unlinks claims before deleting box sessions');
45
+
46
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
47
+
48
+ let passed = 0;
49
+ let failed = 0;
50
+ let client;
51
+ async function test(name, fn) {
52
+ await client.query('SAVEPOINT t');
53
+ try {
54
+ await fn();
55
+ passed++; console.log(` ok ${name}`);
56
+ await client.query('RELEASE SAVEPOINT t');
57
+ } catch (err) {
58
+ failed++; console.error(` FAIL ${name}\n ${err.message}`);
59
+ await client.query('ROLLBACK TO SAVEPOINT t');
60
+ }
61
+ }
62
+
63
+ // Schema-driven insert (the helper of tests/session_earnings_mirror_db.mjs): fills any
64
+ // NOT NULL column with no default. Identifiers come from this file or information_schema.
65
+ const schemaCache = new Map();
66
+ async function insertRow(table, explicit = {}) {
67
+ if (!schemaCache.has(table)) {
68
+ const { rows } = await client.query(
69
+ `SELECT column_name, data_type, is_nullable, column_default, is_identity
70
+ FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [table]);
71
+ schemaCache.set(table, rows);
72
+ }
73
+ const cols = schemaCache.get(table);
74
+ if (cols.length === 0) throw new Error(`table ${table} not found — is this database migrated?`);
75
+ const values = { ...explicit };
76
+ for (const c of cols) {
77
+ if (c.column_name in values || c.is_nullable === 'YES' || c.column_default !== null || c.is_identity === 'YES') continue;
78
+ switch (c.data_type) {
79
+ case 'integer': case 'bigint': case 'smallint': case 'numeric': case 'real': case 'double precision':
80
+ values[c.column_name] = 0; break;
81
+ case 'boolean': values[c.column_name] = false; break;
82
+ case 'timestamp with time zone': case 'timestamp without time zone': case 'date':
83
+ values[c.column_name] = new Date(); break;
84
+ case 'json': case 'jsonb': values[c.column_name] = {}; break;
85
+ case 'ARRAY': values[c.column_name] = []; break;
86
+ default: values[c.column_name] = `c258-${c.column_name}`;
87
+ }
88
+ }
89
+ const names = Object.keys(values);
90
+ const { rows } = await client.query(
91
+ `INSERT INTO ${table} (${names.map((n) => `"${n}"`).join(', ')})
92
+ VALUES (${names.map((_, i) => `$${i + 1}`).join(', ')}) RETURNING *`,
93
+ names.map((n) => values[n]));
94
+ return rows[0];
95
+ }
96
+
97
+ // A builder with one box session and one cli session, and a released claim from each.
98
+ async function fixture() {
99
+ const tag = `c258-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
100
+ const builder = await insertRow('builders', { github_id: `${tag}-gh`, github_login: tag, display_name: tag, total_credits: 0 });
101
+ const version = await insertRow('versions', { id: `${tag}-v`, status: 'building' });
102
+ const claimFrom = async (source) => {
103
+ const session = await insertRow('builder_sessions', { builder_id: builder.id, token_hash: `${tag}-${source}`, source });
104
+ // discipline + kind: core_247's CHECK refuses the column default (task 1004363).
105
+ const task = await insertRow('tasks', { title: `${tag} ${source} task`, status: 'shipped', version_id: version.id, discipline: 'engineer', kind: 'feature' });
106
+ const claim = await insertRow('claims', {
107
+ task_id: task.id, builder_id: builder.id, creator_session_id: session.id,
108
+ worktree_name: `${tag}-${source}`, released_at: new Date(), outcome: 'shipped',
109
+ });
110
+ return { session, claim };
111
+ };
112
+ return { box: await claimFrom('box'), cli: await claimFrom('cli') };
113
+ }
114
+
115
+ async function main() {
116
+ try {
117
+ client = await pool.connect();
118
+ } catch {
119
+ console.log('core_258_box_session_claims_db: no Postgres (DATABASE_URL unset/unreachable) — skipping');
120
+ process.exit(0);
121
+ }
122
+
123
+ await client.query('BEGIN');
124
+ try {
125
+ // The source CHECK as migration 090 left it, so a box session can be seeded.
126
+ await client.query('ALTER TABLE builder_sessions DROP CONSTRAINT IF EXISTS builder_sessions_source_check');
127
+ await client.query(`ALTER TABLE builder_sessions ADD CONSTRAINT builder_sessions_source_check CHECK (source IN ('cli', 'web', 'box'))`);
128
+
129
+ await test('without the unlink, the box-session DELETE fails the claims foreign key (the bug)', async () => {
130
+ await fixture();
131
+ await client.query('SAVEPOINT pre');
132
+ let err = null;
133
+ try { await client.query(preFixBody); } catch (e) { err = e; }
134
+ await client.query('ROLLBACK TO SAVEPOINT pre');
135
+ assert.ok(err, 'the pre-fix body should have failed');
136
+ assert.equal(err.code, '23503', `expected a foreign-key violation, got ${err.code}: ${err.message}`);
137
+ assert.match(err.message, /claims_creator_session_id_fkey/);
138
+ });
139
+
140
+ await test('core_258 applies, unlinks the box claim, keeps it, and leaves the cli claim alone', async () => {
141
+ const { box, cli } = await fixture();
142
+ // Case 1 passed and released its savepoint, so its box session and claim are still
143
+ // here too; this run unlinks and deletes those as well. That is harmless and even
144
+ // realistic (more than one box claim), and every assertion below is scoped by id.
145
+ await client.query(migrationBody);
146
+
147
+ const { rows: claims } = await client.query(
148
+ 'SELECT id, creator_session_id, worktree_name, outcome FROM claims WHERE id = ANY($1::bigint[]) ORDER BY id',
149
+ [[box.claim.id, cli.claim.id]]);
150
+ assert.equal(claims.length, 2, 'both claims must survive the migration');
151
+ const boxClaim = claims.find((c) => String(c.id) === String(box.claim.id));
152
+ const cliClaim = claims.find((c) => String(c.id) === String(cli.claim.id));
153
+ assert.equal(boxClaim.creator_session_id, null, 'the box claim must be unlinked');
154
+ assert.equal(boxClaim.outcome, 'shipped', 'the box claim keeps its history');
155
+ assert.equal(String(cliClaim.creator_session_id), String(cli.session.id), 'a cli claim must keep its session');
156
+
157
+ const { rows: boxSessions } = await client.query(`SELECT count(*)::int AS n FROM builder_sessions WHERE source = 'box'`);
158
+ assert.equal(boxSessions[0].n, 0, 'every box session must be deleted');
159
+ const { rows: cliSession } = await client.query('SELECT id FROM builder_sessions WHERE id = $1', [cli.session.id]);
160
+ assert.equal(cliSession.length, 1, 'a cli session must survive');
161
+
162
+ await client.query('SAVEPOINT chk');
163
+ let err = null;
164
+ try { await insertRow('builder_sessions', { builder_id: cli.session.builder_id, token_hash: `${cli.session.token_hash}-again`, source: 'box' }); } catch (e) { err = e; }
165
+ await client.query('ROLLBACK TO SAVEPOINT chk');
166
+ assert.equal(err && err.code, '23514', 'the narrowed CHECK must refuse a new box session');
167
+ });
168
+ } finally {
169
+ await client.query('ROLLBACK');
170
+ client.release();
171
+ await pool.end();
172
+ }
173
+
174
+ console.log(`\ncore_258_box_session_claims_db: ${passed} passed, ${failed} failed`);
175
+ // Let the socket close before exiting (a libuv abort otherwise on some boxes).
176
+ await new Promise((r) => setTimeout(r, 100));
177
+ process.exit(failed === 0 ? 0 : 1);
178
+ }
179
+
180
+ main().catch((err) => { console.error(err); process.exit(1); });
@@ -435,7 +435,7 @@ await test('1002474 the INSERT still carries a DB-level double-pay backstop', as
435
435
  assert.match(insert.sql, /reward_basis/, 'and it must be keyed on (session_id, reward_basis)');
436
436
  });
437
437
 
438
- // task 1676 — the cost-plus zero-out fix for the dev-box/CI agent ship path.
438
+ // task 1676 — the cost-plus zero-out fix for the CI agent ship path.
439
439
  // Symptom (ADR 0097): those sessions uploaded empty/unpriceable model_usage, so
440
440
  // priceModelUsage returned $0 and the reward rounded to 0 — even though real
441
441
  // token spend happened. The fix prices the session's real total_tokens at the
@@ -444,7 +444,7 @@ await test('awardSessionTokenReward (task 1676): empty model_usage + real total_
444
444
  const client = fakeClient();
445
445
  client.enqueue([]); // idempotency SELECT → no prior row
446
446
  client.enqueue([{ id: 9 }]); // INSERT ... RETURNING id
447
- // The exact dev-box/CI shape: no priceable per-model usage, but the digest
447
+ // The exact CI shape: no priceable per-model usage, but the digest
448
448
  // carried a real total token count (here 73M, an opus-marathon session).
449
449
  const res = await awardSessionTokenReward(client, 7, 'sess-ci', {}, 73_000_000);
450
450
  assert.equal(res.awarded, true, 'reward awarded despite empty model_usage');
@@ -30,7 +30,7 @@ const CRITERIA = [
30
30
  crit(138, 'bx-hall', 'Core task actions work in the builders hall without invoking the LLM.'),
31
31
  crit(139, 'bx-identity', 'A builder can view and edit their own profile and read a leaderboard.'),
32
32
  crit(140, 'bx-onramp', 'Newcomers never dead-end: auth and re-auth are browser-clickable.'),
33
- crit(141, 'bx-box', 'A builder dev box and sandbox is robust: uncommitted work is not lost.'),
33
+ crit(141, 'bx-recovery', 'A builder can resume after a crash: uncommitted work is not lost.'),
34
34
  ];
35
35
 
36
36
  test('ranks the most lexically-relevant criterion first', () => {
@@ -7,7 +7,8 @@
7
7
  // Claude Design round-trip). Nothing tied them together, so the design file kept
8
8
  // the pre-1002749 palette — including the exact accents removed for failing WCAG
9
9
  // AA — and every prototype started from colours the product no longer paints.
10
- // Model: the fallback-equals-pack assertions in tests/devbox_dmg_backdrop.mjs.
10
+ // Model: fallback-equals-pack assertions — a copy of the pack's values is asserted
11
+ // equal to the pack, never trusted.
11
12
  //
12
13
  // Run: node tests/design_tokens_sync.mjs
13
14
 
@@ -25,7 +26,7 @@ const tokens = readJson('config', 'design-tokens.neutral.json');
25
26
 
26
27
  // Strip comments so rationale prose — which must be free to name a removed
27
28
  // colour — never trips a scan of the CODE (the stripCommentsForIdentity
28
- // contract, as applied in tests/devbox_dmg_backdrop.mjs).
29
+ // contract).
29
30
  function codeOnly(text) {
30
31
  return text
31
32
  .replace(/\/\*[\s\S]*?\*\//g, '')
package/tests/fitness.mjs CHANGED
@@ -973,3 +973,92 @@ test('KERNEL_FILES is exported exactly ONCE and still resolves (task 1002795)',
973
973
  assert.ok(Array.isArray(fitness.KERNEL_FILES), 'and it still resolves to an array');
974
974
  assert.ok(fitness.KERNEL_FILES.length > 0, 'with the real kernel roster in it');
975
975
  });
976
+
977
+ // ---- the dev box stays retired (task 1003899, ADR 0346) ---------------------
978
+ //
979
+ // scripts/gds/dev-box-guard.js fails CI when a retired dev-box name reappears.
980
+ // Each rule is proven by PLANTING a violation through the injectable files/read
981
+ // seam, never by assuming the regex is right. The retired names are assembled at
982
+ // runtime (KEY = 'dev' + '-box', …) so this file never contains them literally and
983
+ // needs no exemption of its own.
984
+ const devBoxGuard = require(path.join(ROOT, 'scripts', 'gds', 'dev-box-guard.js'));
985
+ const KEY = 'dev' + '-box';
986
+ const TABLE = 'builder' + '_boxes';
987
+ const COLUMN = 'box' + '_blocked_reason';
988
+ const PERM = 'box' + '.fleet.manage';
989
+ const BOX = 'b' + 'ox';
990
+
991
+ function plant(rel, text) {
992
+ return devBoxGuard.checkNoDevBox({ files: [rel], read: () => text });
993
+ }
994
+
995
+ test('dev-box guard: the clean tree passes, and it scans the real tracked files', () => {
996
+ const r = devBoxGuard.checkNoDevBox();
997
+ assert.equal(r.hardFail, false, r.violations.join('\n'));
998
+ assert.match(r.note, /(\d{4,}) tracked file/, 'a real enumeration, not an empty one');
999
+ });
1000
+
1001
+ test('dev-box guard: it is wired into the fitness CHECKS that CI runs', () => {
1002
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'fitness.js'), 'utf8');
1003
+ assert.match(src, /require\('\.\/dev-box-guard\.js'\)\.checkNoDevBox/);
1004
+ const names = fitness.runAll().map((r) => r.name);
1005
+ assert.ok(names.includes('the dev box stays retired (ADR 0346)'), 'runAll() includes the guard');
1006
+ });
1007
+
1008
+ test('dev-box guard: each rule FAILS on a planted violation', () => {
1009
+ const cases = [
1010
+ ['module-key (quoted)', 'src/x.js', `enable('${KEY}');`],
1011
+ ['module-key (path)', 'src/y.js', `require('../modules/${KEY}/boxes.js');`],
1012
+ ['module-key (json key)', 'config/modules.neutral.json', `{ "modules": { "${KEY}": true } }`],
1013
+ ['identifier (table)', 'src/z.js', `await q('SELECT * FROM ${TABLE}');`],
1014
+ ['identifier (column)', 'src/w.js', `SELECT ${COLUMN} FROM builders`],
1015
+ ['permission', 'modules/government/catalog.js', `{ key: '${PERM}', floor: 'metic' },`],
1016
+ ['session-source', 'src/bongos/auth.js', `if (session.source === '${BOX}') return;`],
1017
+ ];
1018
+ for (const [label, rel, text] of cases) {
1019
+ const r = plant(rel, text);
1020
+ assert.equal(r.hardFail, true, `${label}: a planted violation must fail the check`);
1021
+ assert.match(r.violations[0], /ADR 0346/, `${label}: the failure names the ADR`);
1022
+ assert.match(r.note, /DEV_BOX_EXEMPT in scripts\/gds\/dev-box-guard\.js/, `${label}: and the one exemption list`);
1023
+ }
1024
+ });
1025
+
1026
+ test('dev-box guard: a retired PATH fails even with harmless contents', () => {
1027
+ for (const rel of [`scripts/gds/${BOX}.js`, `infra/${BOX}-provision.sh`, `modules/${KEY}/module.json`, '.devcontainer/devcontainer.json']) {
1028
+ const r = plant(rel, '{}');
1029
+ assert.equal(r.hardFail, true, `${rel} must fail`);
1030
+ assert.match(r.violations.join('\n'), /path: a retired dev-box location/);
1031
+ }
1032
+ });
1033
+
1034
+ test('dev-box guard: near misses do NOT fire (plurals, filenames, prose, a server box)', () => {
1035
+ const clean = [
1036
+ `resolveModules({ modules: { '${KEY}es': true } })`, // the typo guard's test input
1037
+ `see docs/adr/0072-${KEY}-code-staleness-visibility.md`, // an ADR filename
1038
+ `the ${KEY} removal is recorded in ADR 0346`, // prose, not a key
1039
+ `ENV_FILE=/etc/cloudbongos/${BOX}.env`, // box.env is the control-plane runner's env file
1040
+ `ssh into the co-hosting ${BOX}`, // a server
1041
+ `const secret = require('./secret-${BOX}.js');`, // crypto, unrelated
1042
+ `if (session.source === 'cli') return;`,
1043
+ ];
1044
+ for (const text of clean) {
1045
+ const r = plant('src/near-miss.js', text);
1046
+ assert.equal(r.hardFail, false, `should not fire: ${text}\n${r.violations.join('\n')}`);
1047
+ }
1048
+ });
1049
+
1050
+ test('dev-box guard: the exemption list is honoured, and it is the only way out', () => {
1051
+ const text = `const RETIRED = { '${KEY}': 'gone' };`;
1052
+ assert.equal(plant('src/modules.js', text).hardFail, false, 'the retirement guard file is exempt');
1053
+ assert.equal(plant('docs/adr/0999-anything.md', text).hardFail, false, 'the historical record is exempt by prefix');
1054
+ assert.equal(plant('src/other.js', text).hardFail, true, 'the same text anywhere else fails');
1055
+ for (const [entry, why] of devBoxGuard.DEV_BOX_EXEMPT) {
1056
+ assert.ok(typeof why === 'string' && why.length >= 12, `${entry} carries a reason`);
1057
+ }
1058
+ });
1059
+
1060
+ test('dev-box guard: an empty enumeration is a scan defect, not a pass', () => {
1061
+ const r = devBoxGuard.checkNoDevBox({ files: [], read: () => '' });
1062
+ assert.equal(r.hardFail, true);
1063
+ assert.match(r.violations[0], /scan defect/);
1064
+ });
@@ -161,8 +161,8 @@ test('the committed baseline file parses and covers every hermetic metric', () =
161
161
  // `floor === undefined`, which compareToBaselines classifies as a WARNING — so a
162
162
  // checkout with no config/ reported the ratchet PASSED. Measured before the fix: a
163
163
  // planted 5,000-line + 94-cycle regression produced ZERO violations. That was not a
164
- // hypothetical checkout — config/ is absent from every dev box's sparse slice, so the
165
- // builders who most needed the gate were the ones silently exempt from it.
164
+ // hypothetical checkout — a sparse checkout without config/ once left the builders
165
+ // who most needed the gate silently exempt from it.
166
166
  //
167
167
  // checkQualityRatchets() reads the real repo, so these tests drive compareToBaselines
168
168
  // (pure) for the metric-level rules and exercise the file-level rule by pointing at a
@@ -154,7 +154,7 @@ test('landProvenForTask: a non-sha published tip ("HEAD") falls back to the PR c
154
154
 
155
155
  // --- task 1002645: findTaskPullRequest — the PR-title fallback locator --------
156
156
  //
157
- // The reconciler derives claude/<worktree_name> (dev-box convention); a laptop/wave
157
+ // The reconciler derives claude/<worktree_name> first; a laptop/wave
158
158
  // ship pushes worktree-<worktree_name>, so the derived branch finds no PR and the
159
159
  // sweep never engaged (wave PRs 34/35/36 stranded at 'confirmed'). This locates the
160
160
  // task's PR by the "task <id>: …" TITLE-PREFIX convention — deliberately TIGHTER