@bongos/core 1.19.839 → 1.19.841

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.bongos-core.json +73 -33
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/config/scheduled-routines.json +29 -0
  4. package/docs/adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md +49 -0
  5. package/docs/adr/README.md +1 -0
  6. package/docs/api/openapi.json +2 -0
  7. package/docs/architecture.md +2 -2
  8. package/docs/module-api-changelog.md +4 -0
  9. package/modules/government/protected-surfaces.json +2 -1
  10. package/modules/lifecycle/github-push.js +2 -2
  11. package/modules/lifecycle/pr-assign.js +77 -0
  12. package/modules/lifecycle/routes/tasks.js +16 -1
  13. package/modules/provisioning/pollers/liveness-notify.js +110 -0
  14. package/modules/provisioning/pollers/liveness-sweep.js +27 -2
  15. package/modules/provisioning/provisioning.js +9 -9
  16. package/modules/provisioning/tests/liveness-notify.mjs +199 -0
  17. package/modules/provisioning/tests/liveness.mjs +4 -3
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/scripts/gds/paste-token.js +65 -14
  21. package/scripts/gds/provision-config.js +3 -0
  22. package/scripts/gds/provision-net.js +32 -0
  23. package/scripts/gds/provision.js +21 -4
  24. package/scripts/gds/publish-manifest.js +1 -0
  25. package/scripts/gds/ship-land.js +12 -0
  26. package/src/bongos/routes/auth.js +24 -3
  27. package/src/module-api.js +1 -1
  28. package/tests/bongos_cli.mjs +13 -4
  29. package/tests/cli_token_reissue.mjs +54 -7
  30. package/tests/government_protected_surfaces.mjs +181 -0
  31. package/tests/paste_token_api_base.mjs +87 -0
  32. package/tests/pr_assign.mjs +189 -0
  33. package/tests/provision.mjs +12 -0
  34. package/tests/provision_caddy_wiring.mjs +148 -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.19.839",
6
- "core_contract": "1.19.839",
7
- "source_commit": "2799589e44782acf35629e14f332ac30686ed028",
5
+ "core_version": "1.19.841",
6
+ "core_contract": "1.19.841",
7
+ "source_commit": "fd701dc457a5344486573c53ca1f4efcc1948007",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T17:25:32.211Z",
9
+ "built_at": "2026-09-17T18:13:18.546Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 494,
12
+ "docs_redacted": 495,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2258,
14
+ "functional_verbatim": 2265,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2778,
20
- "tree_sha256": "209c682e3b1a302720d23b8e207501aba43d917dc24ebe0fe4d11836c49a6af8",
19
+ "file_count": 2786,
20
+ "tree_sha256": "a54472eaf1c55f1c7f905fb6fd2712a9ab44e8e9f3a731ffba11037c3d9f5b69",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "7f48743a6878f01e484b8f2884b4b12588b956db307fd6bc83a67a19df61a52b"
370
+ "sha256": "031abfbccd58bf9d6cdf9368f0d3bc862ddcd2f373b585a6911196b28596fae2"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -404,6 +404,11 @@
404
404
  "mode": "0000644",
405
405
  "sha256": "9bdd8eb1fbfb9053ed0fb921c0f669dcdaad2fe8b2629ce4cc9457dca995afbe"
406
406
  },
407
+ {
408
+ "path": "config/scheduled-routines.json",
409
+ "mode": "0000644",
410
+ "sha256": "9086dbccb8319aaca6b88757249bc3a5001612f4b5e2afb47ac41e9dfe4fd28f"
411
+ },
407
412
  {
408
413
  "path": "docker-compose.yml",
409
414
  "mode": "0000644",
@@ -1999,10 +2004,15 @@
1999
2004
  "mode": "0000644",
2000
2005
  "sha256": "08ea781a488d7454ca49b17e9540bb4c7bb9f1925aedf8ad4634057a89dd0d22"
2001
2006
  },
2007
+ {
2008
+ "path": "docs/adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md",
2009
+ "mode": "0000644",
2010
+ "sha256": "62ffb9c1aeeaaafd5c51767f1ffff1c67adb3563e8d55ceb81ef77511184faa4"
2011
+ },
2002
2012
  {
2003
2013
  "path": "docs/adr/README.md",
2004
2014
  "mode": "0000644",
2005
- "sha256": "5c20d4bf4c0202a391a617d99daaa97d8a566d9d5c8534b5bd060faa8f8afe02"
2015
+ "sha256": "48323e44fced698a0da997eeb16564ab3b04430c8233a8f7997c0fec2b123401"
2006
2016
  },
2007
2017
  {
2008
2018
  "path": "docs/api-reference.md",
@@ -2012,12 +2022,12 @@
2012
2022
  {
2013
2023
  "path": "docs/api/openapi.json",
2014
2024
  "mode": "0000644",
2015
- "sha256": "11ffe0dfc04855064064f335396b1b57887a1d4e61b21559c376d279ea7513fd"
2025
+ "sha256": "a0107dc012a5355baacf3668c92b17675b35ce0cffc96052b1d92d99533c5744"
2016
2026
  },
2017
2027
  {
2018
2028
  "path": "docs/architecture.md",
2019
2029
  "mode": "0000644",
2020
- "sha256": "02a8f7f07465f7059bc888a367793812c78d689f2335d333529712833c452116"
2030
+ "sha256": "029e8c5a9562171251df353e08791f1520c2e17fa7d286c7b4d278dea195c6d7"
2021
2031
  },
2022
2032
  {
2023
2033
  "path": "docs/branding-contract.md",
@@ -2567,7 +2577,7 @@
2567
2577
  {
2568
2578
  "path": "docs/module-api-changelog.md",
2569
2579
  "mode": "0000644",
2570
- "sha256": "689361320ab6192aade8cb602c862af1f14454238575da829015077f55743628"
2580
+ "sha256": "79b20e6155f002dd63671b994cbbb8e2f269c3634cd213b5695c0de8bb031cc7"
2571
2581
  },
2572
2582
  {
2573
2583
  "path": "docs/modules-contract.md",
@@ -4512,7 +4522,7 @@
4512
4522
  {
4513
4523
  "path": "modules/government/protected-surfaces.json",
4514
4524
  "mode": "0000644",
4515
- "sha256": "ec125c2bfba680c7645dda0b65dd6ece183dd57ceecdb5616ea2b8b34b776e56"
4525
+ "sha256": "aba8a8b1ce22708f5a7c05a64cbb4d1a08db295339a09df3c50a91f2735c1d47"
4516
4526
  },
4517
4527
  {
4518
4528
  "path": "modules/government/ranks.js",
@@ -5682,7 +5692,7 @@
5682
5692
  {
5683
5693
  "path": "modules/lifecycle/github-push.js",
5684
5694
  "mode": "0000644",
5685
- "sha256": "f9c22388c70ba8476a6dba8cd072d92abc44da79c296a4685abab81e24edf6ed"
5695
+ "sha256": "84114ecaf06c6ac52b7d7e4b5eaff97086908d6f7df0a3e07e4be3aa923935bc"
5686
5696
  },
5687
5697
  {
5688
5698
  "path": "modules/lifecycle/goal-advisory.js",
@@ -5804,6 +5814,11 @@
5804
5814
  "mode": "0000644",
5805
5815
  "sha256": "81ed257d2156988e911d737e10cba2b8375f47fc6e27bf77c06d9dc18258380b"
5806
5816
  },
5817
+ {
5818
+ "path": "modules/lifecycle/pr-assign.js",
5819
+ "mode": "0000644",
5820
+ "sha256": "08ace9d2690ef7f563455bdd52f89e8f60587eb399a246726cdb6ae06e98699f"
5821
+ },
5807
5822
  {
5808
5823
  "path": "modules/lifecycle/publish-reconciler.js",
5809
5824
  "mode": "0000644",
@@ -5912,7 +5927,7 @@
5912
5927
  {
5913
5928
  "path": "modules/lifecycle/routes/tasks.js",
5914
5929
  "mode": "0000644",
5915
- "sha256": "22a55af5d5c8453a5c27351a74b03bd635638ea177f599763513752f88f7a224"
5930
+ "sha256": "760c76c6ce7ba52991991e7c5fd00a48466d4ad8ae8e61620d962cd4496ea06c"
5916
5931
  },
5917
5932
  {
5918
5933
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -6504,15 +6519,20 @@
6504
6519
  "mode": "0000644",
6505
6520
  "sha256": "bf4a392ce4c545feb246b818ad2b37dd5e92050538d5202fb8980941d0ec52c9"
6506
6521
  },
6522
+ {
6523
+ "path": "modules/provisioning/pollers/liveness-notify.js",
6524
+ "mode": "0000644",
6525
+ "sha256": "b359eb2c731563e3570f8fbea7128f78070e7bd5737110f739a2b2fcb2cf382b"
6526
+ },
6507
6527
  {
6508
6528
  "path": "modules/provisioning/pollers/liveness-sweep.js",
6509
6529
  "mode": "0000644",
6510
- "sha256": "f47e30b7d4bc1275f65e29a1d9f987c7f20e6baa0a6c441fb4ddbb56f11fe58f"
6530
+ "sha256": "90db999a2e1d3dec2e3910281e2e5dccd55cf294d7c5c38233456c29d71bacd3"
6511
6531
  },
6512
6532
  {
6513
6533
  "path": "modules/provisioning/provisioning.js",
6514
6534
  "mode": "0000644",
6515
- "sha256": "b70d138fb6ed6f0fe4f04d2a37a7a195364250ca10b46a933c5aad0057061c10"
6535
+ "sha256": "c2e7574b84038bd2f4da93d5ec54f8e577353d9c37e5585bff1dd9b8f855cccf"
6516
6536
  },
6517
6537
  {
6518
6538
  "path": "modules/provisioning/rate-limit.js",
@@ -6559,10 +6579,15 @@
6559
6579
  "mode": "0000644",
6560
6580
  "sha256": "cb2ca3bdf6cd99ee66d5242709fed3c167840a90cff9efe83ddb193c00c9719e"
6561
6581
  },
6582
+ {
6583
+ "path": "modules/provisioning/tests/liveness-notify.mjs",
6584
+ "mode": "0000644",
6585
+ "sha256": "27c83fd37966796a6dd0dd55c69b434e77f7782bebe7452706ff00cc99308db8"
6586
+ },
6562
6587
  {
6563
6588
  "path": "modules/provisioning/tests/liveness.mjs",
6564
6589
  "mode": "0000644",
6565
- "sha256": "ead476fd677f78a3daed878fe8916acecbdac25e1ae6e29f340a8d08428c8fbf"
6590
+ "sha256": "c5f7c2d8880f9bfd45346ac8a7b24e3391daa9d96aeffad806d074ac4fffabf4"
6566
6591
  },
6567
6592
  {
6568
6593
  "path": "modules/provisioning/tests/provisioning.mjs",
@@ -7867,12 +7892,12 @@
7867
7892
  {
7868
7893
  "path": "package-lock.json",
7869
7894
  "mode": "0000644",
7870
- "sha256": "fe22ff711989f39c34fa95045c0736e26abc2400d698548f353121fbd1ff8828"
7895
+ "sha256": "d507a842a2ad14e85f0e5564998ab1288afad232febc358f932736b51fd12bd7"
7871
7896
  },
7872
7897
  {
7873
7898
  "path": "package.json",
7874
7899
  "mode": "0000644",
7875
- "sha256": "f15fdfca04599ce879e85f8229aeba9019d17476e6020ebdec4ea03a98d4bd76"
7900
+ "sha256": "81e581275d29c80dc0c2496d83b2a5317da29e43d65aabf7a49309aed72b1003"
7876
7901
  },
7877
7902
  {
7878
7903
  "path": "public-docs/index.html",
@@ -8727,7 +8752,7 @@
8727
8752
  {
8728
8753
  "path": "scripts/gds/paste-token.js",
8729
8754
  "mode": "0000644",
8730
- "sha256": "b61317006e5df92afb5744bd9fb9b3180bf8a674b7f691773114280660a42a2a"
8755
+ "sha256": "cfc9e6a9640f8392a38077a7fa50bd1caae671a60c4735d2758c0cc5a6d10610"
8731
8756
  },
8732
8757
  {
8733
8758
  "path": "scripts/gds/plain-cards.js",
@@ -8767,7 +8792,7 @@
8767
8792
  {
8768
8793
  "path": "scripts/gds/provision-config.js",
8769
8794
  "mode": "0000644",
8770
- "sha256": "1f60ee3d585ad3ab8c84c6b0abac99573ed218183b8e8403f2ddf3e951ff6b3d"
8795
+ "sha256": "b5ec038d150da93b4c54eeb864586a70e46610655ccb5bf10bd1adaa0432a305"
8771
8796
  },
8772
8797
  {
8773
8798
  "path": "scripts/gds/provision-core-upgrade.js",
@@ -8782,7 +8807,7 @@
8782
8807
  {
8783
8808
  "path": "scripts/gds/provision-net.js",
8784
8809
  "mode": "0000644",
8785
- "sha256": "cab7a609869465fcac7234c69e4202f259293e69842163f8ff7b6edfd5d30b4a"
8810
+ "sha256": "f1ca176ff8cdb8de65940af1e30316db600cb5df529abbc997697abf1cbc2c2d"
8786
8811
  },
8787
8812
  {
8788
8813
  "path": "scripts/gds/provision-repo-auth.js",
@@ -8807,7 +8832,7 @@
8807
8832
  {
8808
8833
  "path": "scripts/gds/provision.js",
8809
8834
  "mode": "0000644",
8810
- "sha256": "94a77427cb16c8a959eb279866f3777830d7a33617f02aed5eedb383ca26e84a"
8835
+ "sha256": "43dd54ec69ff29ad83e631d53a65c1a2d22b89ef07181e4df14581e5199bfa61"
8811
8836
  },
8812
8837
  {
8813
8838
  "path": "scripts/gds/publish-credential-check.js",
@@ -8817,7 +8842,7 @@
8817
8842
  {
8818
8843
  "path": "scripts/gds/publish-manifest.js",
8819
8844
  "mode": "0000644",
8820
- "sha256": "3663bea0a86f5f0a32d4bd48d98a78405fcc7a2840b4db081cade8601e9efa5b"
8845
+ "sha256": "da74b7bf9aa1453eb56f922f3733aa06ac2d00216f668a06dc134934f1d4fde7"
8821
8846
  },
8822
8847
  {
8823
8848
  "path": "scripts/gds/publish-watch.js",
@@ -9182,7 +9207,7 @@
9182
9207
  {
9183
9208
  "path": "scripts/gds/ship-land.js",
9184
9209
  "mode": "0000644",
9185
- "sha256": "fafa751359543787133e26a7aa18307fec163b0955eea38677a1b319d4dc6162"
9210
+ "sha256": "805017578a7d69ca2c79d105e21add7862b41e352256e5fbdfad3e3740e69ced"
9186
9211
  },
9187
9212
  {
9188
9213
  "path": "scripts/gds/ship-memory-sync.js",
@@ -9692,7 +9717,7 @@
9692
9717
  {
9693
9718
  "path": "src/bongos/routes/auth.js",
9694
9719
  "mode": "0000644",
9695
- "sha256": "66abf424a565cdcc4de85350774d91e93be826a3b01a23ad75fee1feb7977a3c"
9720
+ "sha256": "c29f47f08ea45126a8ddd6fd432bdf5c17a98cac0ad6f6ae527bf23e62ffa001"
9696
9721
  },
9697
9722
  {
9698
9723
  "path": "src/bongos/routes/backup.js",
@@ -9802,7 +9827,7 @@
9802
9827
  {
9803
9828
  "path": "src/module-api.js",
9804
9829
  "mode": "0000644",
9805
- "sha256": "027988bab5433e279e5b5cd479a768f655dd68bc1e7e19d31279d140be462d2a"
9830
+ "sha256": "d01a900eabeae11f0d03aa7aff1ea89af23f58a3e01c07fc79a4e4ea8b68f4a4"
9806
9831
  },
9807
9832
  {
9808
9833
  "path": "src/module-loader/catalog.js",
@@ -10277,7 +10302,7 @@
10277
10302
  {
10278
10303
  "path": "tests/bongos_cli.mjs",
10279
10304
  "mode": "0000644",
10280
- "sha256": "ef01af16d62c55325a3b12cc7968a9d797994d2f46364149668bf4e2839cb0e0"
10305
+ "sha256": "4e845333aa949256b8b5c6dc73bf75fcc77047543b0f5e15e89172cad4161772"
10281
10306
  },
10282
10307
  {
10283
10308
  "path": "tests/bongos_dispatch.mjs",
@@ -10562,7 +10587,7 @@
10562
10587
  {
10563
10588
  "path": "tests/cli_token_reissue.mjs",
10564
10589
  "mode": "0000644",
10565
- "sha256": "a82544c109464d884e44731cbdd73e9e141d31d8e18a9241222d6a26223e19c2"
10590
+ "sha256": "da6b45619e6848c37ebc8d53ffc9d8f9d5379df26563c9030b339a8c389e645d"
10566
10591
  },
10567
10592
  {
10568
10593
  "path": "tests/client_baseurl_guard.mjs",
@@ -11422,7 +11447,7 @@
11422
11447
  {
11423
11448
  "path": "tests/government_protected_surfaces.mjs",
11424
11449
  "mode": "0000644",
11425
- "sha256": "1d8502e8d25eedb54ed0fcbe3ce1a0fe87495d03bd8bda7322e55d6004669a63"
11450
+ "sha256": "c3c72c3a2cf970222c80051787df7fd7d155b5abd2f80b797a6a58bf5e95e0eb"
11426
11451
  },
11427
11452
  {
11428
11453
  "path": "tests/government_rank_sync.mjs",
@@ -12374,6 +12399,11 @@
12374
12399
  "mode": "0000644",
12375
12400
  "sha256": "fd69c44ebf3d246ecedf527275448389cca809e231d1c6cb34e4c12bfdc38111"
12376
12401
  },
12402
+ {
12403
+ "path": "tests/paste_token_api_base.mjs",
12404
+ "mode": "0000644",
12405
+ "sha256": "e48806b48a9f7a4f9f9c010f45750e6b440d2de68feacdabacd9bd25d98f2046"
12406
+ },
12377
12407
  {
12378
12408
  "path": "tests/patch_gate_ordering.mjs",
12379
12409
  "mode": "0000644",
@@ -12434,6 +12464,11 @@
12434
12464
  "mode": "0000644",
12435
12465
  "sha256": "08a7496090f4617cef7e270afddcef8ee3a0b58bff6920b56bc2f2bbb49ed4ca"
12436
12466
  },
12467
+ {
12468
+ "path": "tests/pr_assign.mjs",
12469
+ "mode": "0000644",
12470
+ "sha256": "1f422614784bff05cfddff5196fabfb9458e8b1247e6e6ecb636f1e4f236fc78"
12471
+ },
12437
12472
  {
12438
12473
  "path": "tests/pre_compact.mjs",
12439
12474
  "mode": "0000644",
@@ -12627,7 +12662,12 @@
12627
12662
  {
12628
12663
  "path": "tests/provision.mjs",
12629
12664
  "mode": "0000644",
12630
- "sha256": "09a759a1b1e0cac3ddafdd64adca4daed0f6a8ab4c48a07f42954f38ed510433"
12665
+ "sha256": "5a1abd44776798ca89266f86663037086c1270aca569e54647607d4488cbfea9"
12666
+ },
12667
+ {
12668
+ "path": "tests/provision_caddy_wiring.mjs",
12669
+ "mode": "0000644",
12670
+ "sha256": "a4d448622c05e963602cbc0488215ba168c909bd12cd0db20eaa6fcb7de7a91e"
12631
12671
  },
12632
12672
  {
12633
12673
  "path": "tests/provision_disconnect.mjs",
@@ -216,7 +216,7 @@ export interface PostAuthAppPairStartRequest { device_name?: string; platform?:
216
216
  export interface PostAuthAppPairStartResponse { pair_code: unknown; poll_token: unknown; verify_path: unknown; verify_url: unknown; expires_in: unknown; interval: unknown }
217
217
  export interface PostAuthBackchannelLogoutRequest { logout_token: string }
218
218
  export interface PostAuthBackchannelLogoutResponse { ok: boolean }
219
- export interface PostAuthCliTokenIssueResponse { ok: boolean; token: unknown; expires_at: unknown; builder_login: unknown; paste_command: unknown }
219
+ export interface PostAuthCliTokenIssueResponse { ok: boolean; token: unknown; expires_at: unknown; builder_login: unknown; api_base: unknown; paste_command: unknown }
220
220
  export interface PostAuthDeviceExchangeRequest { assertion: string }
221
221
  export interface PostAuthDeviceExchangeResponse { token: unknown; builder: unknown }
222
222
  export interface PostAuthDevicePollRequest { device_code: string }
@@ -0,0 +1,29 @@
1
+ {
2
+ "_note": "Scheduled-routines portability manifest (task 2014 / BONGOS-V1, ADR 0115). Declares, per autonomous routine under `.claude/scheduled-tasks/<key>/`, how it ports to a self-hosted, multi-builder instance: its SCOPE, whether it's ON by default, its cadence, and whether it needs the autonomy flag armed. THE CONTRACT: every routine is INSTANCE-WIDE — one scheduled run per instance (a designated scheduler / control plane), NOT one cron per builder. A routine's per-builder EFFECT (bfg scores each builder; newcomer-floor restocks per discipline) comes from iterating the builder set inside the routine body — there is no per-builder schedule in v1. A fresh instance inherits this whole set from the core template; the operator opts in/out + sets cadence HERE, without editing the routine. Builders do not schedule their own routines; their involvement is governed by their per-builder settings (autonomy opt-in, wandering), not this file. Cadence values are hints for the scheduler; the actual cron lives in the operator's scheduler config. `requiresAutonomy: true` means the routine is a no-op until the instance arms the autonomy flag (<PREFIX>_AUTONOMY_ENABLED) — those default OFF.",
3
+ "defaults": { "scope": "instance-wide", "enabled": true, "requiresAutonomy": false },
4
+ "routines": {
5
+ "artifact-audit": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false, "note": "deterministic; task 1003200. Reads main's own history + tree, so it runs in CI (a vendored core is not a git repo) — .github/workflows/artifact-audit.yml." },
6
+ "artifact-staleness": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false, "note": "deterministic; task 1001930 / ADR 0110 pt.5. INSTANCE-SIDE ONLY, and the opposite of artifact-audit above: it reads the gitignored deploy-generated files on the serving machine's disk, which a CI checkout legitimately does not have — so it declares no cadence in its SKILL.md and no workflow fires it. This cadence is the hint the operator's own scheduler reads." },
7
+ "bfg": { "scope": "instance-wide", "builderAware": true, "enabled": true, "cadence": "nightly", "requiresAutonomy": false, "note": "Parts A+B read-only + always-on; Part C (cross-builder write) is separately gated by BFG_WRITE_ENABLED + a provisioned principal token." },
8
+ "bfg-session-eval": { "scope": "instance-wide", "builderAware": true, "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
9
+ "box-dormant-reclaim": { "scope": "instance-wide", "enabled": false, "cadence": "daily", "requiresAutonomy": false, "note": "RETIRED with the dev-box module (goal 1000091, task 1003886). Held OFF so the fleet is inert while the removal lands; the routine body is deleted by the dev-box teardown tasks." },
10
+ "box-idle-suspend": { "scope": "instance-wide", "enabled": false, "cadence": "hourly", "requiresAutonomy": false, "note": "RETIRED with the dev-box module (goal 1000091, task 1003886). Held OFF so the fleet is inert while the removal lands; the routine body is deleted by the dev-box teardown tasks." },
11
+ "core-update-subscription": { "scope": "instance-wide", "enabled": true, "cadence": "15min", "requiresAutonomy": true, "note": "Patch-only auto-upgrade of subscribed consumer instances (ADR 0136 / task 2150). Default OFF + opt-in: the roster (config/update-subscriptions.json) ships empty, so this is a no-op until an operator arms autonomy AND lists an instance. Every upgrade is health-gated with auto-rollback (task 2149). CADENCE (task 1003674): this is the knob that decides how long a published release waits before a subscribed instance takes it — at 'daily' a builder's ship reached the live site up to 24h late, because everything upstream is already fast (CI publishes to npm within ~2 min of merge) and this sweep was the only slow step. 15min is the worst-case lag, not a re-check cost: the sweep exits immediately when the registry has nothing newer. FLOOR ~5min: an upgrading run takes ~2 min, and runs must not overlap. Installing the period is the operator's half — see docs/recipes/upgrading-the-core.md." },
12
+ "dep-audit-nightly": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
13
+ "diagram-drift": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
14
+ "idea-triage-nightly": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true },
15
+ "main-audit": { "scope": "instance-wide", "enabled": true, "cadence": "on-merge", "requiresAutonomy": false, "note": "Trust-boundary backstop — audits commits landed on main." },
16
+ "newcomer-floor": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
17
+ "newcomer-generator": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
18
+ "overnight-builder": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true, "note": "Autonomous claim+ship; OFF until the instance arms autonomy." },
19
+ "overnight-code-review": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true },
20
+ "peer-vote-tally": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
21
+ "rank-roles-audit": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false, "note": "deterministic; read-only; never fatal at boot (idea 1000622) — task 1002873" },
22
+ "redteam-weekly": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false, "note": "deterministic; task 1003128" },
23
+ "repo-simplify-sweep": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false },
24
+ "security-review-weekly": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false },
25
+ "skill-usage-review": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
26
+ "stale-claim-release": { "scope": "instance-wide", "enabled": true, "cadence": "hourly", "requiresAutonomy": false },
27
+ "stranded-rows": { "scope": "instance-wide", "enabled": true, "requiresAutonomy": false, "note": "no cadence: nothing schedules it yet, so it is run by hand — declaring one with no cron firing it fails fitness Check 25 (task 1003775)" }
28
+ }
29
+ }
@@ -0,0 +1,49 @@
1
+ # ADR 0303 — Provisioning verifies the base Caddyfile, and never edits it
2
+
3
+ **Status:** accepted · **Date:** 2026-09-17 · **Task:** [task 1002860](https://cloudbongos.com/builders#/task/1002860) (goal 1000106 — project hosting & fleet; from idea 1000524)
4
+
5
+ **Related:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §4 (on-demand ACME behind the tls-check ask) · [ADR 0128](0128-provisioning-runner-privilege-and-db-auth-model.md) (the runner writes snippets under `sudo install`; it found the live Caddyfile missing the ask)
6
+
7
+ ## Context
8
+
9
+ The provisioning runner writes one `<slug>.caddy` site snippet per instance and reloads Caddy. It
10
+ depends on two lines in the box's base `/etc/caddy/Caddyfile` that it never checked:
11
+ `on_demand_tls { ask …/provisioning/tls-check }` and `import /etc/caddy/sites/*.caddy`. Both were
12
+ hand-applied on the live boxes. The reload ran with `allowFail: true`. So on a box missing
13
+ either line, provisioning reported success and the instance was simply unreachable, with no
14
+ signal to anyone. The base Caddyfile is not in this repository. It is host configuration, and
15
+ it also serves the platform's own apex.
16
+
17
+ ## Decision
18
+
19
+ 1. **Detect and refuse, never edit.** The runner reads the base Caddyfile and does not write it.
20
+ A missing directive stops the run with the exact line to add and the file to add it to.
21
+ An automatic edit was rejected for two reasons. `on_demand_tls` has to sit inside the
22
+ global options block, which an append cannot place and a rewrite could corrupt. And a bad
23
+ write to that file takes down every site on the box, the control plane's own apex included.
24
+ The directives are a one-time setup per box, so the refusal only ever fires on a box that
25
+ was never configured.
26
+ 2. **Read Caddy's resolved config, not the file's text.** The check runs
27
+ `caddy adapt --config <Caddyfile> --adapter caddyfile`. That command reads config files only,
28
+ so it needs no privilege and never touches certificate keys, and the runner's scoped sudo is
29
+ not widened. It inspects the JSON. The ask must appear under `apps.tls.automation.on_demand`,
30
+ which covers both Caddy 2.7's `ask` string and 2.8+'s `permission` module. The domain must
31
+ appear under `apps.http`. Because this is the resolved config, it doesn't matter how the operator
32
+ wrote the directives: an absolute or relative import, a split across imported files, a
33
+ `/api/gds` alias. The base path is `PROVISION_CADDYFILE`, default `/etc/caddy/Caddyfile`.
34
+ 3. **Two checkpoints.** A preflight checks the ask before the box is touched, in both the
35
+ provision and domain-attach legs, beside the DNS-scope preflight. The import can only be
36
+ proven once a snippet exists, so a second check runs after the write and requires the domain
37
+ to be served. It fails the run before any reload. A snippet in a directory nothing imports
38
+ is inert, so leaving it on disk is safe.
39
+ 4. **The reload is loud.** `systemctl reload caddy` no longer passes `allowFail`. Caddy's
40
+ graceful reload validates and swaps atomically, and it keeps the old config when it rejects
41
+ the new one. A failed reload fails the run and never leaves Caddy half-configured. There is
42
+ no separate `caddy validate` step: it would load certificate keys, which needs root, and
43
+ `caddy adapt` plus the reload already cover what it would catch.
44
+
45
+ ## Consequences
46
+
47
+ - A box that was never set up gets a named instruction instead of a silently unreachable project.
48
+ - Domainless instances, dry runs and dedicated droplets (Caddy comes from cloud-init) skip the check.
49
+ - `caddy` must be on the runner's `PATH` on the box that serves the snippet. Every supported shape already has it there, because it reloads that Caddy.
@@ -394,3 +394,4 @@ This keeps the decision history honest and traceable.
394
394
  | 0300 | [**A missing protected-surface registry is a broken install, not a module opt-out** ([task 1003662](https://cloudbongos.com/builders#/task/1003662), goal 1000091). Filed as a bug with a remedy attached: `require('./module-scope-map')` throws when `modules/government/protected-surfaces.json` is absent, so make it degrade to an empty protected set + a warning, matching `resolveOptional`. The throw reproduces, and the rest is wrong four times over. **The throw is not in the named file** — `module-scope-map.js` has no throw and no file read; it requires `permission-path-check.js`, whose `loadRegistry()` throws at module scope. **It is a written contract, not an oversight** — that file's header says FAIL-CLOSED in capitals and already pre-answers the layering worry (the read is `fs`, never `require`, so ADR 0083's one-way rule holds; static data, not code; `route-rank-check.js` reads `modules/*/routes/*.js` the same way). **Disabling the module cannot cause it** — enablement is a runtime flag, never a file deletion, and the read never consults it. **"Not yet materialized" cannot either** — no `files[]` allowlist, so `npm pack --dry-run` puts the 13,274-byte registry in the tarball; per ADR 0108 §1 you cannot have `@bongos/core` without it. The `resolveOptional` precedent does not transfer: that is the module **seam/port** resolver for a missing runtime *capability*, whereas this is the static data the boundary is made of, read where there is no server and no DB (a git hook, a CI job) — borrowing it converts "the boundary is unreadable" into "there is no boundary". **Decision: fail-closed stays, no behavior change, the remedy is rejected — and the one genuine defect is fixed: the message now names the cause and the fix** (reinstall the core; do not empty the set), because stating a refusal without a remedy is exactly how a reader concludes the module is optional and files this task. The asymmetry is the point: a false "nothing is protected" is silent, permanent and grants authority; a false "this install is broken" is loud and grants nothing. Revisit only if a core-only checkout without `modules/` becomes legitimate — and then the question is *where the registry lives*, not whether it is optional.](0300-a-missing-protected-surface-registry-is-a-broken-install.md) | governance / permissions |
395
395
  | 0301 | [**A red PR names its failing tests in a commit status, and says so when it cannot see** ([task 1003988](https://cloudbongos.com/builders#/task/1003988), goal 1000095). One pull request took nine red CI runs in four hours because the builder box could not learn which test failed: without Checks:read GitHub nulls every check-run node, the server dropped the holes silently, and the red PR was reported as a review-or-pending wait. Now the merge state carries checks_unreadable and a red, unreadable rollup reads failing_check_unreadable; the unit workflow posts the failing test names as a unit-report commit STATUS (readable without the permission) from a separate job that holds the only write token and checks out no PR code; the server returns it as failing_tests, reduced to test-list vocabulary because the PR controls the workflow that wrote it.](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) | GDS / shipping pipeline |
396
396
  | 0302 | [**The third identical failure stops the session and asks** ([task 1003990](https://cloudbongos.com/builders#/task/1003990), goal 1000095). Sessions looped without noticing: one PR re-shipped into the same red check nine times, and 45 of 169 claim attempts were the same refusal retried minutes apart. A PostToolUse hook now counts walls per session — the signature is the `[otb-plain]` card's "What happened" line + the task id + the RED line, so a red check naming different failing tests each run reads as progress — and on the third identical one injects a single instruction: stop retrying, diagnose (path named), or /ask-for-help. Waiting cards ("Nothing to do…") are never counted; git-guard and worktree-guard count their own blocks through the same helper; it never blocks a tool call and echoes nothing a tool printed. Rejected: counting any repeated failing command (red→green is that), blocking the fourth attempt (a new gate, ADR 0162), server-side detection (cannot see guard blocks or speak into the session).](0302-the-third-identical-failure-stops-the-session-and-asks.md) | GDS / sessions / hooks |
397
+ | 0303 | [**Provisioning verifies the base Caddyfile, and never edits it** ([task 1002860](https://cloudbongos.com/builders#/task/1002860), goal 1000106, from idea 1000524). The runner assumed the box's `/etc/caddy/Caddyfile` carried the `on_demand_tls` tls-check ask and `import /etc/caddy/sites/*.caddy`, and reloaded with `allowFail` — a box missing either provisioned "successfully" and the instance was unreachable. It now reads Caddy's RESOLVED config (`caddy adapt`: unprivileged, no key access, indifferent to how the directives are written) before touching the box (the ask) and after writing the snippet (the domain is served ⇒ the import reaches it), refusing with the exact line to add, and the reload is loud. Rejected: editing the base file (`on_demand_tls` must sit inside the global block an append cannot place, and a bad write takes down the control plane's own apex); `caddy validate` (loads certificate keys, needs root).](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md) | provisioning / infra |
@@ -20991,6 +20991,7 @@
20991
20991
  "token": {},
20992
20992
  "expires_at": {},
20993
20993
  "builder_login": {},
20994
+ "api_base": {},
20994
20995
  "paste_command": {}
20995
20996
  },
20996
20997
  "required": [
@@ -20998,6 +20999,7 @@
20998
20999
  "token",
20999
21000
  "expires_at",
21000
21001
  "builder_login",
21002
+ "api_base",
21001
21003
  "paste_command"
21002
21004
  ]
21003
21005
  },
@@ -14,7 +14,7 @@
14
14
  - **Cloud Bongos control-plane droplet (NEW 2026-07-05, [ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md), task [#2063](https://example.com/builders#/task/2063)):** DigitalOcean, hostname `cloudbongos`, IPv4 `REDACTED_IP`, NYC3, `s-1vcpu-2gb` (2 GB / 1 vCPU), Ubuntu 24.04, ~$12/mo, tag `cloudbongos-control`. Runs **cloudbongos.com + `builders.` + `status.`** (`src/platform-server.js`, Cloud Bongos brand, port 3002, own `cloudbongos` DB, checkout `/home/lars/cloudbongos-instance`, the root subscribed to the nightly core-patch sweep) and is the **sole token-holding control plane** — a SECOND root, `/home/lars/cloudbongos`, survives only to vendor `infra/` (the box cloud-init the core package does not ship, ADR 0150); it is not subscribed, so nothing may resolve core code from it (task 1002738, [ADR 0144](adr/0144-devbox-rehome-onto-cloudbongos-plane.md) follow-up 5) — `/etc/cloudbongos/box.env` holds the account-wide Cloudflare token (IP-locked to this box) + the DigitalOcean token; this is where `provision.js` provisions instance subdomains. **Structurally separate SSH** (own `cloudbongos_ed25519` key). IPv6 egress disabled so the IPv4-locked CF token can't be spuriously rejected. cloudbongos.com was previously a co-tenant on the co-hosting box; that unit was decommissioned at cutover. **Follow-ups:** deploy automation (task [#2064](https://example.com/builders#/task/2064)) + fully stripping cloud tokens off the co-hosting box (task [#2065](https://example.com/builders#/task/2065)).
15
15
  - **Server stack:** Node.js 22 LTS · PostgreSQL 16 · Caddy 2.11.2. `example.service` (systemd) runs `node server.js` as user `lars` on `127.0.0.1:3000`; restart-on-failure.
16
16
  - **Hardening:** `lars` user with passwordless sudo; SSH key-only (root login disabled, password auth disabled); ufw allows 22/80/443; fail2ban active; unattended-upgrades enabled; timezone `America/New_York`.
17
- - **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
17
+ - **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`); the provisioning runner verifies the box's resolved Caddy config carries that import and the `on_demand_tls` ask before it reports a domain wired, and refuses with the line to add rather than editing the base file ([ADR 0303](adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
18
18
  - **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label ([`infra/prometheus/prometheus.yml`](../infra/prometheus/prometheus.yml)): **cloudbongos** locally (`:3002`); **example + emersonian-circles** on the co-hosting box, reached over the **private VPC** via `socat` forwarders on `10.108.0.2:9101/9102` (→ their loopback `:3000`/`:3004`), ufw-locked to the control-plane box's private IP — raw TCP relays, so the exporter still sees a loopback request and needs no token; nothing is public. **Grafana** (`127.0.0.1:3003`, loopback-only) renders the 4 provisioned-as-code dashboards (folder **Cloud Bongos**: system-overview, error-watch, builder-activity, auth-security), each with a **`project`** template variable for per-instance / fleet views. Reverse-proxied on the `*.cloudbongos.com` origin cert — Archon-only, gated by Caddy `basic_auth` (user `archon`) **and** Grafana's own admin login (both secrets owner-held, never in the repo). Grafana config is a systemd `GF_*` env drop-in (`grafana-server.service.d/override.conf`, `chmod 600`). ~256 MB RAM. A future dedicated-droplet instance is added as a `METRICS_TOKEN` Bearer target instead of a VPC forwarder.
19
19
 
20
20
  **Application stack:**
@@ -226,7 +226,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
226
226
  - Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
227
227
  - Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
228
228
  - Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
229
- - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
229
+ - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
230
230
  - Archon monitoring reads backing the `/watch` page (ADR 0036): `GET /builders/roster`, `GET /grades/by-builder[?days=N]` (**archon-only**, [#726](https://example.com/builders#/task/726) — per-builder grade breakdown for the MARKS section; the project-wide aggregate stays public at `/public/grades`), `GET /audit-log`, `GET /override-requests`, `GET /access-requests`, `GET /security/reports`. Personal-prefs writes backing `/settings`: `GET/PATCH /me/skill-prefs`, `PATCH /me/disciplines`.
231
231
  - Per-builder "needs" + own Gemini key ([ADR 0073](adr/0073-builder-needs-signal-and-byok-gemini-key.md), [#1013](https://example.com/builders#/task/1013)): `GET /me` now also carries `needs` ({items, action_needed_count} — the consistent "the system needs an input from you" signal; `modules/builder-settings/builder-needs.js` is the SSOT — carved out in BV1.R80, resolved by `GET /me` via the `builder-settings` kernel port — rendered by the hall Standing card + the CLI `printNeedsNudge`). The own-key (pragmatic BYOK) endpoints, all own-scoped: `GET /me/art-key/own` (masked meta — last4 only), `PUT /me/art-key/own` (validate-on-save via a Google list-models call → encrypt with `src/bongos/secret-box.js` → store; `503 storage_not_configured` until `BUILDER_SECRET_KEY` is set, `422 invalid_key` on a bad key), `DELETE /me/art-key/own`. The existing `GET /me/art-key` (shared-key box-sync delivery) is extended to also deliver the decrypted own key, which `scripts/gds/fetch-art-key.js` syncs into the local `gemini_api_key` slot (own key wins in the `gen_api.py` cascade).
232
232
  - Builder memory (ADR 0024 cloneable memory + ADR 0026 BFG). Self-scoped (owner is always `req.builder.id`, never input): `POST /memory/sync` (push own memory; own 64 MB parser, vs the global 64 KB), `GET /memory/files`, `GET /memory/file`, `DELETE /memory/file` (the BFG delete affordance, 6C.2). Cross-builder (input-owner) endpoints — all privileged + audit-logged fail-closed: `GET /memory/builders/:id/files`+`/file` (**archon forensic** read), `GET /memory/bfg/builders/:id/files`+`/file` (**BFG-principal only**, 6B.1 read), and the one cross-builder WRITE `POST /memory/builders/:id/bfg-write` (**BFG-principal only**, kill-switched `BFG_WRITE_ENABLED` default-OFF, namespaced `bfg/`, attributed `author: BFG`, write+audit in one tx — dreams + transparent corrections, 6C.1/6C.2).
@@ -2152,5 +2152,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2152
2152
  landed since 1.19.837 with no explicit bump. run 35250225228. (task 1002620)
2153
2153
  1.19.839 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2154
2154
  landed since 1.19.838 with no explicit bump. run 35252591179. (task 1002620)
2155
+ 1.19.840 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2156
+ landed since 1.19.839 with no explicit bump. run 35254616836. (task 1002620)
2157
+ 1.19.841 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2158
+ landed since 1.19.840 with no explicit bump. run 35257419225. (task 1002620)
2155
2159
  ---------------------------------------------------------------------------
2156
2160
  ```
@@ -89,6 +89,7 @@
89
89
  { "glob": "docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md", "group": "docs", "why": "this gate's own ADR" },
90
90
  { "glob": "CLAUDE.md", "group": "docs", "why": "the methodology core" },
91
91
  { "glob": ".claude/hooks/", "group": "automation", "why": "local automation that could weaken a gate" },
92
- { "glob": ".claude/settings.json", "group": "automation", "why": "local automation that could weaken a gate" }
92
+ { "glob": ".claude/settings.json", "group": "automation", "why": "local automation that could weaken a gate" },
93
+ { "glob": "config/scheduled-routines.json", "group": "automation", "why": "task 1002500. The ROUTINE SWITCHBOARD: per routine it decides enabled, cadence and requiresAutonomy — so an edit here can arm overnight-builder (autonomous claim+ship) or silence main-audit, the post-merge trust-boundary backstop ADR 0043 relies on. Sibling of .claude/hooks/ and .claude/settings.json above: the routine BODIES under .claude/scheduled-tasks/ are the autonomy module's source surface and stay unprotected, because protectedness derives per module and walling them is a separate policy decision (the .claude/agents/ precedent recorded in tests/government_protected_surfaces.mjs). This file is kernel-owned territory (module-scope-map's kernel key carries 'config/'), which is already protected, so registering it widens no module wall. The set is computed, not listed: the routine-switch tripwire in tests/government_protected_surfaces.mjs re-detects every config/*.json declaring routine switches and reds if one is unregistered." }
93
94
  ]
94
95
  }
@@ -1479,8 +1479,8 @@ module.exports = {
1479
1479
  landProvenForTask,
1480
1480
  prReferencesTask,
1481
1481
  findTaskPullRequest,
1482
- scrubCredential,
1483
- truncateLoudly, // task 1003222 — exported so its behaviour is testable, not just its effect
1482
+ scrubCredential, ghHeaders,
1483
+ truncateLoudly, // task 1003222 — exported so its behaviour is testable, not just its effect
1484
1484
  STDERR_MAX,
1485
1485
  isAncestorOfDefaultBranch,
1486
1486
  resolveToken,
@@ -0,0 +1,77 @@
1
+ // modules/lifecycle/pr-assign.js — assign a ship's pull request to the builder who wrote
2
+ // it, and name them in its body (task 1003991).
3
+ //
4
+ // WHY. A builder box holds no GitHub credential (ADR 0055), so the server opens the PR
5
+ // with its own — and GitHub records the credential's account as the AUTHOR: 259 of the
6
+ // last 300 PRs. The builder's commits are theirs, but "created by me" is empty for them,
7
+ // and the PR is where CI and merge state live. An ASSIGNEE is the one PR-level link to a
8
+ // person the server can set for someone else; "assigned to me" then finds their work.
9
+ //
10
+ // A COURTESY, NEVER A GATE. assignPullRequest resolves { assigned, reason? } on every
11
+ // path and never rejects: a login GitHub will not assign (not a collaborator — it
12
+ // answers 201 and silently drops the name), a missing permission or a network fault
13
+ // must not be able to fail a ship that already pushed. A sibling of github-push.js
14
+ // rather than a function in it because that file sits at the line cap.
15
+
16
+ 'use strict';
17
+
18
+ const githubPush = require('./github-push');
19
+
20
+ // GitHub's own login rule: 1–39 chars, alphanumeric with single inner hyphens — plus
21
+ // the underscore a managed-user login carries (`name_shortcode`). The login is
22
+ // interpolated into a PR body as an @-mention, so anything else never gets that far.
23
+ const GITHUB_LOGIN_RE = /^[a-z\d](?:[a-z\d]|[-_](?=[a-z\d])){0,38}$/i;
24
+ // The assignment is the one OPTIONAL call between a finished push and the route's 201:
25
+ // a stalled GitHub must not make a ship that succeeded read as a failed publish.
26
+ const ASSIGN_TIMEOUT_MS = 5000;
27
+
28
+ function isGithubLogin(login) {
29
+ return typeof login === 'string' && GITHUB_LOGIN_RE.test(login);
30
+ }
31
+
32
+ // The sentence the PR body carries, or '' when there is no usable login.
33
+ function prBuilderLine(login) {
34
+ return isGithubLogin(login) ? `Built by @${login}.` : '';
35
+ }
36
+
37
+ function logAssignFailure(reason, number, logImpl) {
38
+ try {
39
+ const sink = logImpl || require('../../src/module-api').logger('lifecycle');
40
+ sink.warn('pull request could not be assigned to its builder (the ship is unaffected)',
41
+ { reason: githubPush.scrubCredential(String(reason)), pr: number });
42
+ } catch (_) { /* logging is never load-bearing */ }
43
+ }
44
+
45
+ async function assignPullRequest({ number, login }, deps = {}) {
46
+ const miss = (reason, detail) => {
47
+ logAssignFailure(detail ? `${reason}: ${detail}` : reason, number, deps.logImpl);
48
+ return { assigned: false, reason };
49
+ };
50
+ try {
51
+ if (!isGithubLogin(login)) return miss('bad_login');
52
+ if (!Number.isInteger(number) || number <= 0) return miss('bad_pr_number');
53
+ const token = await githubPush.resolveToken(deps);
54
+ const repoInfo = deps.repoInfo || require('../../src/module-api').repoInfo.loadRepoInfo();
55
+ if (!token || !repoInfo || repoInfo.error) return miss('unconfigured');
56
+
57
+ const doFetch = deps.fetchImpl || fetch;
58
+ const res = await doFetch(
59
+ `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/issues/${number}/assignees`,
60
+ {
61
+ method: 'POST',
62
+ headers: { ...githubPush.ghHeaders(token), 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ assignees: [login] }),
64
+ signal: AbortSignal.timeout(ASSIGN_TIMEOUT_MS),
65
+ }
66
+ );
67
+ if (!res.ok) return miss(`http_${res.status}`);
68
+ const issue = await res.json();
69
+ const took = ((issue && issue.assignees) || [])
70
+ .some((a) => String(a && a.login).toLowerCase() === login.toLowerCase());
71
+ return took ? { assigned: true } : miss('not_assignable');
72
+ } catch (e) {
73
+ return miss('threw', (e && e.message) || '');
74
+ }
75
+ }
76
+
77
+ module.exports = { assignPullRequest, prBuilderLine };