@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
@@ -171,5 +171,67 @@ test('laptop deploy mode → never server-handled, even with server push (false)
171
171
  });
172
172
  });
173
173
 
174
- console.log(`\nship broadcast server (task 1045) tests: ${passed} passed, ${failed} failed`);
174
+ // ---- 3. task 1002606: the strand notice (task.strand-flagged → Discord) ----
175
+ // A strand previously had NO push channel — Discord fired only on task.shipped, so
176
+ // a builder learned of a dead land at their next /builder-start, up to a session
177
+ // later. These pin the poster the reconciler's task.strand-flagged listener fires.
178
+
179
+ test('1002606 chooseStrandTargets: non-security strand → #ship-feed; line does NOT lead with #<id>', () => {
180
+ const t = shipBroadcast.chooseStrandTargets({ securitySensitive: false, taskId: 88, reason: 'strand:pr_closed_unmerged' });
181
+ assert.equal(t.length, 1);
182
+ assert.equal(t[0].channel, 'ship_feed');
183
+ assert.match(t[0].line, /task 88/);
184
+ assert.match(t[0].line, /strand:pr_closed_unmerged/);
185
+ // A LEADING #<id> is the applause marker (discord-reaction.parseShipMarker) —
186
+ // a strand must never collect 👍-votes as if it were shipped work.
187
+ assert.ok(!/^\s*#\d/.test(t[0].line), 'strand line must not start with a #<id> ship marker');
188
+ });
189
+
190
+ test('1002606 chooseStrandTargets: security strand → ONLY #security-ship-feed (reason can name an unpatched fix)', () => {
191
+ const t = shipBroadcast.chooseStrandTargets({ securitySensitive: true, taskId: 91, reason: 'strand:unresolvable_conflict:src/gds/auth.js' });
192
+ assert.equal(t.length, 1);
193
+ assert.equal(t[0].channel, 'security_ship_feed');
194
+ for (const { channel } of t) assert.ok(!PUBLIC_CHANNELS.has(channel));
195
+ });
196
+
197
+ await atest('1002606 postStrandNotice: posts to #ship-feed for a plain strand', async () => {
198
+ const { fn, calls } = fakePoster();
199
+ const res = await shipBroadcast.postStrandNotice(
200
+ { id: 88, builder_id: 7, security_sensitive: false, reason: 'strand:pr_closed_unmerged' },
201
+ { postWebhook: fn, log: quietLog }
202
+ );
203
+ assert.deepEqual(res.posted, ['ship_feed']);
204
+ assert.equal(calls.length, 1);
205
+ assert.match(calls[0].line, /task 88/);
206
+ });
207
+
208
+ await atest('1002606 postStrandNotice: indeterminate security flag → FAIL CLOSED (no post)', async () => {
209
+ for (const sec of [null, undefined, 'true', 1]) {
210
+ const { fn, calls } = fakePoster();
211
+ const res = await shipBroadcast.postStrandNotice(
212
+ { id: 9, security_sensitive: sec, reason: 'strand:x' },
213
+ { postWebhook: fn, log: quietLog }
214
+ );
215
+ assert.equal(calls.length, 0, `flag=${String(sec)} must suppress`);
216
+ assert.equal(res.skipped, 'security_flag_indeterminate');
217
+ }
218
+ });
219
+
220
+ await atest('1002606 postStrandNotice: best-effort — a throwing webhook never propagates; no task → no_task', async () => {
221
+ const boom = async () => { throw new Error('webhook down'); };
222
+ let threw = false;
223
+ let res;
224
+ try {
225
+ res = await shipBroadcast.postStrandNotice(
226
+ { id: 10, security_sensitive: false, reason: 'strand:y' },
227
+ { postWebhook: boom, log: quietLog }
228
+ );
229
+ } catch (_) { threw = true; }
230
+ assert.equal(threw, false, 'postStrandNotice must never throw');
231
+ assert.deepEqual(res.posted, [], 'a failed post is not recorded as posted');
232
+ const { fn } = fakePoster();
233
+ assert.equal((await shipBroadcast.postStrandNotice(null, { postWebhook: fn, log: quietLog })).skipped, 'no_task');
234
+ });
235
+
236
+ console.log(`\nship broadcast server (task 1045 + 1002606) tests: ${passed} passed, ${failed} failed`);
175
237
  process.exit(failed > 0 ? 1 : 0);
@@ -105,6 +105,72 @@ test('confirmed: info tone, auto-landing deploy, merge-mode override action', ()
105
105
  assert.equal(m.action.cmd, '/merge-mode'); // kept as an optional manual override
106
106
  });
107
107
 
108
+ // ---- task 1002541: the card must never claim a bailed merge is landing ----
109
+ //
110
+ // The bug: deployText came from the STATUS alone, so a 'confirmed' task printed
111
+ // "landing automatically" even when auto-merge had already given up — no PR, no
112
+ // merge, and (ADR 0120, pay on land) no credits. The failure announced itself as
113
+ // success, which is why these strands survived for days.
114
+ const bailed = {
115
+ ...shippedInput,
116
+ postGradeStatus: 'confirmed',
117
+ land: { ok: false, pending: false, reason: 'the main checkout is on claude/foo, not main', nextStep: '/merge-mode' },
118
+ };
119
+
120
+ test('confirmed + bailed land: says NOT landed, names the reason, warns', () => {
121
+ const m = card.buildCardModel(bailed);
122
+ assert.equal(m.stage, 'confirmed');
123
+ assert.equal(row(m, 'Deploy'), 'NOT landed — the main checkout is on claude/foo, not main');
124
+ assert.equal(m.cfg.tone, 'warning'); // not the reassuring 'info'
125
+ assert.equal(m.cfg.statusWord, 'Not landed');
126
+ assert.match(m.cfg.headline(1266), /NOT landed/);
127
+ });
128
+
129
+ test('confirmed + bailed land: the bail reaches the printed monospace card', () => {
130
+ // The monospace card is the ALWAYS-printed fallback (headless, cron, dev box).
131
+ // If the reason stops at the model, the builder still never sees it.
132
+ const out = card.renderMonospaceCard(card.buildCardModel(bailed));
133
+ assert.match(out, /NOT landed/);
134
+ assert.ok(!/landing automatically/.test(out), 'the lie must not survive anywhere on the card');
135
+ });
136
+
137
+ test('confirmed + bailed land: the action is the bail-specific recovery command', () => {
138
+ const m = card.buildCardModel({
139
+ ...bailed,
140
+ land: { ok: false, pending: false, reason: 'no commits ahead of main', nextStep: 'node scripts/gds/ship.js 7 --db-only' },
141
+ });
142
+ assert.equal(m.action.cmd, 'node scripts/gds/ship.js 7 --db-only'); // beats the generic /merge-mode
143
+ });
144
+
145
+ test('confirmed + PENDING land still reads "landing automatically"', () => {
146
+ // A poll timeout with auto-merge armed is success-PENDING (task 1049): CI or the
147
+ // server reconciler really does finish it. Crying failure here would be a NEW
148
+ // lie in the opposite direction, and would train builders to ignore the warning.
149
+ const m = card.buildCardModel({ ...bailed, land: { ok: false, pending: true, reason: null, nextStep: null } });
150
+ assert.equal(row(m, 'Deploy'), 'landing automatically');
151
+ assert.equal(m.cfg.tone, 'info');
152
+ assert.equal(m.cfg.statusWord, 'Confirmed');
153
+ });
154
+
155
+ test('confirmed with NO land info is unchanged (back-compat)', () => {
156
+ // Older land-watch breadcrumbs replay stored cardInputs that have no `land` key,
157
+ // and other callers (resumeMerge, the claim card) may pass none. Absent ⇒ the
158
+ // pre-existing behaviour, byte for byte.
159
+ const m = card.buildCardModel({ ...shippedInput, postGradeStatus: 'confirmed', land: null });
160
+ assert.equal(row(m, 'Deploy'), 'landing automatically');
161
+ assert.equal(m.cfg.tone, 'info');
162
+ assert.equal(m.action.cmd, '/merge-mode');
163
+ });
164
+
165
+ test('a stale bailed land never overrides a task that DID reach shipped', () => {
166
+ // land-watch replays the breadcrumb after the server lands it; the status is
167
+ // then 'shipped' and the old bail is history. The card must show live, not NOT-landed.
168
+ const m = card.buildCardModel({ ...bailed, postGradeStatus: 'shipped' });
169
+ assert.equal(m.stage, 'shipped');
170
+ assert.equal(row(m, 'Deploy'), 'live · 967fb4a');
171
+ assert.equal(m.cfg.tone, 'success');
172
+ });
173
+
108
174
  test('failed: danger tone, no credits, issue count, regrade action', () => {
109
175
  const m = card.buildCardModel({
110
176
  ...shippedInput, postGradeStatus: 'completed',
@@ -44,7 +44,7 @@ function test(name, fn) {
44
44
  }
45
45
  }
46
46
 
47
- const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus } = ship;
47
+ const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus, landed, landPending, landBailed, strandCommand, landPendingLines } = ship;
48
48
 
49
49
  // ---- 1. parseDeployMode precedence + safe default ----
50
50
 
@@ -266,6 +266,44 @@ test('reconciledCardStatus: status read failed (null) → keep the pre-merge sta
266
266
  assert.equal(reconciledCardStatus(undefined, 'confirmed'), 'confirmed');
267
267
  });
268
268
 
269
+ // ---- 3d. the land outcome: why this is a 3-way object, not a boolean ----
270
+ // task 1002541. The land chain used to answer true/false, so every bail site's
271
+ // precise reason was discarded and the card fell back to the STATUS — printing
272
+ // "landing automatically" over a merge that had already given up.
273
+
274
+ test('landBailed carries the reason + the recovery command', () => {
275
+ const o = landBailed('the main checkout is on claude/foo, not main', '/merge-mode');
276
+ assert.equal(o.ok, false);
277
+ assert.equal(o.pending, false);
278
+ assert.equal(o.reason, 'the main checkout is on claude/foo, not main');
279
+ assert.equal(o.nextStep, '/merge-mode');
280
+ });
281
+
282
+ test('THE TRAP: every outcome is truthy, so consumers must test .ok', () => {
283
+ // mergeAndDeploy's guard used to be `if (!merged) return;`. With objects that
284
+ // check NEVER fires, so a bailed ship would fall through and POST /ship for a
285
+ // task that never landed. This is the assertion that catches a re-introduction.
286
+ assert.ok(landBailed('nope', '/merge-mode'), 'a bail object is truthy — !merged cannot detect it');
287
+ assert.ok(landPending(), 'a pending object is truthy too');
288
+ assert.equal(landBailed('nope', '/merge-mode').ok, false);
289
+ assert.equal(landPending().ok, false);
290
+ assert.equal(landed().ok, true);
291
+ });
292
+
293
+ test('pending is NOT a bail: no reason, so the card keeps "landing automatically"', () => {
294
+ // A poll timeout with auto-merge armed is success-pending (task 1049) — it does
295
+ // land on its own. The card gates its warning on `reason`, so this stays quiet.
296
+ const o = landPending();
297
+ assert.equal(o.pending, true);
298
+ assert.equal(o.reason, null);
299
+ });
300
+
301
+ test('strandCommand: a box gets the resume, a laptop gets /merge-mode', () => {
302
+ // /merge-mode needs gh + a main checkout a dev box doesn't have (task 1052).
303
+ assert.equal(strandCommand(77, 'server'), 'node scripts/gds/ship.js 77');
304
+ assert.equal(strandCommand(77, 'local'), '/merge-mode');
305
+ });
306
+
269
307
  test('reconciledCardStatus: already shipped stays shipped', () => {
270
308
  assert.equal(reconciledCardStatus('shipped', 'shipped'), 'shipped');
271
309
  });
@@ -506,6 +544,33 @@ test('effectiveDeployMode: tolerates a missing options object (defaults to no fa
506
544
  assert.equal(effectiveDeployMode('laptop'), 'ci'); // canPushLocally undefined ⇒ falsy ⇒ fall back
507
545
  });
508
546
 
547
+ // ---- task 1002606: the success-pending banner carries the strand-detection story ----
548
+ // Finding P-3: the banner ended at "This is NOT a failure", so when a land DID die
549
+ // (hand-written conflict, dead PR, red check) the builder's last signal said all was
550
+ // well. The banner must keep the reassurance AND say how a real failure reaches them.
551
+
552
+ test('1002606 landPendingLines: keeps the "NOT a failure" reassurance + the resume command', () => {
553
+ const lines = landPendingLines(940).join('\n');
554
+ assert.match(lines, /NOT a failure/);
555
+ assert.match(lines, /node scripts\/gds\/ship\.js 940/, 'the resume command names THIS task');
556
+ });
557
+
558
+ test('1002606 landPendingLines: tells the builder how a strand WILL reach them (needs_rebase + /builder-start)', () => {
559
+ const lines = landPendingLines(940).join('\n');
560
+ assert.match(lines, /CANNOT land on its own/i, 'the failure story is present');
561
+ assert.match(lines, /needs_rebase/, 'names the flag the reconciler sets');
562
+ assert.match(lines, /\/builder-start/, 'names WHERE the warning appears');
563
+ assert.match(lines, /release/, 'names the release exit too');
564
+ });
565
+
566
+ test('1002606 landPendingLines: merged variant says deploy-only', () => {
567
+ const merged = landPendingLines(1, { merged: true }).join('\n');
568
+ assert.match(merged, /and the PR has merged/);
569
+ assert.match(merged, /GitHub will deploy automatically/);
570
+ const unmerged = landPendingLines(1, {}).join('\n');
571
+ assert.match(unmerged, /GitHub will merge \+ deploy automatically/);
572
+ });
573
+
509
574
  console.log('');
510
575
  console.log(`ship_ci_deploy: ${passed} passed, ${failed} failed`);
511
576
  process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,76 @@
1
+ // tests/ship_smoke_gate.mjs
2
+ //
3
+ // task 1002605 (choke-point map finding G-18): a FAILED production smoke run used
4
+ // to fall silently through into the claim resolve — the claim was consumed, the
5
+ // grader silently skipped, the task parked at 'completed', and ship.js exited 0
6
+ // printing "Done." The fix aborts BEFORE the resolve (claim intact) unless the
7
+ // builder passes --allow-smoke-fail, and both paths now say exactly what happened.
8
+ // This pins the loudness contract via the pure smokeGateMessages export, so a
9
+ // future edit can't quietly regress the gate back to silence.
10
+ //
11
+ // Run: node tests/ship_smoke_gate.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { smokeGateMessages } = require('../scripts/gds/ship.js');
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+ function t(name, fn) {
22
+ try {
23
+ fn();
24
+ passed++;
25
+ console.log(` PASS ${name}`);
26
+ } catch (err) {
27
+ failed++;
28
+ console.log(` FAIL ${name}`);
29
+ console.log(` ${err.message}`);
30
+ }
31
+ }
32
+
33
+ const m = smokeGateMessages(1234);
34
+ const abortText = m.abort.join('\n');
35
+ const parkedText = m.parked.join('\n');
36
+
37
+ t('abort: says the ship ABORTED because smoke FAILED', () => {
38
+ assert.match(abortText, /ABORTED/);
39
+ assert.match(abortText, /smoke test FAILED/);
40
+ });
41
+
42
+ t('abort: says the claim is still active and nothing changed', () => {
43
+ assert.match(abortText, /claim is still active/);
44
+ assert.match(abortText, /No claim resolved/);
45
+ });
46
+
47
+ t('abort: names the re-run and the deliberate --allow-smoke-fail continuation', () => {
48
+ assert.match(abortText, /ship\.js 1234(?!\S)/);
49
+ assert.match(abortText, /ship\.js 1234 --allow-smoke-fail/);
50
+ });
51
+
52
+ t('abort: explains the smoke suite hits live endpoints (the change is not deployed yet)', () => {
53
+ assert.match(abortText, /LIVE endpoints/);
54
+ assert.match(abortText, /not deployed yet/);
55
+ });
56
+
57
+ t('parked: names the deliberate stop, the exact status, and that nothing was paid', () => {
58
+ assert.match(parkedText, /--allow-smoke-fail/);
59
+ assert.match(parkedText, /Task #1234 is parked at 'completed'/);
60
+ assert.match(parkedText, /ungraded/);
61
+ assert.match(parkedText, /no credits/i);
62
+ });
63
+
64
+ t('parked: names the resume path (--regrade)', () => {
65
+ assert.match(parkedText, /ship\.js 1234 --regrade/);
66
+ });
67
+
68
+ t('both message sets are non-empty arrays of strings (printable as-is)', () => {
69
+ for (const set of [m.abort, m.parked]) {
70
+ assert.ok(Array.isArray(set) && set.length > 0);
71
+ for (const line of set) assert.equal(typeof line, 'string');
72
+ }
73
+ });
74
+
75
+ console.log(`\nship smoke gate (task 1002605): ${passed} passed, ${failed} failed`);
76
+ process.exit(failed ? 1 : 0);
@@ -28,6 +28,7 @@ const VERSIONS_POST_SCHEMA = {
28
28
  id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
29
29
  name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
30
30
  status: { type: 'string', maxLength: 32 },
31
+ track: { type: 'string', maxLength: 16 },
31
32
  done_when: { type: 'string', maxLength: 8000 },
32
33
  scope_doc_path: { type: 'string', maxLength: 256 },
33
34
  criteria: { type: 'array', maxItems: 100 },
@@ -62,6 +63,20 @@ test('INIT: unknown extra fields pass through (forward-compat)', () => {
62
63
  assert.equal(validate({ id: 'V1', name: 'x', future_field: 42 }, VERSIONS_POST_SCHEMA), null);
63
64
  });
64
65
 
66
+ // versions.track is NOT NULL with no DB default (migration 003) — the create
67
+ // path must accept it so callers can choose, and default it so omitting it can
68
+ // never again 500 the route (the @bongos/core 1.18.18 `bongos init` blocker).
69
+ test('INIT: explicit track passes the schema guard (both tracks)', () => {
70
+ assert.equal(validate({ id: 'V1', name: 'x', track: 'product' }, VERSIONS_POST_SCHEMA), null);
71
+ assert.equal(validate({ id: 'GDS-V5', name: 'x', track: 'internal' }, VERSIONS_POST_SCHEMA), null);
72
+ });
73
+
74
+ test('BAD: non-string track → structured 400 shape, not a DB 500', () => {
75
+ const r = validate({ id: 'V1', name: 'x', track: 42 }, VERSIONS_POST_SCHEMA);
76
+ assert.ok(r);
77
+ assert.ok(r.details.some((d) => d.field === 'track'));
78
+ });
79
+
65
80
  // ---- BAD shapes must return a structured 400, never throw / 500 ----
66
81
  test('BAD: missing id → missing', () => {
67
82
  const r = validate({ name: 'x' }, VERSIONS_POST_SCHEMA);
@@ -23,14 +23,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const REPO = path.resolve(__dirname, '..');
24
24
  const CORE_PATH_SH = path.join(REPO, 'infra', 'core-path.sh');
25
25
 
26
+ // infra/ + the gate workflows under .github/ are HOST topology (publish-manifest.js
27
+ // PROTECTED_TOPOLOGY_EXCLUDE) — absent from a neutral core checkout (task 1002470).
28
+ // The real-tree checks only mean something where that tree exists; the synthetic
29
+ // counterfactual below always runs.
30
+ const HAS_GATE_WORKFLOWS = fs.existsSync(path.join(REPO, '.github', 'workflows', 'gate-review.yml'));
31
+ const HAS_CORE_PATH = fs.existsSync(CORE_PATH_SH);
32
+
26
33
  let passed = 0, failed = 0;
27
34
  function test(name, fn) {
28
35
  try { fn(); console.log(` ok ${name}`); passed++; }
29
36
  catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
30
37
  }
31
38
 
39
+ function hostTest(name, present, fn) {
40
+ if (!present) { console.log(` skip ${name} — the host topology it reads is absent in this checkout`); return; }
41
+ test(name, fn);
42
+ }
43
+
32
44
  // ---- A. the workflow lint is green on the real tree ---------------------------------------------
33
- test('lint-workflow-exec: every core invocation is routed + publishable (no bare repo-relative form)', () => {
45
+ hostTest('lint-workflow-exec: every core invocation is routed + publishable (no bare repo-relative form)', HAS_GATE_WORKFLOWS, () => {
34
46
  const r = lintWorkflowExec({ instanceRoot: REPO, coreRoot: REPO });
35
47
  assert.deepEqual(r.violations, [], `expected no violations, got: ${JSON.stringify(r.violations, null, 2)}`);
36
48
  assert.ok(r.ok, 'lint should pass');
@@ -63,7 +75,7 @@ function runCorePath(rel, instanceRoot) {
63
75
  }).trim();
64
76
  }
65
77
 
66
- test('core-path.sh prints the monolith path (script at the instance root)', () => {
78
+ hostTest('core-path.sh prints the monolith path (script at the instance root)', HAS_CORE_PATH, () => {
67
79
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-mono-'));
68
80
  try {
69
81
  fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
@@ -72,7 +84,7 @@ test('core-path.sh prints the monolith path (script at the instance root)', () =
72
84
  } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
73
85
  });
74
86
 
75
- test('core-path.sh falls back to the consumer path (script only under node_modules/@bongos/core)', () => {
87
+ hostTest('core-path.sh falls back to the consumer path (script only under node_modules/@bongos/core)', HAS_CORE_PATH, () => {
76
88
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-cons-'));
77
89
  try {
78
90
  const pkg = path.join(tmp, 'node_modules', '@bongos', 'core', 'scripts');
@@ -82,7 +94,7 @@ test('core-path.sh falls back to the consumer path (script only under node_modul
82
94
  } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
83
95
  });
84
96
 
85
- test('core-path.sh exits non-zero (no stdout) when the script is in NO layout', () => {
97
+ hostTest('core-path.sh exits non-zero (no stdout) when the script is in NO layout', HAS_CORE_PATH, () => {
86
98
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-none-'));
87
99
  let threw = false;
88
100
  try {
@@ -9,9 +9,10 @@
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
- const { decideWorktreeGuard, isUnder } = require('../.claude/hooks/worktree-guard.js');
15
+ const { decideWorktreeGuard, isUnder } = requireHostModule(require, '../.claude/hooks/worktree-guard.js');
15
16
 
16
17
  let passed = 0;
17
18
  let failed = 0;