@bongos/core 1.18.19 → 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.bongos-core.json +547 -197
  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/bin/bongos.js +33 -7
  9. package/clients/bongos-client/README.md +1 -1
  10. package/clients/bongos-client/bongos-client.global.js +63 -41
  11. package/clients/bongos-client/index.cjs +63 -41
  12. package/clients/bongos-client/index.d.ts +78 -44
  13. package/clients/bongos-client/index.mjs +63 -41
  14. package/docs/adr/0020-security-model-public-stub.md +1 -1
  15. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  16. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
  17. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  18. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  19. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  20. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  21. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  22. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  23. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  24. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  25. package/docs/adr/0161-publish-on-merge.md +34 -0
  26. package/docs/adr/README.md +9 -0
  27. package/docs/api/openapi.json +1209 -233
  28. package/docs/api-reference.md +61 -46
  29. package/docs/architecture.md +1 -1
  30. package/docs/canonical-permissions.md +61 -16
  31. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  32. package/docs/file-map.md +4 -1
  33. package/docs/onboarding/diagrams/assertions.json +3 -3
  34. package/docs/recipes/core-release-pipeline.md +181 -0
  35. package/docs/recipes/packaging-the-core.md +2 -0
  36. package/docs/recipes/parallel-goal-waves.md +1 -3
  37. package/docs/recipes/private-npm-distribution.md +6 -2
  38. package/docs/recipes/upgrading-the-core.md +2 -0
  39. package/migrations/core_197_display_name_source.sql +56 -0
  40. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  41. package/migrations/core_205_versions_track_default.sql +28 -0
  42. package/modules/autonomy/routes/autonomy.js +1 -1
  43. package/modules/builder-settings/db.js +7 -1
  44. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  45. package/modules/dev-box/routes/box.js +11 -9
  46. package/modules/discord/auth.js +3 -1
  47. package/modules/discord/routes/discord.js +8 -2
  48. package/modules/discord/ship-broadcast.js +48 -1
  49. package/modules/governance/catalog.js +70 -20
  50. package/modules/governance/db.js +102 -0
  51. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  52. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  53. package/modules/governance/routes/governance.js +235 -6
  54. package/modules/grading/grader-workers/index.js +18 -9
  55. package/modules/hall-ui/public/gate.js +6 -2
  56. package/modules/hall-ui/public/goals-page.js +6 -2
  57. package/modules/hall-ui/public/governance.css +100 -0
  58. package/modules/hall-ui/public/governance.html +77 -0
  59. package/modules/hall-ui/public/governance.js +353 -0
  60. package/modules/hall-ui/public/harbor.js +14 -8
  61. package/modules/hall-ui/public/instances-panel.js +3 -1
  62. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  63. package/modules/hall-ui/public/people.html +51 -0
  64. package/modules/hall-ui/public/people.js +100 -0
  65. package/modules/hall-ui/public/profile.html +2 -1
  66. package/modules/hall-ui/public/profile.js +9 -3
  67. package/modules/hall-ui/public/sessions.js +4 -2
  68. package/modules/hall-ui/public/shell.js +28 -7
  69. package/modules/hall-ui/public/task-detail.js +6 -3
  70. package/modules/hall-ui/public/watch.js +17 -9
  71. package/modules/ideas/blockers.js +44 -7
  72. package/modules/ideas/routes/blockers.js +12 -7
  73. package/modules/ideas/routes/inbox.js +2 -2
  74. package/modules/lifecycle/db.js +84 -17
  75. package/modules/lifecycle/github-push.js +37 -10
  76. package/modules/lifecycle/publish-reconciler.js +128 -31
  77. package/modules/lifecycle/routes/claims.js +10 -4
  78. package/modules/lifecycle/routes/dependencies.js +2 -2
  79. package/modules/lifecycle/routes/done-when.js +4 -4
  80. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  81. package/modules/lifecycle/routes/goals.js +5 -5
  82. package/modules/lifecycle/routes/tasks.js +99 -69
  83. package/modules/lifecycle/routes/versions.js +13 -2
  84. package/modules/lifecycle/ship-card.js +38 -7
  85. package/modules/memory/routes/memory.js +2 -2
  86. package/modules/onboarding/routes/access-requests.js +3 -3
  87. package/modules/platform-identity/routes/projects.js +10 -6
  88. package/modules/platform-identity/routes/scouting.js +2 -2
  89. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  90. package/modules/provisioning/provisioning.js +24 -6
  91. package/modules/provisioning/routes/provisioning.js +24 -6
  92. package/modules/security/artifact-scan/aggregate.js +133 -0
  93. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  94. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  95. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  96. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  97. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  98. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  99. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  100. package/modules/security/artifact-scan/detectors/network.js +67 -0
  101. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  102. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  103. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  104. package/modules/security/artifact-scan/fence-local.js +99 -0
  105. package/modules/security/artifact-scan/fetch.js +330 -0
  106. package/modules/security/artifact-scan/floor.js +319 -0
  107. package/modules/security/artifact-scan/limits.js +66 -0
  108. package/modules/security/artifact-scan/manifest.js +203 -0
  109. package/modules/security/artifact-scan/mirror.js +158 -0
  110. package/modules/security/artifact-scan/quarantine.js +149 -0
  111. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  112. package/modules/security/artifact-scan/report.js +155 -0
  113. package/modules/security/artifact-scan/sanitize.js +317 -0
  114. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  115. package/modules/security/module.json +2 -2
  116. package/modules/security/routes/reports.js +11 -7
  117. package/modules/security/routes/scans.js +115 -0
  118. package/modules/security/scan-store.js +214 -0
  119. package/modules/security/tests/aggregate.mjs +283 -0
  120. package/modules/security/tests/artifact_scan.mjs +591 -0
  121. package/modules/security/tests/floor.mjs +549 -0
  122. package/modules/security/tests/mirror.mjs +350 -0
  123. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  124. package/modules/security/tests/scan_store.mjs +250 -0
  125. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  126. package/modules/sessions/routes/sessions.js +6 -5
  127. package/package-lock.json +2 -2
  128. package/package.json +1 -1
  129. package/public-landing/create.html +13 -3
  130. package/scripts/gds/abandon.js +1 -1
  131. package/scripts/gds/audit-authorship.sh +6 -0
  132. package/scripts/gds/box.js +1 -1
  133. package/scripts/gds/bump-version.js +76 -0
  134. package/scripts/gds/claim.js +15 -0
  135. package/scripts/gds/cli-lib.js +16 -4
  136. package/scripts/gds/gating.js +28 -13
  137. package/scripts/gds/gen-api-docs.js +1 -1
  138. package/scripts/gds/goal.js +234 -0
  139. package/scripts/gds/init.js +77 -15
  140. package/scripts/gds/main-audit.js +80 -11
  141. package/scripts/gds/mirror-redact.js +6 -0
  142. package/scripts/gds/newcomer-floor.js +2 -2
  143. package/scripts/gds/provision.js +230 -94
  144. package/scripts/gds/run-unit-tests.js +9 -1
  145. package/scripts/gds/sandbox-stage.js +66 -9
  146. package/scripts/gds/search-ingest-md.js +1 -0
  147. package/scripts/gds/sequence.js +67 -8
  148. package/scripts/gds/ship.js +183 -50
  149. package/scripts/hall-preview/server.js +1 -1
  150. package/scripts/migrate.sh +5 -5
  151. package/src/bongos/auth.js +129 -27
  152. package/src/bongos/db-kernel.js +66 -5
  153. package/src/bongos/db.js +6 -7
  154. package/src/bongos/github-repos.js +19 -7
  155. package/src/bongos/goal-scope-check.js +1 -1
  156. package/src/bongos/module-scope-map.js +3 -1
  157. package/src/bongos/permission-path-check.js +1 -1
  158. package/src/bongos/route-rank-check.js +73 -30
  159. package/src/bongos/routes/audit-log.js +4 -2
  160. package/src/bongos/routes/auth.js +8 -4
  161. package/src/bongos/routes/backup.js +7 -4
  162. package/src/bongos/routes/builders.js +23 -11
  163. package/src/bongos/routes/github.js +15 -4
  164. package/src/bongos/routes/llm-cache.js +5 -2
  165. package/src/bongos/routes/modules.js +9 -5
  166. package/src/bongos/routes/security.js +21 -12
  167. package/src/bongos/serve-internal.js +22 -7
  168. package/src/module-api.js +96 -13
  169. package/src/platform-server.js +14 -0
  170. package/tests/api_client.mjs +18 -2
  171. package/tests/api_path_404.mjs +3 -0
  172. package/tests/art_stage.mjs +2 -1
  173. package/tests/atlas.mjs +32 -4
  174. package/tests/auth_admission_gate.mjs +17 -5
  175. package/tests/auth_error_brand.mjs +41 -21
  176. package/tests/auth_resolve_failure.mjs +155 -0
  177. package/tests/blocker_link.mjs +88 -0
  178. package/tests/bongos_cli.mjs +91 -0
  179. package/tests/bongos_downloads.mjs +2 -1
  180. package/tests/box_dns_repoint.mjs +2 -0
  181. package/tests/box_ship_permissions.mjs +2 -0
  182. package/tests/box_terminal_ssrf.mjs +23 -17
  183. package/tests/bump_version.mjs +71 -0
  184. package/tests/claim_error_surface.mjs +23 -0
  185. package/tests/claude_materialize.mjs +4 -0
  186. package/tests/cli_box_scope.mjs +40 -0
  187. package/tests/cloud_bongos_pack.mjs +34 -4
  188. package/tests/conductor.mjs +2 -1
  189. package/tests/criterion_authoring_gate.mjs +28 -5
  190. package/tests/currency_label.mjs +10 -1
  191. package/tests/deploy_instance.mjs +2 -0
  192. package/tests/devbox_downloads.mjs +2 -1
  193. package/tests/display_name.mjs +95 -6
  194. package/tests/eviction_policy.mjs +2 -1
  195. package/tests/file_map.mjs +10 -0
  196. package/tests/first_admin_bootstrap.mjs +10 -2
  197. package/tests/first_run_engine_check.mjs +2 -1
  198. package/tests/fitness.mjs +26 -6
  199. package/tests/gate_approvals.mjs +15 -2
  200. package/tests/github_push_land_proof.mjs +50 -4
  201. package/tests/github_repos_helpers.mjs +30 -5
  202. package/tests/github_token_store.mjs +23 -0
  203. package/tests/goal_abuse_matrix.mjs +13 -3
  204. package/tests/goal_cli.mjs +89 -0
  205. package/tests/governance_abuse_matrix.mjs +386 -0
  206. package/tests/governance_fold.mjs +156 -0
  207. package/tests/governance_fold_bench.mjs +50 -0
  208. package/tests/governance_live_proof.mjs +201 -0
  209. package/tests/governance_page_gates.mjs +21 -9
  210. package/tests/governance_parity.mjs +11 -2
  211. package/tests/governance_require_permission.mjs +125 -1
  212. package/tests/governance_routes.mjs +334 -0
  213. package/tests/grader.mjs +7 -3
  214. package/tests/hall_nav.mjs +14 -4
  215. package/tests/helpers.mjs +73 -0
  216. package/tests/hook_core_resolve.mjs +2 -1
  217. package/tests/host_topology_skips.mjs +97 -0
  218. package/tests/infra_exec_paths.mjs +14 -4
  219. package/tests/init.mjs +147 -0
  220. package/tests/known_failing_ratchet.mjs +89 -0
  221. package/tests/lifecycle-card.mjs +2 -1
  222. package/tests/main_audit.mjs +72 -1
  223. package/tests/memory_isolation.mjs +56 -9
  224. package/tests/migrate_root_resolution.mjs +1 -1
  225. package/tests/mirror_redact.mjs +19 -0
  226. package/tests/module_api.mjs +3 -1
  227. package/tests/module_loader.mjs +4 -0
  228. package/tests/modules.mjs +35 -7
  229. package/tests/modules_route.mjs +29 -5
  230. package/tests/onboarding_observability.mjs +49 -11
  231. package/tests/onboarding_route_signals.mjs +1 -0
  232. package/tests/one_active_claim_per_session.mjs +80 -0
  233. package/tests/patch_gate_ordering.mjs +102 -0
  234. package/tests/pre_compact.mjs +2 -1
  235. package/tests/preview_server.mjs +2 -1
  236. package/tests/profile_route.mjs +1 -0
  237. package/tests/provision.mjs +170 -0
  238. package/tests/provisioning_onboard_mode.mjs +180 -0
  239. package/tests/publish_manifest.mjs +16 -3
  240. package/tests/publish_reconciler.mjs +181 -10
  241. package/tests/rank_tier_single_source.mjs +205 -0
  242. package/tests/recall_hints.mjs +2 -1
  243. package/tests/repo_map.mjs +4 -0
  244. package/tests/route_permission_check.mjs +14 -5
  245. package/tests/rubric_fixtures.mjs +3 -0
  246. package/tests/run_routine_two_root.mjs +2 -0
  247. package/tests/sandbox_stage.mjs +69 -7
  248. package/tests/search_ingest.mjs +7 -0
  249. package/tests/security_docs.mjs +28 -6
  250. package/tests/sequence.mjs +64 -0
  251. package/tests/session-cards.mjs +2 -1
  252. package/tests/session_start_freshness.mjs +2 -1
  253. package/tests/ship_broadcast_server.mjs +63 -1
  254. package/tests/ship_card.mjs +66 -0
  255. package/tests/ship_ci_deploy.mjs +66 -1
  256. package/tests/ship_smoke_gate.mjs +76 -0
  257. package/tests/versions_route.mjs +15 -0
  258. package/tests/workflow_exec_paths.mjs +16 -4
  259. package/tests/worktree_guard.mjs +2 -1
@@ -0,0 +1,205 @@
1
+ // tests/rank_tier_single_source.mjs — the rank-tier single-source guard (BV1.R108).
2
+ //
3
+ // The canonical rank → tier map is db-kernel.RANK_TIER, DERIVED from
4
+ // LIVE_RANK_LADDER and exposed on the doorway (api.RANK_TIER). This guard makes
5
+ // the collapse hold three ways:
6
+ //
7
+ // 1. IMPORTABLE code holds the SAME OBJECT, not a copy — modules/lifecycle/db.js
8
+ // (the claim-floor brain) and the doorway getter are Object.is-identical to
9
+ // the kernel map.
10
+ // 2. Every DELIBERATELY-LOCAL copy is pinned byte-equivalent. These exist for
11
+ // documented reasons and may not be force-merged (the D1 decision,
12
+ // modules/discord/discord-channels.js): dependency-free substrate
13
+ // (permission-path-check runs in a bare git hook), source-parsed literals
14
+ // (src/bongos/db.js — gate-review.js + diagram-facts.js regex its text),
15
+ // standalone CLIs (sequence, box-connect-lib), the require-free dev-box
16
+ // module template (boxes.js), zero-dep catalog + its text-parsing auditor
17
+ // (route-rank-check), and the hall browser statics.
18
+ // 3. NO NEW parallel map can land: a repo scan flags any rank→number map or
19
+ // rank-ladder array literal outside the allowlist below, both directions —
20
+ // an unlisted literal fails, and a stale allowlist entry fails too.
21
+ //
22
+ // Run: node tests/rank_tier_single_source.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { test } from 'node:test';
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ import { execFileSync } from 'node:child_process';
29
+ import { createRequire } from 'node:module';
30
+ import { fileURLToPath } from 'node:url';
31
+
32
+ const require = createRequire(import.meta.url);
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const kernel = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
35
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
36
+ const coreDb = require(path.join(ROOT, 'src', 'bongos', 'db.js'));
37
+ const lifecycleDb = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
38
+ const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
39
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
40
+ const sequence = require(path.join(ROOT, 'scripts', 'gds', 'sequence.js'));
41
+ const boxConnect = require(path.join(ROOT, 'scripts', 'gds', 'box-connect-lib.js'));
42
+ const boxes = require(path.join(ROOT, 'modules', 'dev-box', 'boxes.js'));
43
+ const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
44
+
45
+ const LADDER = kernel.LIVE_RANK_LADDER;
46
+ const TIER = kernel.RANK_TIER;
47
+ const RANK_KEYS = ['xenos', 'thetes', 'metic', 'archon'];
48
+
49
+ // ===========================================================================
50
+ // 1 — the canonical derivation
51
+ // ===========================================================================
52
+
53
+ test('RANK_TIER is derived from LIVE_RANK_LADDER (1-based) and frozen', () => {
54
+ assert.deepEqual(LADDER, RANK_KEYS, 'the live ladder itself changed — update this guard deliberately');
55
+ assert.ok(Object.isFrozen(TIER));
56
+ assert.deepEqual(TIER, Object.fromEntries(LADDER.map((r, i) => [r, i + 1])));
57
+ });
58
+
59
+ test('the doorway and the lifecycle brain hold the SAME object, not copies', () => {
60
+ assert.ok(Object.is(api.RANK_TIER, TIER), 'api.RANK_TIER must be the kernel map itself');
61
+ assert.ok(Object.is(api.LIVE_RANK_LADDER, LADDER));
62
+ assert.ok(Object.is(lifecycleDb.REQUIRES_RANK_TIER, TIER),
63
+ 'modules/lifecycle/db.js must derive from the doorway, never hand-copy');
64
+ });
65
+
66
+ // ===========================================================================
67
+ // 2 — every deliberately-local copy is pinned
68
+ // ===========================================================================
69
+
70
+ test('src/bongos/db.js literal (the source-parsed brain) matches the kernel map', () => {
71
+ assert.deepEqual(coreDb.REQUIRES_RANK_TIER, TIER);
72
+ // The literal must stay PARSEABLE TEXT — gate-review.js's root-of-trust region
73
+ // watch and diagram-facts.js both regex it out of the source.
74
+ const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'db.js'), 'utf8');
75
+ const m = /REQUIRES_RANK_TIER\s*=\s*\{([^}]*)\}/.exec(src);
76
+ assert.ok(m, 'db.js must keep an inline REQUIRES_RANK_TIER object literal');
77
+ assert.deepEqual(parsePairs(m[1]), TIER, 'the db.js literal text drifted from the kernel map');
78
+ });
79
+
80
+ test('1-based local copies match: permission-path-check, sequence CLI', () => {
81
+ assert.deepEqual(ppc.RANK_TIER, TIER, 'permission-path-check drifted — the #863 failure class');
82
+ assert.deepEqual(sequence.RANK_TIER, TIER, 'sequence.js CLI copy drifted');
83
+ });
84
+
85
+ test('0-based local copies match: box-connect-lib CLI, dev-box boxes.js', () => {
86
+ for (const [name, map] of [['box-connect-lib', boxConnect.RANK_ORDER], ['boxes', boxes.RANK_ORDER]]) {
87
+ assert.deepEqual(Object.keys(map).sort(), [...RANK_KEYS].sort(), `${name} keys drifted`);
88
+ for (const r of RANK_KEYS) {
89
+ assert.equal(map[r], TIER[r] - 1, `${name}.RANK_ORDER.${r} must be tier-1 (0-based)`);
90
+ }
91
+ }
92
+ });
93
+
94
+ test('ladder mirrors match: governance catalog, route-rank-check', () => {
95
+ assert.deepEqual([...catalog.RANK_ORDER], [...LADDER]);
96
+ assert.deepEqual([...rrc.PERMISSION_RANK_ORDER], [...LADDER]);
97
+ });
98
+
99
+ // Files that cannot require server code (browser statics, dependency-free
100
+ // module CLIs) — their literal must be EXACTLY the canonical form. The loose
101
+ // scanner below flags any rank-ish literal in these files; this exact check is
102
+ // what turns "flagged but allowlisted" into "provably not drifted".
103
+ const EXACT_LADDER_RE = /\[\s*'xenos',\s*'thetes',\s*'metic',\s*'archon'\s*\]/;
104
+ const STATIC_LADDER_FILES = [
105
+ 'modules/discord/discord-channels.js',
106
+ 'modules/memory/search.js',
107
+ 'modules/hall-ui/public/ranks.js',
108
+ 'modules/hall-ui/public/profile.js',
109
+ 'modules/hall-ui/public/watch.js',
110
+ ];
111
+ test('static ladder copies carry the exact canonical ladder', () => {
112
+ for (const rel of STATIC_LADDER_FILES) {
113
+ const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
114
+ assert.ok(EXACT_LADDER_RE.test(src), `${rel}: ladder literal missing or drifted from canonical order`);
115
+ }
116
+ });
117
+
118
+ const STATIC_MAP_FILES = ['modules/hall-ui/public/builders.js', 'modules/hall-ui/public/goals-page.js'];
119
+ test('hall static tier maps match the kernel map', () => {
120
+ for (const rel of STATIC_MAP_FILES) {
121
+ const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
122
+ const maps = extractRankMaps(src);
123
+ assert.ok(maps.length > 0, `${rel}: expected a rank tier map`);
124
+ for (const map of maps) assert.deepEqual(map, TIER, `${rel}: tier map drifted`);
125
+ }
126
+ });
127
+
128
+ // ===========================================================================
129
+ // 3 — the reintroduction scanner (both directions vs the allowlist)
130
+ // ===========================================================================
131
+
132
+ // Where a rank→number map literal is allowed to exist (each is pinned above).
133
+ const MAP_ALLOWLIST = new Set([
134
+ 'src/bongos/db.js',
135
+ 'src/bongos/permission-path-check.js',
136
+ 'scripts/gds/sequence.js',
137
+ 'scripts/gds/box-connect-lib.js',
138
+ 'modules/dev-box/boxes.js',
139
+ 'modules/hall-ui/public/builders.js',
140
+ 'modules/hall-ui/public/goals-page.js',
141
+ ]);
142
+ // Where a rank-ladder array literal is allowed to exist.
143
+ const LADDER_ALLOWLIST = new Set([
144
+ 'src/bongos/db-kernel.js', // the canonical source itself
145
+ 'modules/governance/catalog.js',
146
+ 'src/bongos/route-rank-check.js',
147
+ ...STATIC_LADDER_FILES,
148
+ ]);
149
+
150
+ // A rank→digit pair inside an object block. Hex values (RANK_COLORS) never
151
+ // match: \d+ must run straight into , or }.
152
+ const pairRe = (rank) => new RegExp(`(?:^|[{,\\s'"])${rank}['"]?\\s*:\\s*\\d+\\s*[,}]`);
153
+ // ≥3 rank keys with digit values in one object literal = a tier map — including
154
+ // the drifted 3-key shape that caused #863 (a rank silently missing).
155
+ function hasRankMap(src) {
156
+ for (const m of src.matchAll(/\{[^{}]*\}/g)) {
157
+ if (RANK_KEYS.filter((r) => pairRe(r).test(m[0])).length >= 3) return true;
158
+ }
159
+ return false;
160
+ }
161
+ function parsePairs(inner) {
162
+ const out = {};
163
+ for (const m of inner.matchAll(/([a-z]+)['"]?\s*:\s*(\d+)/g)) out[m[1]] = Number(m[2]);
164
+ return out;
165
+ }
166
+ function extractRankMaps(src) {
167
+ const out = [];
168
+ for (const m of src.matchAll(/\{[^{}]*\}/g)) {
169
+ if (RANK_KEYS.filter((r) => pairRe(r).test(m[0])).length >= 3) {
170
+ out.push(parsePairs(m[0].slice(1, -1)));
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ // ≥3 CONSECUTIVE live-rank strings in one array literal = a ladder copy.
176
+ const LOOSE_LADDER_RE = /\[\s*(?:['"](?:xenos|thetes|metic|archon)['"]\s*,?\s*){3,}\]/;
177
+
178
+ test('no rank map or ladder literal exists outside the allowlist (and none is stale)', () => {
179
+ const tracked = execFileSync('git', ['-C', ROOT, 'ls-files', 'src', 'modules', 'scripts'], { encoding: 'utf8' })
180
+ .split('\n').map((l) => l.trim())
181
+ .filter((f) => f.endsWith('.js') && !f.includes('/tests/') && !f.endsWith('.test.js'));
182
+ assert.ok(tracked.length > 50, `suspiciously few tracked files (${tracked.length}) — scanner broken?`);
183
+
184
+ const mapHits = new Set();
185
+ const ladderHits = new Set();
186
+ for (const rel of tracked) {
187
+ const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
188
+ if (hasRankMap(src)) mapHits.add(rel);
189
+ if (LOOSE_LADDER_RE.test(src)) ladderHits.add(rel);
190
+ }
191
+ for (const f of mapHits) {
192
+ assert.ok(MAP_ALLOWLIST.has(f),
193
+ `${f} declares a rank→tier map literal. Derive it from api.RANK_TIER / db-kernel.RANK_TIER instead — or, if it genuinely cannot require server code, add it here WITH a pinning assertion above.`);
194
+ }
195
+ for (const f of ladderHits) {
196
+ assert.ok(LADDER_ALLOWLIST.has(f),
197
+ `${f} declares a rank-ladder array literal. Derive it from api.LIVE_RANK_LADDER instead — or, if it genuinely cannot require server code, add it here WITH a pinning assertion above.`);
198
+ }
199
+ for (const f of MAP_ALLOWLIST) {
200
+ assert.ok(mapHits.has(f), `allowlist entry ${f} no longer contains a rank map — prune it`);
201
+ }
202
+ for (const f of LADDER_ALLOWLIST) {
203
+ assert.ok(ladderHits.has(f), `allowlist entry ${f} no longer contains a ladder literal — prune it`);
204
+ }
205
+ });
@@ -10,10 +10,11 @@
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
16
  const { taskQuery, pickActiveClaim, formatHints, DEADLINE_MS } =
16
- require('../.claude/hooks/recall-hints.js');
17
+ requireHostModule(require, '../.claude/hooks/recall-hints.js');
17
18
 
18
19
  let passed = 0;
19
20
  let failed = 0;
@@ -71,6 +71,10 @@ test('each nested CLAUDE.md carries a delimited generated block with signatures'
71
71
  const doc = readFileSync(path.join(ROOT, dir, 'CLAUDE.md'), 'utf8');
72
72
  assert.ok(doc.includes(BEGIN), `${dir}/CLAUDE.md should have a generated-block start marker`);
73
73
  assert.ok(doc.includes(END), `${dir}/CLAUDE.md should have a generated-block end marker`);
74
+ // In a PUBLISHED core the nested agent notes are replaced by package-core.js's
75
+ // CORE_AGENT_DOC_STUB, so there is no hand-written prose to preserve (task 1002470).
76
+ // Where the real doc is present the prose must survive the generated-block injection.
77
+ if (doc.includes('@bongos/core — installed platform dependency')) continue;
74
78
  // the hand-written prose is preserved alongside the generated block
75
79
  assert.match(doc, /Nested CLAUDE\.md — loads on demand/);
76
80
  }
@@ -41,12 +41,16 @@ const rowOf = (src) => rrc.classifyText(src)[0];
41
41
  // here ONCE by hand. This is the independent side of every classification assertion
42
42
  // below — if someone lowers `backup.manage` to 'xenos', the classifier follows the
43
43
  // catalog and this file goes red, which is the whole point.
44
+ // ADR 0157 moved 20 atoms archon→metic. `audit_log.read` and `builder.roster.read` are
45
+ // transcribed at their new floor below; the archon entries that remain are the
46
+ // rank/identity core, which is the point of that ADR.
44
47
  const HAND_WRITTEN_FLOORS = {
45
48
  'builder.rank.set': 'archon',
46
- 'audit_log.read': 'archon',
47
49
  'auth.session.revoke': 'archon',
48
- 'builder.roster.read': 'archon',
49
50
  'builder.status.set': 'archon',
51
+ 'access_request.review': 'archon',
52
+ 'audit_log.read': 'metic',
53
+ 'builder.roster.read': 'metic',
50
54
  'backup.manage': 'metic',
51
55
  'idea.triage': 'metic',
52
56
  'security.report.file': 'metic',
@@ -132,13 +136,17 @@ test('P1: the label agrees with the REAL threshold function over the whole rank
132
136
  });
133
137
 
134
138
  test('P1: AND semantics — a multi-atom gate takes the STRICTEST floor', () => {
135
- const src = "router.post('/x', auth.requireBuilder, auth.requirePermission('idea.file', 'audit_log.read'), h);";
139
+ // Uses `builder.rank.set` as the archon end of the pair: ADR 0157 lowered
140
+ // `audit_log.read` (the previous example) to metic, which would no longer exercise the
141
+ // metic-vs-archon spread this test exists to prove. Rank-setting is the one power that
142
+ // ADR 0157 guarantees stays archon, so it is the stable choice here.
143
+ const src = "router.post('/x', auth.requireBuilder, auth.requirePermission('idea.file', 'builder.rank.set'), h);";
136
144
  assert.equal(rankOf(src), 'archon',
137
145
  'requirePermission ANDs, so the gate is as strict as its strictest atom (archon), not its loosest');
138
146
  const src2 = "router.post('/x', auth.requireBuilder, auth.requirePermission('cost.log', 'backup.manage'), h);";
139
147
  assert.equal(rankOf(src2), 'metic+archon');
140
148
  // order must not matter
141
- const src3 = "router.post('/x', auth.requireBuilder, auth.requirePermission('audit_log.read', 'idea.file'), h);";
149
+ const src3 = "router.post('/x', auth.requireBuilder, auth.requirePermission('builder.rank.set', 'idea.file'), h);";
142
150
  assert.equal(rankOf(src3), 'archon');
143
151
  });
144
152
 
@@ -274,7 +282,8 @@ test('P4: gen-api-docs labels a permission-gated route by its floor, not any-bui
274
282
  const trigger = model.find((r) => r.route === '/backup/trigger');
275
283
  const audit = model.find((r) => r.route === '/audit-log');
276
284
  assert.equal(trigger.rank, 'metic+archon', 'the published rank must be the real floor');
277
- assert.equal(audit.rank, 'archon');
285
+ // ADR 0157: `audit_log.read` floor archon→metic, so the published rank follows.
286
+ assert.equal(audit.rank, 'metic+archon');
278
287
  assert.deepEqual(trigger.perms, ['backup.manage'], 'the docs also name the authority atom');
279
288
 
280
289
  const spec = genApiDocs.buildOpenapi(model);
@@ -23,6 +23,7 @@
23
23
  import { strict as assert } from 'node:assert';
24
24
  import { readFileSync, existsSync } from 'node:fs';
25
25
  import { fileURLToPath } from 'node:url';
26
+ import { skipUnlessPresent } from './helpers.mjs';
26
27
  import { dirname, resolve } from 'node:path';
27
28
 
28
29
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -30,6 +31,8 @@ const ROOT = resolve(HERE, '..');
30
31
  const FIXTURES_DIR = resolve(ROOT, 'modules', 'art-pipeline', 'template', 'rubric_fixtures');
31
32
  const MANIFEST = resolve(FIXTURES_DIR, 'fixtures.json');
32
33
  const RUBRIC = resolve(ROOT, 'modules', 'art-pipeline', 'template', 'rubric.json');
34
+ skipUnlessPresent('modules/art-pipeline/template/rubric_fixtures',
35
+ 'The art-pipeline module is not installed in this instance.');
33
36
 
34
37
  let passed = 0;
35
38
  function test(name, fn) {
@@ -19,11 +19,13 @@ import fs from 'node:fs';
19
19
  import os from 'node:os';
20
20
  import path from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
+ import { skipUnlessPresent } from './helpers.mjs';
22
23
 
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  const REPO = path.resolve(__dirname, '..'); // the core root (this repo), fixed by resolveCoreRoot()
25
26
  const RUN_ROUTINE = path.join(REPO, 'scripts', 'gds', 'run-routine.js');
26
27
  const CORE_RESOLVE = path.join(REPO, '.claude', 'hooks', '_core-resolve.js');
28
+ skipUnlessPresent('.claude/hooks/_core-resolve.js');
27
29
 
28
30
  let passed = 0, failed = 0;
29
31
  function test(name, fn) {
@@ -240,7 +240,10 @@ function boxDeps(markerFile, overrides = {}) {
240
240
  if (cmd === 'git') return execFileSync(cmd, args, opts);
241
241
  return '';
242
242
  },
243
- fetchImpl: async () => ({ status: 200 }),
243
+ // task 1002607: emulate a real PREVIEW — 200 on the health URL, 404 on the
244
+ // /api/gds/healthz discriminator (the game-only entry mounts no /api/gds).
245
+ // A flat always-200 mock would now read as "the MAIN server answered".
246
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
244
247
  markerPath: markerFile,
245
248
  cwd: tmp,
246
249
  pollMs: 1,
@@ -346,11 +349,19 @@ await ta('runShipGate: --skip-stage → ok, untouched', async () => {
346
349
  assert.equal(fs.existsSync(mp), false);
347
350
  });
348
351
 
349
- await ta('runShipGate: staging failure (health never green) warns but does not block', async () => {
352
+ // task 1002607 (§4.4 station 9): a staging failure used to WARN one stderr line
353
+ // and return ok — the review gate silently turned itself off. It now fails
354
+ // CLOSED (like the smoke gate, task 1002605): abort, claim intact, --skip-stage
355
+ // as the explicit bypass, and the reason says plainly the review DID NOT run.
356
+ await ta('1002607 runShipGate: staging failure (health never green) ABORTS — review gate cannot silently turn itself off', async () => {
350
357
  const mp = path.join(markerDir, 'm8.json');
351
358
  const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
352
359
  const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
353
- assert.equal(r.ok, true, 'a broken preview must not brick shipping');
360
+ assert.equal(r.ok, false, 'a failed stage must not ship as if the review happened');
361
+ assert.equal(r.review, undefined, 'a failure to present is NOT a calm review pause');
362
+ assert.match(r.reason, /DID NOT run/, 'says plainly the review step did not run');
363
+ assert.match(r.reason, /--skip-stage/, 'names the explicit bypass');
364
+ assert.match(r.reason, /claim is still active/i, 'claim-preserving');
354
365
  });
355
366
 
356
367
  // ---------- recorded self-approval (#1048), box ----------
@@ -502,12 +513,62 @@ await ta('stage(box): a HARD verify failure → {ok:false,hard:true}, no marker'
502
513
  assert.equal(fs.existsSync(mp), false, 'no marker on a hard verify failure');
503
514
  });
504
515
 
505
- await ta('runShipGate(box): a SOFT failure (preview never healthy) still warns + continues', async () => {
516
+ await ta('1002607 runShipGate(box): a SOFT failure (preview never healthy) now fails closed — abort, claim intact', async () => {
506
517
  const mp = path.join(markerDir, 'soft1.json');
507
- // sync ok (injected), but health never green → soft failure → ship continues
518
+ // sync ok (injected), but health never green → the ship must NOT continue
519
+ // as if the review ran (that was the silent-disable defect).
508
520
  const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
509
521
  const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
510
- assert.equal(r.ok, true, 'a broken preview must not brick shipping (soft failure)');
522
+ assert.equal(r.ok, false, 'the soft failure aborts instead of silently skipping the review');
523
+ assert.match(r.reason, /sandbox staging failed/);
524
+ });
525
+
526
+ // ---------- task 1002607: waitHealthy truth table ----------
527
+
528
+ await ta('1002607 waitHealthy: 404 at the health URL is NOT healthy (was: anything < 500 passed)', async () => {
529
+ const r = await sbx.waitHealthy({ fetchImpl: async () => ({ status: 404 }), timeoutMs: 10, pollMs: 1 });
530
+ assert.equal(r.ok, false);
531
+ assert.match(r.reason, /HTTP 404/);
532
+ });
533
+
534
+ await ta('1002607 waitHealthy: a 200 answered by the MAIN server (api healthz 200) is an impostor, not a healthy preview', async () => {
535
+ // The full server binds the same 127.0.0.1:3000 — root answers 200, and so does
536
+ // /api/gds/healthz. The game-only preview (ADR 0052) would 404 the latter.
537
+ const r = await sbx.waitHealthy({ fetchImpl: async () => ({ status: 200 }), timeoutMs: 1000, pollMs: 1 });
538
+ assert.equal(r.ok, false, 'a dead preview must not report healthy off the main server');
539
+ assert.match(r.reason, /MAIN server/);
540
+ assert.match(r.reason, /preview did not start/);
541
+ });
542
+
543
+ await ta('1002607 waitHealthy: a real preview (root 200, api healthz 404) is healthy', async () => {
544
+ const r = await sbx.waitHealthy({
545
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
546
+ timeoutMs: 1000, pollMs: 1,
547
+ });
548
+ assert.equal(r.ok, true);
549
+ });
550
+
551
+ await ta('1002607 waitHealthy: a discriminator probe that ERRORS counts as "not the main server" (preview healthy)', async () => {
552
+ const r = await sbx.waitHealthy({
553
+ fetchImpl: async (url) => {
554
+ if (String(url).includes('/api/gds/healthz')) throw new Error('ECONNREFUSED on a path the preview never mounts');
555
+ return { status: 200 };
556
+ },
557
+ timeoutMs: 1000, pollMs: 1,
558
+ });
559
+ assert.equal(r.ok, true, 'only a definite 200 on api healthz marks the impostor');
560
+ });
561
+
562
+ t('1002607 apiHealthzProbeUrl: same origin as the health URL, api path; null on garbage', () => {
563
+ assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000/api/gds/healthz');
564
+ assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3100/healthz'), 'http://127.0.0.1:3100/api/gds/healthz');
565
+ assert.equal(sbx.apiHealthzProbeUrl('not a url'), null);
566
+ });
567
+
568
+ t('1002607 NO_SANDBOX_MSG: names what is actually missing, not the builder\'s clone', () => {
569
+ assert.match(sbx.NO_SANDBOX_MSG, /src\/preview-server\.js/, 'names the missing entry file');
570
+ assert.match(sbx.NO_SANDBOX_MSG, /ADR 0052/, 'points at the contract that defines it');
571
+ assert.doesNotMatch(sbx.NO_SANDBOX_MSG, /this clone has no game preview entry/, 'the old your-clone-is-at-fault phrasing is gone');
511
572
  });
512
573
 
513
574
  // ---------- runShipGate: LOCAL preview context + recorded approval (task 1056) ----------
@@ -529,7 +590,8 @@ function localDeps(markerFile, overrides = {}) {
529
590
  writeFileSync: (p, d) => fs.writeFileSync(p, d),
530
591
  },
531
592
  execFileSync: (cmd, args, opts) => (cmd === 'git' ? execFileSync(cmd, args, opts) : ''),
532
- fetchImpl: async () => ({ status: 200 }),
593
+ // task 1002607: preview-shaped mock — health 200, API-healthz discriminator 404.
594
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
533
595
  markerPath: markerFile,
534
596
  cwd: tmp,
535
597
  pollMs: 1,
@@ -30,6 +30,13 @@ test('metic-tagging: read-sensitive doc surfaces → metic', () => {
30
30
  assert.equal(minRankForDoc('docs/audits/2026-06-08-server-security-audit.md'), 'metic');
31
31
  assert.equal(minRankForDoc('docs/adr/0016-trust-boundary-server-enforced-permissions.md'), 'metic');
32
32
  assert.equal(minRankForDoc('docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md'), 'metic');
33
+ // task 1002598: docs/governance/ holds the complete gate enumeration (the authority
34
+ // sweep) and the choke-point map, which catalogue live fail-open paths by file:line.
35
+ // Served at xenos they were a searchable exploitation index for the population they
36
+ // describe; the read floor matches security.report.adjudicate, where ADR 0158 §6
37
+ // sends the same class of detail.
38
+ assert.equal(minRankForDoc('docs/governance/rank-choke-point-map-2026-08-06.md'), 'metic');
39
+ assert.equal(minRankForDoc('docs/governance/authority-sweep-2026-07-24.md'), 'metic');
33
40
  });
34
41
 
35
42
  test('metic-tagging: ordinary docs → xenos (read-public)', () => {
@@ -18,8 +18,11 @@
18
18
  // - the slug character-class regex rejects non-kebab slugs
19
19
  //
20
20
  // Rank wiring (source assertion — mirrors tests/cli_token_reissue.mjs):
21
- // - all three /security/docs* routes are auth.requireRank('archon')
22
- // - route-rank-check pins them to 'archon' in EXPECTED_RANKS
21
+ // - all three /security/docs* routes gate on the atom
22
+ // auth.requirePermission('security.report.adjudicate') (BV1.R104; was
23
+ // requireRank('archon') and admitted exactly the same builders, ADR 0151)
24
+ // - that atom's floor moved archon→metic in ADR 0157, so the routes now classify
25
+ // 'metic+archon' and are pinned BY ATOM (`perm:…`) rather than by rank
23
26
  //
24
27
  // We stub the pool by pre-seeding the require cache for src/db.js so the db
25
28
  // module under test never opens a socket — the same monkeypatch seam the repo
@@ -167,18 +170,37 @@ await test('slug character-class regex rejects non-kebab slugs', () => {
167
170
 
168
171
  console.log('\nsecurity_docs — rank wiring (source + pin):');
169
172
 
170
- await test('all three /security/docs* routes are requireRank(archon)', () => {
173
+ await test('all three /security/docs* routes gate on security.report.adjudicate (metic floor)', () => {
171
174
  const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'routes', 'security.js'), 'utf8');
172
175
  const routes = rrc.classifyText(src).filter((r) => r.route.startsWith('/security/docs'));
173
176
  assert.equal(routes.length, 3, 'expected GET list, GET :slug, POST');
174
177
  for (const r of routes) {
175
- assert.equal(r.rank, 'archon', `${r.key} must classify as archon (serves exploit detail)`);
178
+ // The ATOM is the authority once governance grows custom roles; the rank is what
179
+ // that atom's catalog floor resolves to today. Assert both, so neither a swapped
180
+ // atom nor an UNINTENDED floor edit can quietly widen who reads the exploit cookbook.
181
+ //
182
+ // ADR 0157 made that widening INTENTIONALLY: `security.report.adjudicate` moved
183
+ // archon→metic with the rest of the operational block, so the engagement-report
184
+ // store is now readable by the working rank. That is the one place in the ADR where
185
+ // delegation carries a genuine CONFIDENTIALITY cost rather than just an
186
+ // administrative one — every other delegated atom widens who may *act*, this one
187
+ // widens who may *read attacker-usable detail*. If that trade is unwanted, the fix
188
+ // is to split the atom the way ADR 0157 §4 split `gate_approval.read` off
189
+ // `gate_approval.decide`: keep a `security.docs.read` at archon for the cookbook and
190
+ // let report adjudication delegate. Recorded here so the next reader sees the choice
191
+ // rather than inferring it from a floor value.
192
+ assert.deepEqual(r.perms, ['security.report.adjudicate'],
193
+ `${r.key} must gate on the adjudicate atom (serves exploit detail)`);
194
+ assert.equal(r.rank, 'metic+archon', `${r.key} must classify as metic+archon (ADR 0157)`);
176
195
  }
177
196
  });
178
197
 
179
- await test('route-rank-check pins all three /security/docs* routes to archon', () => {
198
+ await test('route-rank-check pins all three /security/docs* routes by atom', () => {
199
+ // Re-pinned from the rank 'archon' to the ATOM in ADR 0157: the invariant worth
200
+ // guarding is "these gate on the adjudicate permission", which survives a floor move.
180
201
  for (const key of ['GET /security/docs', 'GET /security/docs/:slug', 'POST /security/docs']) {
181
- assert.equal(rrc.EXPECTED_RANKS[key], 'archon', `${key} must be pinned archon in EXPECTED_RANKS`);
202
+ assert.equal(rrc.EXPECTED_RANKS[key], 'perm:security.report.adjudicate',
203
+ `${key} must be pinned to the adjudicate atom in EXPECTED_RANKS`);
182
204
  }
183
205
  });
184
206
 
@@ -13,6 +13,7 @@ const {
13
13
  topoOrder,
14
14
  chainFrom,
15
15
  haltReasons,
16
+ pickNext,
16
17
  summarize,
17
18
  MIN_SPEC_CHARS,
18
19
  } = require('../scripts/gds/sequence.js');
@@ -263,5 +264,68 @@ t('summarize tolerates missing numbers', () => {
263
264
  { count: 1, credits: 0, minutes: 0 });
264
265
  });
265
266
 
267
+ // ------------------------------------------------------- pickNext (--next)
268
+
269
+ // The live regression this closes (goal 1000030, 2026-08-05): a backlog task
270
+ // sorted to position 0 and --next reported "nothing runnable" while two ready
271
+ // tasks sat behind it.
272
+ const GOAL_1000030 = () => [
273
+ task({ id: 1002556, status: 'backlog', title: 'perf: fold the resolve' }),
274
+ task({ id: 1001850, status: 'ready', title: 'R108 — collapse rank-tier maps' }),
275
+ task({ id: 1001858, status: 'ready', title: 'R110 — governance API routes' }),
276
+ ];
277
+ const claimable = (...ids) => ({ rank: 'archon', claimableIds: new Set(ids) });
278
+
279
+ t('pickNext offers the head when the head is clear', () => {
280
+ const plan = GOAL_1000030().slice(1);
281
+ const r = pickNext(plan, claimable(1001850, 1001858));
282
+ assert.equal(r.next.id, 1001850);
283
+ assert.deepEqual(r.skipped, []);
284
+ });
285
+
286
+ t('pickNext skips a blocked head and offers the first runnable task', () => {
287
+ const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
288
+ assert.equal(r.next.id, 1001850, 'must not dead-end on the backlog task at position 0');
289
+ assert.equal(r.skipped.length, 1);
290
+ assert.equal(r.skipped[0].id, 1002556);
291
+ });
292
+
293
+ t('pickNext reports WHY each skipped task was passed over', () => {
294
+ const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
295
+ assert.ok(r.skipped[0].reasons.some((x) => x.code === 'deps_unmet'));
296
+ assert.ok(r.skipped[0].title, 'skipped entries carry a title for the printout');
297
+ });
298
+
299
+ t('pickNext keeps topological order among the runnable — first clear wins', () => {
300
+ const r = pickNext(GOAL_1000030(), claimable(1001850, 1001858));
301
+ assert.equal(r.next.id, 1001850, 'not 1001858, which sorts later');
302
+ });
303
+
304
+ t('pickNext still halts when NOTHING in the plan clears', () => {
305
+ const plan = [
306
+ task({ id: 1, status: 'backlog' }),
307
+ task({ id: 2, status: 'shipped' }),
308
+ task({ id: 3, status: 'ready', requires_rank: 'archon' }),
309
+ ];
310
+ const r = pickNext(plan, { rank: 'xenos', claimableIds: new Set([3]) });
311
+ assert.equal(r.next, null);
312
+ assert.equal(r.skipped.length, 3, 'every task was tried');
313
+ assert.equal(r.head.id, 1, 'the head is preserved so the halt prints its reasons');
314
+ });
315
+
316
+ t('pickNext does not weaken a gate — a ready-but-unclaimable task is skipped, not offered', () => {
317
+ const plan = [task({ id: 9, status: 'ready' })];
318
+ const r = pickNext(plan, claimable(/* 9 absent from the claimable feed */));
319
+ assert.equal(r.next, null);
320
+ assert.ok(r.skipped[0].reasons.some((x) => x.code === 'not_claimable'));
321
+ });
322
+
323
+ t('pickNext on an empty plan returns no pick and nothing skipped', () => {
324
+ const r = pickNext([], claimable());
325
+ assert.equal(r.next, null);
326
+ assert.deepEqual(r.skipped, []);
327
+ assert.equal(r.head, null);
328
+ });
329
+
266
330
  console.log(`\nsequence: ${passed} passed, ${failed} failed`);
267
331
  process.exit(failed ? 1 : 0);
@@ -12,9 +12,10 @@
12
12
 
13
13
  import { strict as assert } from 'node:assert';
14
14
  import { createRequire } from 'node:module';
15
+ import { requireHostModule } from './helpers.mjs';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
- const hook = require('../.claude/hooks/session-cards.js');
18
+ const hook = requireHostModule(require, '../.claude/hooks/session-cards.js');
18
19
 
19
20
  let passed = 0;
20
21
  let failed = 0;
@@ -16,9 +16,10 @@
16
16
 
17
17
  import { strict as assert } from 'node:assert';
18
18
  import { createRequire } from 'node:module';
19
+ import { requireHostModule } from './helpers.mjs';
19
20
 
20
21
  const require = createRequire(import.meta.url);
21
- const { decideFreshness } = require('../.claude/hooks/session-start.js');
22
+ const { decideFreshness } = requireHostModule(require, '../.claude/hooks/session-start.js');
22
23
 
23
24
  let passed = 0;
24
25
  let failed = 0;