@bongos/core 1.18.18 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.bongos-core.json +515 -195
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +99 -14
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -1
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +124 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -20,6 +20,7 @@ import { strict as assert } from 'node:assert';
20
20
  import { createRequire } from 'node:module';
21
21
  import { spawnSync } from 'node:child_process';
22
22
  import { fileURLToPath } from 'node:url';
23
+ import { skipUnlessPresent } from './helpers.mjs';
23
24
  import fs from 'node:fs';
24
25
  import os from 'node:os';
25
26
  import path from 'node:path';
@@ -28,6 +29,7 @@ const require = createRequire(import.meta.url);
28
29
  const here = path.dirname(fileURLToPath(import.meta.url));
29
30
  const repo = path.resolve(here, '..');
30
31
  const HOOK = path.join(repo, 'infra', 'box-dns-cloudflare.sh');
32
+ skipUnlessPresent('infra/box-dns-cloudflare.sh');
31
33
 
32
34
  // ── a fake `curl` that emulates the Cloudflare API per FAKE_CF_SCENARIO ─────────
33
35
  const FAKE_CURL = `#!/usr/bin/env node
@@ -12,9 +12,11 @@ import { strict as assert } from 'node:assert';
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { skipUnlessPresent } from './helpers.mjs';
15
16
 
16
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
18
  const settingsPath = path.join(__dirname, '..', '.claude', 'settings.json');
19
+ skipUnlessPresent('.claude/settings.json');
18
20
 
19
21
  let passed = 0, failed = 0;
20
22
  function test(name, fn) {
@@ -5,7 +5,7 @@
5
5
  // the WEB TIER fetch it. The old guard (/^https:\/\/[^\s]+$/) accepted ANY https
6
6
  // host, so a builder could make the prod server probe internal/metadata
7
7
  // endpoints (SSRF). isSafeTerminalUrl(url, login) must allow ONLY the two real
8
- // tunnel shapes — `term-<login>.…amazonprimea.com` (bound to the caller's login)
8
+ // tunnel shapes — `term-<login>.<apex>` (bound to the caller's login)
9
9
  // and `*.trycloudflare.com` — and reject everything else.
10
10
  //
11
11
  // Pure function, no server/DB/network.
@@ -25,19 +25,25 @@ const test = (name, fn) => {
25
25
  };
26
26
 
27
27
  const LOGIN = 'lars589';
28
+ // The dev-box apex is INJECTED rather than taken from the live instance branding
29
+ // (task 1002470). Two reasons: the neutral core has no amazonprimea.com apex, so the
30
+ // three allow-cases could not pass; and every reject-case below was passing for the
31
+ // WRONG reason — an apex mismatch, not the rule it names. With a pinned apex,
32
+ // 'rejects another builder's term- host' really does prove the login binding.
33
+ const APEX = 'apex.example';
28
34
 
29
35
  // ---- allowed: the two legitimate tunnel shapes -------------------------------
30
36
  test('allows the named-tunnel apex host bound to the caller login', () => {
31
- assert.equal(isSafeTerminalUrl('https://term-lars589.amazonprimea.com', LOGIN), true);
37
+ assert.equal(isSafeTerminalUrl(`https://term-lars589.${APEX}`, LOGIN, APEX), true);
32
38
  });
33
- test('allows the named-tunnel dev-zone host (term-<login>.dev.amazonprimea.com)', () => {
34
- assert.equal(isSafeTerminalUrl('https://term-lars589.dev.amazonprimea.com/', LOGIN), true);
39
+ test(`allows the named-tunnel dev-zone host (term-<login>.dev.${APEX})`, () => {
40
+ assert.equal(isSafeTerminalUrl(`https://term-lars589.dev.${APEX}/`, LOGIN, APEX), true);
35
41
  });
36
42
  test('allows a Cloudflare quick-tunnel host (*.trycloudflare.com)', () => {
37
- assert.equal(isSafeTerminalUrl('https://random-words-1234.trycloudflare.com', LOGIN), true);
43
+ assert.equal(isSafeTerminalUrl('https://random-words-1234.trycloudflare.com', LOGIN, APEX), true);
38
44
  });
39
45
  test('login match is case-insensitive (DNS is case-insensitive)', () => {
40
- assert.equal(isSafeTerminalUrl('https://term-LarsCode.amazonprimea.com', 'larscode'), true);
46
+ assert.equal(isSafeTerminalUrl(`https://term-LarsCode.${APEX}`, 'larscode', APEX), true);
41
47
  });
42
48
 
43
49
  // ---- rejected: SSRF targets --------------------------------------------------
@@ -54,36 +60,36 @@ for (const bad of [
54
60
  'https://printer.local/',
55
61
  ]) {
56
62
  test(`rejects SSRF target ${bad}`, () => {
57
- assert.equal(isSafeTerminalUrl(bad, LOGIN), false);
63
+ assert.equal(isSafeTerminalUrl(bad, LOGIN, APEX), false);
58
64
  });
59
65
  }
60
66
 
61
67
  // ---- rejected: wrong scheme / shape / cross-login ----------------------------
62
68
  test('rejects http (must be https)', () => {
63
- assert.equal(isSafeTerminalUrl('http://term-lars589.amazonprimea.com', LOGIN), false);
69
+ assert.equal(isSafeTerminalUrl(`http://term-lars589.${APEX}`, LOGIN, APEX), false);
64
70
  });
65
71
  test('rejects embedded credentials (user:pass@host)', () => {
66
- assert.equal(isSafeTerminalUrl('https://user:pass@term-lars589.amazonprimea.com', LOGIN), false);
72
+ assert.equal(isSafeTerminalUrl(`https://user:pass@term-lars589.${APEX}`, LOGIN, APEX), false);
67
73
  });
68
74
  test('rejects another builder\'s term- host (login binding)', () => {
69
- assert.equal(isSafeTerminalUrl('https://term-someoneelse.amazonprimea.com', LOGIN), false);
75
+ assert.equal(isSafeTerminalUrl(`https://term-someoneelse.${APEX}`, LOGIN, APEX), false);
70
76
  });
71
77
  test('rejects a non-tunnel host on the project domain', () => {
72
- assert.equal(isSafeTerminalUrl('https://api.amazonprimea.com', LOGIN), false);
78
+ assert.equal(isSafeTerminalUrl(`https://api.${APEX}`, LOGIN, APEX), false);
73
79
  });
74
80
  test('rejects an arbitrary external host', () => {
75
- assert.equal(isSafeTerminalUrl('https://evil.example.com', LOGIN), false);
81
+ assert.equal(isSafeTerminalUrl('https://evil.example.com', LOGIN, APEX), false);
76
82
  });
77
83
  test('rejects a lookalike suffix (…trycloudflare.com.evil.com)', () => {
78
- assert.equal(isSafeTerminalUrl('https://x.trycloudflare.com.evil.com', LOGIN), false);
84
+ assert.equal(isSafeTerminalUrl('https://x.trycloudflare.com.evil.com', LOGIN, APEX), false);
79
85
  });
80
86
  test('rejects garbage / unparseable', () => {
81
- assert.equal(isSafeTerminalUrl('not a url', LOGIN), false);
82
- assert.equal(isSafeTerminalUrl('', LOGIN), false);
83
- assert.equal(isSafeTerminalUrl(null, LOGIN), false);
87
+ assert.equal(isSafeTerminalUrl('not a url', LOGIN, APEX), false);
88
+ assert.equal(isSafeTerminalUrl('', LOGIN, APEX), false);
89
+ assert.equal(isSafeTerminalUrl(null, LOGIN, APEX), false);
84
90
  });
85
91
  test('rejects when login is empty (cannot bind the term- form)', () => {
86
- assert.equal(isSafeTerminalUrl('https://term-lars589.amazonprimea.com', ''), false);
92
+ assert.equal(isSafeTerminalUrl(`https://term-lars589.${APEX}`, '', APEX), false);
87
93
  });
88
94
 
89
95
  console.log(`\n${pass} passed, ${fail} failed`);
@@ -33,6 +33,7 @@ const text = (r) => r.lines.join('\n');
33
33
  t('never dumps [object Object] for any known code', () => {
34
34
  const codes = [
35
35
  'stage_required', 'session_already_has_active_claim', 'session_claim_check_failed',
36
+ 'session_already_has_bound_claim',
36
37
  'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
37
38
  'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
38
39
  'some_unmapped_future_code',
@@ -96,6 +97,28 @@ t('MODULE_CONFLICT + TOUCHES_CONFLICT: list conflicting task titles', () => {
96
97
  assert.match(tc, /Baz/);
97
98
  });
98
99
 
100
+ // task 1002589: migration 180's one-bound-claim-per-session DB index tripped
101
+ // (claimTask now maps that 23505 by err.constraint instead of collapsing it
102
+ // onto ALREADY_CLAIMED). The honest render: the TARGET task is still free —
103
+ // the SESSION holds a bound claim elsewhere. Never blame a phantom rival.
104
+ t('session_already_has_bound_claim: names the in-flight claim, not "another session"', () => {
105
+ const out = text(formatClaimFailure(
106
+ envelope('session_already_has_bound_claim', {
107
+ active_task_id: 77, active_task_title: 'Held elsewhere', what_to_do: 'ship 77 or claim unbound',
108
+ }), 12));
109
+ assert.match(out, /#77/, 'should name the in-flight task id');
110
+ assert.match(out, /Held elsewhere/, 'should name the in-flight task title');
111
+ assert.match(out, /ship 77 or claim unbound/, 'should relay the server what_to_do');
112
+ assert.match(out, /still free/i, 'should say the target task is not actually taken');
113
+ assert.ok(!/claimed by another session/i.test(out), 'must not blame a phantom builder');
114
+ });
115
+
116
+ t('session_already_has_bound_claim without details still renders honest guidance', () => {
117
+ const out = text(formatClaimFailure(envelope('session_already_has_bound_claim', {}), 12));
118
+ assert.match(out, /bound claim/i);
119
+ assert.match(out, /bind_session|builder-ship|builder-release/, 'fallback should name a way out');
120
+ });
121
+
99
122
  t('unknown code: fallback carries code + message, exit 1', () => {
100
123
  const r = formatClaimFailure(envelope('weird_thing', {}, 'boom', 500), 1);
101
124
  assert.equal(r.exit, 1);
@@ -109,6 +109,9 @@ t('materializeClaude: copies + rewrites skills into a fresh instance dir (no lef
109
109
  });
110
110
 
111
111
  t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companions keep _core-resolve (task 2138)', () => {
112
+ // There is nothing to materialize FROM when the core checkout carries no
113
+ // .claude/ source tree (package-excluded — task 1002470).
114
+ if (!existsSync(join(ROOT, '.claude', 'scheduled-tasks'))) return;
112
115
  const dir = mkdtempSync(join(tmpdir(), 'w3-rt-'));
113
116
  const res = m.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
114
117
  assert.ok(res.scheduledTasks > 0, 'at least one scheduled-task file materialized');
@@ -204,6 +207,7 @@ t('disabledModuleSkills: only skills of DISABLED modules are collected (default
204
207
 
205
208
  // ---- materializeClaude end-to-end: rebrand + skill-exclude (from the REAL repo) --
206
209
  t('materializeClaude: copied hooks carry NO founder origin/config-dir; a disabled-module skill is excluded', () => {
210
+ if (!existsSync(join(ROOT, '.claude', 'hooks'))) return;
207
211
  const dir = mkdtempSync(join(tmpdir(), 'rb-e2e-'));
208
212
  // Target instance: demo-branded, art-pipeline OFF (so its otb-* skills must be dropped).
209
213
  mkdirSync(join(dir, 'config'), { recursive: true });
@@ -9,12 +9,20 @@
9
9
  //
10
10
  // Run: node tests/cli_box_scope.mjs
11
11
 
12
+ // task 1002162: the detector originally compared `res.data.error` to the STRING
13
+ // 'box_scope'. The live server emits the ADR 0116 (R01) envelope, so that field
14
+ // is an OBJECT and the test never matched — both call sites in cli-lib were dead
15
+ // code. The envelope cases below import the server's REAL `envelope()` formatter
16
+ // rather than hand-typing the shape, so if the wire contract changes again this
17
+ // test fails instead of silently drifting back into a no-op.
18
+
12
19
  import { strict as assert } from 'node:assert';
13
20
  import { createRequire } from 'node:module';
14
21
 
15
22
  const require = createRequire(import.meta.url);
16
23
  const lib = require('../scripts/gds/cli-lib.js');
17
24
  const { isBoxScope } = lib;
25
+ const { envelope } = require('../src/bongos/api-errors.js');
18
26
 
19
27
  let passed = 0, failed = 0;
20
28
  function test(name, fn) {
@@ -47,6 +55,38 @@ test('isBoxScope: false on missing/empty data (no crash)', () => {
47
55
  assert.equal(isBoxScope(undefined), false);
48
56
  });
49
57
 
58
+ // ---------------------------------------------------------------------------
59
+ // task 1002162 — the ADR 0116 envelope. THE REGRESSION: these are the shapes the
60
+ // live server actually sends, built by the server's own formatter.
61
+ // ---------------------------------------------------------------------------
62
+
63
+ test('isBoxScope: true for the REAL server envelope (the shape that was missed)', () => {
64
+ assert.equal(isBoxScope({ status: 403, data: envelope('box_scope') }), true);
65
+ });
66
+
67
+ test('isBoxScope: true for an envelope carrying message + details', () => {
68
+ const body = envelope('box_scope', 'box-scoped session', { what_to_do: 're-auth' });
69
+ assert.equal(body.error.code, 'box_scope', 'guard: envelope() still nests under .error.code');
70
+ assert.equal(isBoxScope({ status: 403, data: body }), true);
71
+ });
72
+
73
+ test('isBoxScope: false for a DIFFERENT code in envelope form — must not hijack', () => {
74
+ assert.equal(isBoxScope({ status: 403, data: envelope('not_your_task') }), false);
75
+ assert.equal(isBoxScope({ status: 403, data: envelope('forbidden') }), false);
76
+ assert.equal(isBoxScope({ status: 403, data: envelope('rank_forbidden') }), false);
77
+ });
78
+
79
+ test('isBoxScope: false for the envelope on the WRONG status', () => {
80
+ assert.equal(isBoxScope({ status: 401, data: envelope('box_scope') }), false);
81
+ assert.equal(isBoxScope({ status: 200, data: envelope('box_scope') }), false);
82
+ });
83
+
84
+ test('isBoxScope: no crash on a null/odd error field', () => {
85
+ assert.equal(isBoxScope({ status: 403, data: { error: null } }), false);
86
+ assert.equal(isBoxScope({ status: 403, data: { error: {} } }), false);
87
+ assert.equal(isBoxScope({ status: 403, data: {} }), false);
88
+ });
89
+
50
90
  console.log('');
51
91
  console.log(`cli_box_scope: ${passed} passed, ${failed} failed`);
52
92
  process.exit(failed === 0 ? 0 : 1);
@@ -5,11 +5,15 @@
5
5
  // the customer-0 pack Cloud Bongos itself runs on.
6
6
  import assert from 'node:assert/strict';
7
7
  import { test } from 'node:test';
8
+ import { existsSync, writeFileSync, rmSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
8
11
  import brandingMod from '../src/branding.js';
9
12
  import serveInternal from '../src/bongos/serve-internal.js';
10
13
 
11
- const { loadBranding, clientBranding } = brandingMod;
14
+ const { loadBranding, clientBranding, INSTANCE_PATH } = brandingMod;
12
15
  const { themeMarkup, injectBrandingGlobal } = serveInternal;
16
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
13
17
 
14
18
  // A standalone/vanilla boot has NO instance config — point the loader at a
15
19
  // nonexistent instance path so it resolves the neutral starter alone (the
@@ -65,6 +69,10 @@ test('customer-0: GDS_BRANDING_FILE selects the cloudbongos.com instance pack (t
65
69
  // The live dogfood instance runs from the SAME checkout as OTB; the env knob
66
70
  // points this process at its own pack (deltas over neutral) instead of OTB's
67
71
  // config/branding.json. Proves the brand-switch + the cloudbongos pack content.
72
+ // Both packs are INSTANCE content — the neutral core ships neither, so this
73
+ // asserts one deployment's config (task 1002470). The selector MECHANISM is
74
+ // proven unconditionally by the fixture test below.
75
+ if (!existsSync(join(ROOT, 'config/branding.cloudbongos.json'))) return;
68
76
  const b = loadBranding({ env: { GDS_BRANDING_FILE: 'config/branding.cloudbongos.json' } });
69
77
  assert.equal(b.identity.productName, 'Cloud Bongos'); // from neutral
70
78
  assert.equal(b.currency.label, 'credits'); // from neutral, not "drachmae"
@@ -77,9 +85,31 @@ test('customer-0: GDS_BRANDING_FILE selects the cloudbongos.com instance pack (t
77
85
  assert.equal(b.firstAdmin, 'lars589');
78
86
  });
79
87
 
80
- test('customer-0: no GDS_BRANDING_FILE still resolves OTB (default unchanged)', () => {
88
+ test('customer-0: no GDS_BRANDING_FILE still resolves the instance pack (default unchanged)', () => {
81
89
  // The OTB process (server.js) sets no GDS_BRANDING_FILE → config/branding.json.
90
+ // Same instance-content caveat as above: with no instance pack the correct default
91
+ // IS the neutral starter, which is what the vanilla tests in this file already pin.
92
+ if (!existsSync(INSTANCE_PATH)) return;
82
93
  const b = loadBranding({ env: {} });
83
- assert.equal(b.identity.worldName, 'Amazonprimea');
84
- assert.equal(b.currency.label, 'drachmae');
94
+ assert.notDeepEqual(b.identity, vanilla().identity, 'the instance pack must win over neutral by default');
95
+ });
96
+
97
+ test('GDS_BRANDING_FILE selects a named pack over the default (the selector mechanism)', () => {
98
+ // The half of the two tests above that must hold on EVERY instance, proven against a
99
+ // throwaway fixture instead of one deployment's committed pack (task 1002470).
100
+ const rel = 'config/branding.__fixture-selector-test.json';
101
+ const abs = join(ROOT, rel);
102
+ writeFileSync(abs, JSON.stringify({ identity: { productName: 'Fixture Product' }, currency: { label: 'zorkmids' } }));
103
+ try {
104
+ const picked = loadBranding({ env: { GDS_BRANDING_FILE: rel } });
105
+ assert.equal(picked.identity.productName, 'Fixture Product', 'the named pack was selected');
106
+ assert.equal(picked.currency.label, 'zorkmids', 'its deltas won over neutral');
107
+ // ...and unspecified keys still fall through to the neutral starter (it is a MERGE,
108
+ // not a replace) — so a partial pack can never blank out the platform defaults.
109
+ assert.equal(picked.copy.hallName, vanilla().copy.hallName, 'unspecified keys fall through to neutral');
110
+ // Counterfactual: without the env knob the fixture must NOT be in play.
111
+ assert.notEqual(loadBranding({ env: {} }).currency.label, 'zorkmids', 'the selector is what chose it');
112
+ } finally {
113
+ rmSync(abs, { force: true });
114
+ }
85
115
  });
@@ -10,9 +10,10 @@
10
10
 
11
11
  import { strict as assert } from 'node:assert';
12
12
  import { createRequire } from 'node:module';
13
+ import { requireHostModule } from './helpers.mjs';
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
- const conductor = require('../.claude/hooks/conductor.js');
16
+ const conductor = requireHostModule(require, '../.claude/hooks/conductor.js');
16
17
 
17
18
  let passed = 0;
18
19
  let failed = 0;
@@ -29,6 +29,7 @@ const buildDoneWhenRouter = require('../modules/lifecycle/routes/done-when.js');
29
29
  const { authorizeCriterionCreate } = buildDoneWhenRouter;
30
30
  const { authorizeGoalManagement, isGoalOwnerOrManager } = require('../modules/lifecycle/goal-authz.js');
31
31
  const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
32
+ const catalog = require('../modules/governance/catalog.js');
32
33
 
33
34
  const doneWhenSrc = readFileSync(new URL('../modules/lifecycle/routes/done-when.js', import.meta.url), 'utf8');
34
35
  const { test, summary } = makeRunner();
@@ -239,12 +240,19 @@ await test('the gate reuses the SAME owner|manager predicate as routes/goals.js
239
240
 
240
241
  // ---- route-source + pin contracts (drift catchers) ----
241
242
 
243
+ // BV1.R105 migrated this middleware from requireRank('metic','archon') to the
244
+ // `criterion.create` ATOM. The audience assertion did not weaken — it got stricter:
245
+ // the spelling check now runs THROUGH the catalog, so a floor change on the atom
246
+ // reds this test even though the route source is untouched. (Pinning the atom name
247
+ // alone would let `criterion.create` quietly drop to xenos and still pass.)
242
248
  await test('the route is gated metic+archon and calls the pure gate', () => {
243
249
  assert.match(
244
250
  doneWhenSrc,
245
- /router\.post\('\/versions\/:id\/done-when',\s*auth\.requireBuilder,\s*auth\.requireRank\('metic',\s*'archon'\)/,
246
- 'the middleware must be requireRank(metic, archon)'
251
+ /router\.post\('\/versions\/:id\/done-when',\s*auth\.requireBuilder,\s*auth\.requirePermission\('criterion\.create'\)/,
252
+ 'the middleware must be requirePermission(criterion.create)'
247
253
  );
254
+ assert.equal(catalog.byKey('criterion.create').floor, 'metic',
255
+ 'criterion.create must floor at metic — that floor IS what makes this route metic+archon');
248
256
  assert.match(doneWhenSrc, /authorizeCriterionCreate\(\{/, 'the handler must call the pure gate, not inline the decision');
249
257
  });
250
258
 
@@ -256,13 +264,28 @@ await test('the `// rank:` annotation matches the middleware (gen-api-docs reads
256
264
  );
257
265
  });
258
266
 
259
- await test('unsatisfy stays ARCHON — the backward ratchet is not part of this grant', () => {
267
+ // ADR 0154 made "unsatisfy stays ARCHON" its safety argument for delegating criterion
268
+ // CREATE to Metic: the forward ratchet moved, the backward one did not. ADR 0157 (owner
269
+ // decision, 2026-08-05) overrides that specific carve-out — `criterion.unsatisfy` is
270
+ // neither rank/identity nor one of the four escalation keys, so it delegated with the rest
271
+ // of the operational block.
272
+ //
273
+ // What this test still guards is the part that never depended on the rank: unsatisfy must
274
+ // remain gated on its OWN dedicated atom, distinct from `criterion.satisfy`. If the two
275
+ // were ever merged, confirming a criterion would silently confer the power to un-confirm
276
+ // one — and THAT is the asymmetry ADR 0154 actually cared about.
277
+ await test('unsatisfy is gated on its OWN atom — the backward ratchet is a separate grant', () => {
260
278
  assert.match(
261
279
  doneWhenSrc,
262
- /router\.post\('\/done-when\/:criterionId\/unsatisfy',\s*auth\.requireBuilder,\s*auth\.requireRank\('archon'\)/,
280
+ /router\.post\('\/done-when\/:criterionId\/unsatisfy',\s*auth\.requireBuilder,\s*auth\.requirePermission\('criterion\.unsatisfy'\)/,
263
281
  'un-flipping a confirmed criterion rewrites shipped scope-truth'
264
282
  );
265
- assert.equal(MODULE_EXPECTED_RANKS.lifecycle['POST /done-when/:criterionId/unsatisfy'], 'archon');
283
+ assert.equal(catalog.byKey('criterion.unsatisfy').floor, 'metic',
284
+ 'ADR 0157 delegated the backward ratchet to the working rank');
285
+ assert.notEqual(catalog.byKey('criterion.unsatisfy').key, catalog.byKey('criterion.satisfy').key,
286
+ 'satisfy and unsatisfy must stay separate atoms, whatever their floors');
287
+ assert.equal(MODULE_EXPECTED_RANKS.lifecycle['POST /done-when/:criterionId/unsatisfy'],
288
+ 'perm:criterion.unsatisfy');
266
289
  });
267
290
 
268
291
  await test('the route-rank PIN records metic+archon (fitness reds the build on drift)', () => {
@@ -55,10 +55,19 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
55
55
  db: { database: 'amazonprimea' },
56
56
  envPrefix: 'OTB',
57
57
  });
58
- assert.deepEqual(Object.keys(projected).sort(), ['copy', 'currency', 'identity', 'rankLabels', 'theme']);
58
+ // `reward` joined the projection in task 1002469 (ADR 0146 follow-up) — the resolved
59
+ // reward POLICY, so a builder on a cost-plus-only instance isn't shown a per-task
60
+ // estimate that will never pay. The allow-list was not updated with it, so this guard
61
+ // had been failing since that landed (task 1002470).
62
+ assert.deepEqual(Object.keys(projected).sort(), ['copy', 'currency', 'identity', 'rankLabels', 'reward', 'theme']);
59
63
  for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
60
64
  assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
61
65
  }
66
+ // The new member is itself held to the same small-surface rule — the point of this
67
+ // test is that every projected key is a reviewed one, not merely that it is listed.
68
+ assert.deepEqual(Object.keys(projected.reward).sort(), ['mode', 'perTaskCredit']);
69
+ assert.equal(typeof projected.reward.mode, 'string');
70
+ assert.equal(typeof projected.reward.perTaskCredit, 'boolean');
62
71
  });
63
72
 
64
73
  test('R56: injectBrandingGlobal inserts a synchronous window.__BRANDING__ before </head>', () => {
@@ -14,9 +14,11 @@ import { mkdtempSync, rmSync, existsSync, statSync } from 'node:fs';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { skipUnlessPresent } from './helpers.mjs';
17
18
 
18
19
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
19
20
  const SCRIPT = join(ROOT, 'scripts', 'deploy', 'deploy-instance.sh');
21
+ skipUnlessPresent('scripts/deploy/deploy-instance.sh');
20
22
 
21
23
  let passed = 0;
22
24
  let failed = 0;
@@ -9,10 +9,11 @@
9
9
 
10
10
  import { strict as assert } from 'node:assert';
11
11
  import { createRequire } from 'node:module';
12
+ import { requireHostModule } from './helpers.mjs';
12
13
 
13
14
  const require = createRequire(import.meta.url);
14
15
  const { buildDevboxDownloads } = require('../src/devbox-downloads.js');
15
- const committedConfig = require('../config/devbox-app.json');
16
+ const committedConfig = requireHostModule(require, '../config/devbox-app.json');
16
17
 
17
18
  let passed = 0;
18
19
  let failed = 0;
@@ -1,11 +1,19 @@
1
1
  // tests/display_name.mjs
2
2
  //
3
- // Unit smoke for modules/builder-settings/display-name.js (task 1669) — the pure
4
- // validator behind GET/PATCH /me/display-name (the self-service display-name
5
- // editor). Covers the rules the route enforces: trim, reject empty /
6
- // whitespace-only, the length ceiling, and control-character rejection. The DB
7
- // write + route plumbing are exercised by the prod smoke; here we cover the
8
- // pure logic without touching pg.
3
+ // Unit smoke for the self-service display-name editor (GET/PATCH
4
+ // /me/display-name), in two parts:
5
+ //
6
+ // 1. modules/builder-settings/display-name.js (task 1669) — the pure
7
+ // validator. Trim, reject empty / whitespace-only, the length ceiling, and
8
+ // control-character rejection.
9
+ // 2. The PERSISTENCE guard (task 1002568, ADR 0156) — "your name wins,
10
+ // always". Only covering the validator is why the bug shipped: the save
11
+ // worked and then upsertBuilder, which runs on EVERY sign-in, re-set
12
+ // display_name from the GitHub name. These tests read the REAL SQL both
13
+ // halves emit and check the conflict branch's behaviour, so deleting the
14
+ // guard fails here instead of in production.
15
+ //
16
+ // No pg: the shared pool's query() is stubbed to record the SQL.
9
17
  //
10
18
  // Run: node tests/display_name.mjs
11
19
 
@@ -119,5 +127,86 @@ t('hasControlChar detects control chars and clears normal text', () => {
119
127
  assert.equal(d.hasControlChar(`with${BEL}bell`), true);
120
128
  });
121
129
 
130
+ // ---------------------------------------------------------------------------
131
+ // Persistence: a user-set name survives the next sign-in (task 1002568)
132
+ // ---------------------------------------------------------------------------
133
+
134
+ // Stub the SHARED pool object before requiring anything that queries it — both
135
+ // db-kernel.js and the module's db.js capture this exact singleton at load.
136
+ const { pool } = require('../src/bongos/pool.js');
137
+ let lastSql = null;
138
+ pool.query = async (sql) => { lastSql = sql; return { rows: [{}] }; };
139
+
140
+ const kernel = require('../src/bongos/db-kernel.js');
141
+ const settingsDb = require('../modules/builder-settings/db.js');
142
+
143
+ // Pull one assignment out of an ON CONFLICT ... DO UPDATE SET clause. None of
144
+ // the assignments contain a comma, so splitting on commas is exact.
145
+ function conflictAssignment(sql, column) {
146
+ const setClause = sql.slice(sql.indexOf('DO UPDATE'), sql.indexOf('RETURNING'));
147
+ for (const part of setClause.replace(/^[\s\S]*?SET/, '').split(',')) {
148
+ const [lhs, ...rhs] = part.split('=');
149
+ if (lhs.trim() === column) return rhs.join('=').trim();
150
+ }
151
+ throw new Error(`no "${column} = …" assignment in the conflict branch`);
152
+ }
153
+
154
+ // Evaluate the display_name assignment for one row, so the tests below read as
155
+ // behaviour ("whose name ends up stored?") rather than string matching. An
156
+ // unguarded assignment — the shape that shipped the bug — is a bare column
157
+ // reference and evaluates to whatever GitHub just sent.
158
+ function resolvedDisplayName(expr, { source, stored, incoming }) {
159
+ const ref = (token) => {
160
+ if (/^EXCLUDED\.display_name$/i.test(token)) return incoming;
161
+ if (/^builders\.display_name$/i.test(token)) return stored;
162
+ throw new Error(`unrecognised display_name reference: ${token}`);
163
+ };
164
+ const norm = expr.replace(/\s+/g, ' ').trim();
165
+ const m = /^CASE WHEN builders\.display_name_source = '(\w+)' THEN (\S+) ELSE (\S+) END$/i.exec(norm);
166
+ if (!m) return ref(norm);
167
+ return source === m[1] ? ref(m[2]) : ref(m[3]);
168
+ }
169
+
170
+ // Drive both halves once (they're async), then assert on the captured SQL with
171
+ // the same synchronous `t` helper the validator tests use.
172
+ await kernel.upsertBuilder({
173
+ githubId: 64553469, githubLogin: 'jaxri', displayName: 'GitHub Name', avatarUrl: null,
174
+ });
175
+ const upsertExpr = conflictAssignment(lastSql, 'display_name');
176
+
177
+ await settingsDb.setBuilderDisplayName(1000095, 'Renée the Builder');
178
+ const setNameSql = lastSql.replace(/\s+/g, ' ');
179
+
180
+ console.log('\nsign-in persistence (upsertBuilder conflict branch):');
181
+
182
+ t('the conflict branch keys display_name off display_name_source', () => {
183
+ assert.ok(
184
+ /display_name_source/.test(upsertExpr),
185
+ `upsertBuilder must not re-set display_name unconditionally; got: ${upsertExpr}`
186
+ );
187
+ });
188
+
189
+ t('a builder who set their own name keeps it when GitHub sends a different one', () => {
190
+ const kept = resolvedDisplayName(upsertExpr, {
191
+ source: 'user', stored: 'Renée the Builder', incoming: 'renee-gh',
192
+ });
193
+ assert.equal(kept, 'Renée the Builder');
194
+ });
195
+
196
+ t('a builder who never renamed still picks up a GitHub rename', () => {
197
+ const synced = resolvedDisplayName(upsertExpr, {
198
+ source: 'github', stored: 'Old GitHub Name', incoming: 'New GitHub Name',
199
+ });
200
+ assert.equal(synced, 'New GitHub Name');
201
+ });
202
+
203
+ t('setBuilderDisplayName stamps the name as user-set', () => {
204
+ assert.match(
205
+ setNameSql,
206
+ /display_name_source = 'user'/,
207
+ 'without the stamp the saved name reverts at the next sign-in'
208
+ );
209
+ });
210
+
122
211
  console.log(`\n${passed} passed, ${failed} failed`);
123
212
  process.exit(failed === 0 ? 0 : 1);
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { strict as assert } from 'node:assert';
6
6
  import { createRequire } from 'node:module';
7
+ import { requireHostModule } from './helpers.mjs';
7
8
  const require = createRequire(import.meta.url);
8
- const { classifyEviction, estTokens, blockText, stubFor, normalize, readRange, isVolatileBash, salientTokens } = require('../.claude/hooks/eviction-policy.js');
9
+ const { classifyEviction, estTokens, blockText, stubFor, normalize, readRange, isVolatileBash, salientTokens } = requireHostModule(require, '../.claude/hooks/eviction-policy.js');
9
10
 
10
11
  let passed = 0, failed = 0;
11
12
  function test(name, fn) {
@@ -31,7 +31,12 @@ test('committed file-map sections are current (the Path-C CI gate)', () => {
31
31
  test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no silent omission, no ghost', () => {
32
32
  const model = gen.buildModel();
33
33
  assert.ok(model.length === 2, 'two generated sections');
34
+ let checked = 0;
34
35
  for (const section of model) {
36
+ // .claude/skills + .claude/scheduled-tasks are INSTANCE-materialized; a neutral
37
+ // core checkout carries neither (task 1002470). Check the sections that exist.
38
+ if (!existsSync(path.join(ROOT, section.dir))) continue;
39
+ checked += 1;
35
40
  const onDisk = readdirSync(path.join(ROOT, section.dir), { withFileTypes: true })
36
41
  .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
37
42
  .map((e) => e.name)
@@ -41,6 +46,11 @@ test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no s
41
46
  assert.equal(section.missing.length, 0, `${section.dir}: dirs without a note: ${section.missing.join(', ')}`);
42
47
  assert.equal(section.stale.length, 0, `${section.dir}: notes with no dir (ghosts): ${section.stale.join(', ')}`);
43
48
  }
49
+ // Non-vacuity: on an instance that materializes .claude/, this must have checked
50
+ // something — a skipped-everything run would otherwise pass while asserting nothing.
51
+ if (existsSync(path.join(ROOT, '.claude'))) {
52
+ assert.ok(checked > 0, '.claude/ exists but no section was checked — the exhaustiveness guard went vacuous');
53
+ }
44
54
  });
45
55
 
46
56
  test('the generated blocks live between markers and carry one ← note per entry', () => {
@@ -6,16 +6,24 @@
6
6
  import assert from 'node:assert/strict';
7
7
  import { test } from 'node:test';
8
8
  import auth from '../src/bongos/auth.js';
9
+ import brandingMod from '../src/branding.js';
9
10
 
10
11
  const { firstAdminLogin, maybeSeatFirstAdmin } = auth;
12
+ const { branding } = brandingMod;
11
13
 
12
14
  function clearEnv() {
13
15
  for (const k of ['OTB_FIRST_ADMIN', 'GDS_FIRST_ADMIN', 'PMS_FIRST_ADMIN', 'CLOUDBONGOS_FIRST_ADMIN']) delete process.env[k];
14
16
  }
15
17
 
16
- test('firstAdminLogin: resolves from branding (OTB pack → lars589) when no env override', () => {
18
+ test('firstAdminLogin: resolves from the branding pack when no env override', () => {
19
+ // Asserts the RESOLUTION, not one instance's owner. This used to hardcode the OTB
20
+ // founder's login, which both broke on any other instance (the neutral pack sets
21
+ // firstAdmin: "") and put a founder identity in a test — the thing
22
+ // scripts/gds/audit-authorship.sh exists to keep out (task 1002470).
17
23
  clearEnv();
18
- assert.equal(firstAdminLogin(), 'lars589');
24
+ const configured = (branding().firstAdmin || '').trim() || null;
25
+ assert.equal(firstAdminLogin(), configured,
26
+ 'with no env override, firstAdminLogin must be exactly what the branding pack configures');
19
27
  });
20
28
 
21
29
  test('firstAdminLogin: env override wins over branding (configured prefix)', () => {
@@ -10,12 +10,13 @@
10
10
 
11
11
  import { strict as assert } from 'node:assert';
12
12
  import { createRequire } from 'node:module';
13
+ import { requireHostModule } from './helpers.mjs';
13
14
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
14
15
  import { tmpdir } from 'node:os';
15
16
  import { join } from 'node:path';
16
17
 
17
18
  const require = createRequire(import.meta.url);
18
- const { firstRunEngineCheck } = require('../.claude/hooks/session-start.js');
19
+ const { firstRunEngineCheck } = requireHostModule(require, '../.claude/hooks/session-start.js');
19
20
 
20
21
  let passed = 0, failed = 0;
21
22
  function t(name, fn) {