@bongos/core 1.21.121 → 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 +47 -27
- package/.claude/skills/builder-ship/SKILL.md +3 -0
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +1 -1
- package/docs/api/openapi.json +5 -3
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/protected-surfaces.json +2 -0
- package/modules/lifecycle/routes/project-settings.js +22 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/claude-materialize.js +3 -0
- package/scripts/gds/claude-md-template.js +1 -0
- package/scripts/gds/ship-flow.js +49 -2
- package/scripts/gds/ship-mode-ask.js +138 -0
- package/scripts/gds/ship-mode-manual.js +54 -0
- package/scripts/gds/ship.js +11 -0
- package/scripts/gds/update-channel.js +13 -2
- package/scripts/gds/upgrade.js +2 -2
- package/src/bongos/module-scope-map.js +3 -1
- package/src/module-api.js +1 -1
- package/tests/cli_allowlist_permissions.mjs +4 -0
- package/tests/government_protected_surfaces.mjs +9 -1
- package/tests/ship_mode_first_ship.mjs +219 -0
- package/tests/ship_mode_hands_off.mjs +146 -0
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.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
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-
|
|
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":
|
|
14
|
+
"functional_verbatim": 3122,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
110
|
+
"sha256": "bac025cc3ff28c70baf2de467e271bdb23fab1f3cf61100381efbc835f88a952"
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"path": ".claude/skills/builder-stage/SKILL.md",
|
|
@@ -337,7 +337,7 @@
|
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "c13884fb0bd175f8bc9745150b2ca5ed254dd0ad14bcaa63511cf892cdbaec36"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -2307,7 +2307,7 @@
|
|
|
2307
2307
|
{
|
|
2308
2308
|
"path": "docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md",
|
|
2309
2309
|
"mode": "0000644",
|
|
2310
|
-
"sha256": "
|
|
2310
|
+
"sha256": "a0dba98c2dfe34ed6110f2d813fef346ad8adf7430f2b7ab4da38be72c37f7c8"
|
|
2311
2311
|
},
|
|
2312
2312
|
{
|
|
2313
2313
|
"path": "docs/adr/0370-a-repo-we-create-is-private.md",
|
|
@@ -2382,7 +2382,7 @@
|
|
|
2382
2382
|
{
|
|
2383
2383
|
"path": "docs/api/openapi.json",
|
|
2384
2384
|
"mode": "0000644",
|
|
2385
|
-
"sha256": "
|
|
2385
|
+
"sha256": "b8d8cae75596996b38203b5b73b8fc4f5cfc4afa8fa6a170541131fdfed924ea"
|
|
2386
2386
|
},
|
|
2387
2387
|
{
|
|
2388
2388
|
"path": "docs/architecture.md",
|
|
@@ -2922,7 +2922,7 @@
|
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "docs/module-api-changelog.md",
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
|
-
"sha256": "
|
|
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": "
|
|
5300
|
+
"sha256": "3f7f0bd81b70a995464993a084d35951c3d98b927d0d02fbe97f84105cbdd89f"
|
|
5301
5301
|
},
|
|
5302
5302
|
{
|
|
5303
5303
|
"path": "modules/government/ranks.js",
|
|
@@ -7622,7 +7622,7 @@
|
|
|
7622
7622
|
{
|
|
7623
7623
|
"path": "modules/lifecycle/routes/project-settings.js",
|
|
7624
7624
|
"mode": "0000644",
|
|
7625
|
-
"sha256": "
|
|
7625
|
+
"sha256": "67d28a56701e45e176236fc0eae3ece9e349cc5c7b61cf6e7b8267afe304ade5"
|
|
7626
7626
|
},
|
|
7627
7627
|
{
|
|
7628
7628
|
"path": "modules/lifecycle/routes/recommendations.js",
|
|
@@ -10467,12 +10467,12 @@
|
|
|
10467
10467
|
{
|
|
10468
10468
|
"path": "package-lock.json",
|
|
10469
10469
|
"mode": "0000644",
|
|
10470
|
-
"sha256": "
|
|
10470
|
+
"sha256": "e92fbdef629a9283a773d94986531ae9d30412fe05dc3012dd918be8fb859030"
|
|
10471
10471
|
},
|
|
10472
10472
|
{
|
|
10473
10473
|
"path": "package.json",
|
|
10474
10474
|
"mode": "0000644",
|
|
10475
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
10815
|
+
"sha256": "eed7d9f195a2197d19bff7e1ebb61562a386db6bdda805db10f970afdf46b2fa"
|
|
10816
10816
|
},
|
|
10817
10817
|
{
|
|
10818
10818
|
"path": "scripts/gds/claude-md-template.js",
|
|
10819
10819
|
"mode": "0000644",
|
|
10820
|
-
"sha256": "
|
|
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": "
|
|
12000
|
+
"sha256": "d944906397fc7936701bd36f3896a418829acf385c70a7ccaef07bd6cc348e91"
|
|
12001
12001
|
},
|
|
12002
12002
|
{
|
|
12003
12003
|
"path": "scripts/gds/ship-git.js",
|
|
@@ -12044,6 +12044,16 @@
|
|
|
12044
12044
|
"mode": "0000644",
|
|
12045
12045
|
"sha256": "8d37d5c6cbaa1ca0e8363fab6c34159d1e16922fdae11d4ffc17b4d9e2c7f795"
|
|
12046
12046
|
},
|
|
12047
|
+
{
|
|
12048
|
+
"path": "scripts/gds/ship-mode-ask.js",
|
|
12049
|
+
"mode": "0000644",
|
|
12050
|
+
"sha256": "7a489ea223b4fa1d7a3b4ee1076c9a0cb65d6d929e9a5a6e0b8e403e15266afc"
|
|
12051
|
+
},
|
|
12052
|
+
{
|
|
12053
|
+
"path": "scripts/gds/ship-mode-manual.js",
|
|
12054
|
+
"mode": "0000644",
|
|
12055
|
+
"sha256": "077ab61bd14d0a5c8b4012103dd7289ba29a6a7d1ac3420ad401a7d6ef365597"
|
|
12056
|
+
},
|
|
12047
12057
|
{
|
|
12048
12058
|
"path": "scripts/gds/ship-preflight-steps.js",
|
|
12049
12059
|
"mode": "0000644",
|
|
@@ -12082,7 +12092,7 @@
|
|
|
12082
12092
|
{
|
|
12083
12093
|
"path": "scripts/gds/ship.js",
|
|
12084
12094
|
"mode": "0000644",
|
|
12085
|
-
"sha256": "
|
|
12095
|
+
"sha256": "44f7161514392afb04a9165086a1fcc12e1f231d3a641220409ffe0c4ae2c9a2"
|
|
12086
12096
|
},
|
|
12087
12097
|
{
|
|
12088
12098
|
"path": "scripts/gds/shipping-mode.js",
|
|
@@ -12222,7 +12232,7 @@
|
|
|
12222
12232
|
{
|
|
12223
12233
|
"path": "scripts/gds/update-channel.js",
|
|
12224
12234
|
"mode": "0000644",
|
|
12225
|
-
"sha256": "
|
|
12235
|
+
"sha256": "66bed9292a4f5ec70b15ba29a94abec5ed8c69891a5b674a030002844275880c"
|
|
12226
12236
|
},
|
|
12227
12237
|
{
|
|
12228
12238
|
"path": "scripts/gds/update-sweep.js",
|
|
@@ -12257,7 +12267,7 @@
|
|
|
12257
12267
|
{
|
|
12258
12268
|
"path": "scripts/gds/upgrade.js",
|
|
12259
12269
|
"mode": "0000644",
|
|
12260
|
-
"sha256": "
|
|
12270
|
+
"sha256": "d55d4ba8c8b7d3abedbae59c28a9b1ef9c7021ccc214c6917d1a042c84019d25"
|
|
12261
12271
|
},
|
|
12262
12272
|
{
|
|
12263
12273
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -12562,7 +12572,7 @@
|
|
|
12562
12572
|
{
|
|
12563
12573
|
"path": "src/bongos/module-scope-map.js",
|
|
12564
12574
|
"mode": "0000644",
|
|
12565
|
-
"sha256": "
|
|
12575
|
+
"sha256": "5def34a161ca00980bf8a68d4273f3183fdd7269f9b83c61e62e82c154ba881d"
|
|
12566
12576
|
},
|
|
12567
12577
|
{
|
|
12568
12578
|
"path": "src/bongos/module-store.js",
|
|
@@ -12802,7 +12812,7 @@
|
|
|
12802
12812
|
{
|
|
12803
12813
|
"path": "src/module-api.js",
|
|
12804
12814
|
"mode": "0000644",
|
|
12805
|
-
"sha256": "
|
|
12815
|
+
"sha256": "01bc6b541cf12105be05701170377582f537316fe3f70fdea2681bae07864bf7"
|
|
12806
12816
|
},
|
|
12807
12817
|
{
|
|
12808
12818
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13617,7 +13627,7 @@
|
|
|
13617
13627
|
{
|
|
13618
13628
|
"path": "tests/cli_allowlist_permissions.mjs",
|
|
13619
13629
|
"mode": "0000644",
|
|
13620
|
-
"sha256": "
|
|
13630
|
+
"sha256": "92bad4d5a6f224b0764bab5e7f906a0990c60f7fab75c5977ce64ba9e9b08b44"
|
|
13621
13631
|
},
|
|
13622
13632
|
{
|
|
13623
13633
|
"path": "tests/cli_dispatcher.mjs",
|
|
@@ -14992,7 +15002,7 @@
|
|
|
14992
15002
|
{
|
|
14993
15003
|
"path": "tests/government_protected_surfaces.mjs",
|
|
14994
15004
|
"mode": "0000644",
|
|
14995
|
-
"sha256": "
|
|
15005
|
+
"sha256": "72fc7970c987dbb78120575546aed5faeb9e55342b68e6ba1668508927a9f103"
|
|
14996
15006
|
},
|
|
14997
15007
|
{
|
|
14998
15008
|
"path": "tests/government_rank_sync.mjs",
|
|
@@ -17939,6 +17949,16 @@
|
|
|
17939
17949
|
"mode": "0000644",
|
|
17940
17950
|
"sha256": "aee20a5f6e2998d18f605b91d1eb1019e8ada824135759b29633caa3153d9255"
|
|
17941
17951
|
},
|
|
17952
|
+
{
|
|
17953
|
+
"path": "tests/ship_mode_first_ship.mjs",
|
|
17954
|
+
"mode": "0000644",
|
|
17955
|
+
"sha256": "361d6c7a364c0c1f6fb8266f887fe25fc357250e4585b438fdbf3fb3fafa6849"
|
|
17956
|
+
},
|
|
17957
|
+
{
|
|
17958
|
+
"path": "tests/ship_mode_hands_off.mjs",
|
|
17959
|
+
"mode": "0000644",
|
|
17960
|
+
"sha256": "78f05df6119d9637994f628f3968eefff34aea8a04d28eaa899acc5a238ce154"
|
|
17961
|
+
},
|
|
17942
17962
|
{
|
|
17943
17963
|
"path": "tests/ship_post_land_verify.mjs",
|
|
17944
17964
|
"mode": "0000644",
|
|
@@ -83,6 +83,8 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
|
|
|
83
83
|
`--notes-file` is refused rather than resolved by precedence, so nothing you
|
|
84
84
|
wrote is dropped without a word.
|
|
85
85
|
|
|
86
|
+
**If the ship stops with `[otb-ask-ship-mode]` (exit 5), ask the person, once** (task 1004630, [ADR 0369](../../../docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md)). The project never chose who starts a ship, so its first ship asks. Nothing has resolved and the claim is still active. Put the question to the person with your question tool if you have one — two options, **Auto (recommended)** first, then **Manual**, each with the one-line meaning the ship printed — and re-run the exact command it printed with `--ship-mode auto` or `--ship-mode manual`. That saves the answer for the whole project and the ship carries on; it never asks again. Never answer it yourself: the choice is the owner's. An unattended run is never asked (it ships on the default, Auto).
|
|
87
|
+
|
|
86
88
|
**Never merge a task branch to main before running `ship.js`.** `ship.js` diffs the claim branch against `main`; an already-merged branch yields an empty diff, and the only exits are `--allow-empty` (dead-ends the task at `completed`, requiring an Archon-only confirm) or reverting the merge. Ship from the branch, then let `ship.js` land it. (This trap was hit on tasks 1002417 and 1002419, producing blockers 1000081/1000082 and an ~8.5-hour stall.)
|
|
87
89
|
|
|
88
90
|
5. **Render the completion card — it IS the summary; do NOT write a prose recap (task 1270).**
|
|
@@ -104,6 +106,7 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
|
|
|
104
106
|
|
|
105
107
|
## Constraints
|
|
106
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.
|
|
107
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.
|
|
108
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.
|
|
109
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.
|
|
@@ -129,7 +129,7 @@ export interface GetPageNotesSiteResponse { notes: unknown }
|
|
|
129
129
|
export interface GetProfilesHandleResponse { account: unknown; cross_project: unknown }
|
|
130
130
|
export interface GetProjectManageLinkResponse { ok: boolean; link: unknown }
|
|
131
131
|
export interface GetProjectSettingsResponse { rows: unknown }
|
|
132
|
-
export interface GetProjectSettingsShipModeResponse { mode: unknown; chosen: unknown; source: unknown; sentence: unknown; choices: unknown }
|
|
132
|
+
export interface GetProjectSettingsShipModeResponse { mode: unknown; chosen: unknown; source: unknown; sentence: unknown; choices: unknown; can_change: unknown }
|
|
133
133
|
export interface GetProjectsFeaturedResponse { projects: unknown; page: PageInfo }
|
|
134
134
|
export interface GetProjectsResponse { projects: unknown }
|
|
135
135
|
export interface GetProvisioningFleetResponse { instances: unknown; capacity: unknown }
|
|
@@ -33,7 +33,7 @@ It lives in `project_settings` (migration `core_231`), so it needs no migration.
|
|
|
33
33
|
|
|
34
34
|
- This task adds the setting only. Behaviour follows in the dependent tasks:
|
|
35
35
|
- the creation wizard asks for it (task 1004629);
|
|
36
|
-
- the first ship asks once if nobody has chosen (task 1004630);
|
|
36
|
+
- the first ship asks once if nobody has chosen (task 1004630). The ship stops before anything resolves and exits 5 with an `[otb-ask-ship-mode]` ask; the AI puts the two choices to the person and re-runs with `--ship-mode auto|manual`, which saves the answer. It never asks a caller who cannot change settings (`can_change` on the read), an unattended run, or when the mode cannot be read; those ship on the default;
|
|
37
37
|
- the AI obeys it (task 1004631).
|
|
38
38
|
- **How the wizard's answer reaches the project (task 1004629).** The wizard runs on the platform, and the new project's database is out of its reach. So the answer travels the way the founding marker does: the platform keeps it on the new project's record, sends it to the project at startup, and the project reads it as a *chosen* mode (`source: 'wizard'`) only while its own setting is unset. The settings page and the CLI write that setting, so a later change always wins. Nothing is written into the project's database on its behalf.
|
|
39
39
|
- A database read failure answers Auto rather than blocking a ship. That matches the setting's default, and blocking would be the worse outcome for a person mid-task.
|
package/docs/api/openapi.json
CHANGED
|
@@ -16489,7 +16489,7 @@
|
|
|
16489
16489
|
"project-settings"
|
|
16490
16490
|
],
|
|
16491
16491
|
"summary": "GET /project-settings/ship-mode",
|
|
16492
|
-
"description": "GET /project-settings/ship-mode — the ship mode alone, for the CLI and the AI. rank: any signed-in builder. Unlike the full settings read this carries no reward internals or file paths an agent could not already read, and every builder's AI session must know whether to ship on its own or stop and ask — gating it Metic+ would leave a newcomer's session guessing.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
16492
|
+
"description": "GET /project-settings/ship-mode — the ship mode alone, for the CLI and the AI. rank: any signed-in builder. Unlike the full settings read this carries no reward internals or file paths an agent could not already read, and every builder's AI session must know whether to ship on its own or stop and ask — gating it Metic+ would leave a newcomer's session guessing. can_change (task 1004630): whether THIS caller holds project.settings.manage, so the first-ship question is put only to someone who can answer it. null when authority could not be established; the CLI then asks, and a refused save lets the ship go on. Cosmetic only — the PATCH below keeps its own gate (ADR 0016).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
16493
16493
|
"x-rank": "any-builder",
|
|
16494
16494
|
"x-source": "modules/lifecycle/routes/project-settings.js",
|
|
16495
16495
|
"responses": {
|
|
@@ -27665,14 +27665,16 @@
|
|
|
27665
27665
|
"chosen": {},
|
|
27666
27666
|
"source": {},
|
|
27667
27667
|
"sentence": {},
|
|
27668
|
-
"choices": {}
|
|
27668
|
+
"choices": {},
|
|
27669
|
+
"can_change": {}
|
|
27669
27670
|
},
|
|
27670
27671
|
"required": [
|
|
27671
27672
|
"mode",
|
|
27672
27673
|
"chosen",
|
|
27673
27674
|
"source",
|
|
27674
27675
|
"sentence",
|
|
27675
|
-
"choices"
|
|
27676
|
+
"choices",
|
|
27677
|
+
"can_change"
|
|
27676
27678
|
]
|
|
27677
27679
|
},
|
|
27678
27680
|
"GetProjectsFeaturedResponse": {
|
|
@@ -3079,5 +3079,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3079
3079
|
landed since 1.21.119 with no explicit bump. run 38597828843. (task 1002620)
|
|
3080
3080
|
1.21.121 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3081
3081
|
landed since 1.21.120 with no explicit bump. run 38604777189. (task 1002620)
|
|
3082
|
+
1.21.122 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
3082
3086
|
---------------------------------------------------------------------------
|
|
3083
3087
|
```
|
|
@@ -71,6 +71,8 @@
|
|
|
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)." },
|
|
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." },
|
|
74
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." },
|
|
75
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." },
|
|
76
78
|
{ "glob": "scripts/gds/ship-resume.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." },
|
|
@@ -58,6 +58,21 @@ function shipModeRow(resolved) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// callerHolds(builderId, atom) → true | false | null. null means authority could
|
|
62
|
+
// not be established (government port missing or the resolver threw); a reader
|
|
63
|
+
// treats that as unknown, never as a grant.
|
|
64
|
+
async function callerHolds(builderId, atom) {
|
|
65
|
+
try {
|
|
66
|
+
const government = api.resolveOptional('government');
|
|
67
|
+
if (!government || typeof government.resolveBuilderPermissions !== 'function') return null;
|
|
68
|
+
const held = (await government.resolveBuilderPermissions(builderId)) || [];
|
|
69
|
+
return held.includes(atom);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
log.error(`ship-mode can_change: ${err && err.message}`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
// --- the three rows that are READ, never written -----------------------------
|
|
62
77
|
//
|
|
63
78
|
// Each names the committed artefact it resolves from, because "you cannot edit this"
|
|
@@ -348,12 +363,18 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
348
363
|
// reward internals or file paths an agent could not already read, and every
|
|
349
364
|
// builder's AI session must know whether to ship on its own or stop and ask —
|
|
350
365
|
// gating it Metic+ would leave a newcomer's session guessing.
|
|
366
|
+
//
|
|
367
|
+
// can_change (task 1004630): whether THIS caller holds project.settings.manage,
|
|
368
|
+
// so the first-ship question is put only to someone who can answer it. null when
|
|
369
|
+
// authority could not be established; the CLI then asks, and a refused save lets
|
|
370
|
+
// the ship go on. Cosmetic only — the PATCH below keeps its own gate (ADR 0016).
|
|
351
371
|
router.get('/project-settings/ship-mode', api.requireBuilder, async (req, res) => {
|
|
352
372
|
try {
|
|
353
373
|
const resolved = await api.projectSettings.resolveShipMode();
|
|
354
374
|
res.set('Cache-Control', 'no-store');
|
|
355
375
|
const row = shipModeRow(resolved);
|
|
356
|
-
|
|
376
|
+
const canChange = req.builder ? await callerHolds(req.builder.id, 'project.settings.manage') : null;
|
|
377
|
+
res.json({ mode: resolved.mode, chosen: resolved.chosen, source: resolved.source, sentence: row.sentence, choices: row.choices, can_change: canChange });
|
|
357
378
|
} catch (err) {
|
|
358
379
|
log.error(`GET /project-settings/ship-mode: ${err && err.message}`);
|
|
359
380
|
res.fail('project_settings_read_failed', { status: 500, message: 'internal error' });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9708,5 +9708,17 @@
|
|
|
9708
9708
|
"id": "1004463",
|
|
9709
9709
|
"text": "Turning a module on or off now says honestly when it takes effect at the next restart, and the platform's own server can be restarted from its project page instead of needing a login to the machine."
|
|
9710
9710
|
}
|
|
9711
|
+
],
|
|
9712
|
+
"1.21.122": [
|
|
9713
|
+
{
|
|
9714
|
+
"id": "1004630",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
9711
9723
|
]
|
|
9712
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
|
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const { cliClient, requireSession, arg, detectWorktreeName, cardFlagsFromMe } = require('./cli-lib');
|
|
11
|
+
const { cliClient, requireSession, arg, detectWorktreeName, cardFlagsFromMe, apiCall, cliExit } = require('./cli-lib');
|
|
12
|
+
// task 1004630: the first ship asks for the ship mode once (ADR 0369).
|
|
13
|
+
const { runFirstShipAsk } = require('./ship-mode-ask.js');
|
|
12
14
|
const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal, staleSummaryNotice } = require('./ship-honesty.js');
|
|
13
15
|
const { plainCardLines } = require('./plain-cards.js');
|
|
14
16
|
const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
|
|
@@ -34,6 +36,25 @@ const { noteServerReward } = require('./ship-state.js');
|
|
|
34
36
|
// task 1004536: each graded round lands in the autonomous run's ledger (inert
|
|
35
37
|
// without one), so ship.js can refuse a third grade at its entry point.
|
|
36
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
|
+
}
|
|
37
58
|
|
|
38
59
|
// ---------- Main flow ----------
|
|
39
60
|
|
|
@@ -42,7 +63,7 @@ async function shipMain() {
|
|
|
42
63
|
const args = process.argv.slice(2);
|
|
43
64
|
const taskId = Number(args[0]);
|
|
44
65
|
if (!Number.isFinite(taskId)) {
|
|
45
|
-
console.error('usage: node scripts/gds/ship.js <task-id> (--notes-file <path|-> | --notes "...") (--summary-file <path|-> | --summary "...") [--visual <image>] [--visual-alt "caption"] [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
|
|
66
|
+
console.error('usage: node scripts/gds/ship.js <task-id> (--notes-file <path|-> | --notes "...") (--summary-file <path|-> | --summary "...") [--visual <image>] [--visual-alt "caption"] [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>] [--ship-mode auto|manual]');
|
|
46
67
|
console.error(' prefer the -file forms for prose: a shell command-substitutes backticks in an inline value before node sees it (learning 1000197).');
|
|
47
68
|
process.exit(2);
|
|
48
69
|
}
|
|
@@ -143,6 +164,7 @@ async function shipMain() {
|
|
|
143
164
|
}
|
|
144
165
|
if (action.action === 'resume-merge') {
|
|
145
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)
|
|
146
168
|
return resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly });
|
|
147
169
|
}
|
|
148
170
|
if (action.action === 'done') {
|
|
@@ -157,6 +179,24 @@ async function shipMain() {
|
|
|
157
179
|
process.exit(1);
|
|
158
180
|
}
|
|
159
181
|
|
|
182
|
+
// ---- First ship asks for the ship mode, once (task 1004630, ADR 0369) ----
|
|
183
|
+
// A project that never chose Auto or Manual stops HERE, before anything resolves,
|
|
184
|
+
// so the AI can ask the person; the re-run carries --ship-mode and saves it. Never
|
|
185
|
+
// blocks a ship nobody can answer for (see ship-mode-ask.js).
|
|
186
|
+
const firstShip = await runFirstShipAsk({
|
|
187
|
+
args,
|
|
188
|
+
getShipMode: async () => {
|
|
189
|
+
const r = await apiCall('GET', '/api/bongos/project-settings/ship-mode');
|
|
190
|
+
return r.ok ? r.data : null;
|
|
191
|
+
},
|
|
192
|
+
saveShipMode: (mode) => apiCall('PATCH', '/api/bongos/project-settings/ship-mode', { mode }),
|
|
193
|
+
});
|
|
194
|
+
if (firstShip.stop) {
|
|
195
|
+
for (const line of firstShip.lines) console.log(line);
|
|
196
|
+
cliExit(firstShip.exit);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
160
200
|
// ---- Hard cap: refuse to ship over the monthly budget (V3.R84 #302, C3) ----
|
|
161
201
|
// Shipping runs the subagent grader, which spends API $. If this builder is at
|
|
162
202
|
// or over their monthly budget, refuse here — BEFORE the grader runs and BEFORE
|
|
@@ -173,6 +213,10 @@ async function shipMain() {
|
|
|
173
213
|
process.exit(1);
|
|
174
214
|
}
|
|
175
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
|
+
|
|
176
220
|
// ---- Surprise gate: forced "anything surprise you?" prompt ----
|
|
177
221
|
//
|
|
178
222
|
// Resolve the user's intent into a single value: either a non-empty surprise
|
|
@@ -830,6 +874,9 @@ async function regradeMain(opts = {}) {
|
|
|
830
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>]');
|
|
831
875
|
process.exit(2);
|
|
832
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;
|
|
833
880
|
// task 1003702: refuse --notes rather than swallowing it. The reasoning — why
|
|
834
881
|
// a refusal and not a persist — is in ship-honesty.js above regradeNotesRefusal
|
|
835
882
|
// and is not repeated here. What is load-bearing at THIS call site is the
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// scripts/gds/ship-mode-ask.js — the first-ship ship-mode question (task 1004630, ADR 0369).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Ship mode says who starts a ship: Auto (the AI ships on its own) or Manual
|
|
4
|
+
// (a person approves each ship). The creation wizard asks for it (task 1004629),
|
|
5
|
+
// but a project made before that step, or by `bongos init`, never answered. Such a
|
|
6
|
+
// project quietly runs on the default, so its owner never learns there was a choice.
|
|
7
|
+
// This asks ONCE, at the next ship, and saves the answer. Once saved the mode reads
|
|
8
|
+
// as chosen and the question never comes back.
|
|
9
|
+
//
|
|
10
|
+
// HOW. The ship stops BEFORE anything resolves (the claim stays active) and prints
|
|
11
|
+
// an ask the AI turns into a two-option question for the person. The AI re-runs the
|
|
12
|
+
// same ship with `--ship-mode auto|manual`; that saves the answer and the ship goes on.
|
|
13
|
+
//
|
|
14
|
+
// It never blocks a ship it cannot get an answer for. The ship goes on with the
|
|
15
|
+
// default, and nobody is asked, when:
|
|
16
|
+
// - the mode could not be read (the resolver's own posture: a ship is never
|
|
17
|
+
// blocked by a settings read);
|
|
18
|
+
// - the caller cannot change project settings (only an owner-side builder can
|
|
19
|
+
// answer, so asking a teammate would stop every ship of theirs for nothing);
|
|
20
|
+
// - nobody is there to answer (an Autobongos worker or a subagent).
|
|
21
|
+
//
|
|
22
|
+
// The decision is pure (shipModeAskVerdict) so tests prove it with no network.
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
// The ship exits with this code when it stops to ask. Distinct from 1 (failure)
|
|
27
|
+
// and 2 (usage), and from the grade cap's 6, so a wrapper can tell "a question is
|
|
28
|
+
// waiting" from "something broke".
|
|
29
|
+
const ASK_EXIT = 5;
|
|
30
|
+
|
|
31
|
+
const ASK_MARKER = '[otb-ask-ship-mode]';
|
|
32
|
+
|
|
33
|
+
// unattended(env) — true when no person is there to answer.
|
|
34
|
+
// OTB_SUBAGENT is set on every Autobongos worker (scripts/gds/autobongos-run.js
|
|
35
|
+
// workerEnv); AUTOBONGOS_GRADE_LEDGER is set only by that supervisor.
|
|
36
|
+
function unattended(env = process.env) {
|
|
37
|
+
return env.OTB_SUBAGENT === '1' || Boolean(env.AUTOBONGOS_GRADE_LEDGER);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// shipModeFlag(args) — the --ship-mode answer, or null when absent.
|
|
41
|
+
// { mode } for auto|manual, { bad } for any other value (the ship refuses it).
|
|
42
|
+
function shipModeFlag(args) {
|
|
43
|
+
const i = args.indexOf('--ship-mode');
|
|
44
|
+
if (i === -1) return null;
|
|
45
|
+
const raw = String(args[i + 1] || '').trim().toLowerCase();
|
|
46
|
+
if (raw === 'auto' || raw === 'manual') return { mode: raw };
|
|
47
|
+
return { bad: raw || '(missing)' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// shipModeAskVerdict({ shipMode, flag, unattended }) →
|
|
51
|
+
// { action: 'proceed', why } — nothing to ask; ship on
|
|
52
|
+
// { action: 'save', mode } — the person answered; save it, then ship on
|
|
53
|
+
// { action: 'ask' } — stop and ask
|
|
54
|
+
// shipMode is the GET /project-settings/ship-mode body, or null when unreadable.
|
|
55
|
+
// can_change is absent on a server older than this task; absent means "unknown",
|
|
56
|
+
// and unknown asks (a refused save then lets the ship go on, see saveFailureLines).
|
|
57
|
+
function shipModeAskVerdict({ shipMode, flag, unattended: alone }) {
|
|
58
|
+
if (!shipMode) return { action: 'proceed', why: 'unreadable' };
|
|
59
|
+
if (shipMode.chosen) return { action: 'proceed', why: 'chosen' };
|
|
60
|
+
if (flag && flag.mode) return { action: 'save', mode: flag.mode };
|
|
61
|
+
if (shipMode.can_change === false) return { action: 'proceed', why: 'cannot_change' };
|
|
62
|
+
if (alone) return { action: 'proceed', why: 'unattended' };
|
|
63
|
+
return { action: 'ask' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// askLines(shipMode, argv) — what the ship prints when it stops to ask. The first
|
|
67
|
+
// lines are for the AI; the choices are the server's own words, so the question a
|
|
68
|
+
// person sees matches the settings page.
|
|
69
|
+
function askLines(shipMode, argv) {
|
|
70
|
+
const choices = (shipMode && Array.isArray(shipMode.choices) && shipMode.choices.length)
|
|
71
|
+
? shipMode.choices
|
|
72
|
+
: [
|
|
73
|
+
{ mode: 'auto', label: 'Auto (recommended)', sentence: 'When the AI finishes a task it ships and lands it. Nobody has to type a command.' },
|
|
74
|
+
{ mode: 'manual', label: 'Manual', sentence: 'The AI stops before each ship and asks a person to approve it.' },
|
|
75
|
+
];
|
|
76
|
+
const rerun = ['node', 'scripts/gds/ship.js', ...argv.filter((a, i) => a !== '--ship-mode' && argv[i - 1] !== '--ship-mode')]
|
|
77
|
+
.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(' ');
|
|
78
|
+
return [
|
|
79
|
+
`${ASK_MARKER} This project has never chosen a ship mode, so this first ship asks once.`,
|
|
80
|
+
'Ask the person this question with two options, Auto first, then re-run this ship with their answer:',
|
|
81
|
+
'',
|
|
82
|
+
' How should finished work ship on this project?',
|
|
83
|
+
...choices.map((c) => ` - ${c.label}: ${c.sentence}`),
|
|
84
|
+
'',
|
|
85
|
+
` Re-run: ${rerun} --ship-mode auto|manual`,
|
|
86
|
+
'',
|
|
87
|
+
'Nothing was resolved: the claim is still active. The answer is saved for the',
|
|
88
|
+
'whole project and can be changed later on the project settings page.',
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// saveFailureLines(status) — a refused or failed save never blocks the ship. The
|
|
93
|
+
// default (Auto) applies, and the person learns who can set it.
|
|
94
|
+
function saveFailureLines(status) {
|
|
95
|
+
const who = status === 403
|
|
96
|
+
? 'Only the project owner (or someone with project settings access) can set the ship mode,'
|
|
97
|
+
: `The ship mode could not be saved (${status || 'no response'}),`;
|
|
98
|
+
return [
|
|
99
|
+
`${who} so this ship goes on with the default, Auto.`,
|
|
100
|
+
'It can be set later on the project settings page, or with: node scripts/gds/shipping-mode.js auto|manual',
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// runFirstShipAsk({ args, getShipMode, saveShipMode, env, log }) — the side-effect
|
|
105
|
+
// half shipMain calls once it knows this is a real ship. Returns
|
|
106
|
+
// { stop: true, lines } when the ship must stop and ask, else { stop: false }.
|
|
107
|
+
// getShipMode() → body or null; saveShipMode(mode) → { ok, status, data }.
|
|
108
|
+
async function runFirstShipAsk({ args, getShipMode, saveShipMode, env = process.env, log = console.log }) {
|
|
109
|
+
const flag = shipModeFlag(args);
|
|
110
|
+
if (flag && flag.bad) {
|
|
111
|
+
return { stop: true, exit: 2, lines: [`--ship-mode must be auto or manual (got ${flag.bad}). Nothing was resolved: the claim is still active.`] };
|
|
112
|
+
}
|
|
113
|
+
let shipMode = null;
|
|
114
|
+
try { shipMode = await getShipMode(); } catch (_) { shipMode = null; }
|
|
115
|
+
const verdict = shipModeAskVerdict({ shipMode, flag, unattended: unattended(env) });
|
|
116
|
+
if (verdict.action === 'ask') return { stop: true, exit: ASK_EXIT, lines: askLines(shipMode, args) };
|
|
117
|
+
if (verdict.action === 'save') {
|
|
118
|
+
let r = null;
|
|
119
|
+
try { r = await saveShipMode(verdict.mode); } catch (_) { r = null; }
|
|
120
|
+
if (r && r.ok) {
|
|
121
|
+
log(`Ship mode saved: ${verdict.mode}. ${(r.data && r.data.sentence) || ''}`.trim());
|
|
122
|
+
} else {
|
|
123
|
+
for (const line of saveFailureLines(r && r.status)) log(line);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { stop: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
ASK_EXIT,
|
|
131
|
+
ASK_MARKER,
|
|
132
|
+
unattended,
|
|
133
|
+
shipModeFlag,
|
|
134
|
+
shipModeAskVerdict,
|
|
135
|
+
askLines,
|
|
136
|
+
saveFailureLines,
|
|
137
|
+
runFirstShipAsk,
|
|
138
|
+
};
|
|
@@ -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 };
|
package/scripts/gds/ship.js
CHANGED
|
@@ -50,11 +50,22 @@
|
|
|
50
50
|
// # game-surface diffs are otherwise staged to
|
|
51
51
|
// # the builder's own local preview (ADR 0044) for
|
|
52
52
|
// # a browser review BEFORE the claim resolves.
|
|
53
|
+
// node scripts/gds/ship.js <task-id> --ship-mode auto|manual
|
|
54
|
+
// # answer the first-ship question (task 1004630,
|
|
55
|
+
// # ADR 0369): a project that never chose a ship
|
|
56
|
+
// # mode stops once, before anything resolves, and
|
|
57
|
+
// # exits 5 with an ask; the re-run with the answer
|
|
58
|
+
// # saves it for the whole project and ships on.
|
|
53
59
|
// node scripts/gds/ship.js <task-id> --approved # record your sandbox sign-off (#1048): re-run with
|
|
54
60
|
// # this AFTER reviewing the staged change at the
|
|
55
61
|
// # sandbox URL. Persists a sandbox_review row on the
|
|
56
62
|
// # task. Honored only when the game state is already
|
|
57
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.
|
|
58
69
|
|
|
59
70
|
|
|
60
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,
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -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,12 +104,14 @@ 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',
|
|
110
111
|
'scripts/gds/ship-grade-steps.js', 'scripts/gds/ship-grade.js', 'scripts/gds/ship-honesty.js',
|
|
111
112
|
'scripts/gds/ship-io.js', 'scripts/gds/ship-land.js', 'scripts/gds/ship-memory-sync.js',
|
|
112
|
-
'scripts/gds/ship-merge-gate.js', 'scripts/gds/ship-merge.js', 'scripts/gds/ship-
|
|
113
|
+
'scripts/gds/ship-merge-gate.js', 'scripts/gds/ship-merge.js', 'scripts/gds/ship-mode-ask.js',
|
|
114
|
+
'scripts/gds/ship-preflight-steps.js',
|
|
113
115
|
'scripts/gds/ship-regen.js', 'scripts/gds/ship-resume.js', 'scripts/gds/ship-session-upload.js',
|
|
114
116
|
'scripts/gds/ship-state.js', 'scripts/gds/ship-surprise.js', 'scripts/gds/ship-visual.js',
|
|
115
117
|
'scripts/gds/merge-mode.js', 'scripts/gds/confirm.js', 'scripts/gds/main-audit.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.
|
|
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
|
|
@@ -266,7 +266,7 @@ const ADDED_SINCE_R101 = [
|
|
|
266
266
|
+ 'score guards a model cannot talk past.'),
|
|
267
267
|
...carve('scripts/gds/ship.js', ['ship-check', 'ship-deploy-target', 'ship-escalation',
|
|
268
268
|
'ship-finish', 'ship-flow', 'ship-git', 'ship-grade-steps', 'ship-grade', 'ship-honesty',
|
|
269
|
-
'ship-io', 'ship-land', 'ship-memory-sync', 'ship-merge-gate', 'ship-merge',
|
|
269
|
+
'ship-io', 'ship-land', 'ship-memory-sync', 'ship-merge-gate', 'ship-merge', 'ship-mode-ask',
|
|
270
270
|
'ship-preflight-steps', 'ship-regen', 'ship-resume', 'ship-session-upload', 'ship-state',
|
|
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 '
|
|
@@ -295,6 +295,14 @@ const ADDED_SINCE_R101 = [
|
|
|
295
295
|
why: 'the core neutral provider set, which declares none. An entry added here would '
|
|
296
296
|
+ 'send every instance\'s diffs to a vendor by default.',
|
|
297
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
|
+
},
|
|
298
306
|
];
|
|
299
307
|
|
|
300
308
|
// Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// tests/ship_mode_first_ship.mjs — the first ship asks for the ship mode once
|
|
2
|
+
// (task 1004630, ADR 0369).
|
|
3
|
+
//
|
|
4
|
+
// Pinned here:
|
|
5
|
+
// 1. the verdict: ask only when the mode was never chosen, the caller can answer,
|
|
6
|
+
// and someone is there to answer; an answer is saved; a chosen mode never asks;
|
|
7
|
+
// 2. the flow: an ask stops the ship with exit 5 and prints the two choices plus
|
|
8
|
+
// the re-run; the re-run saves the answer once and the ship goes on; a refused
|
|
9
|
+
// save never blocks the ship; a bad --ship-mode value is refused;
|
|
10
|
+
// 3. asked exactly once: after the answer is saved, the next ship does not ask;
|
|
11
|
+
// 4. the wiring: shipMain runs the ask after the claim is found and before the
|
|
12
|
+
// budget cap, the grader and the resolve, so nothing resolves while it waits;
|
|
13
|
+
// 5. the read route reports can_change for the caller, and null when authority
|
|
14
|
+
// cannot be established.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/ship_mode_first_ship.mjs
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
ASK_EXIT, ASK_MARKER, unattended, shipModeFlag, shipModeAskVerdict, askLines, runFirstShipAsk,
|
|
29
|
+
} = require('../scripts/gds/ship-mode-ask.js');
|
|
30
|
+
|
|
31
|
+
const unchosen = { mode: 'auto', chosen: false, source: 'default', can_change: true };
|
|
32
|
+
const chosen = { mode: 'manual', chosen: true, source: 'project_settings', can_change: true };
|
|
33
|
+
|
|
34
|
+
// ---- 1. the verdict --------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
test('a never-chosen mode asks; a chosen one never does', () => {
|
|
37
|
+
assert.deepEqual(shipModeAskVerdict({ shipMode: unchosen, flag: null, unattended: false }), { action: 'ask' });
|
|
38
|
+
assert.equal(shipModeAskVerdict({ shipMode: chosen, flag: null, unattended: false }).action, 'proceed');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('an answer on an unchosen project is saved; on a chosen one it changes nothing', () => {
|
|
42
|
+
assert.deepEqual(shipModeAskVerdict({ shipMode: unchosen, flag: { mode: 'manual' }, unattended: false }), { action: 'save', mode: 'manual' });
|
|
43
|
+
assert.equal(shipModeAskVerdict({ shipMode: chosen, flag: { mode: 'auto' }, unattended: false }).action, 'proceed',
|
|
44
|
+
'the flag answers the first-ship question only; changing a chosen mode is the settings page or shipping-mode.js');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('never blocks a ship nobody can answer for', () => {
|
|
48
|
+
assert.equal(shipModeAskVerdict({ shipMode: null, flag: null, unattended: false }).why, 'unreadable');
|
|
49
|
+
assert.equal(shipModeAskVerdict({ shipMode: { ...unchosen, can_change: false }, flag: null, unattended: false }).why, 'cannot_change');
|
|
50
|
+
assert.equal(shipModeAskVerdict({ shipMode: unchosen, flag: null, unattended: true }).why, 'unattended');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('an older server with no can_change still asks (unknown is not "no")', () => {
|
|
54
|
+
const { can_change, ...old } = unchosen;
|
|
55
|
+
assert.equal(shipModeAskVerdict({ shipMode: old, flag: null, unattended: false }).action, 'ask');
|
|
56
|
+
assert.equal(shipModeAskVerdict({ shipMode: { ...unchosen, can_change: null }, flag: null, unattended: false }).action, 'ask');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('unattended means an Autobongos worker or a subagent', () => {
|
|
60
|
+
assert.equal(unattended({}), false);
|
|
61
|
+
assert.equal(unattended({ OTB_SUBAGENT: '1' }), true);
|
|
62
|
+
assert.equal(unattended({ AUTOBONGOS_GRADE_LEDGER: '/tmp/ledger.json' }), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('--ship-mode parses auto|manual in any case and flags anything else', () => {
|
|
66
|
+
assert.equal(shipModeFlag(['1', '--notes', 'x']), null);
|
|
67
|
+
assert.deepEqual(shipModeFlag(['1', '--ship-mode', 'Auto']), { mode: 'auto' });
|
|
68
|
+
assert.deepEqual(shipModeFlag(['1', '--ship-mode', 'manual']), { mode: 'manual' });
|
|
69
|
+
assert.deepEqual(shipModeFlag(['1', '--ship-mode', 'sometimes']), { bad: 'sometimes' });
|
|
70
|
+
assert.deepEqual(shipModeFlag(['1', '--ship-mode']), { bad: '(missing)' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---- 2. the flow -----------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function harness(initial, { saveStatus = 200 } = {}) {
|
|
76
|
+
let current = { ...initial };
|
|
77
|
+
const saves = [];
|
|
78
|
+
const logs = [];
|
|
79
|
+
return {
|
|
80
|
+
saves, logs,
|
|
81
|
+
deps: (args, env = {}) => ({
|
|
82
|
+
args,
|
|
83
|
+
env,
|
|
84
|
+
log: (l) => logs.push(l),
|
|
85
|
+
getShipMode: async () => ({ ...current }),
|
|
86
|
+
saveShipMode: async (mode) => {
|
|
87
|
+
saves.push(mode);
|
|
88
|
+
if (saveStatus !== 200) return { ok: false, status: saveStatus, data: { error: 'permission_forbidden' } };
|
|
89
|
+
current = { ...current, mode, chosen: true, source: 'project_settings' };
|
|
90
|
+
return { ok: true, status: 200, data: { mode, chosen: true, sentence: `Saved ${mode}.` } };
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('an unchosen project stops the ship with exit 5 and asks Auto first', async () => {
|
|
97
|
+
const h = harness(unchosen);
|
|
98
|
+
const args = ['1004630', '--notes-file', 'notes.md', '--summary', 'two words'];
|
|
99
|
+
const r = await runFirstShipAsk(h.deps(args));
|
|
100
|
+
assert.equal(r.stop, true);
|
|
101
|
+
assert.equal(r.exit, ASK_EXIT);
|
|
102
|
+
assert.equal(ASK_EXIT, 5);
|
|
103
|
+
const text = r.lines.join('\n');
|
|
104
|
+
assert.ok(r.lines[0].startsWith(ASK_MARKER));
|
|
105
|
+
assert.match(text, /Auto \(recommended\)[\s\S]*Manual/, 'Auto is offered first and named as recommended');
|
|
106
|
+
assert.match(text, /node scripts\/gds\/ship\.js 1004630 --notes-file notes\.md --summary "two words" --ship-mode auto\|manual/);
|
|
107
|
+
assert.match(text, /claim is still active/);
|
|
108
|
+
assert.deepEqual(h.saves, [], 'asking saves nothing');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('the re-run with the answer saves it and the ship goes on', async () => {
|
|
112
|
+
const h = harness(unchosen);
|
|
113
|
+
const r = await runFirstShipAsk(h.deps(['1004630', '--ship-mode', 'manual']));
|
|
114
|
+
assert.equal(r.stop, false);
|
|
115
|
+
assert.deepEqual(h.saves, ['manual']);
|
|
116
|
+
assert.match(h.logs.join('\n'), /Ship mode saved: manual/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('asked exactly once: after the answer, the next ship neither asks nor saves', async () => {
|
|
120
|
+
const h = harness(unchosen);
|
|
121
|
+
const first = await runFirstShipAsk(h.deps(['1']));
|
|
122
|
+
assert.equal(first.stop, true);
|
|
123
|
+
await runFirstShipAsk(h.deps(['1', '--ship-mode', 'auto']));
|
|
124
|
+
const next = await runFirstShipAsk(h.deps(['2']));
|
|
125
|
+
assert.equal(next.stop, false, 'the second ship does not ask again');
|
|
126
|
+
const nextWithStaleFlag = await runFirstShipAsk(h.deps(['3', '--ship-mode', 'manual']));
|
|
127
|
+
assert.equal(nextWithStaleFlag.stop, false);
|
|
128
|
+
assert.deepEqual(h.saves, ['auto'], 'the answer was written once and never overwritten by a later ship');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a refused save never blocks the ship, and says who can set it', async () => {
|
|
132
|
+
const h = harness(unchosen, { saveStatus: 403 });
|
|
133
|
+
const r = await runFirstShipAsk(h.deps(['1', '--ship-mode', 'manual']));
|
|
134
|
+
assert.equal(r.stop, false);
|
|
135
|
+
assert.match(h.logs.join('\n'), /Only the project owner[\s\S]*default, Auto/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a bad --ship-mode value is refused before anything resolves', async () => {
|
|
139
|
+
const h = harness(unchosen);
|
|
140
|
+
const r = await runFirstShipAsk(h.deps(['1', '--ship-mode', 'sometimes']));
|
|
141
|
+
assert.equal(r.stop, true);
|
|
142
|
+
assert.equal(r.exit, 2);
|
|
143
|
+
assert.match(r.lines[0], /must be auto or manual/);
|
|
144
|
+
assert.deepEqual(h.saves, []);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('an unreadable mode and an unattended run both ship on without asking', async () => {
|
|
148
|
+
const dead = await runFirstShipAsk({ args: ['1'], getShipMode: async () => { throw new Error('down'); }, saveShipMode: async () => assert.fail('no save'), env: {}, log: () => {} });
|
|
149
|
+
assert.equal(dead.stop, false);
|
|
150
|
+
const h = harness(unchosen);
|
|
151
|
+
const alone = await runFirstShipAsk(h.deps(['1'], { OTB_SUBAGENT: '1' }));
|
|
152
|
+
assert.equal(alone.stop, false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('the ask falls back to its own wording when the server sends no choices', () => {
|
|
156
|
+
const lines = askLines({ mode: 'auto', chosen: false }, ['1']);
|
|
157
|
+
assert.match(lines.join('\n'), /Auto \(recommended\)[\s\S]*Manual/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---- 4. the wiring ---------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
test('shipMain asks after finding the claim and before the budget cap and grader', () => {
|
|
163
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship-flow.js'), 'utf8');
|
|
164
|
+
const main = src.slice(src.indexOf('async function shipMain'));
|
|
165
|
+
const claimFound = main.indexOf("const ac = claims.find(");
|
|
166
|
+
const ask = main.indexOf('runFirstShipAsk(');
|
|
167
|
+
const budget = main.indexOf('Hard cap: refuse to ship over the monthly budget');
|
|
168
|
+
assert.ok(claimFound > 0 && ask > claimFound, 'asks only once a real claim is in hand');
|
|
169
|
+
assert.ok(budget > ask, 'asks before the budget cap, so nothing has resolved or been spent');
|
|
170
|
+
assert.match(main.slice(ask, budget), /cliExit\(firstShip\.exit\);\s*return;/, 'stops with cliExit, never a hard exit after a request');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('the --ship-mode flag is documented in the ship usage banner', () => {
|
|
174
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship.js'), 'utf8');
|
|
175
|
+
assert.match(src, /--ship-mode auto\|manual/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---- 5. the read route's can_change ----------------------------------------
|
|
179
|
+
|
|
180
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
181
|
+
const fakeRes = () => ({
|
|
182
|
+
code: null, statusCode: 200, payload: null, headers: {},
|
|
183
|
+
fail(code, opts) { this.code = code; this.statusCode = opts?.status ?? 400; this.payload = { error: code }; return this; },
|
|
184
|
+
status(c) { this.statusCode = c; return this; },
|
|
185
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
186
|
+
json(p) { this.payload = p; return this; },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
async function readAs({ builder, government }) {
|
|
190
|
+
const saved = { resolve: api.projectSettings.resolveShipMode, resolveOptional: api.resolveOptional };
|
|
191
|
+
api.projectSettings.resolveShipMode = async () => ({ mode: 'auto', source: 'default', chosen: false, invalid: null });
|
|
192
|
+
api.resolveOptional = (port) => (port === 'government' ? government : saved.resolveOptional(port));
|
|
193
|
+
try {
|
|
194
|
+
const file = path.join(ROOT, 'modules', 'lifecycle', 'routes', 'project-settings.js');
|
|
195
|
+
delete require.cache[require.resolve(file)];
|
|
196
|
+
const router = require(file)();
|
|
197
|
+
const route = router.stack.find((l) => l.route && l.route.path === '/project-settings/ship-mode' && l.route.methods.get).route;
|
|
198
|
+
const res = fakeRes();
|
|
199
|
+
await route.stack[route.stack.length - 1].handle({ builder }, res);
|
|
200
|
+
return res;
|
|
201
|
+
} finally {
|
|
202
|
+
api.projectSettings.resolveShipMode = saved.resolve;
|
|
203
|
+
api.resolveOptional = saved.resolveOptional;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
test('can_change is true for a settings manager and false for anyone else', async () => {
|
|
208
|
+
const government = { resolveBuilderPermissions: async (id) => (id === 1 ? ['project.settings.manage'] : ['task.claim.any']) };
|
|
209
|
+
assert.equal((await readAs({ builder: { id: 1 }, government })).payload.can_change, true);
|
|
210
|
+
assert.equal((await readAs({ builder: { id: 2 }, government })).payload.can_change, false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('can_change is null, never true, when authority cannot be established', async () => {
|
|
214
|
+
assert.equal((await readAs({ builder: { id: 1 }, government: null })).payload.can_change, null);
|
|
215
|
+
const throwing = { resolveBuilderPermissions: async () => { throw new Error('boom'); } };
|
|
216
|
+
const res = await readAs({ builder: { id: 1 }, government: throwing });
|
|
217
|
+
assert.equal(res.statusCode, 200, 'a permission read failure never fails the read');
|
|
218
|
+
assert.equal(res.payload.can_change, null);
|
|
219
|
+
});
|
|
@@ -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
|
+
});
|