@bongos/core 1.19.1034 → 1.19.1035

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.1034",
6
- "core_contract": "1.19.1034",
7
- "source_commit": "a2446fb2d3a1cb50cc5c61e6aaae4344615e02aa",
5
+ "core_version": "1.19.1035",
6
+ "core_contract": "1.19.1035",
7
+ "source_commit": "984bf7acda7d778bdbac5cb358d4817b17b18d88",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-26T03:45:37.837Z",
9
+ "built_at": "2026-09-26T07:18:36.442Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 541,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2488,
14
+ "functional_verbatim": 2489,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3056,
20
- "tree_sha256": "6d0641af25600fb820e657fe9a5b366956049e9ba1f13d1b052cae54e6847ce4",
19
+ "file_count": 3057,
20
+ "tree_sha256": "625127767ece3a0c054244383e2f3971ffcd5a2fb986f2ea192d4d837c89b237",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -352,12 +352,12 @@
352
352
  {
353
353
  "path": "clients/bongos-client/README.md",
354
354
  "mode": "0000644",
355
- "sha256": "217dec95a590eee3d3b3e7e7670882ff4851d596b9a3329215d563ab9509f5f7"
355
+ "sha256": "e5aa86f924b3c8f3b216daa93f707a583a74e82ad3cb6162288ec987ac70bc24"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/bongos-client.global.js",
359
359
  "mode": "0000644",
360
- "sha256": "f96abe3e0fad14745f5aa2f578e28316cf1c24cffa32aa389db2671ec0daa4b7"
360
+ "sha256": "bafe28d4273b7c1959fe9737bdd17a02cea669fea51495393863570030b605f9"
361
361
  },
362
362
  {
363
363
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -367,17 +367,17 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.cjs",
369
369
  "mode": "0000644",
370
- "sha256": "c4a5ecc76df6099d9e1bd3e76ed0164b460e61aaf34de46fa023e22c8781a122"
370
+ "sha256": "2a53db773795e45a22fad49b6483613219ca7a6d230965ae8974ece117b7ea78"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.d.ts",
374
374
  "mode": "0000644",
375
- "sha256": "5f6b4bc9bb1f216cdedc6a74679d2e38f72ad9a1c561411e39a571d632dccf3d"
375
+ "sha256": "1b3e0d29bec82e3637396c31bb42af2c8a72e6a897a7b83f02dd2e546e4817ce"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/index.mjs",
379
379
  "mode": "0000644",
380
- "sha256": "de19e3ed58c7ba6792de3c1781e3b7fa8342643c35daf713a9cf4290ae61fbbd"
380
+ "sha256": "4eb0b66ce4a75e30a6013a2f03fcc65775d2fa6e0b9caed2d630f1b89418f0da"
381
381
  },
382
382
  {
383
383
  "path": "clients/bongos-client/package.json",
@@ -2197,12 +2197,12 @@
2197
2197
  {
2198
2198
  "path": "docs/api-reference.md",
2199
2199
  "mode": "0000644",
2200
- "sha256": "d6cad3e0d4212d9e595905f44b506e6d656526288b93e00631438c81a829f9bf"
2200
+ "sha256": "39a0682dabb9b1d64c1b8641963e04e1a22ead6bdb29a51ed4d516609f287e4d"
2201
2201
  },
2202
2202
  {
2203
2203
  "path": "docs/api/openapi.json",
2204
2204
  "mode": "0000644",
2205
- "sha256": "f6dbe34c5bf7142fb6ce672c11227ea441620c5e0a6acb7da425b0b4e7be7ef8"
2205
+ "sha256": "d81fad692604b0240f2237d2d33eb0ff503fdfa992f2376d9aa565b8c8c59de9"
2206
2206
  },
2207
2207
  {
2208
2208
  "path": "docs/architecture.md",
@@ -2762,7 +2762,7 @@
2762
2762
  {
2763
2763
  "path": "docs/module-api-changelog.md",
2764
2764
  "mode": "0000644",
2765
- "sha256": "a453af9a4655748e0c20e59569a290edf687fb7445e7bcf5fb003ae3a05fe013"
2765
+ "sha256": "831d4d60af672d57ba41368d631ed8d63dbdccb6da2ac3c5c67f7c08055f6630"
2766
2766
  },
2767
2767
  {
2768
2768
  "path": "docs/modules-contract.md",
@@ -3987,7 +3987,7 @@
3987
3987
  {
3988
3988
  "path": "modules/autonomy/db.js",
3989
3989
  "mode": "0000644",
3990
- "sha256": "722b9f3ddbd59fda3e92ae6c6fb0400eb3eadc343f0a3f84369d69bea72b7d65"
3990
+ "sha256": "445db52d066ca1e26bc1d33d547aa9cb76ebbbd26f518cd2f451c912752cc2de"
3991
3991
  },
3992
3992
  {
3993
3993
  "path": "modules/autonomy/digest.js",
@@ -4027,7 +4027,7 @@
4027
4027
  {
4028
4028
  "path": "modules/autonomy/routes/autonomy.js",
4029
4029
  "mode": "0000644",
4030
- "sha256": "e119e149565972008d42e986f82b4fcbff22d74301222e8772d63de16e936abf"
4030
+ "sha256": "26ed62b15d9d35d02a9b59667538d77d4224967532e8455804f9f254cdcf93b7"
4031
4031
  },
4032
4032
  {
4033
4033
  "path": "modules/autonomy/runner-health.js",
@@ -4432,7 +4432,7 @@
4432
4432
  {
4433
4433
  "path": "modules/government/catalog.js",
4434
4434
  "mode": "0000644",
4435
- "sha256": "6d1895c276755d7a3cbec780f35e20ee9cc6cfee5557a48142bc5dcffb9d3f19"
4435
+ "sha256": "594be00f46a3086fe804d04444061017a21b24aa22d490e82c01e1059fd17a0f"
4436
4436
  },
4437
4437
  {
4438
4438
  "path": "modules/government/config.js",
@@ -6337,7 +6337,7 @@
6337
6337
  {
6338
6338
  "path": "modules/lifecycle/ship-assurance.js",
6339
6339
  "mode": "0000644",
6340
- "sha256": "9bbbabd157420ffc7ed8c323436a27045da0d7bc7c8bfd7f4c44f300855556b1"
6340
+ "sha256": "690171d9e4e7d82537cf1d030d98aa37ee0ac72bbcdce6a04429d3d49ecbe46a"
6341
6341
  },
6342
6342
  {
6343
6343
  "path": "modules/lifecycle/ship-card.js",
@@ -8432,12 +8432,12 @@
8432
8432
  {
8433
8433
  "path": "package-lock.json",
8434
8434
  "mode": "0000644",
8435
- "sha256": "6015018149591834ddba172ad282a9e261f08bb7c1422df2dbe2798b3200d2e8"
8435
+ "sha256": "badec3b8c68b18938b3759d753d7d28e594367c32103cb46a3842e5f0968648f"
8436
8436
  },
8437
8437
  {
8438
8438
  "path": "package.json",
8439
8439
  "mode": "0000644",
8440
- "sha256": "b6ab2ffb9d197a30331f1b963b7543b8858952a45dd5cd6679b73f220bbf05ba"
8440
+ "sha256": "605847b031c0ec07aef44c3b574bf3eafbb320c75280d9af2c8cc1d80c725385"
8441
8441
  },
8442
8442
  {
8443
8443
  "path": "public-docs/index.html",
@@ -8457,7 +8457,7 @@
8457
8457
  {
8458
8458
  "path": "release-notes.json",
8459
8459
  "mode": "0000644",
8460
- "sha256": "5c43a6c1797e7c25442659efef80fcf952bebf22669e8889e9351803423ee81b"
8460
+ "sha256": "368eba38c61e4ecb3613174255b865e9c9fc595ffc04930e35a90a0a5696c38e"
8461
8461
  },
8462
8462
  {
8463
8463
  "path": "scripts/bongos-mcp.js",
@@ -10247,7 +10247,7 @@
10247
10247
  {
10248
10248
  "path": "src/bongos/core-update.js",
10249
10249
  "mode": "0000644",
10250
- "sha256": "b2759f016671a00e6b875ff5faf399b922f64c54bce472e64f813d39070aaf49"
10250
+ "sha256": "27630af327d56e6eea77eb7e402086ec695f9fd02f419e48af463ed1adbfab8c"
10251
10251
  },
10252
10252
  {
10253
10253
  "path": "src/bongos/db-kernel.js",
@@ -10417,7 +10417,7 @@
10417
10417
  {
10418
10418
  "path": "src/bongos/routes/core-update.js",
10419
10419
  "mode": "0000644",
10420
- "sha256": "c58709402311486c0d4acdd408caba45cb4d8bd1de2bc8b345b63747fcf42bbf"
10420
+ "sha256": "5cfcc4d5a48eb6e0c2370adf6c990d86a4af605d664c56b64687920150b16463"
10421
10421
  },
10422
10422
  {
10423
10423
  "path": "src/bongos/routes/github.js",
@@ -10517,7 +10517,7 @@
10517
10517
  {
10518
10518
  "path": "src/module-api.js",
10519
10519
  "mode": "0000644",
10520
- "sha256": "7a87904f92dece324a000b33f1500b825775cd6d4126e6da7f683da17a8b6ccb"
10520
+ "sha256": "896f882c5f8e0e2050bab3d8b44aecfb3967be01cc9328003477016ae7f7fa4a"
10521
10521
  },
10522
10522
  {
10523
10523
  "path": "src/module-loader/catalog.js",
@@ -14249,6 +14249,11 @@
14249
14249
  "mode": "0000644",
14250
14250
  "sha256": "0d45c19886939b8a068df438d2e03e749efb3a84258764ed0a345ce96128827d"
14251
14251
  },
14252
+ {
14253
+ "path": "tests/routine_observability_api.mjs",
14254
+ "mode": "0000644",
14255
+ "sha256": "5769b95af91e3b0ddf7a8c41ffb83a0cc3231dc912c396dc98ac9ae15918c820"
14256
+ },
14252
14257
  {
14253
14258
  "path": "tests/routine_schedule.mjs",
14254
14259
  "mode": "0000644",
@@ -14532,7 +14537,7 @@
14532
14537
  {
14533
14538
  "path": "tests/ship_assurance.mjs",
14534
14539
  "mode": "0000644",
14535
- "sha256": "63f7b9e24126449b55d8c4fd0c5d8ff4dc11fea2e4eeb7b60d87d66722d5f518"
14540
+ "sha256": "ca6e0100049ad284c8467fe4775ed9db4578f11c0704091043dda5e2c25654cf"
14536
14541
  },
14537
14542
  {
14538
14543
  "path": "tests/ship_autolinkify.mjs",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 437 operations across 65 resource groups
8
+ - 439 operations across 66 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -189,6 +189,8 @@ function createClient(opts = {}) {
189
189
  getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
190
190
  // GET /autonomy/runners — rank: any-builder — GET /autonomy/runners
191
191
  getAutonomyRunners: (args) => request("GET", "/autonomy/runners", { hasBody: false }, args),
192
+ // GET /autonomy/runs — rank: metic+archon — GET /autonomy/runs
193
+ getAutonomyRuns: (args) => request("GET", "/autonomy/runs", { hasBody: false }, args),
192
194
  },
193
195
  "backup": {
194
196
  // GET /backup/status — rank: metic+archon — GET /backup/status
@@ -350,6 +352,10 @@ function createClient(opts = {}) {
350
352
  // GET /core-update — rank: archon — GET /core-update
351
353
  getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
352
354
  },
355
+ "coreUpgrades": {
356
+ // GET /core-upgrades — rank: archon — GET /core-upgrades
357
+ getCoreUpgrades: (args) => request("GET", "/core-upgrades", { hasBody: false }, args),
358
+ },
353
359
  "cost": {
354
360
  // POST /cost — rank: any-builder — POST /cost
355
361
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
@@ -188,6 +188,8 @@ function createClient(opts = {}) {
188
188
  getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
189
189
  // GET /autonomy/runners — rank: any-builder — GET /autonomy/runners
190
190
  getAutonomyRunners: (args) => request("GET", "/autonomy/runners", { hasBody: false }, args),
191
+ // GET /autonomy/runs — rank: metic+archon — GET /autonomy/runs
192
+ getAutonomyRuns: (args) => request("GET", "/autonomy/runs", { hasBody: false }, args),
191
193
  },
192
194
  "backup": {
193
195
  // GET /backup/status — rank: metic+archon — GET /backup/status
@@ -349,6 +351,10 @@ function createClient(opts = {}) {
349
351
  // GET /core-update — rank: archon — GET /core-update
350
352
  getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
351
353
  },
354
+ "coreUpgrades": {
355
+ // GET /core-upgrades — rank: archon — GET /core-upgrades
356
+ getCoreUpgrades: (args) => request("GET", "/core-upgrades", { hasBody: false }, args),
357
+ },
352
358
  "cost": {
353
359
  // POST /cost — rank: any-builder — POST /cost
354
360
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
@@ -35,6 +35,7 @@ export interface GetAuditLogResponse { rows: unknown; page: unknown }
35
35
  export interface GetAuthAppPairInfoResponse { pair_code: unknown; device_name: unknown; platform: unknown; status: unknown }
36
36
  export interface GetAuthWebAdmissionStatusResponse { admitted: boolean }
37
37
  export interface GetAutonomyRunnersResponse { runners: unknown; none: unknown }
38
+ export interface GetAutonomyRunsResponse { routine: unknown; limit: unknown; runs: unknown }
38
39
  export interface GetBackupStatusResponse { backup_dir: unknown; db_name: unknown; latest_dump: unknown; stale: unknown; age_hours: unknown; trigger_running: unknown }
39
40
  export interface GetBlockersIdResponse { blocker: unknown }
40
41
  export interface GetBlockersResponse { blockers: unknown; page: unknown }
@@ -61,6 +62,7 @@ export interface GetConnectionsRequestsResponse { incoming: unknown; outgoing: u
61
62
  export interface GetConnectionsResponse { connections: unknown; count: unknown }
62
63
  export interface GetConnectionsSuggestionsResponse { suggestions: unknown; count: unknown }
63
64
  export interface GetCoreUpdateResponse { ok: boolean; update: unknown }
65
+ export interface GetCoreUpgradesResponse { ok: boolean; running: unknown; limit: unknown; upgrades: unknown }
64
66
  export interface GetDiscordChannelsPlanResponse { ok: boolean; guild_id: unknown; op_count: unknown; ops: unknown; warnings: unknown }
65
67
  export interface GetDiscordChannelsSnapshotResponse { ok: boolean; guild_id: unknown; config: unknown; warnings: unknown }
66
68
  export interface GetDoneWhenPendingReviewResponse { criteria: unknown; count: unknown }
@@ -601,6 +603,8 @@ export interface BongosClient {
601
603
  getAutonomyPrecheck(args?: RequestArgs): Promise<ApiResponse>;
602
604
  /** GET /autonomy/runners — rank: any-builder */
603
605
  getAutonomyRunners(args?: RequestArgs): Promise<GetAutonomyRunnersResponse>;
606
+ /** GET /autonomy/runs — rank: metic+archon */
607
+ getAutonomyRuns(args?: RequestArgs): Promise<GetAutonomyRunsResponse>;
604
608
  };
605
609
  "backup": {
606
610
  /** GET /backup/status — rank: metic+archon */
@@ -762,6 +766,10 @@ export interface BongosClient {
762
766
  /** GET /core-update — rank: archon */
763
767
  getCoreUpdate(args?: RequestArgs): Promise<GetCoreUpdateResponse>;
764
768
  };
769
+ "coreUpgrades": {
770
+ /** GET /core-upgrades — rank: archon */
771
+ getCoreUpgrades(args?: RequestArgs): Promise<GetCoreUpgradesResponse>;
772
+ };
765
773
  "cost": {
766
774
  /** POST /cost — rank: any-builder */
767
775
  postCost(args: RequestArgs & { body: PostCostRequest }): Promise<PostCostResponse>;
@@ -185,6 +185,8 @@ export function createClient(opts = {}) {
185
185
  getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
186
186
  // GET /autonomy/runners — rank: any-builder — GET /autonomy/runners
187
187
  getAutonomyRunners: (args) => request("GET", "/autonomy/runners", { hasBody: false }, args),
188
+ // GET /autonomy/runs — rank: metic+archon — GET /autonomy/runs
189
+ getAutonomyRuns: (args) => request("GET", "/autonomy/runs", { hasBody: false }, args),
188
190
  },
189
191
  "backup": {
190
192
  // GET /backup/status — rank: metic+archon — GET /backup/status
@@ -346,6 +348,10 @@ export function createClient(opts = {}) {
346
348
  // GET /core-update — rank: archon — GET /core-update
347
349
  getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
348
350
  },
351
+ "coreUpgrades": {
352
+ // GET /core-upgrades — rank: archon — GET /core-upgrades
353
+ getCoreUpgrades: (args) => request("GET", "/core-upgrades", { hasBody: false }, args),
354
+ },
349
355
  "cost": {
350
356
  // POST /cost — rank: any-builder — POST /cost
351
357
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
@@ -105,6 +105,10 @@
105
105
  "name": "core-update",
106
106
  "description": "Endpoints under `/api/bongos/core-update`."
107
107
  },
108
+ {
109
+ "name": "core-upgrades",
110
+ "description": "Endpoints under `/api/bongos/core-upgrades`."
111
+ },
108
112
  {
109
113
  "name": "cost",
110
114
  "description": "Endpoints under `/api/bongos/cost`."
@@ -2128,6 +2132,47 @@
2128
2132
  ]
2129
2133
  }
2130
2134
  },
2135
+ "/autonomy/runs": {
2136
+ "get": {
2137
+ "operationId": "get_autonomy_runs",
2138
+ "tags": [
2139
+ "autonomy"
2140
+ ],
2141
+ "summary": "GET /autonomy/runs",
2142
+ "description": "The gate's past decisions — did a routine fire, and what did it decide, and why (task 1002669). READ-ONLY and behind the SAME atom as the precheck above (`autonomy.precheck`, Metic+): whoever may ask the gate the question may read its past answers, and a new atom would need a migration for no change in audience. Adds no gate anywhere — pure observability over rows the precheck already writes. ?routine=<name> narrows to one routine; ?limit=1..100 (default 20), newest first.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `autonomy.precheck` (all required).",
2143
+ "x-rank": "metic+archon",
2144
+ "x-source": "modules/autonomy/routes/autonomy.js",
2145
+ "x-permissions": [
2146
+ "autonomy.precheck"
2147
+ ],
2148
+ "responses": {
2149
+ "200": {
2150
+ "description": "Success.",
2151
+ "content": {
2152
+ "application/json": {
2153
+ "schema": {
2154
+ "$ref": "#/components/schemas/GetAutonomyRunsResponse"
2155
+ }
2156
+ }
2157
+ }
2158
+ },
2159
+ "400": {
2160
+ "$ref": "#/components/responses/BadRequest"
2161
+ },
2162
+ "401": {
2163
+ "$ref": "#/components/responses/Unauthorized"
2164
+ },
2165
+ "403": {
2166
+ "$ref": "#/components/responses/Forbidden"
2167
+ }
2168
+ },
2169
+ "security": [
2170
+ {
2171
+ "builderSession": []
2172
+ }
2173
+ ]
2174
+ }
2175
+ },
2131
2176
  "/backup/status": {
2132
2177
  "get": {
2133
2178
  "operationId": "get_backup_status",
@@ -5126,6 +5171,47 @@
5126
5171
  ]
5127
5172
  }
5128
5173
  },
5174
+ "/core-upgrades": {
5175
+ "get": {
5176
+ "operationId": "get_core_upgrades",
5177
+ "tags": [
5178
+ "core-upgrades"
5179
+ ],
5180
+ "summary": "GET /core-upgrades",
5181
+ "description": "GET /core-upgrades — the instance's own core-upgrade ledger, newest first: when the pin last moved, from and to which version, and whether a person or the unattended sweep moved it (task 1002669). READ-ONLY. Gated on the SAME atom as /core-update above and the deploy door, for the same first reason: it names the exact core this box has run and when, which is a vulnerability-window disclosure (ADR 0043). No new atom, so no migration. ?limit=1..100 (default 20).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
5182
+ "x-rank": "archon",
5183
+ "x-source": "src/bongos/routes/core-update.js",
5184
+ "x-permissions": [
5185
+ "core.pin.move"
5186
+ ],
5187
+ "responses": {
5188
+ "200": {
5189
+ "description": "Success.",
5190
+ "content": {
5191
+ "application/json": {
5192
+ "schema": {
5193
+ "$ref": "#/components/schemas/GetCoreUpgradesResponse"
5194
+ }
5195
+ }
5196
+ }
5197
+ },
5198
+ "400": {
5199
+ "$ref": "#/components/responses/BadRequest"
5200
+ },
5201
+ "401": {
5202
+ "$ref": "#/components/responses/Unauthorized"
5203
+ },
5204
+ "403": {
5205
+ "$ref": "#/components/responses/Forbidden"
5206
+ }
5207
+ },
5208
+ "security": [
5209
+ {
5210
+ "builderSession": []
5211
+ }
5212
+ ]
5213
+ }
5214
+ },
5129
5215
  "/cost": {
5130
5216
  "post": {
5131
5217
  "operationId": "post_cost",
@@ -20636,6 +20722,19 @@
20636
20722
  "none"
20637
20723
  ]
20638
20724
  },
20725
+ "GetAutonomyRunsResponse": {
20726
+ "type": "object",
20727
+ "properties": {
20728
+ "routine": {},
20729
+ "limit": {},
20730
+ "runs": {}
20731
+ },
20732
+ "required": [
20733
+ "routine",
20734
+ "limit",
20735
+ "runs"
20736
+ ]
20737
+ },
20639
20738
  "GetBackupStatusResponse": {
20640
20739
  "type": "object",
20641
20740
  "properties": {
@@ -20999,6 +21098,23 @@
20999
21098
  "update"
21000
21099
  ]
21001
21100
  },
21101
+ "GetCoreUpgradesResponse": {
21102
+ "type": "object",
21103
+ "properties": {
21104
+ "ok": {
21105
+ "type": "boolean"
21106
+ },
21107
+ "running": {},
21108
+ "limit": {},
21109
+ "upgrades": {}
21110
+ },
21111
+ "required": [
21112
+ "ok",
21113
+ "running",
21114
+ "limit",
21115
+ "upgrades"
21116
+ ]
21117
+ },
21002
21118
  "GetDiscordChannelsPlanResponse": {
21003
21119
  "type": "object",
21004
21120
  "properties": {
@@ -27782,9 +27898,9 @@
27782
27898
  "description": "A required dependency/feature is not configured or is temporarily down."
27783
27899
  }
27784
27900
  },
27785
- "x-endpoint-count": 437,
27786
- "x-schema-count": 479,
27901
+ "x-endpoint-count": 439,
27902
+ "x-schema-count": 481,
27787
27903
  "x-undocumented-bodies": 9,
27788
- "x-response-schemas": 327,
27904
+ "x-response-schemas": 329,
27789
27905
  "x-generated-by": "scripts/gds/gen-api-docs.js"
27790
27906
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 437 endpoints across 74 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 439 endpoints across 74 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -84,7 +84,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
84
84
  | GET | `/api/bongos/auth/web/callback` | `public` | — | OAuth callback; the state cookie + code are the bearer. |
85
85
  | GET | `/api/bongos/auth/web/start` | `public` | — | /auth/web — browser flow (used by the /builders page). |
86
86
 
87
- ## `autonomy` (7)
87
+ ## `autonomy` (8)
88
88
 
89
89
  | Method | Path | Rank | Body | Description |
90
90
  |---|---|---|---|---|
@@ -95,6 +95,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
95
95
  | POST | `/api/bongos/autonomy/heartbeat` | `any-builder` | `host`, `pid`, `started_at`, `mode`, `consecutive_failures`, `last_event`, `working_task_id` | The runner's check-in, once per loop. |
96
96
  | GET | `/api/bongos/autonomy/precheck` | `metic+archon` | — | |
97
97
  | GET | `/api/bongos/autonomy/runners` | `any-builder` | — | What the hall renders: the CALLER'S OWN runners, with the pure verdict attached so the page and any future alert cannot disagree about wh… |
98
+ | GET | `/api/bongos/autonomy/runs` | `metic+archon` | — | The gate's past decisions — did a routine fire, and what did it decide, and why (task 1002669). |
98
99
 
99
100
  ## `backup` (2)
100
101
 
@@ -224,6 +225,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
224
225
  |---|---|---|---|---|
225
226
  | GET | `/api/bongos/core-update` | `archon` | — | |
226
227
 
228
+ ## `core-upgrades` (1)
229
+
230
+ | Method | Path | Rank | Body | Description |
231
+ |---|---|---|---|---|
232
+ | GET | `/api/bongos/core-upgrades` | `archon` | — | GET /core-upgrades — the instance's own core-upgrade ledger, newest first: when the pin last moved, from and to which version, and whethe… |
233
+
227
234
  ## `cost` (1)
228
235
 
229
236
  | Method | Path | Rank | Body | Description |
@@ -2547,5 +2547,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2547
2547
  landed since 1.19.1032 with no explicit bump. run 36214617025. (task 1002620)
2548
2548
  1.19.1034 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2549
2549
  landed since 1.19.1033 with no explicit bump. run 36215757254. (task 1002620)
2550
+ 1.19.1035 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2551
+ landed since 1.19.1034 with no explicit bump. run 36226356336. (task 1002620)
2550
2552
  ---------------------------------------------------------------------------
2551
2553
  ```
@@ -44,6 +44,24 @@ async function lastAutonomyHead(routine) {
44
44
  return rows[0]?.head_sha || null;
45
45
  }
46
46
 
47
+ // The recent autonomy-gate decisions, newest first — the off-box answer to "did the
48
+ // routine fire, and what did it decide" (task 1002669). Every precheck writes a row
49
+ // here, every SKIP included, so the table is the whole record; before this read
50
+ // existed, finding out needed an SSH session on the host (blocker 1000106). `routine`
51
+ // null means every routine. The (routine, created_at DESC) index serves both shapes.
52
+ async function readAutonomyRuns({ routine = null, limit = 20 } = {}) {
53
+ const { rows } = await pool.query(
54
+ `SELECT id, routine, decision, reason, head_sha, claimable_count, enabled, created_at
55
+ FROM autonomy_runs
56
+ WHERE ($1::text IS NULL OR routine = $1)
57
+ ORDER BY created_at DESC, id DESC
58
+ LIMIT $2`,
59
+ [routine, limit]
60
+ );
61
+ // bigserial ids arrive as strings from node-postgres; keep them strings on purpose.
62
+ return rows.map((r) => ({ ...r, id: String(r.id) }));
63
+ }
64
+
47
65
  // ── the Autobongos fence (task 1003908, ADR 0307 §6) ─────────────────────────
48
66
  //
49
67
  // These back the one control surface the runner host cannot write to. Everything
@@ -162,6 +180,7 @@ async function readRunnerHeartbeats(builderId) {
162
180
  module.exports = {
163
181
  recordAutonomyRun,
164
182
  lastAutonomyHead,
183
+ readAutonomyRuns,
165
184
  recordRunnerHeartbeat,
166
185
  readRunnerHeartbeats,
167
186
  readFence,
@@ -42,6 +42,16 @@ const { validateOrRespond } = api;
42
42
  //
43
43
  // rank: archon — autonomy is an ops control surface, and a precheck records an
44
44
  // autonomy_runs row + git-fetches. Keep it archon-scoped (ADR 0016).
45
+ // ?limit for GET /autonomy/runs: an integer 1..100, else the default 20. Clamped rather
46
+ // than refused — a read an operator types by hand should answer, not argue.
47
+ const RUNS_DEFAULT_LIMIT = 20;
48
+ const RUNS_MAX_LIMIT = 100;
49
+ function runsLimit(raw) {
50
+ const n = Number.parseInt(String(raw ?? ''), 10);
51
+ if (!Number.isFinite(n)) return RUNS_DEFAULT_LIMIT;
52
+ return Math.min(RUNS_MAX_LIMIT, Math.max(1, n));
53
+ }
54
+
45
55
  module.exports = function buildAutonomyRouter() {
46
56
  const router = express.Router();
47
57
 
@@ -58,6 +68,24 @@ module.exports = function buildAutonomyRouter() {
58
68
  }
59
69
  });
60
70
 
71
+ // The gate's past decisions — did a routine fire, and what did it decide, and why
72
+ // (task 1002669). READ-ONLY and behind the SAME atom as the precheck above
73
+ // (`autonomy.precheck`, Metic+): whoever may ask the gate the question may read its
74
+ // past answers, and a new atom would need a migration for no change in audience.
75
+ // Adds no gate anywhere — pure observability over rows the precheck already writes.
76
+ // ?routine=<name> narrows to one routine; ?limit=1..100 (default 20), newest first.
77
+ router.get('/autonomy/runs', auth.requireBuilder, auth.requirePermission('autonomy.precheck'), async (req, res) => {
78
+ const routine = typeof req.query.routine === 'string' && req.query.routine ? req.query.routine.slice(0, 64) : null;
79
+ const limit = runsLimit(req.query.limit);
80
+ try {
81
+ const runs = await db.readAutonomyRuns({ routine, limit });
82
+ res.json({ routine, limit, runs });
83
+ } catch (err) {
84
+ log.error('[gds] GET /autonomy/runs', err);
85
+ res.fail('autonomy_runs_unreadable', { status: 503, message: 'could not read the autonomy decisions' });
86
+ }
87
+ });
88
+
61
89
  // ── the Autobongos fence (task 1003908, ADR 0307 §6) ───────────────────────
62
90
  //
63
91
  // These four routes ARE the fence. Everything the unattended runner is allowed
@@ -108,7 +108,7 @@ const PERMISSIONS = [
108
108
  // owner holds: the catalog's own invariant forbids granting a system key to metic,
109
109
  // so this cannot be delegated downward by a custom rank. The web tier still gains
110
110
  // no shell - the route only QUEUES an intent the control-plane runner drains.
111
- { key: 'core.pin.move', system: true, floor: 'archon', guards: 'POST /provisioning/instances/:id/core-upgrade — gate 3 of the release pipeline (ADR 0293)' },
111
+ { key: 'core.pin.move', system: true, floor: 'archon', guards: 'POST /provisioning/instances/:id/core-upgrade — gate 3 of the release pipeline (ADR 0293); also the reads GET /core-update (the upgrade banner) and GET /core-upgrades (the upgrade ledger, task 1002669)' },
112
112
  // autonomy.fence.manage (task 1003908, ADR 0307 §6) — THE AUTOBONGOS KILL
113
113
  // SWITCH and the allowlist of goals an unattended runner may work. system:true
114
114
  // + archon for core.pin.move's reason, in a slightly sharper form: this decides
@@ -253,7 +253,7 @@ const PERMISSIONS = [
253
253
  { key: 'security.report.adjudicate', system: false, floor: 'metic', guards: 'GET /security/reports, POST /security/reports/:id/{confirm,dispute,fix,link-fix}, /security/docs CRUD (ADR 0157, was archon)' },
254
254
  { key: 'provisioning.fleet.manage', system: false, floor: 'metic', guards: 'GET /provisioning/fleet[/cost-ledger], POST .../force-teardown (ADR 0157, was archon)' },
255
255
  { key: 'box.fleet.manage', system: false, floor: 'metic', scope: 'any', resource: 'box', verb: 'manage', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block (ADR 0157, was archon)' },
256
- { key: 'autonomy.precheck', system: false, floor: 'metic', guards: 'GET /autonomy/precheck — unsupervised-runner gate (ADR 0157, was archon)' },
256
+ { key: 'autonomy.precheck', system: false, floor: 'metic', guards: 'GET /autonomy/precheck — unsupervised-runner gate (ADR 0157, was archon); also GET /autonomy/runs, the gate\'s past decisions (task 1002669)' },
257
257
  { key: 'discord.channels.manage', system: false, floor: 'metic', guards: 'GET /discord/channels/plan, POST .../reconcile (ADR 0157, was archon)' },
258
258
  // Metic by the discord.channels.manage analogue — configuring a project-wide
259
259
  // social surface — rather than archon. The blast radius is bounded by the OTHER
@@ -74,10 +74,26 @@ async function deriveShipAssurance({ taskId, body = {}, db, githubPush, onProbeE
74
74
  // before landProvenForTask ever saw a recorded tip, so a commit provably on main
75
75
  // was refused as "not proven" (strands 1003285/1003286, task 1003434). Each step
76
76
  // now fails on its own: a status miss degrades to status=null, never to no-proof.
77
+ //
78
+ // The status probe needs a BRANCH (task 1004275). It used to be called with
79
+ // { taskId, publishedSha }, which getPublishStatus does not read, so it threw
80
+ // BAD_BRANCH every time and both PR-witness proofs in landProvenForTask (the
81
+ // squash/out-of-band land, and the no-tip fallback) could never fire on this path,
82
+ // while the reconciler proved the same land ~5 min later. So find the task's own PR
83
+ // first, by the "task <id>: …" title both publish paths give it (the same lookup
84
+ // GET /publish-status does since task 1003764), and ask about that branch. No PR,
85
+ // or a failed search, leaves status=null: the tip proof still runs.
77
86
  let publishedSha = null;
78
87
  try { publishedSha = await db.getClaimPublishedSha(taskId); } catch (e) { report(e); }
88
+ let branch = null;
89
+ try {
90
+ const found = await githubPush.findTaskPullRequest({ taskId });
91
+ branch = (found && found.branch) || null;
92
+ } catch (e) { report(e); }
79
93
  let status = null;
80
- try { status = await githubPush.getPublishStatus({ taskId, publishedSha }); } catch (e) { report(e); }
94
+ if (branch) {
95
+ try { status = await githubPush.getPublishStatus({ branch }); } catch (e) { report(e); }
96
+ }
81
97
  try {
82
98
  const proof = await githubPush.landProvenForTask({ taskId, publishedSha, status });
83
99
  if (proof && proof.proven) return { assurance: { kind: 'proof', reason: proof.reason } };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1034",
3
+ "version": "1.19.1035",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1034",
9
+ "version": "1.19.1035",
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.19.1034",
3
+ "version": "1.19.1035",
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",
@@ -7477,5 +7477,15 @@
7477
7477
  "id": "1004288",
7478
7478
  "text": "The setup instructions an AI assistant follows on a fresh copy of a project no longer claim the core package is private, so a real download failure is no longer mistaken for normal."
7479
7479
  }
7480
+ ],
7481
+ "1.19.1035": [
7482
+ {
7483
+ "id": "1004275",
7484
+ "text": "Marking a task shipped by hand now recognises work that already reached the project through its own merged request, instead of refusing it and making the builder wait for the automatic check minutes later."
7485
+ },
7486
+ {
7487
+ "id": "1002669",
7488
+ "text": "The owner can now check from their own laptop what the automatic background jobs decided and when the project's core software was last upgraded, instead of logging into the server to find out."
7489
+ }
7480
7490
  ]
7481
7491
  }
@@ -144,4 +144,26 @@ async function readCoreUpdate({ running, channel, get, ...deps } = {}) {
144
144
  return coreUpdateVerdict({ running, available: r.ok ? r.versions : [], channel });
145
145
  }
146
146
 
147
- module.exports = { readCoreUpdate };
147
+ // readCoreUpgrades — the instance's own core-upgrade ledger, newest first: when the
148
+ // core pin last moved, from which version to which, and who or what moved it (task
149
+ // 1002669). `core_upgrades` is written by `bongos upgrade` on every successful move and
150
+ // rollback (scripts/gds/upgrade.js), including the unattended patch sweep's, and no
151
+ // route read it — so "when did the pin last move" needed an SSH session. The pool is
152
+ // injectable so the test runs with no database; the default is the instance's own.
153
+ async function readCoreUpgrades({ limit = 20, pool = null } = {}) {
154
+ const db = pool || require('./pool').pool;
155
+ const { rows } = await db.query(
156
+ `SELECT id, from_version, to_version, source_commit, note, applied_at, source, requested_by_builder_id
157
+ FROM core_upgrades
158
+ ORDER BY applied_at DESC, id DESC
159
+ LIMIT $1`,
160
+ [limit]
161
+ );
162
+ return rows.map((r) => ({
163
+ ...r,
164
+ id: String(r.id),
165
+ requested_by_builder_id: r.requested_by_builder_id == null ? null : String(r.requested_by_builder_id),
166
+ }));
167
+ }
168
+
169
+ module.exports = { readCoreUpdate, readCoreUpgrades };
@@ -21,9 +21,16 @@
21
21
  const express = require('express');
22
22
  const { asyncHandler } = require('./_helpers');
23
23
  const auth = require('../auth');
24
- const { readCoreUpdate } = require('../core-update');
24
+ const { readCoreUpdate, readCoreUpgrades } = require('../core-update');
25
25
 
26
- module.exports = function buildCoreUpdateRouter() {
26
+ // ?limit for GET /core-upgrades: an integer 1..100, else 20.
27
+ function upgradesLimit(raw) {
28
+ const n = Number.parseInt(String(raw ?? ''), 10);
29
+ if (!Number.isFinite(n)) return 20;
30
+ return Math.min(100, Math.max(1, n));
31
+ }
32
+
33
+ module.exports = function buildCoreUpdateRouter({ upgrades = readCoreUpgrades } = {}) {
27
34
  const router = express.Router();
28
35
 
29
36
  router.get(
@@ -45,5 +52,23 @@ module.exports = function buildCoreUpdateRouter() {
45
52
  }, { errorCode: 'core_update_failed', message: false }),
46
53
  );
47
54
 
55
+ // GET /core-upgrades — the instance's own core-upgrade ledger, newest first: when
56
+ // the pin last moved, from and to which version, and whether a person or the
57
+ // unattended sweep moved it (task 1002669). READ-ONLY. Gated on the SAME atom as
58
+ // /core-update above and the deploy door, for the same first reason: it names the
59
+ // exact core this box has run and when, which is a vulnerability-window disclosure
60
+ // (ADR 0043). No new atom, so no migration. ?limit=1..100 (default 20).
61
+ router.get(
62
+ '/core-upgrades',
63
+ auth.requireBuilder,
64
+ auth.requirePermission('core.pin.move'),
65
+ asyncHandler('GET /core-upgrades', async (req, res) => {
66
+ let running = null;
67
+ try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
68
+ const limit = upgradesLimit(req.query.limit);
69
+ res.json({ ok: true, running, limit, upgrades: await upgrades({ limit }) });
70
+ }, { errorCode: 'core_upgrades_unreadable', message: false }),
71
+ );
72
+
48
73
  return router;
49
74
  };
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1034'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1035'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,195 @@
1
+ // tests/routine_observability_api.mjs — the unattended routines are diagnosable from off
2
+ // the box (task 1002669).
3
+ //
4
+ // Every autonomy-gate decision already lands in `autonomy_runs` and every core upgrade in
5
+ // `core_upgrades`, but no route read either, so "did the routine fire, what did it
6
+ // decide, and when did the pin last move" needed an SSH session (blocker 1000106). Two
7
+ // read-only routes answer it now:
8
+ // GET /api/bongos/autonomy/runs — behind `autonomy.precheck` (Metic+), the atom the
9
+ // precheck itself uses
10
+ // GET /api/bongos/core-upgrades — behind `core.pin.move` (Archon), the atom the
11
+ // upgrade banner and the deploy door use
12
+ // No new atom, so no migration. These tests drive REAL requests through the assembled
13
+ // routers with only the two auth gates stubbed (the core_update_banner precedent: an
14
+ // arity-mangled handler 500s, and only a real request sees that), read the atom each
15
+ // gate was built with off the router stack, and pin the SQL against a stubbed pool.
16
+ //
17
+ // DB-free, no network. Run: node --test tests/routine_observability_api.mjs
18
+
19
+ import { test } from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { createRequire } from 'node:module';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const req = createRequire(import.meta.url);
27
+ const express = req('express');
28
+ const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
29
+
30
+ // A permission stub that remembers which atom it was built with, so the test can read
31
+ // the gate's authority off the router instead of grepping the source for it.
32
+ const permStub = (key) => {
33
+ const mw = (_req, _res, next) => next();
34
+ mw.permKey = key;
35
+ return mw;
36
+ };
37
+ const passBuilder = (reqq, _res, next) => { reqq.builder = { id: 3, rank: 'archon' }; next(); };
38
+
39
+ async function serve(router) {
40
+ const app = express();
41
+ app.use(attachFail);
42
+ app.use(router);
43
+ const server = app.listen(0);
44
+ await new Promise((r) => server.once('listening', r));
45
+ const base = `http://127.0.0.1:${server.address().port}`;
46
+ return { base, close: () => server.close() };
47
+ }
48
+
49
+ function layerFor(router, p) {
50
+ const layer = router.stack.find((l) => l.route && l.route.path === p);
51
+ assert.ok(layer, `${p} is registered`);
52
+ return layer;
53
+ }
54
+
55
+ // ── GET /autonomy/runs ──────────────────────────────────────────────────────────────
56
+
57
+ function autonomyRouter() {
58
+ const api = req(path.join(ROOT, 'src/module-api.js'));
59
+ const saved = { b: api.requireBuilder, p: api.requirePermission };
60
+ api.requireBuilder = passBuilder;
61
+ api.requirePermission = permStub;
62
+ const routePath = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
63
+ delete req.cache[routePath];
64
+ try {
65
+ return req(routePath)();
66
+ } finally {
67
+ api.requireBuilder = saved.b;
68
+ api.requirePermission = saved.p;
69
+ }
70
+ }
71
+
72
+ test('GET /autonomy/runs is gated by requireBuilder + the precheck\'s own atom', () => {
73
+ const layer = layerFor(autonomyRouter(), '/autonomy/runs');
74
+ assert.equal(layer.route.methods.get, true);
75
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
76
+ assert.equal(layer.route.stack[1].handle.permKey, 'autonomy.precheck');
77
+ });
78
+
79
+ test('GET /autonomy/runs returns the decisions, narrowed by routine and a clamped limit', async () => {
80
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
81
+ const saved = db.readAutonomyRuns;
82
+ const seen = [];
83
+ const rows = [{ id: '7', routine: 'overnight-builder', decision: 'no-go', reason: 'no claimable work', head_sha: 'abc', claimable_count: 0, enabled: true, created_at: '2026-09-25T02:00:00.000Z' }];
84
+ db.readAutonomyRuns = async (args) => { seen.push(args); return rows; };
85
+ const s = await serve(autonomyRouter());
86
+ try {
87
+ let r = await fetch(`${s.base}/autonomy/runs?routine=overnight-builder&limit=500`);
88
+ assert.equal(r.status, 200);
89
+ assert.deepEqual(await r.json(), { routine: 'overnight-builder', limit: 100, runs: rows });
90
+ assert.deepEqual(seen.pop(), { routine: 'overnight-builder', limit: 100 }, 'a limit above 100 is clamped, not refused');
91
+
92
+ r = await fetch(`${s.base}/autonomy/runs`);
93
+ assert.equal(r.status, 200);
94
+ assert.deepEqual(seen.pop(), { routine: null, limit: 20 }, 'no routine = every routine; the default limit is 20');
95
+
96
+ await fetch(`${s.base}/autonomy/runs?limit=banana`);
97
+ assert.equal(seen.pop().limit, 20, 'a non-number falls back to the default');
98
+ await fetch(`${s.base}/autonomy/runs?limit=0`);
99
+ assert.equal(seen.pop().limit, 1);
100
+
101
+ db.readAutonomyRuns = async () => { throw new Error('relation "autonomy_runs" does not exist'); };
102
+ r = await fetch(`${s.base}/autonomy/runs`);
103
+ assert.equal(r.status, 503, 'an unreadable table is a 503 that says so, never an empty list');
104
+ const body = await r.json();
105
+ assert.equal(body.error.code, 'autonomy_runs_unreadable');
106
+ assert.ok(!JSON.stringify(body).includes('does not exist'), 'the database error text stays in the log');
107
+ } finally {
108
+ s.close();
109
+ db.readAutonomyRuns = saved;
110
+ }
111
+ });
112
+
113
+ test('readAutonomyRuns: newest first, one routine or all, ids as strings', async () => {
114
+ const api = req(path.join(ROOT, 'src/module-api.js'));
115
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
116
+ const saved = api.pool.query;
117
+ const calls = [];
118
+ api.pool.query = async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 42, routine: 'bfg', decision: 'go' }] }; };
119
+ try {
120
+ const out = await db.readAutonomyRuns({ routine: 'bfg', limit: 5 });
121
+ assert.deepEqual(out, [{ id: '42', routine: 'bfg', decision: 'go' }]);
122
+ assert.match(calls[0].sql, /FROM autonomy_runs/);
123
+ assert.match(calls[0].sql, /\$1::text IS NULL OR routine = \$1/);
124
+ assert.match(calls[0].sql, /ORDER BY created_at DESC/);
125
+ assert.deepEqual(calls[0].params, ['bfg', 5]);
126
+ await db.readAutonomyRuns();
127
+ assert.deepEqual(calls[1].params, [null, 20]);
128
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
129
+ } finally {
130
+ api.pool.query = saved;
131
+ }
132
+ });
133
+
134
+ // ── GET /core-upgrades ──────────────────────────────────────────────────────────────
135
+
136
+ function coreUpdateRouter(deps) {
137
+ const auth = req(path.join(ROOT, 'src/bongos/auth.js'));
138
+ const saved = { b: auth.requireBuilder, p: auth.requirePermission };
139
+ auth.requireBuilder = passBuilder;
140
+ auth.requirePermission = permStub;
141
+ const routePath = req.resolve(path.join(ROOT, 'src/bongos/routes/core-update.js'));
142
+ delete req.cache[routePath];
143
+ try {
144
+ return req(routePath)(deps);
145
+ } finally {
146
+ auth.requireBuilder = saved.b;
147
+ auth.requirePermission = saved.p;
148
+ delete req.cache[routePath];
149
+ }
150
+ }
151
+
152
+ test('GET /core-upgrades is gated by requireBuilder + core.pin.move, like the banner beside it', () => {
153
+ const router = coreUpdateRouter();
154
+ const layer = layerFor(router, '/core-upgrades');
155
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
156
+ assert.equal(layer.route.stack[1].handle.permKey, 'core.pin.move');
157
+ assert.equal(layerFor(router, '/core-update').route.stack[1].handle.permKey, 'core.pin.move', 'the existing route is untouched');
158
+ });
159
+
160
+ test('GET /core-upgrades answers "when did the pin last move", newest first', async () => {
161
+ const seen = [];
162
+ const ledger = [
163
+ { id: '12', from_version: '1.19.1022', to_version: '1.19.1023', source_commit: 'b50d32f1', note: null, applied_at: '2026-09-24T10:00:00.000Z', source: 'door', requested_by_builder_id: '3' },
164
+ { id: '11', from_version: '1.19.1010', to_version: '1.19.1022', source_commit: null, note: 'auto-subscription (patch)', applied_at: '2026-09-23T10:00:00.000Z', source: 'cli', requested_by_builder_id: null },
165
+ ];
166
+ const s = await serve(coreUpdateRouter({ upgrades: async (args) => { seen.push(args); return ledger; } }));
167
+ try {
168
+ const r = await fetch(`${s.base}/core-upgrades?limit=2`);
169
+ assert.equal(r.status, 200, 'the handler runs — a 500 here is the asyncHandler arity bug');
170
+ const body = await r.json();
171
+ assert.equal(body.ok, true);
172
+ assert.equal(body.limit, 2);
173
+ assert.deepEqual(body.upgrades, ledger);
174
+ assert.equal(body.upgrades[0].applied_at, '2026-09-24T10:00:00.000Z', 'the first row is the last pin move');
175
+ assert.ok('running' in body, 'the running core version rides along, so behind-or-not is one read');
176
+ assert.deepEqual(seen, [{ limit: 2 }]);
177
+ } finally {
178
+ s.close();
179
+ }
180
+ });
181
+
182
+ test('readCoreUpgrades: the ledger, newest first, read-only, ids as strings', async () => {
183
+ const { readCoreUpgrades } = req(path.join(ROOT, 'src/bongos/core-update.js'));
184
+ const calls = [];
185
+ const pool = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 5, to_version: '1.19.1023', requested_by_builder_id: 3 }, { id: 4, to_version: '1.19.1022', requested_by_builder_id: null }] }; } };
186
+ const out = await readCoreUpgrades({ limit: 7, pool });
187
+ assert.deepEqual(out, [
188
+ { id: '5', to_version: '1.19.1023', requested_by_builder_id: '3' },
189
+ { id: '4', to_version: '1.19.1022', requested_by_builder_id: null },
190
+ ]);
191
+ assert.match(calls[0].sql, /FROM core_upgrades/);
192
+ assert.match(calls[0].sql, /ORDER BY applied_at DESC/);
193
+ assert.deepEqual(calls[0].params, [7]);
194
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
195
+ });
@@ -194,14 +194,15 @@ await t('the CLI carries the declaration through its deliberate-empty path', ()
194
194
  const { deriveShipAssurance } = require(path.join(ROOT, 'modules/lifecycle/ship-assurance.js'));
195
195
  const TIP = 'a'.repeat(40);
196
196
  console.log('\nthe tip proof does not depend on the branch/PR probe');
197
- await t('tip in main + getPublishStatus throwing (no branch on the claim) → proof', async () => {
197
+ await t('tip in main + getPublishStatus throwing → proof (the status probe is only a witness)', async () => {
198
198
  const seen = [];
199
199
  const r = await deriveShipAssurance({
200
200
  taskId: 1003285,
201
201
  body: {},
202
202
  db: { getClaimPublishedSha: async () => TIP },
203
203
  githubPush: {
204
- getPublishStatus: async () => { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; },
204
+ findTaskPullRequest: async () => ({ branch: 'task-1003285', pr_number: 7, merged: true }),
205
+ getPublishStatus: async () => { const e = new Error('STATUS_FETCH_FAILED'); e.code = 'STATUS_FETCH_FAILED'; throw e; },
205
206
  landProvenForTask: async ({ publishedSha, status }) => {
206
207
  seen.push({ publishedSha, status });
207
208
  return publishedSha === TIP ? { proven: true, reason: 'published tip in main' } : { proven: false };
@@ -210,9 +211,28 @@ await t('tip in main + getPublishStatus throwing (no branch on the claim) → pr
210
211
  onProbeError: (e) => seen.push({ probeError: e.code }),
211
212
  });
212
213
  assert.equal(seen.some((s) => s.publishedSha === TIP && s.status === null), true, 'the proof ran with status=null');
213
- assert.equal(seen.some((s) => s.probeError === 'BAD_BRANCH'), true, 'the probe failure was still reported');
214
+ assert.equal(seen.some((s) => s.probeError === 'STATUS_FETCH_FAILED'), true, 'the probe failure was still reported');
214
215
  assert.equal(r.assurance && r.assurance.kind, 'proof');
215
216
  });
217
+ await t('tip in main + the PR search throwing → proof, and the status probe is never asked', async () => {
218
+ let statusCalls = 0;
219
+ const errs = [];
220
+ const r = await deriveShipAssurance({
221
+ taskId: 1003286,
222
+ body: {},
223
+ db: { getClaimPublishedSha: async () => TIP },
224
+ githubPush: {
225
+ findTaskPullRequest: async () => { const e = new Error('PUSH_UNCONFIGURED'); e.code = 'PUSH_UNCONFIGURED'; throw e; },
226
+ getPublishStatus: async () => { statusCalls++; return null; },
227
+ landProvenForTask: async ({ publishedSha, status }) => (publishedSha === TIP && status === null
228
+ ? { proven: true, reason: 'published tip in main' } : { proven: false }),
229
+ },
230
+ onProbeError: (e) => errs.push(e.code),
231
+ });
232
+ assert.equal(r.assurance && r.assurance.kind, 'proof');
233
+ assert.deepEqual(errs, ['PUSH_UNCONFIGURED'], 'the search failure is reported on its own');
234
+ assert.equal(statusCalls, 0, 'no branch was found, so there is nothing to ask the status probe about');
235
+ });
216
236
  await t('no tip + getPublishStatus throwing → still the 409 refusal (nothing to prove)', async () => {
217
237
  const r = await deriveShipAssurance({
218
238
  taskId: 1003253,
@@ -238,5 +258,68 @@ await t('a proof failure (compare API down) still falls through to the refusal,
238
258
  assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
239
259
  });
240
260
 
261
+ // task 1004275 — the status probe was called with { taskId, publishedSha }, which
262
+ // getPublishStatus does not read: it wants { branch }, so it threw BAD_BRANCH on EVERY
263
+ // manual ship and both PR-witness proofs could never fire here. A squash-landed or
264
+ // no-tip task was refused ship_land_not_proven while the reconciler (which resolves a
265
+ // branch) shipped it minutes later. The branch now comes from the task's own PR, found
266
+ // by title — the lookup GET /publish-status has used since task 1003764.
267
+ console.log('\nthe PR witnesses reach the proof (task 1004275)');
268
+ const realPush = require(path.join(ROOT, 'modules/lifecycle/github-push.js'));
269
+ // The REAL getPublishStatus contract, minus the network: anything but a safe branch is
270
+ // refused exactly as the real one refuses it, so the pre-fix call shape goes red here.
271
+ const statusFor = (merged) => async (args) => {
272
+ if (!args || !realPush.isSafeBranch(args.branch)) { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; }
273
+ return { branch: args.branch, pr_state: merged ? 'closed' : 'open', merged, pr_number: 1214,
274
+ pr_title: 'task 1004275: fix the land proof', pr_body: '', merge_sha: null };
275
+ };
276
+ await t('no tip + a merged PR titled for the task → proof (the no-tip fallback now fires)', async () => {
277
+ const asked = [];
278
+ const errs = [];
279
+ const r = await deriveShipAssurance({
280
+ taskId: 1004275,
281
+ body: {},
282
+ db: { getClaimPublishedSha: async () => null },
283
+ githubPush: {
284
+ findTaskPullRequest: async (a) => { asked.push(a); return { branch: '1004275-assurance-branch-w4q8', pr_number: 1214, merged: true }; },
285
+ getPublishStatus: statusFor(true),
286
+ landProvenForTask: realPush.landProvenForTask, // no tip + merged PR → decided with no network call
287
+ },
288
+ onProbeError: (e) => errs.push(e.code),
289
+ });
290
+ assert.deepEqual(errs, [], 'no probe failed');
291
+ assert.deepEqual(asked, [{ taskId: 1004275 }], 'the PR is found by the task id');
292
+ assert.equal(r.assurance && r.assurance.kind, 'proof');
293
+ assert.match(r.assurance.reason, /matched PR references task 1004275/);
294
+ });
295
+ await t('no tip + the task\'s PR still OPEN → the 409 refusal (a PR is a witness, not a land)', async () => {
296
+ const r = await deriveShipAssurance({
297
+ taskId: 1004275,
298
+ body: {},
299
+ db: { getClaimPublishedSha: async () => null },
300
+ githubPush: {
301
+ findTaskPullRequest: async () => ({ branch: '1004275-assurance-branch-w4q8', pr_number: 1214, merged: false }),
302
+ getPublishStatus: statusFor(false),
303
+ landProvenForTask: realPush.landProvenForTask,
304
+ },
305
+ });
306
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
307
+ });
308
+ await t('no tip + no PR for the task → the 409 refusal, and getPublishStatus is not called with nothing', async () => {
309
+ let statusCalls = 0;
310
+ const r = await deriveShipAssurance({
311
+ taskId: 1004276,
312
+ body: {},
313
+ db: { getClaimPublishedSha: async () => null },
314
+ githubPush: {
315
+ findTaskPullRequest: async () => null,
316
+ getPublishStatus: async () => { statusCalls++; throw new Error('BAD_BRANCH'); },
317
+ landProvenForTask: realPush.landProvenForTask,
318
+ },
319
+ });
320
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
321
+ assert.equal(statusCalls, 0);
322
+ });
323
+
241
324
  console.log(`\n${passed} passed, ${failed} failed`);
242
325
  process.exit(failed ? 1 : 0);