@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
@@ -56,7 +56,7 @@ for (const fn of ['function ensureModsCatalog(', 'function renderMods(', 'functi
56
56
  const CATALOG = {
57
57
  core: ['memory', 'lifecycle', 'hall-ui'],
58
58
  optional: [
59
- { key: 'dev-box', label: 'Dev box', summary: 'A shared machine your builders work on.' },
59
+ { key: 'npm-release', label: 'npm release', summary: 'Publishes each release of your project to npm.' },
60
60
  { key: 'discord', label: 'Discord', summary: 'Mirrors what ships into a Discord channel.' },
61
61
  { key: 'agents', label: 'Agents', summary: 'Lets AI builders claim and ship work on their own.' },
62
62
  ],
@@ -76,7 +76,7 @@ function parseRows(html) {
76
76
 
77
77
  // Boot the region against a scripted catalog + detail channel and hand back the
78
78
  // handles a test drives it with.
79
- function boot({ selected = ['dev-box'], source = 'chosen', catalog = CATALOG, catalogOk = true, patchAnswer } = {}) {
79
+ function boot({ selected = ['npm-release'], source = 'chosen', catalog = CATALOG, catalogOk = true, patchAnswer } = {}) {
80
80
  const requests = [];
81
81
  const els = new Map();
82
82
  const listeners = new Map();
@@ -156,14 +156,14 @@ function boot({ selected = ['dev-box'], source = 'chosen', catalog = CATALOG, ca
156
156
 
157
157
  test('the band renders the optional catalog the server offered — a row and a sentence each', async () => {
158
158
  // Arrange
159
- const ui = boot({ selected: ['dev-box'] });
159
+ const ui = boot({ selected: ['npm-release'] });
160
160
 
161
161
  // Act
162
162
  await ui.render();
163
163
 
164
164
  // Assert
165
- assert.deepEqual(ui.rows().map((r) => r.key), ['dev-box', 'discord', 'agents']);
166
- assert.deepEqual(ui.rows().map((r) => r.label), ['Dev box', 'Discord', 'Agents']);
165
+ assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents']);
166
+ assert.deepEqual(ui.rows().map((r) => r.label), ['npm release', 'Discord', 'Agents']);
167
167
  for (const r of ui.rows()) assert.ok(r.summary.length > 20, `${r.label} says what it is for`);
168
168
  });
169
169
 
@@ -171,11 +171,11 @@ test('what the project runs today is what is switched on — resolved by the ser
171
171
  // A project that SKIPPED the picker reads as its own starter bundle, and the
172
172
  // band must show that set rather than an empty one: the whole point of
173
173
  // effectiveModules resolving `source: 'bundle'` on the way out.
174
- const ui = boot({ selected: ['dev-box', 'discord'], source: 'bundle' });
174
+ const ui = boot({ selected: ['npm-release', 'discord'], source: 'bundle' });
175
175
 
176
176
  await ui.render();
177
177
 
178
- assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['dev-box', 'discord']);
178
+ assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release', 'discord']);
179
179
  assert.deepEqual(ui.rows().filter((r) => !r.on).map((r) => r.key), ['agents']);
180
180
  });
181
181
 
@@ -185,7 +185,7 @@ test('the always-on core is COUNTED and stated, and is not a row anyone can swit
185
185
  await ui.render();
186
186
 
187
187
  assert.match(ui.slot(), /the same 3 core parts/, 'the clause names how many, from the server\'s own list');
188
- assert.deepEqual(ui.rows().map((r) => r.key), ['dev-box', 'discord', 'agents'], 'and no core key is on the band');
188
+ assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents'], 'and no core key is on the band');
189
189
  for (const key of CATALOG.core) {
190
190
  assert.equal(ui.rows().some((r) => r.key === key), false, `${key} is not a choice`);
191
191
  }
@@ -196,26 +196,26 @@ test('a toggle issues the write — the WHOLE list, in catalog order, on the own
196
196
  const ui = boot({ selected: ['discord'] });
197
197
  await ui.render();
198
198
 
199
- // Act — switch the dev box ON
200
- await ui.click('dev-box');
199
+ // Act — switch npm release ON
200
+ await ui.click('npm-release');
201
201
 
202
202
  // Assert
203
203
  const patch = ui.requests.find((r) => r.method === 'PATCH');
204
204
  assert.ok(patch, 'a PATCH was sent');
205
205
  assert.equal(patch.url, '/api/bongos/provisioning/instances/18/detail');
206
- assert.deepEqual(JSON.parse(patch.body), { modules: ['dev-box', 'discord'] },
206
+ assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release', 'discord'] },
207
207
  'catalog order, exactly as the column stores it — never append order');
208
208
  assert.equal(patch.credentials, 'same-origin', 'the cookie session is the auth — this page carries no token');
209
209
  });
210
210
 
211
211
  test('a toggle can REMOVE one too — the same list, one key shorter', async () => {
212
- const ui = boot({ selected: ['dev-box', 'agents'] });
212
+ const ui = boot({ selected: ['npm-release', 'agents'] });
213
213
  await ui.render();
214
214
 
215
215
  await ui.click('agents');
216
216
 
217
217
  const patch = ui.requests.find((r) => r.method === 'PATCH');
218
- assert.deepEqual(JSON.parse(patch.body), { modules: ['dev-box'] });
218
+ assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release'] });
219
219
  });
220
220
 
221
221
  test('switching the last one off sends an empty list — a real answer, not a skipped write', async () => {
@@ -243,8 +243,8 @@ test('the band repaints from the PLATFORM’S answer, and says the save changed
243
243
 
244
244
  test('a refusal is SAID in the server’s own words, and the band goes back to what the row still holds', async () => {
245
245
  const ui = boot({
246
- selected: ['dev-box'],
247
- patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are: dev-box, discord, agents.' } } }),
246
+ selected: ['npm-release'],
247
+ patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are: npm-release, discord, agents.' } } }),
248
248
  });
249
249
  await ui.render();
250
250
 
@@ -252,7 +252,7 @@ test('a refusal is SAID in the server’s own words, and the band goes back to w
252
252
 
253
253
  assert.equal(ui.msg().className, 'projMsg err');
254
254
  assert.match(ui.msg().textContent, /Unknown module "discrod"/);
255
- assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['dev-box'],
255
+ assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release'],
256
256
  'a refused write leaves the band showing the project’s real set');
257
257
  });
258
258
 
@@ -269,7 +269,7 @@ test('a signed-out save offers a way back in rather than a dead end', async () =
269
269
  });
270
270
 
271
271
  test('a catalog that ANSWERED badly says so; nothing about the project is claimed to have changed', async () => {
272
- const ui = boot({ selected: ['dev-box'], catalogOk: false });
272
+ const ui = boot({ selected: ['npm-release'], catalogOk: false });
273
273
 
274
274
  await ui.render();
275
275
  await ui.settle();
@@ -59,21 +59,22 @@ const PAYLOAD = {
59
59
  // `summary` is part of every real bundle row and is the why-line's FLOOR since task
60
60
  // 1003684 — the sentence shown when no rule moved anything. The fixture carried none
61
61
  // before that, which quietly made every "no adjustment, no sentence" case pass for the
62
- // wrong reason. These are the real strings from starter-bundles.js.
62
+ // wrong reason. Shaped like the real strings in starter-bundles.js (the live payload is pinned
63
+ // separately, at the bottom of the team-shape section).
63
64
  bundles: [
64
- { type: 'game', bundle: ['dev-box', 'discord'], summary: 'Somewhere for players to gather, and a place to walk a change through before anyone plays it.' },
65
+ { type: 'game', bundle: ['npm-release', 'discord'], summary: 'Somewhere for players to gather, and a way to publish each release of the game.' },
65
66
  { type: 'research', bundle: ['agents'], summary: 'Extra agents to run the reading and the experiments the question needs.' },
66
- { type: 'business', bundle: ['agents', 'dev-box'], summary: 'A place to try a change before customers see it, and agents to look it over first.' },
67
+ { type: 'business', bundle: ['agents', 'npm-release'], summary: 'A way to publish each release, and agents to look it over first.' },
67
68
  { type: 'non-profit', bundle: ['discord'], summary: 'Somewhere the community around the cause can gather and be heard.' },
68
69
  { type: 'not-sure', bundle: [], summary: 'Just the core, and nothing else to decide today. Add modules whenever you know what you need.' },
69
70
  ],
70
71
  // `aliases` — search words a row is findable under that its own copy never uses
71
- // (task 1003500, amending ADR 0244 §3). Never rendered, like the key. dev-box
72
+ // (task 1003500, amending ADR 0244 §3). Never rendered, like the key. npm-release
72
73
  // carries none on purpose, so the "a row without aliases still matches on its own
73
74
  // words" path is exercised by the same fixture rather than by a special case.
74
75
  optional: [
75
76
  { key: 'agents', label: 'Extra agents', summary: 'More AI agents alongside the ones every project gets.', aliases: ['bots', 'assistants', 'automation'] },
76
- { key: 'dev-box', label: 'Dev boxes', summary: 'A cloud workspace where a change can be tried out first.' },
77
+ { key: 'npm-release', label: 'npm releases', summary: 'Publishes each release of your package to the registry.' },
77
78
  { key: 'discord', label: 'Discord', summary: 'A Discord server that carries your build news.', aliases: ['chatroom', 'talk', 'community'] },
78
79
  ],
79
80
  };
@@ -92,18 +93,18 @@ const TAILORED = {
92
93
  bundles: PAYLOAD.bundles.map((b) => {
93
94
  if (b.type === 'game') {
94
95
  return { ...b, byTeamShape: {
95
- // solo: the shared environment comes off
96
- solo: { bundle: ['discord'], adjustments: [{ rule: 'building-alone-needs-no-shared-environment', effect: 'remove', module: 'dev-box', reason: 'You are building this alone, so the shared cloud environment is off.' }] },
97
- // small-team: the rule wants dev-box, the bundle already has it — a rule
96
+ // solo: the release pipeline comes off
97
+ solo: { bundle: ['discord'], adjustments: [{ rule: 'building-alone-needs-no-release-pipeline', effect: 'remove', module: 'npm-release', reason: 'You are building this alone, so the release pipeline is off.' }] },
98
+ // small-team: the rule wants npm-release, the bundle already has it — a rule
98
99
  // that changed nothing claims nothing
99
- 'small-team': { bundle: ['dev-box', 'discord'], adjustments: [] },
100
- community: { bundle: ['dev-box', 'discord'], adjustments: [] },
100
+ 'small-team': { bundle: ['npm-release', 'discord'], adjustments: [] },
101
+ community: { bundle: ['npm-release', 'discord'], adjustments: [] },
101
102
  } };
102
103
  }
103
104
  if (b.type === 'research') {
104
105
  return { ...b, byTeamShape: {
105
106
  solo: { bundle: ['agents'], adjustments: [] },
106
- 'small-team': { bundle: ['agents', 'dev-box'], adjustments: [{ rule: 'a-team-shares-one-environment', effect: 'add', module: 'dev-box', reason: 'A small team gets one shared environment to build in.' }] },
107
+ 'small-team': { bundle: ['agents', 'npm-release'], adjustments: [{ rule: 'a-team-shares-one-release-pipeline', effect: 'add', module: 'npm-release', reason: 'A small team gets one release pipeline to publish through.' }] },
107
108
  community: { bundle: ['agents', 'discord'], adjustments: [{ rule: 'a-community-needs-somewhere-to-gather', effect: 'add', module: 'discord', reason: 'You said an open community is building this, so somewhere for it to gather is switched on.' }] },
108
109
  } };
109
110
  }
@@ -232,16 +233,16 @@ test('the declared project type preselects its bundle, and the rest of the catal
232
233
 
233
234
  // Assert — every opt-in module is a row; the bundle's two are on
234
235
  const rows = h.rows();
235
- assert.deepEqual(rows.map((r) => r.key), ['agents', 'dev-box', 'discord'], 'the whole opt-in catalog is offered');
236
- assert.deepEqual(rows.filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord'], 'the game bundle is preselected');
236
+ assert.deepEqual(rows.map((r) => r.key), ['agents', 'npm-release', 'discord'], 'the whole opt-in catalog is offered');
237
+ assert.deepEqual(rows.filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord'], 'the game bundle is preselected');
237
238
  assert.equal(rows.find((r) => r.key === 'agents').pressed, false, 'and nothing else is');
238
239
  // each row carries the copy an owner can actually read
239
- assert.equal(rows[1].label, 'Dev boxes');
240
- assert.match(rows[1].summary, /cloud workspace/);
240
+ assert.equal(rows[1].label, 'npm releases');
241
+ assert.match(rows[1].summary, /package to the registry/);
241
242
  });
242
243
 
243
244
  test('a different type preselects a different bundle, and "not sure" preselects nothing', async () => {
244
- for (const [type, expected] of [['research', ['agents']], ['business', ['agents', 'dev-box']], ['non-profit', ['discord']], ['not-sure', []], [null, []]]) {
245
+ for (const [type, expected] of [['research', ['agents']], ['business', ['agents', 'npm-release']], ['non-profit', ['discord']], ['not-sure', []], [null, []]]) {
245
246
  const h = boot({ state: { projType: type } });
246
247
  await h.open();
247
248
  assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), expected, `type=${type}`);
@@ -272,8 +273,8 @@ test('a toggle flips one module and leaves the rest of the preselect standing',
272
273
  h.click('agents');
273
274
 
274
275
  // Assert
275
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'dev-box']);
276
- assert.deepEqual(h.state.modules, ['agents', 'dev-box'], 'and the draft holds the real answer, in catalog order');
276
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'npm-release']);
277
+ assert.deepEqual(h.state.modules, ['agents', 'npm-release'], 'and the draft holds the real answer, in catalog order');
277
278
  assert.ok(h.saves() >= 2, 'each toggle is written to the draft');
278
279
  });
279
280
 
@@ -285,7 +286,7 @@ test('the first toggle is what turns "not answered" into an answer', async () =>
285
286
  assert.equal(h.forCreate(), undefined, 'so the create request carries no modules key');
286
287
 
287
288
  h.click('agents');
288
- assert.deepEqual(h.forCreate(), ['agents', 'dev-box', 'discord'], 'after a touch it carries the whole set');
289
+ assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord'], 'after a touch it carries the whole set');
289
290
  });
290
291
 
291
292
  test('switching every module off is a real answer, not a reset to the bundle', async () => {
@@ -293,7 +294,7 @@ test('switching every module off is a real answer, not a reset to the bundle', a
293
294
  // said "none", and Next must not hand them the bundle back.
294
295
  const h = boot({ state: { projType: 'game' } });
295
296
  await h.open();
296
- h.click('dev-box');
297
+ h.click('npm-release');
297
298
  h.click('discord');
298
299
 
299
300
  assert.deepEqual(h.state.modules, []);
@@ -314,7 +315,7 @@ test('Skip walks past WITHOUT answering, so the server applies the bundle the pa
314
315
  assert.deepEqual(h.shown, [5], 'it advances to the Home step');
315
316
  assert.equal(h.state.modules, null, 'and answers nothing');
316
317
  assert.equal(h.forCreate(), undefined);
317
- assert.deepEqual(h.picked(), ['dev-box', 'discord'], 'while the set they were shown is still the set they get');
318
+ assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'while the set they were shown is still the set they get');
318
319
  });
319
320
 
320
321
  test('Skip does not erase a toggle already made', async () => {
@@ -325,7 +326,7 @@ test('Skip does not erase a toggle already made', async () => {
325
326
  h.click('agents');
326
327
  h.skip();
327
328
 
328
- assert.deepEqual(h.state.modules, ['agents', 'dev-box', 'discord']);
329
+ assert.deepEqual(h.state.modules, ['agents', 'npm-release', 'discord']);
329
330
  });
330
331
 
331
332
  test('Next CONFIRMS the set on screen — looking at it and accepting it is not the same as never being asked', async () => {
@@ -334,8 +335,8 @@ test('Next CONFIRMS the set on screen — looking at it and accepting it is not
334
335
 
335
336
  h.next();
336
337
 
337
- assert.deepEqual(h.state.modules, ['dev-box', 'discord'], 'the preselect becomes the answer');
338
- assert.deepEqual(h.forCreate(), ['dev-box', 'discord'], 'and rides the create request');
338
+ assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'the preselect becomes the answer');
339
+ assert.deepEqual(h.forCreate(), ['npm-release', 'discord'], 'and rides the create request');
339
340
  });
340
341
 
341
342
  test('changing the project type re-preselects while nothing has been said, and never after', async () => {
@@ -425,7 +426,7 @@ test('the review names the set even when the picker was never opened (task 10023
425
426
 
426
427
  await h.settle();
427
428
  assert.equal(h.reviews(), 1, 'the review repaints itself once the names are known');
428
- assert.equal(h.extrasRow(), 'Dev boxes, Discord', 'and then it names them');
429
+ assert.equal(h.extrasRow(), 'npm releases, Discord', 'and then it names them');
429
430
  });
430
431
 
431
432
  test('the review reads an answered picker back, empty included', async () => {
@@ -454,26 +455,26 @@ test('the team shape adjusts the type bundle, and the panel says why in the owne
454
455
  // Act
455
456
  await h.open();
456
457
 
457
- // Assert — the shared environment came off, and the reason is on screen
458
+ // Assert — the release pipeline came off, and the reason is on screen
458
459
  assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['discord'],
459
- 'solo drops the dev box the game bundle would have preselected');
460
+ 'solo drops npm releases, which the game bundle would have preselected');
460
461
  assert.equal(h.whyShown(), true);
461
- assert.deepEqual(h.why(), ['You are building this alone, so the shared cloud environment is off.']);
462
+ assert.deepEqual(h.why(), ['You are building this alone, so the release pipeline is off.']);
462
463
  });
463
464
 
464
465
  test('a rule that changed nothing claims nothing', async () => {
465
- // small-team ADDS dev-box, and the game bundle already carries it. A reason
466
+ // small-team ADDS npm-release, and the game bundle already carries it. A reason
466
467
  // shown for a change that did not happen is a claim the owner cannot check.
467
468
  const h = boot({ state: { projType: 'game', teamShape: 'small-team' }, payload: TAILORED });
468
469
  await h.open();
469
470
 
470
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord']);
471
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
471
472
  // No adjustment, so no adjustment-sentence — the rule still claims nothing. But the
472
473
  // panel is not silent: the TYPE explains its own preselect (task 1003684), because two
473
474
  // extras switched on with no reason beside them is the state ADR 0237 calls worse than
474
475
  // no preselect at all. The rule's voice and the type's voice are different things.
475
- assert.deepEqual(h.why(), ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
476
- assert.ok(!h.why().some((s) => /shared cloud environment|somewhere for it to gather is switched on/.test(s)),
476
+ assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
477
+ assert.ok(!h.why().some((s) => /release pipeline|somewhere for it to gather is switched on/.test(s)),
477
478
  'and no RULE sentence is invented for a change that did not happen');
478
479
  assert.equal(h.whyShown(), true, 'the block carries the type\'s reason');
479
480
  });
@@ -496,7 +497,7 @@ test('answering the About step later re-preselects, with no second read', async
496
497
  h.state.teamShape = 'small-team';
497
498
  h.render();
498
499
 
499
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'dev-box']);
500
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'npm-release']);
500
501
  assert.equal(h.fetches(), 1, 'and the answer cost no round trip');
501
502
  });
502
503
 
@@ -520,9 +521,9 @@ test('once the owner has answered, the reasons go — the panel stops arguing',
520
521
  await h.open();
521
522
  assert.equal(h.why().length, 1);
522
523
 
523
- h.click('dev-box');
524
+ h.click('npm-release');
524
525
 
525
- assert.deepEqual(h.state.modules, ['dev-box', 'discord'], 'their toggle stands over the rule');
526
+ assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'their toggle stands over the rule');
526
527
  assert.deepEqual(h.why(), [], 'and the rule stops explaining a set that is no longer its own');
527
528
  assert.equal(h.whyShown(), false);
528
529
  });
@@ -533,10 +534,10 @@ test('a payload with no byTeamShape degrades to the untailored bundle, never to
533
534
  const h = boot({ state: { projType: 'game', teamShape: 'solo' }, payload: PAYLOAD });
534
535
  await h.open();
535
536
 
536
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord']);
537
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
537
538
  // Degraded, but still explained: with no byTeamShape there is no adjustment to report,
538
539
  // and the type's own summary is exactly the untailored reason (task 1003684).
539
- assert.deepEqual(h.why(), ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
540
+ assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
540
541
  });
541
542
 
542
543
  test('an off-vocabulary team shape adjusts nothing', async () => {
@@ -545,10 +546,10 @@ test('an off-vocabulary team shape adjusts nothing', async () => {
545
546
  const h = boot({ state: { projType: 'game', teamShape: 'a-huge-corporation' }, payload: TAILORED });
546
547
  await h.open();
547
548
 
548
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord']);
549
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
549
550
  // No rule fires, so nothing is CLAIMED about the unknown shape — the sentence shown is
550
551
  // the type's, which is true regardless of who is building it (task 1003684).
551
- assert.deepEqual(h.why(), ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
552
+ assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
552
553
  });
553
554
 
554
555
  test('a couldn\'t-load read clears any reasons it had already painted', async () => {
@@ -617,17 +618,17 @@ test('the picker and the server resolve the same set for every answer an owner c
617
618
  // not move.
618
619
  // ─────────────────────────────────────────────────────────────────────────────
619
620
 
620
- const DEV_BOX = PAYLOAD.optional[1];
621
+ const NPM_RELEASE = PAYLOAD.optional[1];
621
622
 
622
623
  test('the matcher reads a row\'s own words — its name, its summary and its key', () => {
623
624
  // Still lexical, never semantic: the only words it knows are the ones the owner
624
625
  // can see, plus the two it cannot — the key, and the declared aliases below.
625
626
  const h = boot({ state: { projType: 'game' } });
626
627
 
627
- assert.equal(h.matches(DEV_BOX, 'boxes'), true, 'by a word in the name');
628
- assert.equal(h.matches(DEV_BOX, 'workspace'), true, 'by a word in the summary');
629
- assert.equal(h.matches(DEV_BOX, 'dev-box'), true, 'by the key');
630
- assert.equal(h.matches(DEV_BOX, 'discord'), false, 'and never by a word that is in none of them');
628
+ assert.equal(h.matches(NPM_RELEASE, 'releases'), true, 'by a word in the name');
629
+ assert.equal(h.matches(NPM_RELEASE, 'package'), true, 'by a word in the summary');
630
+ assert.equal(h.matches(NPM_RELEASE, 'npm-release'), true, 'by the key');
631
+ assert.equal(h.matches(NPM_RELEASE, 'discord'), false, 'and never by a word that is in none of them');
631
632
  });
632
633
 
633
634
  // ── task 1003500: the aliases, and the gap they close ────────────────────────
@@ -686,9 +687,9 @@ test('aliases narrow like any other word, and a row without them still matches o
686
687
  const discord = PAYLOAD.optional[2];
687
688
 
688
689
  assert.equal(h.matches(discord, 'chat news'), true, 'an alias AND a summary word still narrows to the row that has both');
689
- assert.equal(h.matches(discord, 'chat workspace'), false, 'an alias does not rescue a term the row cannot answer');
690
- assert.equal(h.matches(DEV_BOX, 'chat'), false, 'and a row declaring no aliases is not matched by another row\'s');
691
- assert.equal(h.matches(DEV_BOX, 'workspace'), true, 'while its own words keep working untouched');
690
+ assert.equal(h.matches(discord, 'chat package'), false, 'an alias does not rescue a term the row cannot answer');
691
+ assert.equal(h.matches(NPM_RELEASE, 'chat'), false, 'and a row declaring no aliases is not matched by another row\'s');
692
+ assert.equal(h.matches(NPM_RELEASE, 'package'), true, 'while its own words keep working untouched');
692
693
  });
693
694
 
694
695
  test('typing an alias filters the band to that row, and the count says so', async () => {
@@ -704,14 +705,14 @@ test('typing an alias filters the band to that row, and the count says so', asyn
704
705
  test('a term matches as a SUBSTRING, so a half-typed word still finds its module', () => {
705
706
  const h = boot({ state: { projType: 'game' } });
706
707
 
707
- assert.equal(h.matches(DEV_BOX, 'work'), true, 'the owner is still typing');
708
- assert.equal(h.matches(DEV_BOX, 'ace'), true, 'even mid-word — a dumb filter beats a clever one that finds nothing');
708
+ assert.equal(h.matches(NPM_RELEASE, 'pack'), true, 'the owner is still typing');
709
+ assert.equal(h.matches(NPM_RELEASE, 'kag'), true, 'even mid-word — a dumb filter beats a clever one that finds nothing');
709
710
  });
710
711
 
711
712
  test('matching is case-insensitive in both directions', () => {
712
713
  const h = boot({ state: { projType: 'game' } });
713
714
 
714
- assert.equal(h.matches(DEV_BOX, 'DEV BOXES'), true, 'a shouted query finds a title-cased row');
715
+ assert.equal(h.matches(NPM_RELEASE, 'NPM RELEASES'), true, 'a shouted query finds a title-cased row');
715
716
  assert.equal(h.matches(PAYLOAD.optional[0], 'ai'), true, 'and a quiet one finds a shouted row');
716
717
  });
717
718
 
@@ -719,15 +720,15 @@ test('every term must match — typing more words narrows, it never widens', ()
719
720
  // AND, not OR. An owner adding a word is telling us the list is still too long.
720
721
  const h = boot({ state: { projType: 'game' } });
721
722
 
722
- assert.equal(h.matches(DEV_BOX, 'cloud change'), true, 'both words are in the row');
723
- assert.equal(h.matches(DEV_BOX, 'cloud discord'), false, 'one of them is not, so the row is out');
723
+ assert.equal(h.matches(NPM_RELEASE, 'package registry'), true, 'both words are in the row');
724
+ assert.equal(h.matches(NPM_RELEASE, 'package discord'), false, 'one of them is not, so the row is out');
724
725
  });
725
726
 
726
727
  test('an empty search is not a search — every row stays', () => {
727
728
  const h = boot({ state: { projType: 'game' } });
728
729
 
729
- assert.equal(h.matches(DEV_BOX, ''), true);
730
- assert.equal(h.matches(DEV_BOX, ' '), true, 'and a box holding only spaces is the same as an empty one');
730
+ assert.equal(h.matches(NPM_RELEASE, ''), true);
731
+ assert.equal(h.matches(NPM_RELEASE, ' '), true, 'and a box holding only spaces is the same as an empty one');
731
732
  });
732
733
 
733
734
  test('typing filters the band to the rows that answer, in catalog order', async () => {
@@ -737,21 +738,21 @@ test('typing filters the band to the rows that answer, in catalog order', async
737
738
  assert.equal(h.rows().length, 3, 'the whole catalog before anything is typed');
738
739
 
739
740
  // Act — a word from one row's NAME
740
- h.type('boxes');
741
+ h.type('releases');
741
742
 
742
743
  // Assert
743
- assert.deepEqual(h.rows().map((r) => r.key), ['dev-box']);
744
+ assert.deepEqual(h.rows().map((r) => r.key), ['npm-release']);
744
745
  });
745
746
 
746
747
  test('a summary word and a key find their row just as a name word does', async () => {
747
748
  const h = boot({ state: { projType: 'game' } });
748
749
  await h.open();
749
750
 
750
- h.type('workspace');
751
- assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'by summary');
751
+ h.type('package');
752
+ assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'by summary');
752
753
 
753
- h.type('dev-box');
754
- assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'by key');
754
+ h.type('npm-release');
755
+ assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'by key');
755
756
 
756
757
  h.type('DISCORD');
757
758
  assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'and case is not part of the question');
@@ -761,10 +762,10 @@ test('two terms narrow to the rows that carry both', async () => {
761
762
  const h = boot({ state: { projType: 'game' } });
762
763
  await h.open();
763
764
 
764
- h.type('cloud change');
765
- assert.deepEqual(h.rows().map((r) => r.key), ['dev-box']);
765
+ h.type('package registry');
766
+ assert.deepEqual(h.rows().map((r) => r.key), ['npm-release']);
766
767
 
767
- h.type('cloud discord');
768
+ h.type('package discord');
768
769
  assert.deepEqual(h.rows(), [], 'and no row carries both of these');
769
770
  });
770
771
 
@@ -785,7 +786,7 @@ test('a switched-on module the search is HIDING is still switched on', async ()
785
786
  // toggles, and a view must not be able to edit it.
786
787
  const h = boot({ state: { projType: 'game' } });
787
788
  await h.open();
788
- assert.deepEqual(h.picked(), ['dev-box', 'discord'], 'the game bundle is on');
789
+ assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'the game bundle is on');
789
790
 
790
791
  // Act — search for the one module that is OFF, hiding both that are on
791
792
  h.type('agents');
@@ -793,7 +794,7 @@ test('a switched-on module the search is HIDING is still switched on', async ()
793
794
  // Assert
794
795
  assert.deepEqual(h.rows().map((r) => r.key), ['agents'], 'only the match is on screen');
795
796
  assert.equal(h.rows()[0].pressed, false, 'and it is off, as it was');
796
- assert.deepEqual(h.picked(), ['dev-box', 'discord'], 'the two hidden ones are still switched on');
797
+ assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'the two hidden ones are still switched on');
797
798
  assert.equal(h.state.modules, null, 'and typing answered nothing on the owner\'s behalf');
798
799
  assert.match(h.filterNote(), /2 extras are switched on/, 'the count under the field says so out loud');
799
800
  assert.match(h.filterNote(), /including any this search is hiding/, 'and that some of them are hidden');
@@ -823,13 +824,13 @@ test('the review names the hidden picks too — a filter never reaches the creat
823
824
  const h = boot({ state: { projType: 'game', step: 8 } }); // 8 = the review since task 1004351
824
825
  await h.open();
825
826
  h.click('agents');
826
- assert.deepEqual(h.forCreate(), ['agents', 'dev-box', 'discord']);
827
+ assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
827
828
 
828
829
  h.type('discord');
829
830
 
830
831
  assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'one row on screen');
831
- assert.deepEqual(h.forCreate(), ['agents', 'dev-box', 'discord'], 'three modules in the answer');
832
- assert.equal(h.extrasRow(), 'Extra agents, Dev boxes, Discord', 'and the review names all three');
832
+ assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord'], 'three modules in the answer');
833
+ assert.equal(h.extrasRow(), 'Extra agents, npm releases, Discord', 'and the review names all three');
833
834
  });
834
835
 
835
836
  test('clearing the search brings every row back, untouched', async () => {
@@ -840,8 +841,8 @@ test('clearing the search brings every row back, untouched', async () => {
840
841
 
841
842
  h.type('');
842
843
 
843
- assert.deepEqual(h.rows().map((r) => r.key), ['agents', 'dev-box', 'discord']);
844
- assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord'],
844
+ assert.deepEqual(h.rows().map((r) => r.key), ['agents', 'npm-release', 'discord']);
845
+ assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord'],
845
846
  'with the preselect exactly as it was before the search');
846
847
  assert.equal(h.filterNote(), '', 'and the count goes quiet again');
847
848
  });
@@ -851,11 +852,11 @@ test('a toggle made while filtered lands in the answer in catalog order, and the
851
852
  const h = boot({ state: { projType: 'non-profit' } });
852
853
  await h.open();
853
854
 
854
- h.type('workspace');
855
- h.click('dev-box');
855
+ h.type('package');
856
+ h.click('npm-release');
856
857
 
857
- assert.deepEqual(h.state.modules, ['dev-box', 'discord'], 'catalog order, not click order');
858
- assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'and the search is still the search');
858
+ assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'catalog order, not click order');
859
+ assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'and the search is still the search');
859
860
  assert.equal(h.rows()[0].pressed, true, 'the row they just switched on shows it');
860
861
  });
861
862
 
@@ -248,7 +248,7 @@ await ta('dry-run measures nothing and refuses nothing, even on a full box; an u
248
248
 
249
249
  await ta('statusCapacityLine prints "box at N of M" for the CLI', async () => {
250
250
  delete process.env.PROVISION_BOX_CEILING;
251
- // fleetCapacity reads THIS host's /proc/meminfo (absent on a Windows dev box → default);
251
+ // fleetCapacity reads THIS host's /proc/meminfo (absent on a Windows machine → default);
252
252
  // the line must carry the count and the source whichever it is.
253
253
  const line = await statusCapacityLine(fakeDb(12));
254
254
  assert.match(line, /control-plane box: 12 of \d+ project slots taken \((measured|default)/);
@@ -103,7 +103,7 @@ test('an open community adds somewhere to gather to a bundle that lacks one', ()
103
103
 
104
104
  test('a community adds the gathering place to a bundle that lacks it', () => {
105
105
  // The surviving bundle rule's ADD path. The two rules that added and removed
106
- // `dev-box` by team shape went with the module (task 1003886), which leaves
106
+ // the dev box by team shape went with the module (task 1003886), which leaves
107
107
  // `effect: 'remove'` declared in adjustBundle but named by no live rule - it is
108
108
  // kept for the next rule that needs it, not because anything exercises it today.
109
109
  // Arrange - the research bundle is `agents` alone, so the addition bites.
@@ -147,7 +147,8 @@ test('a rule whose module the bundle already carries records no adjustment', ()
147
147
  });
148
148
 
149
149
  test('a removal rule whose module is absent records no adjustment', () => {
150
- // Arrange — `non-profit` is `discord` alone, so solo\'s dev-box removal is a no-op.
150
+ // Arrange — `non-profit` is `discord` alone, and `solo` names no live rule (the removal rule for
151
+ // it left with the dev-box module, task 1003886), so there is nothing to remove or to claim.
151
152
  const answers = { type: 'non-profit', team_shape: 'solo' };
152
153
 
153
154
  // Act
@@ -1,7 +1,7 @@
1
1
  // tests/publish_branch_route.mjs
2
2
  //
3
3
  // Coverage for server-mediated branch publish (ADR 0055 / task 1025) — the path
4
- // that lets a dev-box builder (no GitHub push credential) ship: the box uploads a
4
+ // that lets a builder on a checkout with no GitHub push credential ship: the client uploads a
5
5
  // thin git bundle, the server pushes it + opens the PR + auto-merges with the
6
6
  // server-side GITHUB_PUSH_TOKEN.
7
7
  //
@@ -115,7 +115,7 @@ const EXCLUDE = [
115
115
  // instance identity + history
116
116
  'config/branding.json',
117
117
  'config/modules.json',
118
- 'config/devbox-app.json',
118
+ 'config/hierarchy.json',
119
119
  'CODEOWNERS',
120
120
  'docs/project-context.md',
121
121
  'docs/lore/founding-myth.md',
@@ -3,7 +3,7 @@
3
3
  // Unit tests for the server-owned publish reconciler (task 1134) — the durable
4
4
  // cure for the "strands at confirmed" class. A server-mediated publish (ADR 0055)
5
5
  // is async; ship.js stops polling after ~9 min, so the confirmed→shipped flip had
6
- // no driver once the box exited. This module sweeps confirmed tasks on a timer and
6
+ // no driver once the client exited. This module sweeps confirmed tasks on a timer and
7
7
  // runs the EXACT shipTask + postShipBroadcast the publish-status route runs.
8
8
  //
9
9
  // runSweep is pure w.r.t. injected deps (db / githubPush / emit / loggers)
@@ -1366,7 +1366,7 @@ test('1002606 the aged stale strand now ALSO emits task.strand-flagged', async (
1366
1366
 
1367
1367
  // ---------------------------------------------------------------------------
1368
1368
  // task 1002645 — the reconciler was blind to laptop/wave ships. The sweep derives
1369
- // claude/<worktree_name> (dev-box convention); a wave ship pushes
1369
+ // claude/<worktree_name> first; a wave ship pushes
1370
1370
  // worktree-<worktree_name>, so getPublishStatus saw pr_state:'none' and the whole
1371
1371
  // PR path (mergeIfGreen nudge — the ONLY merger where native auto-merge is
1372
1372
  // repo-disallowed — conflict heal, land proof) never engaged. PRs 34/35/36 all
@@ -1378,7 +1378,7 @@ test('1002645 THE PIN: a worktree-named branch (laptop wave ship) lands UNATTEND
1378
1378
  const deps = mkDeps({
1379
1379
  rows: [{ task_id: 2645, builder_id: 95, worktree_name: 'task-2645', published_head_sha: ON_MAIN }],
1380
1380
  statusByBranch: {
1381
- 'claude/task-2645': NONE, // the derived dev-box branch has no PR
1381
+ 'claude/task-2645': NONE, // the derived claude/ branch has no PR
1382
1382
  'worktree-task-2645': LANDED_NO_WORKFLOW, // the REAL branch: merged, pin-bump repo
1383
1383
  },
1384
1384
  taskPrByTask: { 2645: { branch: 'worktree-task-2645', pr_number: 36, pr_state: 'closed', merged: true } },
@@ -1418,14 +1418,14 @@ test('1002645 a legacy laptop strand (no recorded tip) heals retroactively via t
1418
1418
  assert.deepEqual(deps._calls.ship, [{ taskId: 2616, builderId: 95 }]);
1419
1419
  });
1420
1420
 
1421
- test('1002645 the fallback does NOT fire when the derived branch already has a PR (dev-box path unchanged)', async () => {
1421
+ test('1002645 the fallback does NOT fire when the derived branch already has a PR (derived-branch path unchanged)', async () => {
1422
1422
  const deps = mkDeps({
1423
- rows: [{ task_id: 900, builder_id: 1, worktree_name: 'wt-box' }],
1424
- statusByBranch: { 'claude/wt-box': PENDING },
1423
+ rows: [{ task_id: 900, builder_id: 1, worktree_name: 'wt-derived' }],
1424
+ statusByBranch: { 'claude/wt-derived': PENDING },
1425
1425
  taskPrByTask: { 900: new Error('search must not run') },
1426
1426
  });
1427
1427
  await reconciler.runSweep(deps);
1428
- assert.equal(deps._calls.search.length, 0, 'a dev-box ship keeps the derived-branch path — no extra API call');
1428
+ assert.equal(deps._calls.search.length, 0, 'a ship whose derived branch has a PR keeps that path — no extra API call');
1429
1429
  });
1430
1430
 
1431
1431
  test('1002645 a PR-search failure is swallowed — the sweep continues to the next task and retries next sweep', async () => {
@@ -1481,7 +1481,7 @@ test('1002723 THE PIN: a PR on the BARE worktree branch (task-<id>, no prefix) l
1481
1481
  });
1482
1482
  const r = await reconciler.runSweep(deps);
1483
1483
  assert.equal(r.reconciled, 1, 'the merged-PR-on-bare-branch shape must land with no manual flip');
1484
- assert.ok(deps._calls.status.includes('claude/task-77'), 'dev-box convention tried first');
1484
+ assert.ok(deps._calls.status.includes('claude/task-77'), 'the derived claude/<worktree_name> branch is tried first');
1485
1485
  assert.ok(deps._calls.status.includes('task-77'), 'then the bare worktree branch');
1486
1486
  assert.deepEqual(deps._calls.ship, [{ taskId: 77, builderId: 3 }]);
1487
1487
  });