@bongos/core 1.21.122 → 1.21.123

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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.122",
6
- "core_contract": "1.21.122",
7
- "source_commit": "1744caf4412e029c7cf9b465a9fae581bc2deea7",
5
+ "core_version": "1.21.123",
6
+ "core_contract": "1.21.123",
7
+ "source_commit": "bfe95a4e390577d4579a3da16d81d5ada6bc8b9a",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-14T19:02:19.295Z",
9
+ "built_at": "2026-10-14T19:30:01.919Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 605,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3120,
14
+ "functional_verbatim": 3122,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3754,
20
- "tree_sha256": "df175c2d7b99fee7204cf032575b41a9fa13988b1a3635717568822cf1d84d6b",
19
+ "file_count": 3756,
20
+ "tree_sha256": "c5becc4dfe0e822803424a46f1041b66adce70cbe65f85be941603de3db80cee",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -107,7 +107,7 @@
107
107
  {
108
108
  "path": ".claude/skills/builder-ship/SKILL.md",
109
109
  "mode": "0000644",
110
- "sha256": "1053b6160f72c6f78d9608b8431ba59b707fb310493fdb24ea52efd52f1ef825"
110
+ "sha256": "bac025cc3ff28c70baf2de467e271bdb23fab1f3cf61100381efbc835f88a952"
111
111
  },
112
112
  {
113
113
  "path": ".claude/skills/builder-stage/SKILL.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "05a2ac9355a782e35f6af18287d0395ece73a55b0541f680106c250d1f604b63"
2925
+ "sha256": "f2b2bf656d2fa819a7bd0f10ceb966b5414c1c06f05f2e8ccaa9ed326a7227ed"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -5297,7 +5297,7 @@
5297
5297
  {
5298
5298
  "path": "modules/government/protected-surfaces.json",
5299
5299
  "mode": "0000644",
5300
- "sha256": "9aedf10b3cb611343baf96662e6f79d45ed2218b83abfb3ad8c03536c722d37c"
5300
+ "sha256": "3f7f0bd81b70a995464993a084d35951c3d98b927d0d02fbe97f84105cbdd89f"
5301
5301
  },
5302
5302
  {
5303
5303
  "path": "modules/government/ranks.js",
@@ -10467,12 +10467,12 @@
10467
10467
  {
10468
10468
  "path": "package-lock.json",
10469
10469
  "mode": "0000644",
10470
- "sha256": "0698dfed8aaee5032ad705115bcbc515cc7328a78c31a30018a2e2f5df0cf1ca"
10470
+ "sha256": "e92fbdef629a9283a773d94986531ae9d30412fe05dc3012dd918be8fb859030"
10471
10471
  },
10472
10472
  {
10473
10473
  "path": "package.json",
10474
10474
  "mode": "0000644",
10475
- "sha256": "aefc81df15571d208d96577a7ade7189f1d1e064ae9a1af132b0725f871fcb40"
10475
+ "sha256": "cf96b6c94e06d4306f0b3e3c0626c051729a0498bd3e6760012ce86350286d16"
10476
10476
  },
10477
10477
  {
10478
10478
  "path": "public-docs/index.html",
@@ -10492,7 +10492,7 @@
10492
10492
  {
10493
10493
  "path": "release-notes.json",
10494
10494
  "mode": "0000644",
10495
- "sha256": "43b16f9fbd2c3834c02bd141ce4b9e737003d1c42112e6cb607e7a118e29e7dd"
10495
+ "sha256": "86c4edcb22bf8005ac7f4e479a9b4037d2bdad36618ed1e8f739a3a786952344"
10496
10496
  },
10497
10497
  {
10498
10498
  "path": "scripts/cron/style_guide_audit.sh",
@@ -10812,12 +10812,12 @@
10812
10812
  {
10813
10813
  "path": "scripts/gds/claude-materialize.js",
10814
10814
  "mode": "0000644",
10815
- "sha256": "1d141943f8367b8814e5a455c0c5d68a3915ac802df6cae9c3550f0628dbc384"
10815
+ "sha256": "eed7d9f195a2197d19bff7e1ebb61562a386db6bdda805db10f970afdf46b2fa"
10816
10816
  },
10817
10817
  {
10818
10818
  "path": "scripts/gds/claude-md-template.js",
10819
10819
  "mode": "0000644",
10820
- "sha256": "7659039447e370fe143327a8e55a730781d035fe6831e2af895288680ac5334f"
10820
+ "sha256": "d8560389c6a72cda1f7fe958af67b943e825b68fdd8f0fa28bf9143e42a1ece9"
10821
10821
  },
10822
10822
  {
10823
10823
  "path": "scripts/gds/cleanup-orphans.js",
@@ -11997,7 +11997,7 @@
11997
11997
  {
11998
11998
  "path": "scripts/gds/ship-flow.js",
11999
11999
  "mode": "0000644",
12000
- "sha256": "bb8edf9166b1c452b87d75e009d7404758bcba6ea58aa9011848ec44e761bb52"
12000
+ "sha256": "d944906397fc7936701bd36f3896a418829acf385c70a7ccaef07bd6cc348e91"
12001
12001
  },
12002
12002
  {
12003
12003
  "path": "scripts/gds/ship-git.js",
@@ -12049,6 +12049,11 @@
12049
12049
  "mode": "0000644",
12050
12050
  "sha256": "7a489ea223b4fa1d7a3b4ee1076c9a0cb65d6d929e9a5a6e0b8e403e15266afc"
12051
12051
  },
12052
+ {
12053
+ "path": "scripts/gds/ship-mode-manual.js",
12054
+ "mode": "0000644",
12055
+ "sha256": "077ab61bd14d0a5c8b4012103dd7289ba29a6a7d1ac3420ad401a7d6ef365597"
12056
+ },
12052
12057
  {
12053
12058
  "path": "scripts/gds/ship-preflight-steps.js",
12054
12059
  "mode": "0000644",
@@ -12087,7 +12092,7 @@
12087
12092
  {
12088
12093
  "path": "scripts/gds/ship.js",
12089
12094
  "mode": "0000644",
12090
- "sha256": "9f00804212ef485fd8cfd02be19cddb4be2570a34aa2a284f237940aff17753e"
12095
+ "sha256": "44f7161514392afb04a9165086a1fcc12e1f231d3a641220409ffe0c4ae2c9a2"
12091
12096
  },
12092
12097
  {
12093
12098
  "path": "scripts/gds/shipping-mode.js",
@@ -12227,7 +12232,7 @@
12227
12232
  {
12228
12233
  "path": "scripts/gds/update-channel.js",
12229
12234
  "mode": "0000644",
12230
- "sha256": "f6b0d78ff92c9962b158bc5b36bae021c63fcc24095c7b10866f1505ee3f5232"
12235
+ "sha256": "66bed9292a4f5ec70b15ba29a94abec5ed8c69891a5b674a030002844275880c"
12231
12236
  },
12232
12237
  {
12233
12238
  "path": "scripts/gds/update-sweep.js",
@@ -12262,7 +12267,7 @@
12262
12267
  {
12263
12268
  "path": "scripts/gds/upgrade.js",
12264
12269
  "mode": "0000644",
12265
- "sha256": "f4f158948ab880ceee1cf745ba0071983054c20deba885fed735b7828fe7c1ca"
12270
+ "sha256": "d55d4ba8c8b7d3abedbae59c28a9b1ef9c7021ccc214c6917d1a042c84019d25"
12266
12271
  },
12267
12272
  {
12268
12273
  "path": "scripts/gds/validate-design.js",
@@ -12567,7 +12572,7 @@
12567
12572
  {
12568
12573
  "path": "src/bongos/module-scope-map.js",
12569
12574
  "mode": "0000644",
12570
- "sha256": "1b0cec97fbdf2c14f48354741f5537a7db3dbc947e7b7182eb2aa9173cfa00dc"
12575
+ "sha256": "5def34a161ca00980bf8a68d4273f3183fdd7269f9b83c61e62e82c154ba881d"
12571
12576
  },
12572
12577
  {
12573
12578
  "path": "src/bongos/module-store.js",
@@ -12807,7 +12812,7 @@
12807
12812
  {
12808
12813
  "path": "src/module-api.js",
12809
12814
  "mode": "0000644",
12810
- "sha256": "d49bdec14177f77cd4e6dd63ac2532a095565373c0948d9d7b91c809e4e3e6f3"
12815
+ "sha256": "01bc6b541cf12105be05701170377582f537316fe3f70fdea2681bae07864bf7"
12811
12816
  },
12812
12817
  {
12813
12818
  "path": "src/module-loader/catalog.js",
@@ -13622,7 +13627,7 @@
13622
13627
  {
13623
13628
  "path": "tests/cli_allowlist_permissions.mjs",
13624
13629
  "mode": "0000644",
13625
- "sha256": "ce1d4031bf29064a8522440116e4da5dde78ffd59eb0c396b5d32a72790a260a"
13630
+ "sha256": "92bad4d5a6f224b0764bab5e7f906a0990c60f7fab75c5977ce64ba9e9b08b44"
13626
13631
  },
13627
13632
  {
13628
13633
  "path": "tests/cli_dispatcher.mjs",
@@ -14997,7 +15002,7 @@
14997
15002
  {
14998
15003
  "path": "tests/government_protected_surfaces.mjs",
14999
15004
  "mode": "0000644",
15000
- "sha256": "3518806d6b065029677e77a242604b6dc85bfd28537c2d109dacaedd29ac6d49"
15005
+ "sha256": "72fc7970c987dbb78120575546aed5faeb9e55342b68e6ba1668508927a9f103"
15001
15006
  },
15002
15007
  {
15003
15008
  "path": "tests/government_rank_sync.mjs",
@@ -17949,6 +17954,11 @@
17949
17954
  "mode": "0000644",
17950
17955
  "sha256": "361d6c7a364c0c1f6fb8266f887fe25fc357250e4585b438fdbf3fb3fafa6849"
17951
17956
  },
17957
+ {
17958
+ "path": "tests/ship_mode_hands_off.mjs",
17959
+ "mode": "0000644",
17960
+ "sha256": "78f05df6119d9637994f628f3968eefff34aea8a04d28eaa899acc5a238ce154"
17961
+ },
17952
17962
  {
17953
17963
  "path": "tests/ship_post_land_verify.mjs",
17954
17964
  "mode": "0000644",
@@ -106,6 +106,7 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
106
106
 
107
107
  ## Constraints
108
108
 
109
+ - **Check the ship mode before you ship** (`node scripts/gds/shipping-mode.js`, task 1004631, [ADR 0369](../../../docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md)). In **Auto** you run the ship yourself — never hand the person `cd …` and a ship command to type; a permission refusal on the ship command is a missing allow-rule to fix (the project's next core upgrade writes them), not a reason to hand it over. In **Manual** ask first; if the ship stops with `[otb-ask-ship-approval]` (exit 5), ask the person "Ship it, or not yet?" and re-run with `--person-approved` only after a real yes.
109
110
  - **Never ship a task you don't have an active claim on.** The API will reject it (`NOT_YOUR_CLAIM`); surface clearly and run `/builder-start` to find the right state.
110
111
  - **Don't fabricate the value summary.** If you can't summarize the value in non-jargon English, the work is probably not actually shippable yet — release instead and surface the gap to the user.
111
112
  - **Be honest in handoff notes.** Use `[verified-prod]` only when a real human exercised the feature on the live site. Otherwise `[verified-smoke]` or `[implemented-not-verified]`. The whole project relies on this honesty.
@@ -3081,5 +3081,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3081
3081
  landed since 1.21.120 with no explicit bump. run 38604777189. (task 1002620)
3082
3082
  1.21.122 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3083
3083
  landed since 1.21.121 with no explicit bump. run 38620147259. (task 1002620)
3084
+ 1.21.123 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3085
+ landed since 1.21.122 with no explicit bump. run 38624070870. (task 1002620)
3084
3086
  ---------------------------------------------------------------------------
3085
3087
  ```
@@ -71,6 +71,7 @@
71
71
  { "glob": "scripts/gds/ship-memory-sync.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
72
72
  { "glob": "scripts/gds/ship-merge-gate.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
73
73
  { "glob": "scripts/gds/ship-merge.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
74
+ { "glob": "scripts/gds/ship-mode-manual.js", "group": "pipeline", "why": "task 1004631. Split out of the protected scripts/gds/ship.js beside it: the ship-mode step every ship runs before it lands, so it is protected at ship.js's floor (the 1004024 carve tripwire)." },
74
75
  { "glob": "scripts/gds/ship-mode-ask.js", "group": "pipeline", "why": "task 1004630. Split out beside ship.js: the first-ship ask that decides whether a project ships in Auto or Manual mode, so an edit here changes who may start a ship." },
75
76
  { "glob": "scripts/gds/ship-preflight-steps.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
76
77
  { "glob": "scripts/gds/ship-regen.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
@@ -79,7 +80,6 @@
79
80
  { "glob": "scripts/gds/ship-state.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
80
81
  { "glob": "scripts/gds/ship-surprise.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
81
82
  { "glob": "scripts/gds/ship-visual.js", "group": "pipeline", "why": "task 1004024. The 2026-08-23 carve of ship.js (task 1003205): the land path, the ship state machine, the grade gate steps, the merge and pushVia, about 6,200 lines behind a 373-line protected entry point." },
82
- { "glob": "scripts/gds/ship-mode-ask.js", "group": "pipeline", "why": "task 1004630. Split out of the protected scripts/gds/ship.js beside it: the ship-mode step every ship runs before it lands, so it is protected at ship.js's floor (the 1004024 carve tripwire)." },
83
83
  { "glob": "scripts/gds/confirm.js", "group": "pipeline", "why": "the confirm step" },
84
84
  { "glob": "scripts/gds/merge-mode.js", "group": "pipeline", "why": "the manual land fallback" },
85
85
  { "glob": "scripts/gds/ci-grade.js", "group": "pipeline", "why": "CI grade-gate driver — steers the grade check (ADR 0058)" },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.122",
3
+ "version": "1.21.123",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.122",
9
+ "version": "1.21.123",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.122",
3
+ "version": "1.21.123",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9714,5 +9714,11 @@
9714
9714
  "id": "1004630",
9715
9715
  "text": "If a project never picked a ship mode, its next ship now pauses once and asks: Auto (recommended) or Manual. The answer is saved for the whole project and it never asks again. People who cannot change project settings, and una"
9716
9716
  }
9717
+ ],
9718
+ "1.21.123": [
9719
+ {
9720
+ "id": "1004631",
9721
+ "text": "Found why the AI refused to ship on Windows projects: the core update failed on Windows, so projects never got the permission that lets the AI run a ship. That is fixed. New projects also tell the AI to ship on its own in Auto"
9722
+ }
9717
9723
  ]
9718
9724
  }
@@ -440,6 +440,9 @@ const STARTER_COMMANDS = [
440
440
  // tests/cli_allowlist_permissions.mjs pins this list to the core's, rule for rule.
441
441
  'npx bongos start:*',
442
442
  'npx bongos status:*',
443
+ // Exact form, no `:*`: reading the ship mode is how the agent knows whether to ship on its own
444
+ // (task 1004631, ADR 0369). `ship-mode auto|manual` WRITES it, and stays a prompt.
445
+ 'npx bongos ship-mode',
443
446
  'npx bongos recall:*',
444
447
  'npx bongos exec scripts/gds/me.js:*',
445
448
  'npx bongos doctor:*',
@@ -77,6 +77,7 @@ ${ident}
77
77
  - **Start every session with \`npx bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
78
78
  - **\`npx bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
79
79
  - **\`npx bongos ship\`** runs the smoke + grade, then merges and deploys; **\`npx bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
80
+ - **The project's ship mode says who starts a ship** — \`npx bongos ship-mode\` prints it. In **Auto** (the default) you run \`npx bongos ship\` yourself when the task is done; never hand the person commands to run. In **Manual** ask the person first, and ship only after they say yes.
80
81
  - **Dependencies gate everything** — a task becomes claimable only once every task it depends on has shipped.
81
82
  - **Code you write or touch carries a doc-comment** — each module a header, each public function/class a line or two, saying *what* it does and *why* it exists (never how), in the words someone would search for. Agents find code by searching text, so meaning written into the source is what makes it findable.
82
83
 
@@ -36,6 +36,25 @@ const { noteServerReward } = require('./ship-state.js');
36
36
  // task 1004536: each graded round lands in the autonomous run's ledger (inert
37
37
  // without one), so ship.js can refuse a third grade at its entry point.
38
38
  const { recordGradeOutcome } = require('./autobongos-grade-cap.js');
39
+ // task 1004631: on a Manual project an AI session asks before it ships (ADR 0369).
40
+ const manualShip = require('./ship-mode-manual.js');
41
+
42
+ // stopForManualApproval(args, taskId) — Manual ship mode (task 1004631, ADR 0369). Every path
43
+ // that can LAND work calls this first: a fresh ship, a resumed merge, and a re-grade (whose pass
44
+ // runs finishShip). When an AI session runs it on a Manual project without the person's yes, it
45
+ // prints the ask, exits 5 and returns true; the caller returns. A mode that cannot be read is
46
+ // Auto and never stops a ship. Who is stopped is ship-mode-manual.js's pure verdict.
47
+ async function stopForManualApproval(args, taskId) {
48
+ let shipMode = null;
49
+ try {
50
+ const r = await apiCall('GET', '/api/bongos/project-settings/ship-mode');
51
+ shipMode = r.ok ? r.data : null;
52
+ } catch (_) { shipMode = null; }
53
+ if (!manualShip.manualShipVerdict({ shipMode, args }).stop) return false;
54
+ for (const line of manualShip.manualAskLines(args, taskId)) console.log(line);
55
+ cliExit(manualShip.MANUAL_ASK_EXIT);
56
+ return true;
57
+ }
39
58
 
40
59
  // ---------- Main flow ----------
41
60
 
@@ -145,6 +164,7 @@ async function shipMain() {
145
164
  }
146
165
  if (action.action === 'resume-merge') {
147
166
  console.log(`Task #${taskId} is at 'confirmed' (merge/deploy stalled) and ${action.via === 'owner' ? 'you last held the claim' : 'you are an Archon'} — resuming the merge + deploy (no re-claim needed).`);
167
+ if (await stopForManualApproval(args, taskId)) return; // a resumed land is a ship too (task 1004631)
148
168
  return resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly });
149
169
  }
150
170
  if (action.action === 'done') {
@@ -193,6 +213,10 @@ async function shipMain() {
193
213
  process.exit(1);
194
214
  }
195
215
 
216
+ // ---- Manual ship mode: an AI session asks the person first (task 1004631, ADR 0369) ----
217
+ // Before anything resolves, so a "not yet" leaves the claim exactly as it was.
218
+ if (await stopForManualApproval(args, taskId)) return;
219
+
196
220
  // ---- Surprise gate: forced "anything surprise you?" prompt ----
197
221
  //
198
222
  // Resolve the user's intent into a single value: either a non-empty surprise
@@ -850,6 +874,9 @@ async function regradeMain(opts = {}) {
850
874
  console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary-file <path|-> | --summary "..."] [--response-file <path|-> | --response "..."] [--visual <image>] [--visual-alt "caption"] [--no-merge] [--force] [--verified] [--base <ref>]');
851
875
  process.exit(2);
852
876
  }
877
+ // A passing re-grade runs finishShip, the same land as a fresh ship, so Manual mode asks
878
+ // here too — whether reached by --regrade or by shipMain's resume-grade dispatch (task 1004631).
879
+ if (await stopForManualApproval(args, taskId)) return;
853
880
  // task 1003702: refuse --notes rather than swallowing it. The reasoning — why
854
881
  // a refusal and not a persist — is in ship-honesty.js above regradeNotesRefusal
855
882
  // and is not repeated here. What is load-bearing at THIS call site is the
@@ -0,0 +1,54 @@
1
+ // scripts/gds/ship-mode-manual.js — Manual ship mode: the AI asks before it ships (task 1004631, ADR 0369).
2
+ //
3
+ // WHY. Ship mode says who starts a ship. In Auto the AI ships on its own; in Manual a person
4
+ // approves each ship. The instance CLAUDE.md and the /builder-ship skill tell the AI to ask in
5
+ // Manual, but words alone are what failed the owner in Auto (the AI refused to ship), so the ship
6
+ // itself also stops: when an AI session runs it on a Manual project without the person's yes, it
7
+ // exits before anything resolves and prints the question. The re-run with `--person-approved`
8
+ // carries on.
9
+ //
10
+ // WHO IT STOPS. Only an AI session: Claude Code sets CLAUDECODE=1 in the shell it runs commands
11
+ // in, and a person typing `npx bongos ship` in their own terminal has already decided. An
12
+ // Autobongos worker (OTB_SUBAGENT=1) is not stopped: the runner is the owner's own opt-in to
13
+ // unattended shipping, and stopping it would only park every task it takes.
14
+ //
15
+ // WHAT IT IS NOT. A security wall. The flag is one word an agent could add, the same posture as
16
+ // `--approved` on the sandbox review: the rule is behavioural, and the skill says never to pass it
17
+ // without a real yes. Rank and every server gate are unchanged (ADR 0016). A mode that cannot be
18
+ // read never stops a ship (the resolver's own posture: Auto).
19
+ //
20
+ // The decision is pure (manualShipVerdict) so tests prove it with no network.
21
+
22
+ 'use strict';
23
+
24
+ // Same code as the first-ship question (ship-mode-ask.js, task 1004630): "a question for the
25
+ // person is waiting", distinct from failure (1), usage (2) and the grade cap (6).
26
+ const MANUAL_ASK_EXIT = 5;
27
+ const MANUAL_ASK_MARKER = '[otb-ask-ship-approval]';
28
+ const APPROVED_FLAG = '--person-approved';
29
+
30
+ // manualShipVerdict({ shipMode, args, env }) → { stop: boolean, why }
31
+ function manualShipVerdict({ shipMode, args = [], env = process.env }) {
32
+ if (!shipMode || shipMode.mode !== 'manual') return { stop: false, why: 'not_manual' };
33
+ if (args.includes(APPROVED_FLAG)) return { stop: false, why: 'approved' };
34
+ if (env.OTB_SUBAGENT === '1') return { stop: false, why: 'unattended_runner' };
35
+ if (env.CLAUDECODE !== '1') return { stop: false, why: 'person_at_terminal' };
36
+ return { stop: true, why: 'needs_approval' };
37
+ }
38
+
39
+ // manualAskLines(args, taskId) — the question the AI puts to the person, and the re-run.
40
+ function manualAskLines(args, taskId) {
41
+ const rerun = ['node', 'scripts/gds/ship.js', ...args]
42
+ .map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(' ');
43
+ return [
44
+ `${MANUAL_ASK_MARKER} This project's ship mode is Manual: a person approves each ship.`,
45
+ `Ask the person whether to ship task ${taskId} now, with two options: Ship it, or Not yet.`,
46
+ 'Only after they say yes, re-run:',
47
+ '',
48
+ ` ${rerun} ${APPROVED_FLAG}`,
49
+ '',
50
+ 'Nothing was resolved: the claim is still active. Never add the flag without their yes.',
51
+ ];
52
+ }
53
+
54
+ module.exports = { MANUAL_ASK_EXIT, MANUAL_ASK_MARKER, APPROVED_FLAG, manualShipVerdict, manualAskLines };
@@ -61,6 +61,11 @@
61
61
  // # sandbox URL. Persists a sandbox_review row on the
62
62
  // # task. Honored only when the game state is already
63
63
  // # staged-fresh (a re-run); it can't pre-approve.
64
+ // node scripts/gds/ship.js <task-id> --person-approved
65
+ // # Manual ship mode (task 1004631, ADR 0369): an AI
66
+ // # session on a Manual project stops before anything
67
+ // # resolves and exits 5 with an ask; re-run with this
68
+ // # ONLY after the person said yes to shipping now.
64
69
 
65
70
 
66
71
  // ---------------------------------------------------------------------------
@@ -179,6 +179,16 @@ function resolveChannelTarget({ installed, channel, available } = {}) {
179
179
 
180
180
  // ---- registry: what versions of the core are published? ---------------------
181
181
 
182
+ // npmSpawnOpts — what a spawn of `npm` needs on this platform (task 1004631). On Windows `npm` is
183
+ // `npm.cmd`, and Node refuses to spawn a .cmd without a shell (CVE-2024-27980), so a bare
184
+ // spawnSync('npm') fails with ENOENT there. That failure made `bongos upgrade` roll back on every
185
+ // Windows laptop before it wrote the project's .claude/ files — so the project had no permission
186
+ // rules, and Claude Code's safety check refused `npx bongos ship`. A shell is safe here because
187
+ // every argument passed to npm by these callers is a fixed word or a package@version string.
188
+ function npmSpawnOpts(platform = process.platform) {
189
+ return platform === 'win32' ? { shell: true } : {};
190
+ }
191
+
182
192
  // List published versions of @bongos/core from the registry via `npm view … versions --json`.
183
193
  // The package is public (task 1003872), so this needs no credential and no instance-local
184
194
  // `.npmrc` — any cwd resolves it. Fail-SOFT: any error returns
@@ -188,7 +198,7 @@ function resolveChannelTarget({ installed, channel, available } = {}) {
188
198
  function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env, timeoutMs = NPM_VIEW_TIMEOUT_MS } = {}) {
189
199
  let res;
190
200
  try {
191
- res = run('npm', ['view', pkg, 'versions', '--json'], { cwd, env, encoding: 'utf8', timeout: timeoutMs });
201
+ res = run('npm', ['view', pkg, 'versions', '--json'], { cwd, env, encoding: 'utf8', timeout: timeoutMs, ...npmSpawnOpts() });
192
202
  } catch (e) {
193
203
  return { ok: false, error: e && e.message ? e.message : String(e), versions: [] };
194
204
  }
@@ -220,7 +230,7 @@ function parseNpmViewVersions(stdout) {
220
230
  function readReleaseTags({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env, timeoutMs = NPM_VIEW_TIMEOUT_MS } = {}) {
221
231
  let res;
222
232
  try {
223
- res = run('npm', ['view', pkg, 'dist-tags', '--json'], { cwd, env, encoding: 'utf8', timeout: timeoutMs });
233
+ res = run('npm', ['view', pkg, 'dist-tags', '--json'], { cwd, env, encoding: 'utf8', timeout: timeoutMs, ...npmSpawnOpts() });
224
234
  } catch (e) {
225
235
  return { ok: false, error: e && e.message ? e.message : String(e), released: null, tags: {} };
226
236
  }
@@ -463,6 +473,7 @@ function describeStaleness(st, { slug = 'instance' } = {}) {
463
473
  }
464
474
 
465
475
  module.exports = {
476
+ npmSpawnOpts,
466
477
  CORE_PKG,
467
478
  VALID_CHANNELS,
468
479
  DEFAULT_CHANNEL,
@@ -29,7 +29,7 @@ const path = require('node:path');
29
29
  const crypto = require('node:crypto');
30
30
  const { spawnSync } = require('node:child_process');
31
31
  const { arg, hasFlag } = require('./cli-lib');
32
- const { parseSemver, compareSemver, isStable, listAvailableVersions } = require('./update-channel');
32
+ const { parseSemver, compareSemver, isStable, listAvailableVersions, npmSpawnOpts } = require('./update-channel');
33
33
  const { checkArtistGate } = require('./upgrade-artist-gate');
34
34
  // The database side of a move: which command migrates, as which account, and whether that
35
35
  // account can reach the database (tasks 1004274, 1002712, 1004512).
@@ -514,7 +514,7 @@ function npmInstall(instanceDir, run = spawnSync) {
514
514
  // This used to take an extraEnv carrying a resolved npm credential, because the instance
515
515
  // `.npmrc`'s env expansion could shadow a valid user-level one (task 1002754). The core is
516
516
  // public and writes no `.npmrc`, so there is no credential to resolve (task 1003878).
517
- const opts = { cwd: instanceDir, stdio: 'inherit' };
517
+ const opts = { cwd: instanceDir, stdio: 'inherit', ...npmSpawnOpts() }; // Windows: npm.cmd needs a shell (task 1004631)
518
518
  const r = run('npm', ['install', '--no-audit', '--no-fund'], opts);
519
519
  return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
520
520
  }
@@ -104,6 +104,7 @@ const MODULE_GLOBS = {
104
104
  lifecycle: [
105
105
  'modules/lifecycle/',
106
106
  'scripts/gds/ship.js', 'scripts/gds/claim.js', 'scripts/gds/release.js',
107
+ 'scripts/gds/ship-mode-manual.js', // task 1004631
107
108
  // task 1004024: the 2026-08-23 ship.js carve, now protected (by name: no '*' here).
108
109
  'scripts/gds/ship-check.js', 'scripts/gds/ship-deploy-target.js', 'scripts/gds/ship-escalation.js',
109
110
  'scripts/gds/ship-finish.js', 'scripts/gds/ship-flow.js', 'scripts/gds/ship-git.js',
@@ -113,7 +114,6 @@ const MODULE_GLOBS = {
113
114
  'scripts/gds/ship-preflight-steps.js',
114
115
  'scripts/gds/ship-regen.js', 'scripts/gds/ship-resume.js', 'scripts/gds/ship-session-upload.js',
115
116
  'scripts/gds/ship-state.js', 'scripts/gds/ship-surprise.js', 'scripts/gds/ship-visual.js',
116
- 'scripts/gds/ship-mode-ask.js', // task 1004630
117
117
  'scripts/gds/merge-mode.js', 'scripts/gds/confirm.js', 'scripts/gds/main-audit.js',
118
118
  'scripts/gds/gate-review.js', 'scripts/gds/push-main.js', 'scripts/gds/ci-grade.js',
119
119
  'scripts/gds/merge-lock.js', 'scripts/gds/version-close.js',
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.122'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.123'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -214,6 +214,10 @@ const GIT_GH_COMMANDS = [
214
214
  // CLI access gives `npx bongos paste-token <token>` (task 1004575), a blessed writer
215
215
  // (see BLESSED_WRITERS) spelled the way a clone can run it.
216
216
  'npx bongos paste-token:*',
217
+ // EXACT form, no `:*` (task 1004631, ADR 0369): with no argument shipping-mode.js only
218
+ // READS the ship mode, which the agent must know before it ships. With `auto|manual` it
219
+ // WRITES the project setting, so a prefix rule would pre-allow that write.
220
+ 'node scripts/gds/shipping-mode.js',
217
221
  ];
218
222
  // gds CLIs audited read-only under EVERY flag they take — a prefix rule allows them all.
219
223
  // Audited and left OUT: backlog-review.js (POSTs from its default walk) and
@@ -271,14 +271,6 @@ const ADDED_SINCE_R101 = [
271
271
  'ship-surprise', 'ship-visual'],
272
272
  'the 2026-08-23 ship.js carve: the land path, the ship state machine, the grade gate '
273
273
  + 'steps and pushVia, ~6,200 lines behind a 373-line protected entry point.'),
274
- // task 1004630: a ship.js sibling added after the carve — registered so the carve
275
- // tripwire below stays green and the file sits at ship.js's floor.
276
- {
277
- after: 'scripts/gds/ship-visual.js',
278
- glob: 'scripts/gds/ship-mode-ask.js',
279
- why: 'split out of the protected ship.js: the ship-mode step every ship runs '
280
- + 'before it lands, so an edit here could change how work reaches main.',
281
- },
282
274
  // task 1002839 (ADR 0089 §1). Not a `grader-*.js` sibling, so the carve tripwire
283
275
  // below would not demand it — registered by hand because it owns the gate.
284
276
  {
@@ -303,6 +295,14 @@ const ADDED_SINCE_R101 = [
303
295
  why: 'the core neutral provider set, which declares none. An entry added here would '
304
296
  + 'send every instance\'s diffs to a vendor by default.',
305
297
  },
298
+ // task 1004631: a ship.js sibling added after the carve — registered so the carve
299
+ // tripwire below stays green and the file sits at ship.js's floor.
300
+ {
301
+ after: 'scripts/gds/ship-merge.js',
302
+ glob: 'scripts/gds/ship-mode-manual.js',
303
+ why: 'split out of the protected ship.js: the ship-mode step every ship runs '
304
+ + 'before it lands, so an edit here could change how work reaches main.',
305
+ },
306
306
  ];
307
307
 
308
308
  // Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
@@ -0,0 +1,146 @@
1
+ // tests/ship_mode_hands_off.mjs — Auto ships hands-off, Manual asks first (task 1004631, ADR 0369).
2
+ //
3
+ // The owner's AI refused `npx bongos ship` on their own project. The cause: `bongos upgrade`
4
+ // spawned bare `npm`, which is ENOENT on Windows, so the upgrade rolled back before it wrote
5
+ // the project's .claude/settings.json, and with no allow-rule Claude Code's safety check denied
6
+ // the ship. Pinned here:
7
+ // 1. npm is spawned with a shell on Windows (and only there) by the upgrade and the registry reads;
8
+ // 2. a fresh project's starter allows the ship and the read-only ship-mode check, but NOT the
9
+ // ship-mode write;
10
+ // 3. the instance CLAUDE.md tells the AI to follow the ship mode and never hand over commands;
11
+ // 4. Manual mode: an AI session stops before anything resolves and asks; a person at a
12
+ // terminal, an approved re-run, an Autobongos worker, Auto and an unreadable mode never stop;
13
+ // 5. the wiring: the Manual check runs after the claim is found and before the grade.
14
+ //
15
+ // Run: node --test tests/ship_mode_hands_off.mjs
16
+ import { strict as assert } from 'node:assert';
17
+ import { test } from 'node:test';
18
+ import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+
26
+ const { npmSpawnOpts, listAvailableVersions } = require('../scripts/gds/update-channel.js');
27
+ const { STARTER_ALLOW } = require('../scripts/gds/claude-materialize.js');
28
+ const {
29
+ MANUAL_ASK_EXIT, MANUAL_ASK_MARKER, APPROVED_FLAG, manualShipVerdict, manualAskLines,
30
+ } = require('../scripts/gds/ship-mode-manual.js');
31
+
32
+ // ---- 1. npm on Windows -----------------------------------------------------
33
+
34
+ test('npm gets a shell on Windows only (npm is npm.cmd there, and Node will not spawn a .cmd bare)', () => {
35
+ assert.deepEqual(npmSpawnOpts('win32'), { shell: true });
36
+ assert.deepEqual(npmSpawnOpts('linux'), {});
37
+ assert.deepEqual(npmSpawnOpts('darwin'), {});
38
+ });
39
+
40
+ test('the registry read passes the platform spawn options to npm', () => {
41
+ const calls = [];
42
+ listAvailableVersions({ run: (cmd, args, opts) => { calls.push({ cmd, args, opts }); return { status: 0, stdout: '["1.0.0"]' }; } });
43
+ assert.equal(calls[0].cmd, 'npm');
44
+ assert.equal(calls[0].opts.shell, npmSpawnOpts().shell);
45
+ });
46
+
47
+ test('the upgrade\'s npm install uses the same spawn options', () => {
48
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'upgrade.js'), 'utf8');
49
+ const fn = src.slice(src.indexOf('function npmInstall'), src.indexOf('async function runMigrate'));
50
+ assert.match(fn, /npmSpawnOpts\(\)/);
51
+ });
52
+
53
+ // ---- 2. the starter permissions --------------------------------------------
54
+
55
+ test('a fresh project allows the ship, and reading the ship mode, for both shell tools', () => {
56
+ for (const tool of ['Bash', 'PowerShell']) {
57
+ assert.ok(STARTER_ALLOW.includes(`${tool}(npx bongos ship:*)`), `${tool} ship`);
58
+ assert.ok(STARTER_ALLOW.includes(`${tool}(npx bongos ship-mode)`), `${tool} ship-mode read`);
59
+ }
60
+ });
61
+
62
+ test('the starter never pre-allows WRITING the ship mode', () => {
63
+ assert.ok(!STARTER_ALLOW.some((r) => /ship-mode[: ]/.test(r) || /ship-mode\*/.test(r)),
64
+ 'only the exact no-argument read is allowed; `ship-mode auto|manual` stays a prompt');
65
+ });
66
+
67
+ // ---- 3. the instance CLAUDE.md ---------------------------------------------
68
+
69
+ test('a new project\'s CLAUDE.md tells the AI to follow the ship mode and never hand over commands', () => {
70
+ const tpl = require('../scripts/gds/claude-md-template.js');
71
+ const render = Object.values(tpl).find((v) => typeof v === 'function');
72
+ const text = render({});
73
+ assert.match(text, /npx bongos ship-mode/);
74
+ assert.match(text, /In \*\*Auto\*\*[^\n]*run `npx bongos ship` yourself[^\n]*never hand the person commands/);
75
+ assert.match(text, /In \*\*Manual\*\* ask the person first/);
76
+ });
77
+
78
+ // ---- 4. Manual mode --------------------------------------------------------
79
+
80
+ const AI = { CLAUDECODE: '1' };
81
+ const manual = { mode: 'manual', chosen: true };
82
+
83
+ test('an AI session on a Manual project stops and asks', () => {
84
+ assert.deepEqual(manualShipVerdict({ shipMode: manual, args: ['7'], env: AI }), { stop: true, why: 'needs_approval' });
85
+ });
86
+
87
+ test('the approved re-run, a person at a terminal, and an Autobongos worker are never stopped', () => {
88
+ assert.equal(manualShipVerdict({ shipMode: manual, args: ['7', APPROVED_FLAG], env: AI }).stop, false);
89
+ assert.equal(manualShipVerdict({ shipMode: manual, args: ['7'], env: {} }).why, 'person_at_terminal');
90
+ assert.equal(manualShipVerdict({ shipMode: manual, args: ['7'], env: { ...AI, OTB_SUBAGENT: '1' } }).why, 'unattended_runner');
91
+ });
92
+
93
+ test('Auto and an unreadable mode ship on', () => {
94
+ assert.equal(manualShipVerdict({ shipMode: { mode: 'auto', chosen: true }, args: ['7'], env: AI }).stop, false);
95
+ assert.equal(manualShipVerdict({ shipMode: null, args: ['7'], env: AI }).stop, false);
96
+ });
97
+
98
+ test('the ask names the task, both answers, the exact re-run, and forbids a yes nobody gave', () => {
99
+ const lines = manualAskLines(['7', '--summary', 'two words'], 7);
100
+ const text = lines.join('\n');
101
+ assert.ok(lines[0].startsWith(MANUAL_ASK_MARKER));
102
+ assert.match(text, /ship task 7 now[\s\S]*Ship it, or Not yet/);
103
+ assert.match(text, /node scripts\/gds\/ship\.js 7 --summary "two words" --person-approved/);
104
+ assert.match(text, /Never add the flag without their yes/);
105
+ assert.equal(MANUAL_ASK_EXIT, 5);
106
+ });
107
+
108
+ // ---- 5. the wiring ---------------------------------------------------------
109
+
110
+ const flowSrc = () => fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship-flow.js'), 'utf8');
111
+
112
+ test('the Manual check stops with cliExit and returns, never a hard exit after a request', () => {
113
+ const src = flowSrc();
114
+ const helper = src.slice(src.indexOf('async function stopForManualApproval'), src.indexOf('async function shipMain'));
115
+ assert.match(helper, /manualShip\.manualShipVerdict\(/);
116
+ assert.match(helper, /cliExit\(manualShip\.MANUAL_ASK_EXIT\);\s*return true;/);
117
+ assert.doesNotMatch(helper, /process\.exit\(/);
118
+ });
119
+
120
+ test('a fresh ship asks after the claim is found and before preflight, the grade and the resolve', () => {
121
+ const main = flowSrc().slice(flowSrc().indexOf('async function shipMain'));
122
+ const claimFound = main.indexOf('const ac = claims.find(');
123
+ const gate = main.indexOf('if (await stopForManualApproval(args, taskId)) return;', claimFound);
124
+ const preflight = main.indexOf('Step 0: preflight');
125
+ assert.ok(claimFound > 0 && gate > claimFound && preflight > gate);
126
+ });
127
+
128
+ test('a resumed merge asks before it lands (the stranded-ship re-run)', () => {
129
+ const main = flowSrc().slice(flowSrc().indexOf('async function shipMain'));
130
+ const branch = main.slice(main.indexOf("action.action === 'resume-merge'"), main.indexOf('return resumeMerge(taskId'));
131
+ assert.match(branch, /if \(await stopForManualApproval\(args, taskId\)\) return;/);
132
+ });
133
+
134
+ test('a re-grade asks before it can pass into finishShip (direct --regrade and the resume-grade dispatch)', () => {
135
+ const src = flowSrc();
136
+ const regrade = src.slice(src.indexOf('async function regradeMain'));
137
+ const gate = regrade.indexOf('if (await stopForManualApproval(args, taskId)) return;');
138
+ const finish = regrade.indexOf('finishShip(');
139
+ assert.ok(gate > 0 && finish > gate, 'the gate runs before the land');
140
+ const main = src.slice(src.indexOf('async function shipMain'), src.indexOf('async function regradeMain'));
141
+ assert.match(main, /action\.action === 'resume-grade'[\s\S]*?return regradeMain\(/, 'resume-grade goes through regradeMain, so its gate covers it');
142
+ });
143
+
144
+ test('--person-approved is documented in the ship usage banner', () => {
145
+ assert.match(fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship.js'), 'utf8'), /--person-approved/);
146
+ });