@bongos/core 1.18.19 → 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 +514 -194
  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 +5 -5
  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 -13
  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 +1 -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
@@ -1103,6 +1103,34 @@ function shouldBailOnFetchFailure(fetchedMain, deployMode, via) {
1103
1103
  return !serverPublish;
1104
1104
  }
1105
1105
 
1106
+ // ---------- the land outcome (task 1002541, part 1) ----------
1107
+ //
1108
+ // The land chain used to answer with a bare boolean. Every bail site already
1109
+ // computes a precise reason and logs it, but `false` threw that reason away — so
1110
+ // the completion card had only the task's STATUS to go on ('confirmed') and
1111
+ // printed "Deploy landing automatically" even when nothing was landing and the
1112
+ // builder was, under ADR 0120, unpaid. The failure announced itself as success.
1113
+ //
1114
+ // "Not shipped" is not one thing, so this is a three-way, not a boolean:
1115
+ // landed() — merged + deployed; the task flips to shipped.
1116
+ // landPending() — published and auto-merge armed: it DOES land on its own
1117
+ // (CI / the server reconciler), so "landing automatically"
1118
+ // is honest here. A poll timeout is this, not a failure
1119
+ // (task 1049) — keep it out of the loud path.
1120
+ // landBailed(r, n) — nothing is landing. `r` is the reason already logged,
1121
+ // `n` the command that recovers it.
1122
+ // Only landBailed makes the card say NOT landed. Purely additive: no bail site
1123
+ // changes what it DOES, only what it now reports back.
1124
+ function landed() { return { ok: true, pending: false, reason: null, nextStep: null }; }
1125
+ function landPending() { return { ok: false, pending: true, reason: null, nextStep: null }; }
1126
+ function landBailed(reason, nextStep = null) { return { ok: false, pending: false, reason, nextStep }; }
1127
+
1128
+ // The short COMMAND form of strandNextStep (which is prose, sized for the log).
1129
+ // The card has room for one command, not a sentence.
1130
+ function strandCommand(taskId, via) {
1131
+ return via === 'server' ? `node scripts/gds/ship.js ${taskId}` : '/merge-mode';
1132
+ }
1133
+
1106
1134
  // ciLand — dispatch the 'ci' confirmed->shipped land to the local or the
1107
1135
  // server-mediated path. Async because the server path awaits GDS API calls.
1108
1136
  async function ciLand(taskId, branch, valueSummary) {
@@ -1129,7 +1157,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
1129
1157
  try {
1130
1158
  if (!gitArgvOk(['bundle', 'create', bundlePath, `origin/main..${branch}`], { stdio: 'ignore' })) {
1131
1159
  console.log(` ci-land: could not build the branch bundle (nothing new to push, or a git error); task stays at confirmed. ${strandNextStep(taskId, 'server')}`);
1132
- return false;
1160
+ return landBailed('could not build the branch bundle', strandCommand(taskId, 'server'));
1133
1161
  }
1134
1162
  const bundleB64 = fs.readFileSync(bundlePath).toString('base64');
1135
1163
  // allowBoxScope: handle the box_scope 403 locally (graceful "stays at
@@ -1142,16 +1170,16 @@ async function ciLandServer(taskId, branch, valueSummary) {
1142
1170
  });
1143
1171
  if (r.status === 403 && r.data && r.data.error === 'box_scope') {
1144
1172
  console.log(' ci-land: this session is BOX-SCOPED — run /builder-reauth (browser, no terminal) to mint a real session, then re-ship. Task stays at confirmed.');
1145
- return false;
1173
+ return landBailed('this CLI session is box-scoped', '/builder-reauth');
1146
1174
  }
1147
1175
  if (r.status === 503) {
1148
1176
  console.log(' ci-land: server-mediated push is not configured on the server yet (no GitHub App nor GITHUB_PUSH_TOKEN — operator action, ADR 0055). Task stays at confirmed.');
1149
- return false;
1177
+ return landBailed('server-mediated push is not configured (operator action)', strandCommand(taskId, 'server'));
1150
1178
  }
1151
1179
  if (!r.ok) {
1152
1180
  const e = (r.data && (r.data.message || r.data.error)) || `HTTP ${r.status}`;
1153
1181
  console.log(` ci-land: server publish failed — ${e}. Task stays at confirmed. ${strandNextStep(taskId, 'server')}`);
1154
- return false;
1182
+ return landBailed(`server publish failed — ${e}`, strandCommand(taskId, 'server'));
1155
1183
  }
1156
1184
  console.log(` ci-land: server pushed ${branch} + PR ${r.data.pr_url} (auto-merge ${r.data.auto_merge ? 'enabled' : 'NOT enabled — merge it on GitHub'}).`);
1157
1185
  return pollServerPublish(taskId, branch);
@@ -1230,17 +1258,33 @@ function strandNextStep(taskId, via) {
1230
1258
  return `Run /merge-mode to land it — or re-run \`node scripts/gds/ship.js ${taskId}\`, which resumes from here (task 941).`;
1231
1259
  }
1232
1260
 
1233
- // Print the standard "published — it'll land on its own" success-pending banner
1261
+ // The standard "published — it'll land on its own" success-pending banner
1234
1262
  // (task 1049). Shared by both poll paths so the message is identical and never
1235
1263
  // reads as a failure. The task legitimately stays at 'confirmed' until CI lands
1236
1264
  // it; the server reconciles it to 'shipped' (publish-status reconcile-on-read)
1237
1265
  // and a re-run resumes (task 941).
1238
- function printLandPending(taskId, { merged } = {}) {
1266
+ //
1267
+ // task 1002606 (finding P-3): the banner used to end at "NOT a failure", so when
1268
+ // a land DID fail (hand-written conflict, PR closed unmerged, persistent red
1269
+ // check) the builder's last signal said everything was fine and the strand sat
1270
+ // invisible until they happened to look. The banner now tells the failure story
1271
+ // too: the server flags a strand needs_rebase (definitive shapes within a sweep,
1272
+ // ambiguous ones at ~30 min), /builder-start warns, and new claims gate on it.
1273
+ // Pure + exported so the wording is pinned by tests/ship_ci_deploy.mjs.
1274
+ function landPendingLines(taskId, { merged } = {}) {
1275
+ return [
1276
+ ` ci-land: ✓ Published — auto-merge is armed${merged ? ' and the PR has merged' : ''}. This is NOT a failure.`,
1277
+ ` CI is still finishing; GitHub will ${merged ? 'deploy' : 'merge + deploy'} automatically once the required checks pass,`,
1278
+ ` and the task flips to 'shipped' on its own. Re-run \`node scripts/gds/ship.js ${taskId}\` anytime to confirm`,
1279
+ ` it landed — it resumes from here (task 941). The task is at 'confirmed' until the deploy completes.`,
1280
+ ` If it CANNOT land on its own (a hand-written merge conflict, the PR closed without merging, a check`,
1281
+ ` that keeps failing), the server flags the task needs_rebase — /builder-start will warn you and new`,
1282
+ ` claims are gated until you fix + re-land it (re-run the ship after fixing), or release it.`,
1283
+ ];
1284
+ }
1285
+ function printLandPending(taskId, opts = {}) {
1239
1286
  console.log('');
1240
- console.log(` ci-land: ✓ Published — auto-merge is armed${merged ? ' and the PR has merged' : ''}. This is NOT a failure.`);
1241
- console.log(` CI is still finishing; GitHub will ${merged ? 'deploy' : 'merge + deploy'} automatically once the required checks pass,`);
1242
- console.log(` and the task flips to 'shipped' on its own. Re-run \`node scripts/gds/ship.js ${taskId}\` anytime to confirm`);
1243
- console.log(` it landed — it resumes from here (task 941). The task is at 'confirmed' until the deploy completes.`);
1287
+ for (const line of landPendingLines(taskId, opts)) console.log(line);
1244
1288
  }
1245
1289
 
1246
1290
  // One module-scope sleep command shared by both poll loops (task 1093 / C9). A
@@ -1264,17 +1308,17 @@ async function pollServerPublish(taskId, branch) {
1264
1308
  const outcome = classifyLandOutcome(r.data);
1265
1309
  if (outcome === 'shipped') {
1266
1310
  console.log(` ci-land: merged (${String(r.data.merge_sha || '').slice(0, 8)}) + deploy-prod succeeded. Prod is live.`);
1267
- return true;
1311
+ return landed();
1268
1312
  }
1269
1313
  if (outcome === 'failed') {
1270
1314
  console.log(` ci-land: PR was CLOSED without merging (a required check failed, or it was closed manually). Fix it and re-run \`node scripts/gds/ship.js ${taskId}\` to resume. Task stays at 'confirmed'.`);
1271
- return false;
1315
+ return landBailed('the PR was closed without merging (a check failed, or it was closed manually)', `node scripts/gds/ship.js ${taskId}`);
1272
1316
  }
1273
1317
  }
1274
1318
  shellOk(POLL_SLEEP_CMD);
1275
1319
  }
1276
1320
  printLandPending(taskId);
1277
- return false;
1321
+ return landPending();
1278
1322
  }
1279
1323
 
1280
1324
  // attestGateTrust — task 1169 (ADR 0058). Ask the SERVER to post the trustless
@@ -1346,7 +1390,7 @@ async function ciLandLocal(taskId, branch, valueSummary) {
1346
1390
  console.log(` pushing ${branch} to origin...`);
1347
1391
  if (!gitArgvOk(['push', 'origin', branch])) {
1348
1392
  console.log(' ci-land: push failed; task stays at confirmed. Fix the push and re-ship.');
1349
- return false;
1393
+ return landBailed(`could not push ${branch} to origin`, `node scripts/gds/ship.js ${taskId}`);
1350
1394
  }
1351
1395
 
1352
1396
  // task 1169 (ADR 0058): attest the author's trust tier on the head SHA BEFORE
@@ -1377,7 +1421,7 @@ async function ciLandLocal(taskId, branch, valueSummary) {
1377
1421
  console.log(' ci-land: opening PR -> main...');
1378
1422
  if (!ghArgvOk(buildPrCreateArgv(branch, title, body))) {
1379
1423
  console.log(' ci-land: `gh pr create` failed. Is the GitHub CLI installed + authenticated (`gh auth status`)? Open the PR manually; task stays at confirmed.');
1380
- return false;
1424
+ return landBailed('`gh pr create` failed (is gh installed + authenticated?)', `node scripts/gds/ship.js ${taskId}`);
1381
1425
  }
1382
1426
  } else {
1383
1427
  console.log(` ci-land: reusing open PR ${existing.url}`);
@@ -1428,7 +1472,7 @@ function pollCiDeploy(taskId, branch, headSha) {
1428
1472
  }
1429
1473
  if (pr && pr.state === 'CLOSED') {
1430
1474
  console.log(` ci-land: PR was CLOSED without merging (a required check failed, or it was closed manually). Fix it and re-run \`node scripts/gds/ship.js ${taskId}\` to resume. Task stays at 'confirmed'.`);
1431
- return false;
1475
+ return landBailed('the PR was closed without merging (a check failed, or it was closed manually)', `node scripts/gds/ship.js ${taskId}`);
1432
1476
  }
1433
1477
  shellOk(POLL_SLEEP_CMD);
1434
1478
  }
@@ -1436,7 +1480,7 @@ function pollCiDeploy(taskId, branch, headSha) {
1436
1480
  // task 1049: PR open + auto-merge armed but not merged in-window = the publish
1437
1481
  // SUCCEEDED and is still in CI — success-pending, not a failure.
1438
1482
  printLandPending(taskId);
1439
- return false;
1483
+ return landPending();
1440
1484
  }
1441
1485
  console.log(` ci-land: merged to main (${mergeSha.slice(0, 8)}). Waiting for the prod deploy...`);
1442
1486
 
@@ -1464,14 +1508,14 @@ function pollCiDeploy(taskId, branch, headSha) {
1464
1508
  } else {
1465
1509
  console.log(' ci-land: deploy run succeeded but /version + healthz probes both missed (likely transient). Treating the successful deploy run as authoritative.');
1466
1510
  }
1467
- return true;
1511
+ return landed();
1468
1512
  }
1469
1513
  shellOk(POLL_SLEEP_CMD);
1470
1514
  }
1471
1515
  // task 1049: merged but the deploy run hasn't confirmed in-window — still
1472
1516
  // success-pending (it deploys on its own), not a failure.
1473
1517
  printLandPending(taskId, { merged: true });
1474
- return false;
1518
+ return landPending();
1475
1519
  }
1476
1520
 
1477
1521
  // ---------- Step 0: preflight (working tree + branch freshness) ----------
@@ -1997,6 +2041,32 @@ function runSmoke() {
1997
2041
  return { ran: true, clean: okPms };
1998
2042
  }
1999
2043
 
2044
+ // task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
2045
+ // wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
2046
+ // FAIL used to fall silently through into the claim resolve — claim consumed,
2047
+ // grading skipped with no message, task parked at 'completed', exit 0 — the
2048
+ // choke-point map's likeliest "my task vanished" shape. `abort` is the default
2049
+ // (printed BEFORE the resolve; the claim stays active); `parked` is the
2050
+ // --allow-smoke-fail continuation (printed where the grader would have run, so
2051
+ // the builder knows exactly where the task stopped and how to resume).
2052
+ function smokeGateMessages(taskId) {
2053
+ return {
2054
+ abort: [
2055
+ 'Ship ABORTED: the production smoke test FAILED — see the FAIL line(s) above.',
2056
+ 'No claim resolved, no credits awarded — your claim is still active and nothing changed.',
2057
+ 'The smoke suite hits LIVE endpoints (your change is not deployed yet), so this usually means production is briefly degraded — not that your change broke it.',
2058
+ ` - Re-run once it recovers: node scripts/gds/ship.js ${taskId}`,
2059
+ ` - Ship anyway (deliberate): node scripts/gds/ship.js ${taskId} --allow-smoke-fail`,
2060
+ ` (parks the task at 'completed' ungraded; resume later with --regrade)`,
2061
+ ],
2062
+ parked: [
2063
+ '--allow-smoke-fail: smoke FAILED and the ship deliberately stops here.',
2064
+ `Task #${taskId} is parked at 'completed' — ungraded, unconfirmed, no credits awarded.`,
2065
+ `Resume when production is healthy: node scripts/gds/ship.js ${taskId} --regrade`,
2066
+ ],
2067
+ };
2068
+ }
2069
+
2000
2070
  // ---------- Step 3: merge + deploy chain ----------
2001
2071
 
2002
2072
  function currentBranch() {
@@ -2263,7 +2333,10 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2263
2333
  const branch = currentBranch();
2264
2334
  if (!branch || branch === 'main' || branch === 'HEAD') {
2265
2335
  console.log(` auto-merge: skipped (current branch is ${branch})`);
2266
- return false;
2336
+ // The merge source IS the current branch, so on main there is nothing to
2337
+ // merge. Half of the single-checkout catch-22 this task documents: the other
2338
+ // half is the "main worktree is on <branch>, not main" bail below.
2339
+ return landBailed(`nothing to merge — this checkout is on ${branch}, not a task branch`, '/merge-mode');
2267
2340
  }
2268
2341
 
2269
2342
  // Refresh the data-driven diagrams before computing commits-ahead, so a
@@ -2299,7 +2372,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2299
2372
  const fetchedMain = shellOk('git fetch origin main');
2300
2373
  if (!fetchedMain && shouldBailOnFetchFailure(fetchedMain, deployMode, pushVia())) {
2301
2374
  console.log(' auto-merge: fetch failed; task stays at confirmed for /merge-mode');
2302
- return false;
2375
+ return landBailed('could not fetch origin/main', '/merge-mode');
2303
2376
  }
2304
2377
  const commitsAhead = gitOk('git rev-list origin/main..HEAD --count');
2305
2378
  const verdict = zeroCommitVerdict(commitsAhead, dbOnly);
@@ -2318,11 +2391,11 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2318
2391
  console.error(' DB — nothing to merge or deploy), re-run with --db-only:');
2319
2392
  console.error(` node scripts/gds/ship.js ${taskId} --db-only`);
2320
2393
  console.error(' The task stays at confirmed; nothing was merged or deployed.');
2321
- return false;
2394
+ return landBailed('no commits ahead of main, and --db-only was not passed', `node scripts/gds/ship.js ${taskId} --db-only`);
2322
2395
  }
2323
2396
  if (verdict === 'db-only') {
2324
2397
  console.log(' auto-merge: deliberate empty ship (--db-only / --allow-empty) - no commits ahead of main; advancing to shipped without merge/deploy.');
2325
- return true;
2398
+ return landed();
2326
2399
  }
2327
2400
 
2328
2401
  // #679 / ADR 0042: in 'ci' deploy mode the operator's machine never merges or
@@ -2336,7 +2409,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2336
2409
  console.log(` pushing ${branch} to origin...`);
2337
2410
  if (!gitArgvOk(['push', 'origin', branch])) {
2338
2411
  console.log(' auto-merge: push failed; task stays at confirmed for /merge-mode');
2339
- return false;
2412
+ return landBailed(`could not push ${branch} to origin`, '/merge-mode');
2340
2413
  }
2341
2414
 
2342
2415
  // Serialize the shared-main-worktree merge (#462 C4; #269 C6). Concurrent
@@ -2345,7 +2418,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2345
2418
  // than racing its working tree.
2346
2419
  if (!mergeLock.acquire(REPO_ROOT)) {
2347
2420
  console.log(' auto-merge: another active ship or /merge-mode run holds the merge lock; task stays at confirmed for /merge-mode');
2348
- return false;
2421
+ return landBailed('another ship or /merge-mode run holds the merge lock', '/merge-mode');
2349
2422
  }
2350
2423
  let mergedClean = false;
2351
2424
  try {
@@ -2355,7 +2428,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2355
2428
  const busy = mainWorktreeBusy();
2356
2429
  if (busy) {
2357
2430
  console.log(` auto-merge: main worktree busy (${busy}); task stays at confirmed for /merge-mode`);
2358
- return false;
2431
+ return landBailed(`the main worktree is busy (${busy})`, '/merge-mode');
2359
2432
  }
2360
2433
 
2361
2434
  console.log(` switching to main worktree (${REPO_ROOT}) for merge...`);
@@ -2369,19 +2442,26 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2369
2442
  }
2370
2443
  if (!shellOk(`git fetch origin`, { cwd: REPO_ROOT })) {
2371
2444
  console.log(' auto-merge: fetch failed; task stays at confirmed for /merge-mode');
2372
- return false;
2445
+ return landBailed('could not fetch origin in the main worktree', '/merge-mode');
2373
2446
  }
2374
2447
 
2375
2448
  // The main worktree must currently have main checked out.
2376
2449
  const onMain = gitOk('git rev-parse --abbrev-ref HEAD', { cwd: REPO_ROOT });
2377
2450
  if (onMain !== 'main') {
2378
2451
  console.log(` auto-merge: main worktree is on ${onMain}, not main; task stays at confirmed`);
2379
- return false;
2452
+ // The other half of the single-checkout catch-22: the merge target tree is
2453
+ // the one holding the feature branch, so there is nowhere to merge INTO.
2454
+ // Name the cure, not just the symptom — this is the bail that used to be
2455
+ // invisible behind "landing automatically".
2456
+ return landBailed(
2457
+ `the main checkout is on ${onMain}, not main — work a task in its own worktree so the main checkout stays on main`,
2458
+ '/merge-mode',
2459
+ );
2380
2460
  }
2381
2461
 
2382
2462
  if (!gitArgvOk(['merge', '--ff-only', 'origin/main'], { cwd: REPO_ROOT })) {
2383
2463
  console.log(' auto-merge: main is ahead of origin somehow; task stays at confirmed');
2384
- return false;
2464
+ return landBailed('the local main is ahead of origin/main', '/merge-mode');
2385
2465
  }
2386
2466
 
2387
2467
  // #681: build the merge argv and run it WITHOUT a shell. valueSummary is
@@ -2389,13 +2469,13 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2389
2469
  // `$(...)`/backticks in it can never be interpreted as a command.
2390
2470
  if (!gitArgvOk(buildMergeArgv(branch, valueSummary), { cwd: REPO_ROOT })) {
2391
2471
  console.log(' auto-merge: merge failed (conflicts?); task stays at confirmed for manual /merge-mode');
2392
- return false;
2472
+ return landBailed(`merging ${branch} into main failed (conflicts?)`, '/merge-mode');
2393
2473
  }
2394
2474
 
2395
2475
  console.log(' smoke against post-merge state...');
2396
2476
  if (!runSmoke().clean) {
2397
2477
  console.log(' auto-merge: post-merge smoke failed; task stays at confirmed; investigate');
2398
- return false;
2478
+ return landBailed('post-merge smoke tests failed', '/merge-mode');
2399
2479
  }
2400
2480
 
2401
2481
  console.log(' pushing merged main to origin...');
@@ -2406,7 +2486,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2406
2486
  // cross-platform reason as the other node spawns here (#660).
2407
2487
  if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'push-main.js'))}`, { cwd: REPO_ROOT })) {
2408
2488
  console.log(' auto-merge: push failed; task stays at confirmed');
2409
- return false;
2489
+ return landBailed('could not push the merged main to origin', '/merge-mode');
2410
2490
  }
2411
2491
 
2412
2492
  // Deploy step. The core carries NO deploy target of its own — under the
@@ -2445,11 +2525,11 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2445
2525
  console.log(' auto-merge: /healthz 200 on re-probe — deploy actually succeeded (deploy.sh raced the restart window). Proceeding.');
2446
2526
  } else {
2447
2527
  console.log(' auto-merge: deploy failed (/healthz never returned 200 after retries); task stays at confirmed');
2448
- return false;
2528
+ return landBailed('merged to main, but the deploy failed (/healthz never returned 200)', '/merge-mode');
2449
2529
  }
2450
2530
  } else {
2451
2531
  console.log(' auto-merge: ~/deploy.sh returned non-zero and no healthzUrl configured to re-probe; task stays at confirmed');
2452
- return false;
2532
+ return landBailed('merged to main, but ~/deploy.sh returned non-zero (no healthz to re-probe)', '/merge-mode');
2453
2533
  }
2454
2534
  }
2455
2535
  }
@@ -2470,7 +2550,7 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2470
2550
 
2471
2551
  console.log(' flipping task to shipped...');
2472
2552
  mergedClean = true;
2473
- return true;
2553
+ return landed();
2474
2554
  } finally {
2475
2555
  // Leave REPO_ROOT clean for the NEXT ship/merge no matter how we exited.
2476
2556
  // A failed merge here wedges everyone else: a conflict leaves the tree
@@ -2759,14 +2839,19 @@ async function mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOn
2759
2839
  if (advanceToMerge && skipMerge) {
2760
2840
  console.log('');
2761
2841
  console.log('--no-merge: stopping at confirmed. Run /merge-mode later to land it.');
2762
- return;
2842
+ return landBailed('--no-merge was passed, so the land was skipped', '/merge-mode');
2763
2843
  }
2764
- if (!advanceToMerge) return;
2844
+ // Grade failed: the card renders the 'failed' stage ('not deployed'), so there
2845
+ // is no land outcome to report.
2846
+ if (!advanceToMerge) return null;
2765
2847
 
2766
2848
  console.log('');
2767
2849
  console.log('Auto-merge chain (confirmed → shipped):');
2768
2850
  const merged = await autoMerge(taskId, summary, { dbOnly });
2769
- if (!merged) return;
2851
+ // NOTE: `merged` is an OBJECT now — every value is truthy, so this MUST test
2852
+ // .ok. A bare `if (!merged)` would fall through on a bail and POST /ship for a
2853
+ // task that never landed.
2854
+ if (!merged.ok) return merged;
2770
2855
 
2771
2856
  const ship = await (await cliClient()).tasks.postTasksIdShip({ id: taskId, body: {} });
2772
2857
  if (!ship.ok) {
@@ -2777,7 +2862,11 @@ async function mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOn
2777
2862
  console.log(`Task #${taskId} is now: shipped (the server reconciled it from the merged PR).`);
2778
2863
  } else {
2779
2864
  console.error(` /tasks/${taskId}/ship failed:`, ship.data);
2780
- return;
2865
+ // The merge itself succeeded — only the status flip failed, so a re-run
2866
+ // resumes cleanly (task 941). Report it rather than letting the card claim
2867
+ // the land is in progress.
2868
+ const e = (ship.data && (ship.data.message || ship.data.error)) || `HTTP ${ship.status}`;
2869
+ return landBailed(`merged, but the ship call failed — ${e}`, `node scripts/gds/ship.js ${taskId}`);
2781
2870
  }
2782
2871
  } else {
2783
2872
  console.log(`Task #${taskId} is now: shipped`);
@@ -2815,6 +2904,7 @@ async function mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOn
2815
2904
  // server-side in ship-broadcast.postShipBroadcast, so it is preserved.
2816
2905
  console.log(' Discord broadcast: deferred to the server — it posts #ship-feed after the deploy lands.');
2817
2906
  }
2907
+ return merged;
2818
2908
  }
2819
2909
 
2820
2910
  // Memory + session-digest push, gated on a successful land (confirmed/shipped).
@@ -2891,7 +2981,10 @@ function postShipCloseout(taskId) {
2891
2981
  }
2892
2982
 
2893
2983
  async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine, card, dbOnly = false }) {
2894
- await mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOnly });
2984
+ // task 1002541: the land outcome carries WHY a land didn't happen. Without it
2985
+ // the card had only the status to go on and printed 'landing automatically'
2986
+ // over a merge that had already bailed.
2987
+ const land = await mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOnly });
2895
2988
  // task 1719: mergeAndDeploy may have landed the task to 'shipped' IN-SESSION — the
2896
2989
  // enable-success poll (pollCiDeploy) or, since task 1718, the enable-failure server
2897
2990
  // poll (pollServerPublish). finishShip was handed postGradeStatus from the CONFIRM
@@ -2924,7 +3017,7 @@ async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGrad
2924
3017
  // always-printed fallback + the headless summary). Strictly best-effort.
2925
3018
  // task 1719: pass the reconciled status so a just-landed ship shows SHIPPED (and
2926
3019
  // writes no breadcrumb), not the stale pre-merge 'confirmed'.
2927
- await printCompletionCard({ taskId, postGradeStatus: landedStatus, card, usage });
3020
+ await printCompletionCard({ taskId, postGradeStatus: landedStatus, card, usage, land });
2928
3021
  // task 932: reconcile the working tree — ONLY on a successful land. A
2929
3022
  // grade-failed ship stays at 'completed'; leave that tree untouched so the
2930
3023
  // builder can fix findings + re-ship.
@@ -2959,7 +3052,7 @@ function diffstatFor(baseline) {
2959
3052
  // in-feed HTML to a temp file and prints an [otb-card-html] marker the
2960
3053
  // builder-ship skill picks up to render it in the chat feed. Every step is
2961
3054
  // best-effort — a summary card must NEVER fail or undo a completed ship.
2962
- async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
3055
+ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land = null }) {
2963
3056
  if (!card) return;
2964
3057
  try {
2965
3058
  const shipCard = require('../../modules/lifecycle/ship-card');
@@ -3029,6 +3122,10 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
3029
3122
  credits: { awarded: card.creditsAwarded, total, perTaskCredit: rewardPolicy(serverReward).perTaskCredit },
3030
3123
  usage: { tokens, costUsd, newTokens, cachedTokens },
3031
3124
  deploy: { commit },
3125
+ // task 1002541: absent (the default) ⇒ the card behaves exactly as before,
3126
+ // which keeps every other caller — resumeMerge, the claim card, and
3127
+ // land-watch replaying breadcrumbs written by older versions — unchanged.
3128
+ land,
3032
3129
  };
3033
3130
  const model = shipCard.buildCardModel(cardInputs);
3034
3131
 
@@ -3179,7 +3276,7 @@ async function resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly = f
3179
3276
  // The first ship stored value_summary; reuse it so the merge commit + broadcast
3180
3277
  // carry meaningful text even when --summary is omitted on the re-run.
3181
3278
  const effSummary = summary || task?.value_summary || '';
3182
- await mergeAndDeploy({ taskId, summary: effSummary, advanceToMerge: true, skipMerge, dbOnly });
3279
+ const land = await mergeAndDeploy({ taskId, summary: effSummary, advanceToMerge: true, skipMerge, dbOnly });
3183
3280
  const usage = await postShipSync(taskId, 'confirmed');
3184
3281
  console.log('');
3185
3282
  console.log('Done — merge/deploy resume complete.');
@@ -3192,7 +3289,7 @@ async function resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly = f
3192
3289
  const t = await (await cliClient()).tasks.getTasksId({ id: taskId });
3193
3290
  const status = (t.ok && t.data && t.data.task) ? t.data.task.status : 'shipped';
3194
3291
  await printCompletionCard({
3195
- taskId, postGradeStatus: status, usage,
3292
+ taskId, postGradeStatus: status, usage, land,
3196
3293
  card: { title: task?.title, valueSummary: effSummary, notes: '', creditsAwarded: null },
3197
3294
  });
3198
3295
  } catch (_) { /* the resume already succeeded; the card is a bonus */ }
@@ -3205,7 +3302,7 @@ async function shipMain() {
3205
3302
  const args = process.argv.slice(2);
3206
3303
  const taskId = Number(args[0]);
3207
3304
  if (!Number.isFinite(taskId)) {
3208
- console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--base <ref>]');
3305
+ console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
3209
3306
  process.exit(2);
3210
3307
  }
3211
3308
  const notes = arg('--notes', args) || '';
@@ -3235,6 +3332,11 @@ async function shipMain() {
3235
3332
  // empty diff is refused (it used to slip through under --skip-grade — the
3236
3333
  // task-1363 phantom ship). Never overrides a dirty working tree.
3237
3334
  const allowEmpty = args.includes('--allow-empty');
3335
+ // task 1002605 (G-18): explicit opt-in to continue past a FAILED production
3336
+ // smoke run. Without it a smoke FAIL now ABORTS before the claim resolves
3337
+ // (claim intact); with it, the resolve proceeds and the task deliberately
3338
+ // parks at 'completed' ungraded — and the ship says so instead of "Done."
3339
+ const allowSmokeFail = args.includes('--allow-smoke-fail');
3238
3340
  // Surprise flag handling — see captureSurpriseLearning(). Resolved below
3239
3341
  // once we know which claim is being shipped (so touches[] can become tags).
3240
3342
  const surpriseRaw = arg('--surprise', args);
@@ -3303,16 +3405,16 @@ async function shipMain() {
3303
3405
  // ---- Hard cap: refuse to ship over the monthly budget (V3.R84 #302, C3) ----
3304
3406
  // Shipping runs the subagent grader, which spends API $. If this builder is at
3305
3407
  // or over their monthly budget, refuse here — BEFORE the grader runs and BEFORE
3306
- // the claim resolves, so the claim stays active and no cost is incurred. An
3307
- // Archon lifts the block by raising the builder's budget
3308
- // (PATCH /builders/:id/budget). Fail-open: a missing/unreadable cost status
3309
- // (e.g. an older server) never blocks a ship.
3408
+ // the claim resolves, so the claim stays active and no cost is incurred. Any
3409
+ // Metic+ builder lifts the block by raising the builder's budget
3410
+ // (PATCH /builders/:id/budget — builder.budget.set, metic-floored since ADR 0157).
3411
+ // Fail-open: a missing/unreadable cost status (e.g. an older server) never blocks a ship.
3310
3412
  const costStatus = me.data.cost;
3311
3413
  if (costStatus && costStatus.level === 'over') {
3312
3414
  console.error('');
3313
3415
  console.error(`Ship refused: monthly budget exceeded — $${costStatus.spend_mtd_usd} of $${costStatus.budget_mtd_usd} spent this month (${costStatus.pct_used}%).`);
3314
3416
  console.error('Shipping runs the quality grader, which spends API budget. No claim resolved, no credits awarded.');
3315
- console.error('Ask an Archon to raise your monthly budget (PATCH /builders/:id/budget), then re-ship.');
3417
+ console.error('Ask any Metic or Archon to raise your monthly budget (PATCH /builders/:id/budget), then re-ship.');
3316
3418
  process.exit(1);
3317
3419
  }
3318
3420
 
@@ -3539,6 +3641,19 @@ async function shipMain() {
3539
3641
  console.log(`Verification: smoke=${smoke.clean ? 'clean' : 'FAIL'}, overall=${passed ? 'PASS' : 'FAIL'}`);
3540
3642
  console.log('');
3541
3643
 
3644
+ // task 1002605 (G-18): a smoke FAIL used to fall straight through into the
3645
+ // claim resolve — claim consumed, grading silently skipped, task parked at
3646
+ // 'completed', exit 0, and the builder read "Done." believing they shipped.
3647
+ // Abort BEFORE the resolve instead — the same claim-preserving posture as the
3648
+ // budget cap and the sandbox-review gate above. --allow-smoke-fail is the
3649
+ // deliberate continuation; the parked message where the grader would have run
3650
+ // says exactly where that leaves the task.
3651
+ if (!passed && !allowSmokeFail) {
3652
+ console.error('');
3653
+ for (const line of smokeGateMessages(taskId).abort) console.error(line);
3654
+ process.exit(1);
3655
+ }
3656
+
3542
3657
  // ---- Step 1: resolve the claim (lands at completed) ----
3543
3658
  // V3.R18 (#209): resolveClaim no longer runs the v1 deterministic grader.
3544
3659
  // It now lands the task at `completed` when verification (smoke) passes, and
@@ -3673,6 +3788,12 @@ async function shipMain() {
3673
3788
  ideaBonusInfo = gradePost.data.ideaBonus ?? null;
3674
3789
 
3675
3790
  printGradeOutcome(g, taskId);
3791
+ } else if (!passed && taskStatus === 'completed') {
3792
+ // task 1002605 (G-18): the --allow-smoke-fail continuation. The resolve above
3793
+ // consumed the claim with verification.passed=false, so the grader deliberately
3794
+ // does not run. This used to be a silent fall-through; say where the task is.
3795
+ console.log('');
3796
+ for (const line of smokeGateMessages(taskId).parked) console.log(line);
3676
3797
  }
3677
3798
 
3678
3799
  printCreditsAndAchievements({
@@ -3898,6 +4019,10 @@ module.exports = {
3898
4019
  // #943: pure grade-outcome formatter — exported so the grader-unavailable vs
3899
4020
  // FAIL display branch is unit-tested without capturing stdout.
3900
4021
  formatGradeOutcome,
4022
+ // task 1002605 (G-18): the pre-resolve smoke gate's abort/parked wording —
4023
+ // exported pure so the loudness contract (claim intact on abort; parked +
4024
+ // resume path on --allow-smoke-fail) is pinned without running a ship.
4025
+ smokeGateMessages,
3901
4026
  // task 941 (V4.R03): pure resume-action decision for a claim-less `ship.js <id>`
3902
4027
  // re-run — exported so the truth table (completed→grade, confirmed→merge,
3903
4028
  // ownership, terminal states) is unit-tested with no git/network.
@@ -3926,6 +4051,11 @@ module.exports = {
3926
4051
  // dead-end strand at confirmed), enable-success watches GitHub. Exported so the
3927
4052
  // regression test locks in that a worktree enable-failure still waits for the land.
3928
4053
  landPollAfterAutoMerge,
4054
+ // task 1002541: the land-outcome constructors. Exported so the test can pin the
4055
+ // trap that made this a 3-way instead of a boolean — every outcome is a TRUTHY
4056
+ // object, so a consumer MUST test `.ok`; the old `if (!merged)` would sail past
4057
+ // a bail. Also pins pending (a poll timeout) as distinct from bailed.
4058
+ landed, landPending, landBailed, strandCommand,
3929
4059
  // task 1719: pure post-land status reconciler — a task that reached 'shipped'
3930
4060
  // in-session shows the SHIPPED banner + writes no breadcrumb; otherwise keep the
3931
4061
  // pre-merge status (confirmed → next-turn breadcrumb fallback). Exported for tests.
@@ -3944,6 +4074,9 @@ module.exports = {
3944
4074
  // task 1052 (E): box-aware "what to do next" for a confirmed-strand — a box
3945
4075
  // (via 'server') is pointed at the resume, never at gh-only /merge-mode.
3946
4076
  strandNextStep,
4077
+ // task 1002606: the success-pending banner lines, pure — pinned so the strand
4078
+ // detection story ("NOT a failure" + how you WILL find out if it fails) can't drift.
4079
+ landPendingLines,
3947
4080
  // #857: resolveDeployMode (env + committed config) so doctor.js reports the
3948
4081
  // active deploy mode exactly as ship will use it.
3949
4082
  resolveDeployMode,
@@ -44,7 +44,7 @@ const EMPTY = process.argv.includes('--empty');
44
44
  // --rank: view the hall as a different rank — the /me fixture's builder.rank is
45
45
  // overridden in the response, so the Archon-gated pages (watch, harbor, gate)
46
46
  // render their real content instead of the sealed notice. Preview-only theatre:
47
- // the real pages are server-gated (requireArchonPage / ADR 0016).
47
+ // the real pages are server-gated (requireGovernancePage / ADR 0016).
48
48
  const RANK_OVERRIDE = arg('--rank');
49
49
  const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
50
50
  // --modules-on: force-report modules as enabled — a dev box's local config
@@ -51,7 +51,7 @@ fi
51
51
 
52
52
  DB="${PGDATABASE:-amazonprimea}"
53
53
 
54
- # Test/ops seam (task 1002611): --print-migrations walks the REAL core-migration loop but reports
54
+ # Test/ops seam (added with the 1.18.19 space-in-path fix): --print-migrations walks the REAL core-migration loop but reports
55
55
  # each path instead of applying it, and touches NO DB. tests/migrate_root_resolution.mjs uses it to
56
56
  # assert that a checkout whose path contains a space still yields whole, readable paths. Reporting
57
57
  # through the real loop (rather than a copy of it in the test) is the point: if the loop ever reverts
@@ -107,7 +107,7 @@ fi
107
107
  # key, so moving a file between migrations/ and migrations/game/ does NOT re-run it on prod.
108
108
  #
109
109
  # The two counters are what the no-op guard below reads: a run that LISTED core migrations but
110
- # neither applied nor recognized a single one is the space-in-path failure (task 1002611) and must
110
+ # neither applied nor recognized a single one is the space-in-path failure (fixed in 1.18.19) and must
111
111
  # be fatal, not silent. Both loops that call this run in the CURRENT shell (herestring / plain glob,
112
112
  # never a `... | while` pipeline), so these increments are visible to the caller.
113
113
  MIGRATIONS_APPLIED=0
@@ -160,7 +160,7 @@ fi
160
160
  # affects whether it re-runs on OTB prod. Today CORE_ROOT == INSTANCE_ROOT (single checkout);
161
161
  # once the core installs from node_modules the two roots diverge and this still merges correctly.
162
162
  #
163
- # PATHS WITH SPACES (task 1002611): this list is NEWLINE-separated and must be read a line at a
163
+ # PATHS WITH SPACES (fixed in 1.18.19): this list is NEWLINE-separated and must be read a line at a
164
164
  # time. The old `for f in $MIGRATIONS` word-split it on IFS — which includes space — so any checkout
165
165
  # whose absolute path contains a space ("/Users/.../Hermeslines Marketing/...") fragmented every
166
166
  # path, the `[ -e ]` guard below silently dropped all of them, and ZERO core migrations ran.
@@ -191,7 +191,7 @@ while IFS= read -r f; do
191
191
  apply_migration "$f"
192
192
  done <<< "$MIGRATIONS"
193
193
 
194
- # --- No-op guard (task 1002611) -------------------------------------------
194
+ # --- No-op guard (added in 1.18.19) -------------------------------------------
195
195
  # Silence is what made the space-in-path bug expensive to diagnose: core migrations were skipped
196
196
  # without a word, and the first error the operator saw came from an unrelated MODULE migration
197
197
  # hitting the empty schema. If the glob LISTED core migrations but we neither applied nor recognized
@@ -289,7 +289,7 @@ if [ "${OTB_POST_DEPLOY_CHECK:-0}" = "1" ]; then
289
289
  echo "post-deploy: no prior score ledger — running in baseline-only mode"
290
290
  fi
291
291
  # `${VAR:+"$VAR"}` expands to NOTHING when empty and to the quoted value otherwise. A bare
292
- # $BASELINE_FLAG relied on word-splitting to vanish (the habit that caused task 1002611), and
292
+ # $BASELINE_FLAG relied on word-splitting to vanish (the habit that caused the space-in-path migration skip), and
293
293
  # quoting it outright would instead pass an empty argv entry that argparse rejects. An array
294
294
  # would need bash 4 — `set -u` + `"${arr[@]}"` on an empty array aborts under the bash 3.2 that
295
295
  # macOS still ships, and this script's shebang takes whatever bash is on PATH.