@bongos/core 1.19.1074 → 1.19.1075

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 (114) hide show
  1. package/.bongos-core.json +122 -122
  2. package/bin/bongos.js +1 -3
  3. package/docs/api/openapi.json +4 -4
  4. package/docs/module-api-changelog.md +2 -0
  5. package/modules/copy-desk/routes/copy-desk.js +1 -1
  6. package/modules/discord/ship-broadcast.js +3 -3
  7. package/modules/economy/credits.js +2 -2
  8. package/modules/ideas/projection.js +1 -1
  9. package/modules/lifecycle/db-ship.js +4 -4
  10. package/modules/lifecycle/github-push.js +12 -14
  11. package/modules/lifecycle/publish-reconciler.js +10 -10
  12. package/modules/lifecycle/routes/tasks.js +8 -8
  13. package/modules/lifecycle/ship-card.js +1 -1
  14. package/modules/lifecycle/task-visuals.js +2 -2
  15. package/modules/memory/routes/memory.js +2 -2
  16. package/modules/platform-identity/routes/sso.js +3 -2
  17. package/modules/provisioning/capacity.js +1 -1
  18. package/modules/provisioning/provisioning.js +4 -3
  19. package/modules/provisioning/routes/provisioning.js +4 -4
  20. package/modules/sessions/db.js +1 -1
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +6 -0
  24. package/scripts/gds/artifact-staleness.js +2 -2
  25. package/scripts/gds/backfill-task-rewards.js +5 -5
  26. package/scripts/gds/claude-materialize.js +2 -2
  27. package/scripts/gds/cli-lib.js +7 -7
  28. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  29. package/scripts/gds/context-pack.js +5 -7
  30. package/scripts/gds/doctor.js +3 -3
  31. package/scripts/gds/fitness-checks-packaging.js +4 -4
  32. package/scripts/gds/fitness-ratchets.js +1 -1
  33. package/scripts/gds/gen-api-client.js +4 -4
  34. package/scripts/gds/gen-api-docs.js +1 -1
  35. package/scripts/gds/gen-repo-map.js +13 -14
  36. package/scripts/gds/gen-session-index.js +8 -8
  37. package/scripts/gds/leak-scan-allowlist.js +1 -1
  38. package/scripts/gds/plain-cards.js +1 -1
  39. package/scripts/gds/push-path-brief.js +10 -10
  40. package/scripts/gds/rename-history-check.js +1 -1
  41. package/scripts/gds/session-digest-build.js +2 -2
  42. package/scripts/gds/ship-deploy-target.js +13 -13
  43. package/scripts/gds/ship-flow.js +1 -1
  44. package/scripts/gds/ship-land.js +6 -6
  45. package/scripts/gds/ship-merge.js +4 -4
  46. package/scripts/gds/ship-preflight-steps.js +4 -4
  47. package/scripts/gds/ship-regen.js +9 -12
  48. package/scripts/gds/ship.js +9 -9
  49. package/scripts/gds/skill-preflight.js +9 -8
  50. package/src/bongos/api-prefix.js +4 -4
  51. package/src/bongos/platform-visibility-gate.js +1 -1
  52. package/src/bongos/routes/me.js +1 -1
  53. package/src/bongos/routes/security.js +3 -3
  54. package/src/bongos/routes.js +2 -2
  55. package/src/bongos/serve-internal.js +5 -6
  56. package/src/bongos-downloads.js +1 -6
  57. package/src/branding.js +2 -2
  58. package/src/build-info.js +4 -6
  59. package/src/instance-config.js +2 -2
  60. package/src/module-api.js +3 -3
  61. package/src/module-loader/manifest-schema.js +1 -1
  62. package/tests/api_alias_caller_ratchet.mjs +1 -1
  63. package/tests/api_alias_exceptions.mjs +1 -1
  64. package/tests/api_client.mjs +1 -1
  65. package/tests/api_path_404.mjs +1 -1
  66. package/tests/backfill_task_rewards.mjs +1 -1
  67. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  68. package/tests/cli_sessions.mjs +1 -1
  69. package/tests/context_pack.mjs +13 -13
  70. package/tests/copy_inventory.mjs +7 -7
  71. package/tests/credit_grant.mjs +2 -2
  72. package/tests/criterion_suggest.mjs +1 -1
  73. package/tests/design_tokens_sync.mjs +3 -2
  74. package/tests/fitness_ratchets.mjs +2 -2
  75. package/tests/github_push_land_proof.mjs +1 -1
  76. package/tests/goal_advisory.mjs +3 -3
  77. package/tests/goal_suggest.mjs +3 -3
  78. package/tests/government_abuse_matrix.mjs +4 -3
  79. package/tests/hall_audit.mjs +1 -1
  80. package/tests/helpers.mjs +1 -1
  81. package/tests/host_topology_skips.mjs +2 -2
  82. package/tests/migration_namespace.mjs +6 -6
  83. package/tests/module_api.mjs +2 -2
  84. package/tests/module_cli.mjs +5 -5
  85. package/tests/module_manifest.mjs +13 -13
  86. package/tests/module_route_rank.mjs +2 -2
  87. package/tests/modules_route_wiring.mjs +1 -1
  88. package/tests/onboarding_route_signals.mjs +1 -1
  89. package/tests/paste_token_session_store.mjs +1 -1
  90. package/tests/platform_visibility_gate.mjs +1 -1
  91. package/tests/project_modules_ui.mjs +17 -17
  92. package/tests/projects_hub_module_picker.mjs +75 -74
  93. package/tests/provisioning_capacity.mjs +1 -1
  94. package/tests/provisioning_recommendations.mjs +2 -1
  95. package/tests/publish_branch_route.mjs +1 -1
  96. package/tests/publish_manifest.mjs +1 -1
  97. package/tests/publish_reconciler.mjs +8 -8
  98. package/tests/push_path_brief.mjs +10 -9
  99. package/tests/regrade_eligibility.mjs +1 -1
  100. package/tests/rename_history_restraint.mjs +1 -1
  101. package/tests/repo_map.mjs +9 -9
  102. package/tests/seam_wiring_guard.mjs +11 -11
  103. package/tests/search_isolation.mjs +3 -3
  104. package/tests/session_records.mjs +1 -1
  105. package/tests/session_rename_fallback.mjs +1 -1
  106. package/tests/session_start_freshness.mjs +3 -3
  107. package/tests/session_token_hash_db.mjs +1 -1
  108. package/tests/ship_card.mjs +1 -1
  109. package/tests/ship_ci_deploy.mjs +17 -17
  110. package/tests/ship_premerge.mjs +1 -1
  111. package/tests/ship_resume.mjs +1 -1
  112. package/tests/skill_preflight.mjs +2 -2
  113. package/tests/skip_is_not_pass.mjs +1 -1
  114. package/tests/terms_acceptance.mjs +1 -1
@@ -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)/);
@@ -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
  });
@@ -1,11 +1,12 @@
1
1
  // tests/push_path_brief.mjs
2
2
  //
3
- // Task 1003259 / idea 1000838 — a dev-box agent, unable to see WHY its git probes were
4
- // failing, asked the owner to mint a PAT and store it on the box. push-path-brief.js is
5
- // the SessionStart line that states the credential model natively so that request never
6
- // gets made again. These tests pin BOTH halves: the line fires on the server-publish
7
- // path with the load-bearing phrases intact, and it is SILENT everywhere else — the
8
- // silence is half the design (a line firing on every healthy laptop becomes wallpaper).
3
+ // Task 1003259 / idea 1000838 — an agent on a checkout with no GitHub push credential, unable
4
+ // to see WHY its git probes were failing, asked the owner to mint a PAT and store it on the
5
+ // machine. push-path-brief.js is the SessionStart line that states the credential model
6
+ // natively so that request never gets made again. These tests pin BOTH halves: the line
7
+ // fires on the server-publish path with the load-bearing phrases intact, and it is SILENT
8
+ // everywhere else — the silence is half the design (a line firing on every healthy laptop
9
+ // becomes wallpaper).
9
10
  //
10
11
  // DB-free + network-free: briefLine is pure, and the CLI cases force the path with
11
12
  // OTB_PUSH_VIA_SERVER so pushVia() never spawns gh. Run: node tests/push_path_brief.mjs
@@ -31,14 +32,14 @@ t('server path gets the brief, with every load-bearing phrase', () => {
31
32
  const l = brief.briefLine('server');
32
33
  assert.ok(typeof l === 'string' && l.length > 0, 'expected a line on the server path');
33
34
  // Each phrase is one of the three facts the 2026-08-24 incident showed an agent
34
- // cannot infer from inside a box. Losing any of them re-opens the gap.
35
+ // cannot infer from inside such a checkout. Losing any of them re-opens the gap.
35
36
  for (const must of [
36
37
  'NO GitHub push credential',
37
38
  'BY DESIGN',
38
39
  '/builder-ship',
39
40
  'NEVER ask the owner for a PAT',
40
41
  'publish-status',
41
- // task 1003988: a box that cannot see which test failed re-pushes blind.
42
+ // task 1003988: a session that cannot see which test failed re-pushes blind.
42
43
  'merge_driver.failing_tests',
43
44
  'checks_unreadable',
44
45
  ]) {
@@ -58,7 +59,7 @@ t('undetermined path stays silent (fail-open, exact vocabulary only)', () => {
58
59
  });
59
60
 
60
61
  t('the line is the exported static constant — nothing is interpolated', () => {
61
- // Trust boundary: this string is injected into every box session's context. Pinning
62
+ // Trust boundary: this string is injected into every server-publish session's context. Pinning
62
63
  // it to the constant proves no builder-authored text can ever ride along.
63
64
  assert.equal(brief.briefLine('server'), brief.BRIEF);
64
65
  });
@@ -202,7 +202,7 @@ test('buildGradePayload confidence defaults to null when signals omit it', () =>
202
202
  //
203
203
  // The 'confirmed' answer hardcoded "run /merge-mode", which lands by pushing main
204
204
  // via push-main.js (SEC #863 / ADR 0043) and needs an authenticated gh plus a main
205
- // checkout. A server-mediated builder (pushVia()==='server' — every dev box) has
205
+ // checkout. A server-mediated builder (pushVia()==='server' — a checkout with no GitHub push credential) has
206
206
  // neither, so the advice was unreachable by construction. It cost a real builder
207
207
  // 163 minutes on 2026-08-21. strandNextStep already had the right wording for both
208
208
  // modes; these pin that the two modes now diverge and that neither regresses.
@@ -123,7 +123,7 @@ test('the alias mount passes while present and FAILS when dropped', () => {
123
123
  assert.equal(check.checkAliasMounts({ mounts: ['/api/bongos/v1', '/api/bongos', '/api/gds'], required }).ok, true);
124
124
 
125
125
  const r = check.checkAliasMounts({ mounts: ['/api/bongos/v1', '/api/bongos'], required });
126
- assert.equal(r.ok, false, 'shipped Dev Box binaries call the alias and cannot be force-updated');
126
+ assert.equal(r.ok, false, 'shipped binaries and live instances call the alias and cannot be force-updated');
127
127
  assert.deepEqual(r.missing, ['/api/gds']);
128
128
  });
129
129
 
@@ -94,14 +94,14 @@ test('extractor parses functions, classes, arrow consts, and module.exports', ()
94
94
  assert.ok(apiCall && apiCall.exported && apiCall.kind === 'fn', 'exported arrow/fn const detected with export flag');
95
95
  });
96
96
 
97
- test('--whole-only never writes a TRACKED nested block (the sparse-box deadlock guard, task 1002962)', () => {
98
- // A dev box is a SPARSE checkout — box-access.js materializes only the claim's
99
- // module cone. The block ordering is `b.refs - a.refs || name` and `refs` counts
100
- // references across the VISIBLE corpus, so a box with 7 of 20 modules emits a
101
- // legitimately different block. box-source-fetch.sh used to write it, dirtying
102
- // tracked files, and the NEXT `git pull` aborted on "local changes would be
103
- // overwritten" — a silent permanent sync deadlock (one box sat 257 commits behind
104
- // for three days). A box CONSUMES these blocks; only a full checkout may write them.
97
+ test('--whole-only never writes a TRACKED nested block (the sparse-checkout deadlock guard, task 1002962)', () => {
98
+ // A SPARSE checkout holds only part of the module tree. The block ordering is
99
+ // `b.refs - a.refs || name` and `refs` counts references across the VISIBLE
100
+ // corpus, so a checkout with 7 of 20 modules emits a legitimately different block.
101
+ // A fetch script used to write it, dirtying tracked files, and the NEXT `git pull`
102
+ // aborted on "local changes would be overwritten" — a silent permanent sync
103
+ // deadlock (one checkout sat 257 commits behind for three days). A sparse checkout
104
+ // CONSUMES these blocks; only a full checkout may write them.
105
105
  //
106
106
  // Asserted on CONTENT, not on the log line: dirty a tracked block, run --whole-only,
107
107
  // and require the block to come back byte-identical. A regression that re-enabled the
@@ -116,7 +116,7 @@ test('--whole-only never writes a TRACKED nested block (the sparse-box deadlock
116
116
  assert.equal(
117
117
  readFileSync(tracked, 'utf8'),
118
118
  drifted,
119
- '--whole-only rewrote a tracked nested block — this is the deadlock that stranded a box 257 commits behind',
119
+ '--whole-only rewrote a tracked nested block — this is the deadlock that stranded a checkout 257 commits behind',
120
120
  );
121
121
  assert.ok(!r.stdout.includes('CLAUDE.md'), `--whole-only must not report a nested-doc write; got: ${r.stdout.trim()}`);
122
122
  } finally {