@bongos/core 1.20.60 → 1.20.61
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 +33 -23
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +16 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +6 -5
- package/docs/copy-registry.json +15 -6
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +2 -2
- package/modules/autonomy/db.js +13 -6
- package/modules/autonomy/migrations/autonomy_004_runner_identity.sql +30 -0
- package/modules/autonomy/routes/autonomy.js +16 -0
- package/modules/autonomy/runner-health.js +66 -5
- package/modules/hall-ui/public/gate.js +24 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/autobongos-run.js +43 -2
- package/src/module-api.js +1 -1
- package/tests/autonomy_runner_identity.mjs +215 -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.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.61",
|
|
6
|
+
"core_contract": "1.20.61",
|
|
7
|
+
"source_commit": "dfbe913b447708d16a0e3bd1b01f22a3423078ca",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-01T10:03:11.617Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 572,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2781,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3381,
|
|
20
|
+
"tree_sha256": "de019aef7d57c2169e2176dd4ee7d0461fbd0c1ec64e475bbca8b3afc13c9eae",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -337,7 +337,7 @@
|
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "d4bb4d0fc24c4ad077f939eb4e899d6b0eeeceef7d922ef35ce55bb2c8db4c41"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -2262,12 +2262,12 @@
|
|
|
2262
2262
|
{
|
|
2263
2263
|
"path": "docs/api-reference.md",
|
|
2264
2264
|
"mode": "0000644",
|
|
2265
|
-
"sha256": "
|
|
2265
|
+
"sha256": "f527f77fcc1957831e89e46991a2916966dca81ffcf5a63832bf3cb6a23ef38f"
|
|
2266
2266
|
},
|
|
2267
2267
|
{
|
|
2268
2268
|
"path": "docs/api/openapi.json",
|
|
2269
2269
|
"mode": "0000644",
|
|
2270
|
-
"sha256": "
|
|
2270
|
+
"sha256": "4411ebb2e8bf4dbdd6d403671af60d030294a9171807e466b611acb3274eda2b"
|
|
2271
2271
|
},
|
|
2272
2272
|
{
|
|
2273
2273
|
"path": "docs/architecture.md",
|
|
@@ -2292,12 +2292,12 @@
|
|
|
2292
2292
|
{
|
|
2293
2293
|
"path": "docs/copy-inventory.md",
|
|
2294
2294
|
"mode": "0000644",
|
|
2295
|
-
"sha256": "
|
|
2295
|
+
"sha256": "2daaf44779e2c5870d95d87f76a1e03f96d51e9e24e49ba88dedafde3b041554"
|
|
2296
2296
|
},
|
|
2297
2297
|
{
|
|
2298
2298
|
"path": "docs/copy-registry.json",
|
|
2299
2299
|
"mode": "0000644",
|
|
2300
|
-
"sha256": "
|
|
2300
|
+
"sha256": "c2c3e3a91b2f005cea4795176fef9065db8b617126dafb6f9f9fe0b06183a376"
|
|
2301
2301
|
},
|
|
2302
2302
|
{
|
|
2303
2303
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "2c33e494daebf6147d0587edb3f3125cc3cc5a5f647c3a1db2d7968f05fa4184"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -2892,7 +2892,7 @@
|
|
|
2892
2892
|
{
|
|
2893
2893
|
"path": "docs/page-readings.json",
|
|
2894
2894
|
"mode": "0000644",
|
|
2895
|
-
"sha256": "
|
|
2895
|
+
"sha256": "b9606273995e2ffc2079ee7c93dc1b13177ecdeded8e2a2e2a7e0bb1d768cdab"
|
|
2896
2896
|
},
|
|
2897
2897
|
{
|
|
2898
2898
|
"path": "docs/project-context.template.md",
|
|
@@ -4092,7 +4092,7 @@
|
|
|
4092
4092
|
{
|
|
4093
4093
|
"path": "modules/autonomy/db.js",
|
|
4094
4094
|
"mode": "0000644",
|
|
4095
|
-
"sha256": "
|
|
4095
|
+
"sha256": "7a35aedbf486079f1330b060280abe70898ce676f1bd0f01fe3bc0b3ab8b967c"
|
|
4096
4096
|
},
|
|
4097
4097
|
{
|
|
4098
4098
|
"path": "modules/autonomy/digest.js",
|
|
@@ -4124,6 +4124,11 @@
|
|
|
4124
4124
|
"mode": "0000644",
|
|
4125
4125
|
"sha256": "5cee946899d3cbf9c73c02977a6441ce34b0f41e36997dbf27cb11f90fb89178"
|
|
4126
4126
|
},
|
|
4127
|
+
{
|
|
4128
|
+
"path": "modules/autonomy/migrations/autonomy_004_runner_identity.sql",
|
|
4129
|
+
"mode": "0000644",
|
|
4130
|
+
"sha256": "84b2cea5e4991017c907f07cedd5a75448f5f4dc5cdc009df46b0ad2bd89780c"
|
|
4131
|
+
},
|
|
4127
4132
|
{
|
|
4128
4133
|
"path": "modules/autonomy/module.json",
|
|
4129
4134
|
"mode": "0000644",
|
|
@@ -4137,12 +4142,12 @@
|
|
|
4137
4142
|
{
|
|
4138
4143
|
"path": "modules/autonomy/routes/autonomy.js",
|
|
4139
4144
|
"mode": "0000644",
|
|
4140
|
-
"sha256": "
|
|
4145
|
+
"sha256": "bd2d65b731ff1e9eea66cb028ec246fe95b8e1e867386baa9ad6fc9f79bd2a5b"
|
|
4141
4146
|
},
|
|
4142
4147
|
{
|
|
4143
4148
|
"path": "modules/autonomy/runner-health.js",
|
|
4144
4149
|
"mode": "0000644",
|
|
4145
|
-
"sha256": "
|
|
4150
|
+
"sha256": "24c56042659ce118ef552da93610d75b9d4e17c3ea49f2fe78667892c943d43a"
|
|
4146
4151
|
},
|
|
4147
4152
|
{
|
|
4148
4153
|
"path": "modules/builder-settings/CLAUDE.md",
|
|
@@ -5432,7 +5437,7 @@
|
|
|
5432
5437
|
{
|
|
5433
5438
|
"path": "modules/hall-ui/public/gate.js",
|
|
5434
5439
|
"mode": "0000644",
|
|
5435
|
-
"sha256": "
|
|
5440
|
+
"sha256": "2bd2d51a984ea6cab821d46ec9f1480e7231c5f4b32a46ce2d3c01a7adc21967"
|
|
5436
5441
|
},
|
|
5437
5442
|
{
|
|
5438
5443
|
"path": "modules/hall-ui/public/gate.states.json",
|
|
@@ -9412,12 +9417,12 @@
|
|
|
9412
9417
|
{
|
|
9413
9418
|
"path": "package-lock.json",
|
|
9414
9419
|
"mode": "0000644",
|
|
9415
|
-
"sha256": "
|
|
9420
|
+
"sha256": "ab6fcfd7d4d42e1528dd531ec990be71b3b4d2221654c735ae4e1301726bb8a8"
|
|
9416
9421
|
},
|
|
9417
9422
|
{
|
|
9418
9423
|
"path": "package.json",
|
|
9419
9424
|
"mode": "0000644",
|
|
9420
|
-
"sha256": "
|
|
9425
|
+
"sha256": "24ea0d2cdd97a8a47824d23c4dabce026af3fe98273b3df4c07427466d983866"
|
|
9421
9426
|
},
|
|
9422
9427
|
{
|
|
9423
9428
|
"path": "public-docs/index.html",
|
|
@@ -9437,7 +9442,7 @@
|
|
|
9437
9442
|
{
|
|
9438
9443
|
"path": "release-notes.json",
|
|
9439
9444
|
"mode": "0000644",
|
|
9440
|
-
"sha256": "
|
|
9445
|
+
"sha256": "027d1fbff3c739f12f9de1f3853a4e8cdb3f2dd7acc50e5a6b623a208186eeac"
|
|
9441
9446
|
},
|
|
9442
9447
|
{
|
|
9443
9448
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -9597,7 +9602,7 @@
|
|
|
9597
9602
|
{
|
|
9598
9603
|
"path": "scripts/gds/autobongos-run.js",
|
|
9599
9604
|
"mode": "0000644",
|
|
9600
|
-
"sha256": "
|
|
9605
|
+
"sha256": "05dd20d28db2ddbb19710bb32fd1c3df560da4e9a2150b9f9f315956682c52ee"
|
|
9601
9606
|
},
|
|
9602
9607
|
{
|
|
9603
9608
|
"path": "scripts/gds/autobongos-service.cmd",
|
|
@@ -11612,7 +11617,7 @@
|
|
|
11612
11617
|
{
|
|
11613
11618
|
"path": "src/module-api.js",
|
|
11614
11619
|
"mode": "0000644",
|
|
11615
|
-
"sha256": "
|
|
11620
|
+
"sha256": "54e3ec3b65078a04894736e6f52c1bf1d31b0f56fa9d9d4b439fdf60623b7a11"
|
|
11616
11621
|
},
|
|
11617
11622
|
{
|
|
11618
11623
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12119,6 +12124,11 @@
|
|
|
12119
12124
|
"mode": "0000644",
|
|
12120
12125
|
"sha256": "e5dc6ec6261a18113876f4e362e8cb801d78f5266e077f0b8a27eeda70fe800b"
|
|
12121
12126
|
},
|
|
12127
|
+
{
|
|
12128
|
+
"path": "tests/autonomy_runner_identity.mjs",
|
|
12129
|
+
"mode": "0000644",
|
|
12130
|
+
"sha256": "2bc963a47a41a90711bec4d23fef9e4a51203136f446a0abab272d78d2f6f6bf"
|
|
12131
|
+
},
|
|
12122
12132
|
{
|
|
12123
12133
|
"path": "tests/backfill_sessions.mjs",
|
|
12124
12134
|
"mode": "0000644",
|
|
@@ -257,7 +257,7 @@ export interface PostAutonomyFenceGoalsRequest { goal_id: StringifiableId; note?
|
|
|
257
257
|
export interface PostAutonomyFenceGoalsResponse { goal: unknown }
|
|
258
258
|
export interface PostAutonomyFencePriorityRequest { goal_id?: StringifiableId }
|
|
259
259
|
export interface PostAutonomyFenceRequest { enabled: boolean; reason?: string }
|
|
260
|
-
export interface PostAutonomyHeartbeatRequest { host: string; pid?: number; started_at?: string; mode?: string; consecutive_failures?: number; last_event?: string; working_task_id?: number; working_goal_id?: number }
|
|
260
|
+
export interface PostAutonomyHeartbeatRequest { host: string; pid?: number; started_at?: string; mode?: string; consecutive_failures?: number; last_event?: string; working_task_id?: number; working_goal_id?: number; head?: string; disk_head?: string; main_head?: string; claude_account?: string }
|
|
261
261
|
export interface PostAutonomyHeartbeatResponse { ok: boolean; heartbeat: unknown }
|
|
262
262
|
export interface PostBackupTriggerResponse { ok: boolean; message: unknown; started_at: unknown }
|
|
263
263
|
export interface PostBlockersIdLinkRequest { task_id: StringifiableId }
|
package/docs/api/openapi.json
CHANGED
|
@@ -26060,6 +26060,22 @@
|
|
|
26060
26060
|
},
|
|
26061
26061
|
"working_goal_id": {
|
|
26062
26062
|
"type": "number"
|
|
26063
|
+
},
|
|
26064
|
+
"head": {
|
|
26065
|
+
"type": "string",
|
|
26066
|
+
"maxLength": 40
|
|
26067
|
+
},
|
|
26068
|
+
"disk_head": {
|
|
26069
|
+
"type": "string",
|
|
26070
|
+
"maxLength": 40
|
|
26071
|
+
},
|
|
26072
|
+
"main_head": {
|
|
26073
|
+
"type": "string",
|
|
26074
|
+
"maxLength": 40
|
|
26075
|
+
},
|
|
26076
|
+
"claude_account": {
|
|
26077
|
+
"type": "string",
|
|
26078
|
+
"maxLength": 200
|
|
26063
26079
|
}
|
|
26064
26080
|
},
|
|
26065
26081
|
"required": [
|
package/docs/api-reference.md
CHANGED
|
@@ -90,7 +90,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
90
90
|
| POST | `/api/bongos/autonomy/fence/goals` | `archon` | `goal_id`, `note` | Add a goal to the allowlist. |
|
|
91
91
|
| DELETE | `/api/bongos/autonomy/fence/goals/:goalId` | `archon` | — | Revoke one. |
|
|
92
92
|
| POST | `/api/bongos/autonomy/fence/priority` | `archon` | `goal_id` | The priority goal (task 1004453): which allowlisted goal the runner works first. |
|
|
93
|
-
| POST | `/api/bongos/autonomy/heartbeat` | `any-builder` | `host`, `pid`, `started_at`, `mode`, `consecutive_failures`, `last_event`, `working_task_id`, `working_goal_id` | The runner's check-in, once per loop. |
|
|
93
|
+
| POST | `/api/bongos/autonomy/heartbeat` | `any-builder` | `host`, `pid`, `started_at`, `mode`, `consecutive_failures`, `last_event`, `working_task_id`, `working_goal_id`, `head`, `disk_head`, `main_head`, `claude_account` | The runner's check-in, once per loop. |
|
|
94
94
|
| GET | `/api/bongos/autonomy/precheck` | `metic+archon` | — | |
|
|
95
95
|
| 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… |
|
|
96
96
|
| 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). |
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**2318 strings** across 3 surfaces · 1997 distinct pieces of copy · 2318 certain, 0 heuristic · 221 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 116 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 116 | 1718 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 6 | 500 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `4e25e6872321` | 3x | Your projects | modules/public-landing/public/index.html:691, modules/public-landing/public/projects.html:586, modules/public-landing/public/projects.html:748 |
|
|
79
79
|
| … | | _181 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1718 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -257,7 +257,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
257
257
|
| `906c2947dfc1` | certain | Changes save at once and take effect the next time you start a session on this machine. | `modules/hall-ui/public/settings.html:346` |
|
|
258
258
|
| `52384bdc035c` | certain | Changes save at once. Your new choice plays the next time that event fires. | `modules/hall-ui/public/settings.html:359` |
|
|
259
259
|
| `cab7a9526ca1` | certain | Changes save the moment you pick. “Default” clears your override and falls back to the system's choice. | `modules/hall-ui/public/settings.html:329` |
|
|
260
|
-
| `4b714191b423` | certain | Changes touching protected build surfaces, held for your approval. | `modules/hall-ui/public/gate.js:
|
|
260
|
+
| `4b714191b423` | certain | Changes touching protected build surfaces, held for your approval. | `modules/hall-ui/public/gate.js:448` |
|
|
261
261
|
| `864503935eca` | certain | Changing the rule… | `modules/hall-ui/public/deploy.js:671` |
|
|
262
262
|
| `55ad21bd03eb` | certain | Chart the routes | `modules/hall-ui/public/goal-map.html:100` |
|
|
263
263
|
| `55ad21bd03eb` | certain | Chart the routes | `modules/hall-ui/public/thinking.html:108` |
|
|
@@ -281,6 +281,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
281
281
|
| `c10806b50c61` | certain | Claim task {…} | `modules/hall-ui/public/work.js:482` |
|
|
282
282
|
| `84664050c372` | certain | Claimed by another builder | `modules/hall-ui/public/goals-render.js:372` |
|
|
283
283
|
| `18a03475e536` | certain | Claude Code | `modules/hall-ui/public/hall-render.js:832` |
|
|
284
|
+
| `fe6fab20b149` | certain | Claude account {…} | `modules/hall-ui/public/gate.js:393` |
|
|
284
285
|
| `ccb94814e3c2` | certain | Claude can start it when you ask in plain words. | `modules/hall-ui/public/commands.html:36` |
|
|
285
286
|
| `6cb414d119be` | certain | Claude sees it and can start it when you ask in plain words | `modules/hall-ui/public/commands-lib.js:94` |
|
|
286
287
|
| `5b3b75ee0596` | certain | Claude's notes on this goal | `modules/hall-ui/public/goals.html:86` |
|
|
@@ -1518,7 +1519,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1518
1519
|
| `6b530a7007bc` | certain | jump to a goal or a thought | `modules/hall-ui/public/goal-map.html:84` |
|
|
1519
1520
|
| `6b530a7007bc` | certain | jump to a goal or a thought | `modules/hall-ui/public/thinking.html:92` |
|
|
1520
1521
|
| `9328033a890a` | certain | land every shuttle | `modules/hall-ui/public/sky-interaction.js:45` |
|
|
1521
|
-
| `966b32c8af7e` | certain | last check-in {…} ago | `modules/hall-ui/public/gate.js:
|
|
1522
|
+
| `966b32c8af7e` | certain | last check-in {…} ago | `modules/hall-ui/public/gate.js:405` |
|
|
1522
1523
|
| `9018bf6e99eb` | certain | last used {…} · signed in {…} | `modules/hall-ui/public/settings-sessions.js:46` |
|
|
1523
1524
|
| `2ce7738b1eb0` | certain | loading goals… | `modules/hall-ui/public/task-detail.js:482` |
|
|
1524
1525
|
| `754bdec368ed` | certain | lowest credits | `modules/hall-ui/public/hall-widgets.js:167` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 2318,
|
|
7
|
+
"unique_ids": 1997,
|
|
8
|
+
"certain": 2318,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 221
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 116,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1718
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -3930,7 +3930,7 @@
|
|
|
3930
3930
|
"surface": "builders-hall",
|
|
3931
3931
|
"text": "Changes touching protected build surfaces, held for your approval.",
|
|
3932
3932
|
"file": "modules/hall-ui/public/gate.js",
|
|
3933
|
-
"line":
|
|
3933
|
+
"line": 448,
|
|
3934
3934
|
"origin": "js-text-assign",
|
|
3935
3935
|
"confidence": "certain"
|
|
3936
3936
|
},
|
|
@@ -4141,6 +4141,15 @@
|
|
|
4141
4141
|
"origin": "js-markup",
|
|
4142
4142
|
"confidence": "certain"
|
|
4143
4143
|
},
|
|
4144
|
+
{
|
|
4145
|
+
"id": "fe6fab20b149",
|
|
4146
|
+
"surface": "builders-hall",
|
|
4147
|
+
"text": "Claude account {…}",
|
|
4148
|
+
"file": "modules/hall-ui/public/gate.js",
|
|
4149
|
+
"line": 393,
|
|
4150
|
+
"origin": "js-markup",
|
|
4151
|
+
"confidence": "certain"
|
|
4152
|
+
},
|
|
4144
4153
|
{
|
|
4145
4154
|
"id": "ccb94814e3c2",
|
|
4146
4155
|
"surface": "builders-hall",
|
|
@@ -15279,7 +15288,7 @@
|
|
|
15279
15288
|
"surface": "builders-hall",
|
|
15280
15289
|
"text": "last check-in {…} ago",
|
|
15281
15290
|
"file": "modules/hall-ui/public/gate.js",
|
|
15282
|
-
"line":
|
|
15291
|
+
"line": 405,
|
|
15283
15292
|
"origin": "js-markup",
|
|
15284
15293
|
"confidence": "certain"
|
|
15285
15294
|
},
|
|
@@ -2777,5 +2777,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2777
2777
|
landed since 1.20.58 with no explicit bump. run 36828212808. (task 1002620)
|
|
2778
2778
|
1.20.60 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2779
2779
|
landed since 1.20.59 with no explicit bump. run 36830884810. (task 1002620)
|
|
2780
|
+
1.20.61 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2781
|
+
landed since 1.20.60 with no explicit bump. run 36846600005. (task 1002620)
|
|
2780
2782
|
---------------------------------------------------------------------------
|
|
2781
2783
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -946,7 +946,7 @@
|
|
|
946
946
|
"surface": "builders",
|
|
947
947
|
"title": "Gate",
|
|
948
948
|
"states_read": ["queue","out","approve"],
|
|
949
|
-
"files_hash": "
|
|
949
|
+
"files_hash": "fce003b152adb982",
|
|
950
950
|
"reading_hash": "08d3e5d3657e5afb",
|
|
951
951
|
"counts": {"lines":41,"placed":17,"shared":5,"unplaced":19},
|
|
952
952
|
"omitted": {"data":34},
|
|
@@ -957,7 +957,7 @@
|
|
|
957
957
|
{"key":"L0003","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":495,"string_id":"eba610d25523"},
|
|
958
958
|
{"key":"L0004","section":"top bar","text":"Ctrl K","placement":"shared"},
|
|
959
959
|
{"key":"L0005","section":"Gate","text":"Gate","placement":"unplaced"},
|
|
960
|
-
{"key":"L0006","section":"Gate","text":"Changes touching protected build surfaces, held for your approval.","placement":"placed","file":"modules/hall-ui/public/gate.js","line":
|
|
960
|
+
{"key":"L0006","section":"Gate","text":"Changes touching protected build surfaces, held for your approval.","placement":"placed","file":"modules/hall-ui/public/gate.js","line":448,"string_id":"4b714191b423"},
|
|
961
961
|
{"key":"L0007","section":"Gate","text":"Awaiting your approval","placement":"unplaced"},
|
|
962
962
|
{"key":"L0008","section":"Gate","text":"held at the gate check","placement":"unplaced"},
|
|
963
963
|
{"key":"L0009","section":"Gate","text":"Hard-floor holds","placement":"unplaced"},
|
package/modules/autonomy/db.js
CHANGED
|
@@ -162,11 +162,11 @@ async function disallowGoal(goalId) {
|
|
|
162
162
|
// recordRunnerHeartbeat — upsert one host's check-in. ON CONFLICT so the normal
|
|
163
163
|
// case is one write, and `host` is the key so a second runner appears as a second
|
|
164
164
|
// row rather than silently overwriting the first.
|
|
165
|
-
async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt = null, mode = null, consecutiveFailures = null, lastEvent = null, workingTaskId = null, workingGoalId = null }) {
|
|
165
|
+
async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt = null, mode = null, consecutiveFailures = null, lastEvent = null, workingTaskId = null, workingGoalId = null, head = null, diskHead = null, mainHead = null, claudeAccount = null }) {
|
|
166
166
|
const { rows } = await pool.query(
|
|
167
167
|
`INSERT INTO autonomy_runner_heartbeat
|
|
168
|
-
(builder_id, host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, last_seen_at, updated_at)
|
|
169
|
-
VALUES ($8, $1, $2, $3, $4, $5, $6, $7, $9, now(), now())
|
|
168
|
+
(builder_id, host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, head, disk_head, main_head, claude_account, last_seen_at, updated_at)
|
|
169
|
+
VALUES ($8, $1, $2, $3, $4, $5, $6, $7, $9, $10, $11, $12, $13, now(), now())
|
|
170
170
|
ON CONFLICT (builder_id, host) DO UPDATE SET
|
|
171
171
|
pid = EXCLUDED.pid,
|
|
172
172
|
started_at = COALESCE(EXCLUDED.started_at, autonomy_runner_heartbeat.started_at),
|
|
@@ -175,10 +175,16 @@ async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt =
|
|
|
175
175
|
last_event = EXCLUDED.last_event,
|
|
176
176
|
working_task_id = EXCLUDED.working_task_id,
|
|
177
177
|
working_goal_id = EXCLUDED.working_goal_id,
|
|
178
|
+
-- Overwritten, never COALESCEd (task 1004407): a fresh process that cannot
|
|
179
|
+
-- read its own commit must say "unknown", not inherit the last process's.
|
|
180
|
+
head = EXCLUDED.head,
|
|
181
|
+
disk_head = EXCLUDED.disk_head,
|
|
182
|
+
main_head = EXCLUDED.main_head,
|
|
183
|
+
claude_account = EXCLUDED.claude_account,
|
|
178
184
|
last_seen_at = now(),
|
|
179
185
|
updated_at = now()
|
|
180
|
-
RETURNING host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, last_seen_at`,
|
|
181
|
-
[String(host).toLowerCase().slice(0, 255), pid, startedAt, mode, consecutiveFailures, lastEvent, workingTaskId, builderId, workingGoalId]
|
|
186
|
+
RETURNING host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, head, disk_head, main_head, claude_account, last_seen_at`,
|
|
187
|
+
[String(host).toLowerCase().slice(0, 255), pid, startedAt, mode, consecutiveFailures, lastEvent, workingTaskId, builderId, workingGoalId, head, diskHead, mainHead, claudeAccount]
|
|
182
188
|
);
|
|
183
189
|
return rows[0] || null;
|
|
184
190
|
}
|
|
@@ -193,7 +199,8 @@ async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt =
|
|
|
193
199
|
// liveness view entirely (grader finding, task 1003905).
|
|
194
200
|
async function readRunnerHeartbeats(builderId) {
|
|
195
201
|
const { rows } = await pool.query(
|
|
196
|
-
`SELECT host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id,
|
|
202
|
+
`SELECT host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id,
|
|
203
|
+
head, disk_head, main_head, claude_account, last_seen_at
|
|
197
204
|
FROM autonomy_runner_heartbeat
|
|
198
205
|
WHERE builder_id = $1
|
|
199
206
|
ORDER BY last_seen_at DESC
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- autonomy_004_runner_identity.sql — what the runner card can say about a runner
|
|
2
|
+
-- (task 1004407).
|
|
3
|
+
--
|
|
4
|
+
-- Owner ruling, 2026-10-01: the gate page must show which code each runner is
|
|
5
|
+
-- running, whether it is live or restarting, which always-on PC it is on, and
|
|
6
|
+
-- which Claude account its workers spend. `host` already names the PC
|
|
7
|
+
-- (autonomy_002); these four columns carry the rest.
|
|
8
|
+
--
|
|
9
|
+
-- head the commit the supervisor process is RUNNING (40-char hex)
|
|
10
|
+
-- disk_head the commit on disk, when the checkout was refreshed in place
|
|
11
|
+
-- past what is running (task 1004406) — null otherwise
|
|
12
|
+
-- main_head the newest main the runner has seen, so the card can say
|
|
13
|
+
-- "behind main" without the server reaching GitHub
|
|
14
|
+
-- claude_account a LABEL for the Claude login its workers use (an email, or
|
|
15
|
+
-- "API key" / "Claude token" when one is set). Never a token or
|
|
16
|
+
-- a key: the route caps it at 200 characters and the runner only
|
|
17
|
+
-- ever sends the label.
|
|
18
|
+
--
|
|
19
|
+
-- Nullable, no defaults: a runner that predates this reports nothing, and the
|
|
20
|
+
-- card says nothing rather than guessing. Additive and idempotent.
|
|
21
|
+
|
|
22
|
+
BEGIN;
|
|
23
|
+
|
|
24
|
+
ALTER TABLE autonomy_runner_heartbeat
|
|
25
|
+
ADD COLUMN IF NOT EXISTS head text,
|
|
26
|
+
ADD COLUMN IF NOT EXISTS disk_head text,
|
|
27
|
+
ADD COLUMN IF NOT EXISTS main_head text,
|
|
28
|
+
ADD COLUMN IF NOT EXISTS claude_account text;
|
|
29
|
+
|
|
30
|
+
COMMIT;
|
|
@@ -52,6 +52,11 @@ function runsLimit(raw) {
|
|
|
52
52
|
return Math.min(RUNS_MAX_LIMIT, Math.max(1, n));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// commitOrNull — a full lowercase commit sha, or null (task 1004407).
|
|
56
|
+
function commitOrNull(v) {
|
|
57
|
+
return typeof v === 'string' && /^[0-9a-f]{40}$/.test(v) ? v : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
module.exports = function buildAutonomyRouter() {
|
|
56
61
|
const router = express.Router();
|
|
57
62
|
|
|
@@ -219,6 +224,11 @@ module.exports = function buildAutonomyRouter() {
|
|
|
219
224
|
last_event: { type: 'string', maxLength: 64 },
|
|
220
225
|
working_task_id: { type: 'number' },
|
|
221
226
|
working_goal_id: { type: 'number' },
|
|
227
|
+
// What the runner card says about the runner (task 1004407).
|
|
228
|
+
head: { type: 'string', maxLength: 40 },
|
|
229
|
+
disk_head: { type: 'string', maxLength: 40 },
|
|
230
|
+
main_head: { type: 'string', maxLength: 40 },
|
|
231
|
+
claude_account: { type: 'string', maxLength: 200 },
|
|
222
232
|
})) return;
|
|
223
233
|
const b = req.body || {};
|
|
224
234
|
try {
|
|
@@ -234,6 +244,12 @@ module.exports = function buildAutonomyRouter() {
|
|
|
234
244
|
lastEvent: b.last_event || null,
|
|
235
245
|
workingTaskId: Number.isInteger(b.working_task_id) ? b.working_task_id : null,
|
|
236
246
|
workingGoalId: Number.isInteger(b.working_goal_id) ? b.working_goal_id : null,
|
|
247
|
+
// A commit is stored only if it IS one: anything that is not 40 hex
|
|
248
|
+
// characters is dropped to null rather than rendered as a version.
|
|
249
|
+
head: commitOrNull(b.head),
|
|
250
|
+
diskHead: commitOrNull(b.disk_head),
|
|
251
|
+
mainHead: commitOrNull(b.main_head),
|
|
252
|
+
claudeAccount: typeof b.claude_account === 'string' && b.claude_account.trim() ? b.claude_account.trim() : null,
|
|
237
253
|
});
|
|
238
254
|
res.json({ ok: true, heartbeat: row });
|
|
239
255
|
} catch (err) {
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
// exactly what it is for, and crying dead every time it works is worse than
|
|
20
20
|
// saying nothing.
|
|
21
21
|
// · dead — quiet longer than any legitimate explanation.
|
|
22
|
+
// · restarting — its last word was "exiting to pick up new code" (upgrade_exit,
|
|
23
|
+
// task 1004406), and that was recently. The wrapper fetches, resets and
|
|
24
|
+
// relaunches, which takes a minute or two, and the owner ruled that a restart
|
|
25
|
+
// must read as a restart, never as a death (task 1004407). Past RESTART_S the
|
|
26
|
+
// relaunch has failed and it is dead again — with a detail that says so.
|
|
22
27
|
|
|
23
28
|
// Reasoned from the runner's own numbers, not picked. The loop beats at the top of
|
|
24
29
|
// every pass and again on each poll inside a wait (every 60s), so a healthy runner
|
|
@@ -28,6 +33,43 @@ const ALIVE_S = 15 * 60;
|
|
|
28
33
|
// worker holds the loop for its whole run. Two hours is that ceiling plus room for
|
|
29
34
|
// the ship, the grade and the verify that follow it.
|
|
30
35
|
const LATE_S = 2 * 60 * 60;
|
|
36
|
+
// A relaunch is fetch + reset + boot + the first beat. Ten minutes is several
|
|
37
|
+
// times that, so a slow fetch is not called a death, and short enough that a
|
|
38
|
+
// relaunch which never happened is noticed the same evening.
|
|
39
|
+
const RESTART_S = 10 * 60;
|
|
40
|
+
|
|
41
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
42
|
+
const sha = (v) => (typeof v === 'string' && SHA_RE.test(v) ? v : null);
|
|
43
|
+
|
|
44
|
+
// describeCode — what the card says about the code a runner is on (task 1004407).
|
|
45
|
+
// `behind` is true only when BOTH sides are known and differ: the runner reports
|
|
46
|
+
// the newest main it has seen, and an unknown is never drawn as "out of date".
|
|
47
|
+
// A checkout refreshed in place counts by what is on disk, because that is what
|
|
48
|
+
// every worker and child process runs (task 1004406).
|
|
49
|
+
function describeCode(hb) {
|
|
50
|
+
const head = sha(hb && hb.head);
|
|
51
|
+
if (!head) return null;
|
|
52
|
+
const disk = sha(hb.disk_head);
|
|
53
|
+
const main = sha(hb.main_head);
|
|
54
|
+
const current = disk || head;
|
|
55
|
+
return {
|
|
56
|
+
head, short: head.slice(0, 8),
|
|
57
|
+
diskShort: disk && disk !== head ? disk.slice(0, 8) : null,
|
|
58
|
+
mainShort: main ? main.slice(0, 8) : null,
|
|
59
|
+
behind: main ? main !== current : null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// What the runner was last doing, in words. Keyed on the run-log event names the
|
|
64
|
+
// runner sends as last_event; an unknown event says nothing rather than guessing.
|
|
65
|
+
const ACTIVITY = {
|
|
66
|
+
boot: 'just started',
|
|
67
|
+
waiting: 'waiting for work',
|
|
68
|
+
code_refreshed: 'updated its code in place',
|
|
69
|
+
nothing_claimable: 'waiting — nothing to claim',
|
|
70
|
+
fenced: 'held by the fence',
|
|
71
|
+
hold: 'pausing',
|
|
72
|
+
};
|
|
31
73
|
|
|
32
74
|
function ageS(lastSeenAt, nowMs) {
|
|
33
75
|
const t = Date.parse(lastSeenAt || '');
|
|
@@ -68,19 +110,38 @@ function describeRunner(heartbeat, { nowMs = Date.now(), aliveS = ALIVE_S, lateS
|
|
|
68
110
|
const mode = heartbeat.mode === 'probing' ? 'probing' : (heartbeat.mode === 'full' ? 'full' : null);
|
|
69
111
|
const working = heartbeat.working_task_id ? String(heartbeat.working_task_id) : null;
|
|
70
112
|
const where = host ? ` on ${host}` : '';
|
|
113
|
+
const ident = {
|
|
114
|
+
host, mode, workingTaskId: working,
|
|
115
|
+
account: typeof heartbeat.claude_account === 'string' && heartbeat.claude_account ? heartbeat.claude_account : null,
|
|
116
|
+
code: describeCode(heartbeat),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (heartbeat.last_event === 'upgrade_exit') {
|
|
120
|
+
if (age <= RESTART_S) {
|
|
121
|
+
return {
|
|
122
|
+
state: 'restarting', ok: true, ageS: age, age: humanAge(age), ...ident,
|
|
123
|
+
detail: `restarting${where} to pick up new code`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
state: 'dead', ok: false, ageS: age, age: humanAge(age), ...ident,
|
|
128
|
+
detail: `exited${where} ${humanAge(age)} ago to restart on new code and has not come back`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
71
131
|
|
|
72
132
|
if (age <= aliveS) {
|
|
133
|
+
const doing = ACTIVITY[heartbeat.last_event];
|
|
73
134
|
return {
|
|
74
|
-
state: 'alive', ok: true, ageS: age, age: humanAge(age),
|
|
135
|
+
state: 'alive', ok: true, ageS: age, age: humanAge(age), ...ident,
|
|
75
136
|
detail: working
|
|
76
137
|
? `working task ${working}${where}`
|
|
77
|
-
: `${mode === 'probing' ? 'probing after failures' : 'running'}${where}`,
|
|
138
|
+
: `${mode === 'probing' ? 'probing after failures' : (doing || 'running')}${where}`,
|
|
78
139
|
};
|
|
79
140
|
}
|
|
80
141
|
|
|
81
142
|
if (age <= lateS) {
|
|
82
143
|
return {
|
|
83
|
-
state: 'late', ok: true, ageS: age, age: humanAge(age),
|
|
144
|
+
state: 'late', ok: true, ageS: age, age: humanAge(age), ...ident,
|
|
84
145
|
// Deliberately NOT an alarm. This is what a runner doing its job looks like.
|
|
85
146
|
detail: working
|
|
86
147
|
? `quiet for ${humanAge(age)} — expected while task ${working} builds`
|
|
@@ -89,9 +150,9 @@ function describeRunner(heartbeat, { nowMs = Date.now(), aliveS = ALIVE_S, lateS
|
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
return {
|
|
92
|
-
state: 'dead', ok: false, ageS: age, age: humanAge(age),
|
|
153
|
+
state: 'dead', ok: false, ageS: age, age: humanAge(age), ...ident,
|
|
93
154
|
detail: `no check-in for ${humanAge(age)}${where} — longer than any worker takes, so the runner is not running`,
|
|
94
155
|
};
|
|
95
156
|
}
|
|
96
157
|
|
|
97
|
-
module.exports = { ALIVE_S, LATE_S, ageS, humanAge, describeRunner };
|
|
158
|
+
module.exports = { ALIVE_S, LATE_S, RESTART_S, ageS, humanAge, describeRunner };
|
|
@@ -358,7 +358,7 @@
|
|
|
358
358
|
// exist here, resolved to inherited grey, and drew a dead runner in exactly the
|
|
359
359
|
// same colour as a healthy one. Named classes cannot fail that way silently —
|
|
360
360
|
// a missing class is visible in the stylesheet, a missing var is not.
|
|
361
|
-
const HEALTH_CLASS = { alive: 'fact-pill--ok', late: 'fact-pill--warn', dead: 'fact-pill--danger', never_seen: 'fact-pill--info' };
|
|
361
|
+
const HEALTH_CLASS = { alive: 'fact-pill--ok', late: 'fact-pill--warn', dead: 'fact-pill--danger', never_seen: 'fact-pill--info', restarting: 'fact-pill--info' };
|
|
362
362
|
|
|
363
363
|
async function loadRunners() {
|
|
364
364
|
const el = document.getElementById('fence-runner');
|
|
@@ -384,13 +384,29 @@
|
|
|
384
384
|
// NOT the word "running": the switch below already uses it for a different
|
|
385
385
|
// thing (allowed to work), and the two lines sit one above the other. A
|
|
386
386
|
// reader should never have to work out which sense is meant.
|
|
387
|
-
const label = { dead: 'Not checking in', late: 'Quiet', alive: 'Checking in' }[h.state] || 'Unknown';
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
</
|
|
387
|
+
const label = { dead: 'Not checking in', late: 'Quiet', alive: 'Checking in', restarting: 'Restarting' }[h.state] || 'Unknown';
|
|
388
|
+
// Which PC, which Claude account, which code (task 1004407). Each line is
|
|
389
|
+
// drawn only when the runner reported it, so an older runner shows less
|
|
390
|
+
// rather than a guess.
|
|
391
|
+
const facts = [];
|
|
392
|
+
if (h.host) facts.push(`<span class="ov-fact">PC <strong>${escapeHtml(h.host)}</strong></span>`);
|
|
393
|
+
if (h.account) facts.push(`<span class="ov-fact">Claude account ${escapeHtml(h.account)}</span>`);
|
|
394
|
+
if (h.code) {
|
|
395
|
+
const c = h.code;
|
|
396
|
+
const status = c.behind === true ? ` — behind main (${escapeHtml(c.mainShort)})`
|
|
397
|
+
: c.behind === false ? ' — up to date' : '';
|
|
398
|
+
const disk = c.diskShort ? `, updated in place to ${escapeHtml(c.diskShort)}` : '';
|
|
399
|
+
facts.push(`<span class="ov-fact">code <code>${escapeHtml(c.short)}</code>${disk}${status}</span>`);
|
|
400
|
+
}
|
|
401
|
+
return `<div class="ov-foot">
|
|
402
|
+
<p style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:0;">
|
|
403
|
+
<span class="fact-pill ${cls}">${escapeHtml(label)}</span>
|
|
404
|
+
<span>${escapeHtml(h.detail || '')}</span>
|
|
405
|
+
${r.working_goal_id ? `<span class="ov-fact">from goal ${escapeHtml(String(r.working_goal_id))}</span>` : ''}
|
|
406
|
+
<span class="ov-fact">last check-in ${escapeHtml(h.age || 'unknown')} ago</span>
|
|
407
|
+
</p>
|
|
408
|
+
${facts.length ? `<p style="display:flex;gap:14px;flex-wrap:wrap;margin:4px 0 0;">${facts.join('')}</p>` : ''}
|
|
409
|
+
</div>`;
|
|
394
410
|
}).join('');
|
|
395
411
|
}
|
|
396
412
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.61",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.61",
|
|
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.20.
|
|
3
|
+
"version": "1.20.61",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8477,5 +8477,11 @@
|
|
|
8477
8477
|
"id": "1004421",
|
|
8478
8478
|
"text": "Every project can now have its own look, so builders who work in several projects always know which hall they are in. Creating a project has a new Look step: four ready-made looks or your own colour, a switch to tint dark mode"
|
|
8479
8479
|
}
|
|
8480
|
+
],
|
|
8481
|
+
"1.20.61": [
|
|
8482
|
+
{
|
|
8483
|
+
"id": "1004407",
|
|
8484
|
+
"text": "The Gate page now shows, for each Autobongos runner, which PC it is on, which Claude account it is spending, which code version it runs (and whether that is behind), and says \"Restarting\" while it relaunches for new code inste"
|
|
8485
|
+
}
|
|
8480
8486
|
]
|
|
8481
8487
|
}
|
|
@@ -368,7 +368,7 @@ const MAX_CLAIM_ATTEMPTS = 5;
|
|
|
368
368
|
const LIMIT_UNKNOWN_RESET_MS = 30 * 60 * 1000;
|
|
369
369
|
|
|
370
370
|
function newRunnerState() {
|
|
371
|
-
return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null };
|
|
371
|
+
return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null, mainHead: null };
|
|
372
372
|
}
|
|
373
373
|
const RUNNER_STATE = newRunnerState();
|
|
374
374
|
|
|
@@ -703,6 +703,43 @@ function writeHeartbeat(now = Date.now(), runnerState = RUNNER_STATE) {
|
|
|
703
703
|
// is the signal.
|
|
704
704
|
const RUNNER_STARTED_AT = new Date().toISOString();
|
|
705
705
|
|
|
706
|
+
// claudeAccountLabel — WHICH Claude account this machine's workers spend
|
|
707
|
+
// (task 1004407), as a label and never a secret. Workers inherit
|
|
708
|
+
// ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN from this process
|
|
709
|
+
// (WORKER_ENV_ALLOW), and either one outranks the stored login, so those are
|
|
710
|
+
// named first, by the variable that is set and never by its value. Otherwise it
|
|
711
|
+
// is the login Claude Code itself records in ~/.claude.json (oauthAccount): the
|
|
712
|
+
// email, plus the organisation when it has one. Read on every beat, so signing
|
|
713
|
+
// a different account in shows up without a restart; null when nothing can be
|
|
714
|
+
// read, which the card leaves blank rather than guessing.
|
|
715
|
+
function claudeAccountLabel({ env = process.env, readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
|
|
716
|
+
if (env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
|
|
717
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN) return 'a Claude token (CLAUDE_CODE_OAUTH_TOKEN)';
|
|
718
|
+
try {
|
|
719
|
+
const dir = env.CLAUDE_CONFIG_DIR || os.homedir();
|
|
720
|
+
const acct = JSON.parse(readFile(path.join(dir, '.claude.json'))).oauthAccount || {};
|
|
721
|
+
const email = typeof acct.emailAddress === 'string' ? acct.emailAddress.trim() : '';
|
|
722
|
+
if (!email) return null;
|
|
723
|
+
const org = typeof acct.organizationName === 'string' ? acct.organizationName.trim() : '';
|
|
724
|
+
return (org ? `${email} · ${org}` : email).slice(0, 200);
|
|
725
|
+
} catch (_) { return null; }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// heartbeatIdentity — the fields that say which code and which account (task
|
|
729
|
+
// 1004407). Undefined fields are dropped, so a runner that cannot read its own
|
|
730
|
+
// commit sends nothing for it rather than an empty string the route would refuse.
|
|
731
|
+
function heartbeatIdentity(deps = {}) {
|
|
732
|
+
const st = deps.state || RUNNER_STATE;
|
|
733
|
+
const out = {
|
|
734
|
+
head: LOADED_HEAD || undefined,
|
|
735
|
+
disk_head: st.diskHead && st.diskHead !== LOADED_HEAD ? st.diskHead : undefined,
|
|
736
|
+
main_head: st.mainHead || undefined,
|
|
737
|
+
claude_account: (deps.claudeAccountLabel || claudeAccountLabel)() || undefined,
|
|
738
|
+
};
|
|
739
|
+
for (const k of Object.keys(out)) if (out[k] === undefined) delete out[k];
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
706
743
|
async function postHeartbeat(fields = {}, deps = {}) {
|
|
707
744
|
try {
|
|
708
745
|
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
@@ -711,6 +748,7 @@ async function postHeartbeat(fields = {}, deps = {}) {
|
|
|
711
748
|
host: os.hostname(),
|
|
712
749
|
pid: process.pid,
|
|
713
750
|
started_at: RUNNER_STARTED_AT,
|
|
751
|
+
...heartbeatIdentity(deps),
|
|
714
752
|
...fields,
|
|
715
753
|
},
|
|
716
754
|
});
|
|
@@ -870,6 +908,9 @@ async function codeDrifted(deps = {}) {
|
|
|
870
908
|
let tip = null;
|
|
871
909
|
try { tip = await runFetchHead(deps); } catch (_) { return null; }
|
|
872
910
|
if (!tip) return null;
|
|
911
|
+
// Kept for the heartbeat (task 1004407), so the hall can say "behind main"
|
|
912
|
+
// without the server ever reaching GitHub itself.
|
|
913
|
+
(deps.state || RUNNER_STATE).mainHead = tip;
|
|
873
914
|
return tip !== base ? { from: base.slice(0, 8), to: String(tip).slice(0, 8) } : false;
|
|
874
915
|
}
|
|
875
916
|
|
|
@@ -1147,6 +1188,6 @@ module.exports = {
|
|
|
1147
1188
|
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
|
|
1148
1189
|
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, codeDrifted, loadedHead, UPGRADE_EXIT_CODE, MIN_UPTIME_BEFORE_UPGRADE_MS,
|
|
1149
1190
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
1150
|
-
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT, failureReason,
|
|
1191
|
+
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, claudeAccountLabel, RUNNER_STARTED_AT, failureReason,
|
|
1151
1192
|
newRunnerState, loadedFiles, supervisorTouched, refreshCheckout, LIMIT_UNKNOWN_RESET_MS, QUEUE_GATED_EXIT, CLAIM_SET_ASIDE_MS, MAX_CLAIM_ATTEMPTS, owedTaskIds,
|
|
1152
1193
|
};
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.61'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// tests/autonomy_runner_identity.mjs — the runner card says which PC, which Claude
|
|
2
|
+
// account, which code, and whether the runner is live or restarting (task 1004407).
|
|
3
|
+
//
|
|
4
|
+
// Owner ruling, 2026-10-01: showing the code version is good, and showing when it
|
|
5
|
+
// is restarting versus live is better, along with which always-on PC and which
|
|
6
|
+
// Claude account it runs on. These pin each of those from the runner that reports
|
|
7
|
+
// them, through the route and the SQL, to the verdict the hall renders. The rule
|
|
8
|
+
// that holds across all of it: an unknown is shown as nothing, never as a guess,
|
|
9
|
+
// and a secret is never what gets sent.
|
|
10
|
+
//
|
|
11
|
+
// DB-free, no network. Run: node --test tests/autonomy_runner_identity.mjs
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const req = createRequire(import.meta.url);
|
|
22
|
+
const express = req('express');
|
|
23
|
+
const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
24
|
+
const health = req(path.join(ROOT, 'modules/autonomy/runner-health.js'));
|
|
25
|
+
const runner = req(path.join(ROOT, 'scripts/gds/autobongos-run.js'));
|
|
26
|
+
|
|
27
|
+
const NOW = Date.parse('2026-10-01T12:00:00.000Z');
|
|
28
|
+
const agoS = (s) => new Date(NOW - s * 1000).toISOString();
|
|
29
|
+
const A = 'a'.repeat(40);
|
|
30
|
+
const B = 'b'.repeat(40);
|
|
31
|
+
const hb = (over = {}) => ({ host: 'gmk-always-on', mode: 'full', last_event: 'loop', last_seen_at: agoS(30), ...over });
|
|
32
|
+
|
|
33
|
+
// ── live or restarting ───────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
test('a runner that just exited to pick up new code reads RESTARTING, not dead', () => {
|
|
36
|
+
const d = health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(90) }), { nowMs: NOW });
|
|
37
|
+
assert.equal(d.state, 'restarting');
|
|
38
|
+
assert.equal(d.ok, true, 'a restart is not a fault');
|
|
39
|
+
assert.match(d.detail, /restarting on gmk-always-on to pick up new code/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a restart that never came back is dead, and says it was a restart', () => {
|
|
43
|
+
const d = health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S + 60) }), { nowMs: NOW });
|
|
44
|
+
assert.equal(d.state, 'dead');
|
|
45
|
+
assert.equal(d.ok, false);
|
|
46
|
+
assert.match(d.detail, /exited on gmk-always-on .* to restart on new code and has not come back/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the restart window ends exactly at RESTART_S', () => {
|
|
50
|
+
assert.equal(health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S) }), { nowMs: NOW }).state, 'restarting');
|
|
51
|
+
assert.equal(health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S + 1) }), { nowMs: NOW }).state, 'dead');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a live runner says what it last did, in words', () => {
|
|
55
|
+
assert.match(health.describeRunner(hb({ last_event: 'code_refreshed' }), { nowMs: NOW }).detail, /updated its code in place on gmk-always-on/);
|
|
56
|
+
assert.match(health.describeRunner(hb({ last_event: 'waiting' }), { nowMs: NOW }).detail, /waiting for work/);
|
|
57
|
+
assert.match(health.describeRunner(hb({ last_event: 'loop' }), { nowMs: NOW }).detail, /^running on/, 'an event with no words keeps the old wording');
|
|
58
|
+
assert.match(health.describeRunner(hb({ working_task_id: '4242', last_event: 'worked' }), { nowMs: NOW }).detail, /working task 4242/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── which code ───────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
test('the code version is shown short, and flagged when it is behind main', () => {
|
|
64
|
+
const d = health.describeRunner(hb({ head: A, main_head: B }), { nowMs: NOW });
|
|
65
|
+
assert.equal(d.code.short, 'aaaaaaaa');
|
|
66
|
+
assert.equal(d.code.behind, true);
|
|
67
|
+
assert.equal(d.code.mainShort, 'bbbbbbbb');
|
|
68
|
+
assert.equal(health.describeRunner(hb({ head: A, main_head: A }), { nowMs: NOW }).code.behind, false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('an unknown main is never drawn as "out of date"', () => {
|
|
72
|
+
const c = health.describeRunner(hb({ head: A }), { nowMs: NOW }).code;
|
|
73
|
+
assert.equal(c.behind, null);
|
|
74
|
+
assert.equal(c.mainShort, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a checkout refreshed in place is current by what is on DISK', () => {
|
|
78
|
+
// Workers and child processes run what is on disk (task 1004406), so a
|
|
79
|
+
// refreshed runner whose own process is older is still up to date.
|
|
80
|
+
const c = health.describeRunner(hb({ head: A, disk_head: B, main_head: B }), { nowMs: NOW }).code;
|
|
81
|
+
assert.equal(c.behind, false);
|
|
82
|
+
assert.equal(c.diskShort, 'bbbbbbbb');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('no head, or a head that is not a commit, shows no code line at all', () => {
|
|
86
|
+
for (const head of [null, undefined, '', 'abc', 'Z'.repeat(40), A.toUpperCase()]) {
|
|
87
|
+
assert.equal(health.describeRunner(hb({ head }), { nowMs: NOW }).code, null, String(head));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── which PC and which account ───────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
test('the PC and the Claude account ride on every state, dead included', () => {
|
|
94
|
+
for (const [over, state] of [[{}, 'alive'], [{ last_seen_at: agoS(health.LATE_S + 1) }, 'dead'], [{ last_event: 'upgrade_exit' }, 'restarting']]) {
|
|
95
|
+
const d = health.describeRunner(hb({ claude_account: 'me@example.com', head: A, ...over }), { nowMs: NOW });
|
|
96
|
+
assert.equal(d.state, state);
|
|
97
|
+
assert.equal(d.host, 'gmk-always-on');
|
|
98
|
+
assert.equal(d.account, 'me@example.com', `${state} must still say whose account it spent`);
|
|
99
|
+
assert.ok(d.code, `${state} must still say which code`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the account label names a key or a token by its variable, never its value', () => {
|
|
104
|
+
const secret = 'sk-ant-SECRET-VALUE';
|
|
105
|
+
const viaKey = runner.claudeAccountLabel({ env: { ANTHROPIC_API_KEY: secret }, readFile: () => { throw new Error('unread'); } });
|
|
106
|
+
assert.equal(viaKey, 'an API key (ANTHROPIC_API_KEY)');
|
|
107
|
+
const viaToken = runner.claudeAccountLabel({ env: { CLAUDE_CODE_OAUTH_TOKEN: secret }, readFile: () => { throw new Error('unread'); } });
|
|
108
|
+
assert.equal(viaToken, 'a Claude token (CLAUDE_CODE_OAUTH_TOKEN)');
|
|
109
|
+
assert.ok(!viaKey.includes(secret) && !viaToken.includes(secret));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('otherwise the label is the login Claude Code records, with its organisation', () => {
|
|
113
|
+
const seen = [];
|
|
114
|
+
const file = (o) => (p) => { seen.push(p); return JSON.stringify({ oauthAccount: o, primaryApiKey: 'never-read' }); };
|
|
115
|
+
assert.equal(runner.claudeAccountLabel({ env: { CLAUDE_CONFIG_DIR: '/cfg' }, readFile: file({ emailAddress: 'me@example.com', organizationName: 'Acme' }) }), 'me@example.com · Acme');
|
|
116
|
+
assert.equal(path.normalize(seen[0]), path.normalize('/cfg/.claude.json'), 'CLAUDE_CONFIG_DIR is where Claude Code keeps it when set');
|
|
117
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: file({ emailAddress: 'me@example.com' }) }), 'me@example.com');
|
|
118
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: file({}) }), null, 'no login reads as unknown');
|
|
119
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: () => 'not json' }), null);
|
|
120
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: () => { throw new Error('ENOENT'); } }), null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── the runner sends it ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test('every server check-in carries the code and the account, and the newest main seen', async () => {
|
|
126
|
+
const bodies = [];
|
|
127
|
+
const api = { autonomy: { postAutonomyHeartbeat: async ({ body }) => { bodies.push(body); return { ok: true }; } } };
|
|
128
|
+
const state = runner.newRunnerState();
|
|
129
|
+
// codeDrifted is what learns main's tip; it must keep it for the heartbeat.
|
|
130
|
+
await runner.codeDrifted({ loadedHead: async () => A, run: async () => ({ ok: true, stdout: `${B}\trefs/heads/main\n` }), state });
|
|
131
|
+
assert.equal(state.mainHead, B);
|
|
132
|
+
await runner.loadedHead({ run: async () => ({ ok: true, stdout: `${A}\n` }) });
|
|
133
|
+
await runner.postHeartbeat({ last_event: 'loop' }, { api, state, claudeAccountLabel: () => 'me@example.com' });
|
|
134
|
+
assert.equal(bodies[0].head, A);
|
|
135
|
+
assert.equal(bodies[0].main_head, B);
|
|
136
|
+
assert.equal(bodies[0].claude_account, 'me@example.com');
|
|
137
|
+
assert.equal(bodies[0].last_event, 'loop');
|
|
138
|
+
assert.equal('disk_head' in bodies[0], false, 'no refresh in place, so no disk_head');
|
|
139
|
+
|
|
140
|
+
state.diskHead = B;
|
|
141
|
+
await runner.postHeartbeat({}, { api, state, claudeAccountLabel: () => null });
|
|
142
|
+
assert.equal(bodies[1].disk_head, B);
|
|
143
|
+
assert.equal('claude_account' in bodies[1], false, 'an unknown account is left out, not sent empty');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── the route and the SQL ────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function autonomyRouter() {
|
|
149
|
+
const api = req(path.join(ROOT, 'src/module-api.js'));
|
|
150
|
+
const saved = { b: api.requireBuilder, p: api.requirePermission };
|
|
151
|
+
api.requireBuilder = (r, _s, next) => { r.builder = { id: 3, rank: 'archon' }; next(); };
|
|
152
|
+
api.requirePermission = () => (_r, _s, next) => next();
|
|
153
|
+
const routePath = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
|
|
154
|
+
delete req.cache[routePath];
|
|
155
|
+
try { return req(routePath)(); } finally { api.requireBuilder = saved.b; api.requirePermission = saved.p; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('the route stores a commit only if it is one, and the account label as sent', async () => {
|
|
159
|
+
const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
|
|
160
|
+
const saved = db.recordRunnerHeartbeat;
|
|
161
|
+
const seen = [];
|
|
162
|
+
db.recordRunnerHeartbeat = async (a) => { seen.push(a); return {}; };
|
|
163
|
+
const app = express(); app.use(express.json()); app.use(attachFail); app.use(autonomyRouter());
|
|
164
|
+
const server = app.listen(0); await new Promise((r) => server.once('listening', r));
|
|
165
|
+
const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/autonomy/heartbeat`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
166
|
+
try {
|
|
167
|
+
let r = await post({ host: 'gmk-always-on', head: A, disk_head: B, main_head: B, claude_account: ' me@example.com ' });
|
|
168
|
+
assert.equal(r.status, 200);
|
|
169
|
+
assert.deepEqual([seen[0].head, seen[0].diskHead, seen[0].mainHead, seen[0].claudeAccount], [A, B, B, 'me@example.com']);
|
|
170
|
+
r = await post({ host: 'gmk-always-on', head: 'not-a-commit', main_head: A.toUpperCase(), claude_account: '' });
|
|
171
|
+
assert.equal(r.status, 200, 'a bad version never costs the runner its check-in');
|
|
172
|
+
assert.deepEqual([seen[1].head, seen[1].mainHead, seen[1].claudeAccount], [null, null, null]);
|
|
173
|
+
r = await post({ host: 'gmk-always-on', claude_account: 'x'.repeat(201) });
|
|
174
|
+
assert.equal(r.status, 400, 'the label is capped, so a key pasted into it by mistake is refused, not stored');
|
|
175
|
+
} finally {
|
|
176
|
+
server.close();
|
|
177
|
+
db.recordRunnerHeartbeat = saved;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('the heartbeat SQL overwrites the identity every beat and reads it back', async () => {
|
|
182
|
+
const api = req(path.join(ROOT, 'src/module-api.js'));
|
|
183
|
+
const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
|
|
184
|
+
const saved = api.pool.query;
|
|
185
|
+
const calls = [];
|
|
186
|
+
api.pool.query = async (sql, params) => { calls.push({ sql, params }); return { rows: [{ head: A }] }; };
|
|
187
|
+
try {
|
|
188
|
+
await db.recordRunnerHeartbeat({ builderId: 3, host: 'gmk-always-on', head: A, diskHead: B, mainHead: B, claudeAccount: 'me@example.com' });
|
|
189
|
+
for (const col of ['head', 'disk_head', 'main_head', 'claude_account']) {
|
|
190
|
+
assert.match(calls[0].sql, new RegExp(`\\b${col} = EXCLUDED\\.${col}\\b`), `${col} is overwritten, never kept from the last process`);
|
|
191
|
+
}
|
|
192
|
+
assert.deepEqual(calls[0].params.slice(9), [A, B, B, 'me@example.com']);
|
|
193
|
+
await db.readRunnerHeartbeats(3);
|
|
194
|
+
assert.match(calls[1].sql, /head, disk_head, main_head, claude_account/);
|
|
195
|
+
} finally {
|
|
196
|
+
api.pool.query = saved;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('the migration adds the four columns, additively', () => {
|
|
201
|
+
const sql = fs.readFileSync(path.join(ROOT, 'modules/autonomy/migrations/autonomy_004_runner_identity.sql'), 'utf8')
|
|
202
|
+
.split('\n').filter((l) => !l.trim().startsWith('--')).join('\n');
|
|
203
|
+
for (const col of ['head', 'disk_head', 'main_head', 'claude_account']) {
|
|
204
|
+
assert.match(sql, new RegExp(`ADD COLUMN IF NOT EXISTS ${col} text`));
|
|
205
|
+
}
|
|
206
|
+
assert.doesNotMatch(sql, /\bDROP\b/i);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the gate card draws the PC, the account, the code and the restarting label', () => {
|
|
210
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/gate.js'), 'utf8');
|
|
211
|
+
assert.match(src, /restarting: 'Restarting'/);
|
|
212
|
+
assert.match(src, /PC <strong>\$\{escapeHtml\(h\.host\)\}<\/strong>/);
|
|
213
|
+
assert.match(src, /Claude account \$\{escapeHtml\(h\.account\)\}/);
|
|
214
|
+
assert.match(src, /behind main/);
|
|
215
|
+
});
|