@bongos/core 1.19.852 → 1.19.854
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 +18 -13
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/builder-box-lifecycle.md +22 -16
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/box_sweep_docs.mjs +115 -0
- package/tests/rank_gate.mjs +35 -7
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.854",
|
|
6
|
+
"core_contract": "1.19.854",
|
|
7
|
+
"source_commit": "9048e308289cb5583549d6c2baefdcf15f992409",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T21:
|
|
9
|
+
"built_at": "2026-09-17T21:24:11.133Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 497,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2279,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2802,
|
|
20
|
+
"tree_sha256": "131a9a8838d352e2a854548f61384dd844e5c2ad280f4af89a4d907462f90cec",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2587,7 +2587,7 @@
|
|
|
2587
2587
|
{
|
|
2588
2588
|
"path": "docs/module-api-changelog.md",
|
|
2589
2589
|
"mode": "0000644",
|
|
2590
|
-
"sha256": "
|
|
2590
|
+
"sha256": "b8e548e543bbe1dc32e042a3b81735bf6681976a1c2dbd2aed964902766ec117"
|
|
2591
2591
|
},
|
|
2592
2592
|
{
|
|
2593
2593
|
"path": "docs/modules-contract.md",
|
|
@@ -2697,7 +2697,7 @@
|
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "docs/recipes/builder-box-lifecycle.md",
|
|
2699
2699
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2700
|
+
"sha256": "dfffcee084534239d8a748b7468409644d9375e952143b0bd09b9c2c7341e844"
|
|
2701
2701
|
},
|
|
2702
2702
|
{
|
|
2703
2703
|
"path": "docs/recipes/claude-design-loop.md",
|
|
@@ -7912,12 +7912,12 @@
|
|
|
7912
7912
|
{
|
|
7913
7913
|
"path": "package-lock.json",
|
|
7914
7914
|
"mode": "0000644",
|
|
7915
|
-
"sha256": "
|
|
7915
|
+
"sha256": "79f7b3c7784020460b50dfa7da87eb875e7b16fe577054c32c66c6aac23ba9b3"
|
|
7916
7916
|
},
|
|
7917
7917
|
{
|
|
7918
7918
|
"path": "package.json",
|
|
7919
7919
|
"mode": "0000644",
|
|
7920
|
-
"sha256": "
|
|
7920
|
+
"sha256": "5822641b3fd9ddb6a4ce55e3843b31bc8cc6cef6a0c1eba7aadf87ad8073b87a"
|
|
7921
7921
|
},
|
|
7922
7922
|
{
|
|
7923
7923
|
"path": "public-docs/index.html",
|
|
@@ -9857,7 +9857,7 @@
|
|
|
9857
9857
|
{
|
|
9858
9858
|
"path": "src/module-api.js",
|
|
9859
9859
|
"mode": "0000644",
|
|
9860
|
-
"sha256": "
|
|
9860
|
+
"sha256": "9dbc00468675431bc3646ba3326916ac68b50d221922861c91425a8b96ff4cd8"
|
|
9861
9861
|
},
|
|
9862
9862
|
{
|
|
9863
9863
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10424,6 +10424,11 @@
|
|
|
10424
10424
|
"mode": "0000644",
|
|
10425
10425
|
"sha256": "063c94d759fa8a79b8c402145801ae2a4d9863bef591fb5adf57697e15effc39"
|
|
10426
10426
|
},
|
|
10427
|
+
{
|
|
10428
|
+
"path": "tests/box_sweep_docs.mjs",
|
|
10429
|
+
"mode": "0000644",
|
|
10430
|
+
"sha256": "e56b09e961b0c68ee27a6e5049750cd2828e94d9870ddc7837bf9de44fcaa0b7"
|
|
10431
|
+
},
|
|
10427
10432
|
{
|
|
10428
10433
|
"path": "tests/box_sync.mjs",
|
|
10429
10434
|
"mode": "0000644",
|
|
@@ -12932,7 +12937,7 @@
|
|
|
12932
12937
|
{
|
|
12933
12938
|
"path": "tests/rank_gate.mjs",
|
|
12934
12939
|
"mode": "0000644",
|
|
12935
|
-
"sha256": "
|
|
12940
|
+
"sha256": "05e193a23bb1218c1c96a4f4bbc734f2f09a6dd76434eb59750014277f0cef3b"
|
|
12936
12941
|
},
|
|
12937
12942
|
{
|
|
12938
12943
|
"path": "tests/rank_roles_audit.mjs",
|
|
@@ -2178,5 +2178,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2178
2178
|
landed since 1.19.850 with no explicit bump. run 35274042996. (task 1002620)
|
|
2179
2179
|
1.19.852 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2180
2180
|
landed since 1.19.851 with no explicit bump. run 35274827497. (task 1002620)
|
|
2181
|
+
1.19.853 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2182
|
+
landed since 1.19.852 with no explicit bump. run 35275456041. (task 1002620)
|
|
2183
|
+
1.19.854 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2184
|
+
landed since 1.19.853 with no explicit bump. run 35276402271. (task 1002620)
|
|
2181
2185
|
---------------------------------------------------------------------------
|
|
2182
2186
|
```
|
|
@@ -7,26 +7,30 @@
|
|
|
7
7
|
Each builder gets **one** box (ADR 0031 §2). Its lifecycle is a small state machine tracked in the `builder_boxes` table:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
none --provision--> active
|
|
11
|
+
^ |
|
|
12
|
+
wake (manual, or | | idle > BOX_IDLE_MINUTES (auto, hourly)
|
|
13
|
+
on next connect) | | park (manual)
|
|
14
|
+
| v
|
|
15
|
+
parked --- parked > BOX_DORMANT_DAYS ---> destroyed
|
|
16
|
+
(auto, daily, WARNS FIRST) snapshot deleted
|
|
17
|
+
|
|
18
|
+
park keeps the SNAPSHOT (only the droplet is destroyed, to stop billing),
|
|
19
|
+
so it is reversible with `wake`. Dormant reclaim is the ONLY automated
|
|
20
|
+
path that destroys a snapshot, and it warns the builder first.
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
- **provision** — create + boot a droplet running the [#596](https://example.com/builders#/task/596) devcontainer. **Rank-gated.**
|
|
20
|
-
- **idle
|
|
21
|
-
- **park** (
|
|
24
|
+
- **idle park** (auto, hourly) — when an `active` box is idle > `BOX_IDLE_MINUTES`, snapshot it and **destroy the droplet only**, keeping the snapshot ([task 1002726](https://example.com/builders#/task/1002726)). Compute billing stops; the box is **reversible** via `wake`. An earlier revision ([task 804](https://example.com/builders#/task/804)) destroyed the snapshot too; that was reverted because it took uncommitted work with it (spike [task 837](https://example.com/builders#/task/837)).
|
|
25
|
+
- **park** (the idle sweep, or an Archon by hand) — snapshot the droplet then **destroy the droplet**. A powered-off DO droplet still bills; only a destroyed one stops. The snapshot (~$0.30/mo) is the cheap residual you can `wake` from.
|
|
22
26
|
- **wake** (manual, on demand) — recreate the droplet from its snapshot. New IP; the `hostname` is repointed via `BOX_DNS_HOOK`.
|
|
23
|
-
- **dormant reclaim** (auto, daily) — destroy the snapshot of a box `parked` > `BOX_DORMANT_DAYS`.
|
|
27
|
+
- **dormant reclaim** (auto, daily) — destroy the snapshot of a box `parked` > `BOX_DORMANT_DAYS`. This is the **only automated path that destroys a snapshot**, so it **warns the builder first** and defers the reclaim if that warning was never delivered ([task 1002727](https://example.com/builders#/task/1002727)). Its input is whatever the idle sweep parked, plus any manual Archon park.
|
|
24
28
|
|
|
25
29
|
### How "idle" is detected
|
|
26
30
|
|
|
27
|
-
The idle sweep **
|
|
31
|
+
The idle sweep **parks** a box when its `last_activity_at` (else `active_since`) is older than `BOX_IDLE_MINUTES` **and** `claude_active` is not true. Both signals are set by **`infra/box-heartbeat.sh`**, which runs **inside the box** every ~5 min and POSTs `/api/gds/box/heartbeat` *only while the box is in use* — an interactive login session, an established SSH connection, an **open browser terminal** (ttyd websocket, [#1161](https://example.com/builders#/task/1161)), or non-trivial CPU load. Silence = the sweep is free to park it.
|
|
28
32
|
|
|
29
|
-
> ⚠️ **The idle sweep's correctness depends on the box-side heartbeat reaching the server.** If it isn't pinging — no Bongos token on the box yet, a pre-[#1161](https://example.com/builders#/task/1161) script that can't see a browser terminal, or the cron not installed — `last_activity_at` never advances and the sweep falls back to `active_since`, i.e. it measures **uptime**, not activity, and will **
|
|
33
|
+
> ⚠️ **The idle sweep's correctness depends on the box-side heartbeat reaching the server.** If it isn't pinging — no Bongos token on the box yet, a pre-[#1161](https://example.com/builders#/task/1161) script that can't see a browser terminal, or the cron not installed — `last_activity_at` never advances and the sweep falls back to `active_since`, i.e. it measures **uptime**, not activity, and will **park** a box someone is actively using. Because the idle path keeps the snapshot ([task 1002726](https://example.com/builders#/task/1002726)), that box can be restored with `wake` and no work is lost — it is disruptive, not destructive. The destructive case is the dormant sweep, which warns first ([task 1002727](https://example.com/builders#/task/1002727)).
|
|
30
34
|
>
|
|
31
35
|
> **Live mitigation (2026-06-16):** `BOX_IDLE_MINUTES` is raised to **120** in `/etc/example/box.env` as a soak margin until [#1161](https://example.com/builders#/task/1161)'s browser-terminal heartbeat is verified on a newcomer box; lower it back toward the 10-min default once that's proven.
|
|
32
36
|
|
|
@@ -34,12 +38,14 @@ Two sweeps automate the middle (both mirror `stale-claim-release` — dry-run fi
|
|
|
34
38
|
|
|
35
39
|
| Sweep | Cadence | What it does | Threshold |
|
|
36
40
|
|---|---|---|---|
|
|
37
|
-
| `box-idle-suspend` |
|
|
38
|
-
| `box-dormant-reclaim` | daily | destroy the snapshot of long-`parked` boxes
|
|
41
|
+
| `box-idle-suspend` | hourly | **park** `active` boxes with no activity — snapshot kept, droplet destroyed ([task 1002726](https://example.com/builders#/task/1002726)); reversible via `wake` | `BOX_IDLE_MINUTES` (default 10; **currently 120 live**) |
|
|
42
|
+
| `box-dormant-reclaim` | daily | destroy the snapshot of long-`parked` boxes — the only automated snapshot delete, and it warns first ([task 1002727](https://example.com/builders#/task/1002727)) | `BOX_DORMANT_DAYS` (14) |
|
|
43
|
+
|
|
44
|
+
> ⚠️ **Both sweeps are currently `enabled: false`** in `config/scheduled-routines.json` — retired with the dev-box module (goal 1000091, [task 1003886](https://example.com/builders#/task/1003886)). Neither fires; the CLI below still works by hand.
|
|
39
45
|
|
|
40
46
|
## Why a self-built sweep (not Coder)
|
|
41
47
|
|
|
42
|
-
ADR 0031 §9.3, resolved 2026-06-04: Coder needs an always-on control-plane droplet (~$12–24/mo fixed) + ops; a self-built sweep has ≈ $0 fixed cost and rides the existing sweep/timer pattern. See the ADR for the full rationale. (The original sweep snapshotted-and-parked idle boxes; [
|
|
48
|
+
ADR 0031 §9.3, resolved 2026-06-04: Coder needs an always-on control-plane droplet (~$12–24/mo fixed) + ops; a self-built sweep has ≈ $0 fixed cost and rides the existing sweep/timer pattern. See the ADR for the full rationale. (The original sweep snapshotted-and-parked idle boxes; [task 804](https://example.com/builders#/task/804) briefly simplified the idle path to a direct deprovision, and [task 1002726](https://example.com/builders#/task/1002726) reverted it — so the snapshot/wake machinery serves the automatic idle path again, as well as the manual `park`/`wake` commands.)
|
|
43
49
|
|
|
44
50
|
## The CLI — `node scripts/gds/box.js`
|
|
45
51
|
|
|
@@ -95,7 +101,7 @@ Put these in `/etc/example/box.env` (chmod 600 — **secrets never go in the rep
|
|
|
95
101
|
| `BOX_REGION` | `nyc3` | droplet region |
|
|
96
102
|
| `BOX_SIZE` | `s-2vcpu-4gb` | droplet size (4 GB floor — the devcontainer sets a 4 GB Node heap) |
|
|
97
103
|
| `BOX_BASE_IMAGE` | `ubuntu-24-04-x64` | first-provision base image (cloud-init installs Docker + the devcontainer) |
|
|
98
|
-
| `BOX_IDLE_MINUTES` | `10` | idle-**
|
|
104
|
+
| `BOX_IDLE_MINUTES` | `10` | idle-**park** threshold (minutes). **Currently overridden to `120` live** as a soak margin — see "How idle is detected". |
|
|
99
105
|
| `BOX_DORMANT_DAYS` | `14` | dormant-reclaim threshold |
|
|
100
106
|
| `BOX_SSH_KEY_IDS` | — | comma-list of DO SSH key ids/fingerprints to inject (so you can SSH in) |
|
|
101
107
|
| `BOX_REPO_URL` | — | git URL the cloud-init clones into `/workspace`. The scoped **credential** is [#600](https://example.com/builders#/task/600); no identity is hardcoded. |
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.854",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.854",
|
|
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.
|
|
3
|
+
"version": "1.19.854",
|
|
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/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.
|
|
74
|
+
const CORE_VERSION = '1.19.854'; // 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,115 @@
|
|
|
1
|
+
// tests/box_sweep_docs.mjs — the box sweep docs must not invert against box.js
|
|
2
|
+
// a third time (task 1003823).
|
|
3
|
+
//
|
|
4
|
+
// HISTORY, because it is the whole reason this file exists. The idle sweep has
|
|
5
|
+
// changed direction twice:
|
|
6
|
+
//
|
|
7
|
+
// task 804 — sweep-idle went from park to a direct deprovision, and
|
|
8
|
+
// task 1001135 dragged four doc surfaces along with it.
|
|
9
|
+
// task 1002726 — the CODE went BACK to park-and-keep-the-snapshot, because
|
|
10
|
+
// annihilating an idle box took the builder's uncommitted work
|
|
11
|
+
// with it. The four doc surfaces were NOT walked back.
|
|
12
|
+
//
|
|
13
|
+
// So for months the routine's own SKILL.md told an agent "your single job is to
|
|
14
|
+
// DESTROY" while cmdSweepIdle called parkOne. The danger is specific and worth
|
|
15
|
+
// naming: an agent that believes it is supposed to destroy, and finds the sweep
|
|
16
|
+
// merely parking, is being invited to reach for `box.js deprovision` to finish a
|
|
17
|
+
// job it was never given — which is the one path that can silently discard a
|
|
18
|
+
// builder's work.
|
|
19
|
+
//
|
|
20
|
+
// This guard is therefore NOT a spell-check. It asserts the one relationship
|
|
21
|
+
// that matters: WHILE cmdSweepIdle parks, the docs may not say it destroys.
|
|
22
|
+
// If a future task legitimately moves the idle path back to a destroy, this test
|
|
23
|
+
// goes green on its own as soon as the code changes — it is pinned to the code,
|
|
24
|
+
// not to a preferred wording.
|
|
25
|
+
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { test } from 'node:test';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
34
|
+
|
|
35
|
+
const BOX_JS = 'scripts/gds/box.js';
|
|
36
|
+
const IDLE_SKILL = '.claude/scheduled-tasks/box-idle-suspend/SKILL.md';
|
|
37
|
+
const DORMANT_SKILL = '.claude/scheduled-tasks/box-dormant-reclaim/SKILL.md';
|
|
38
|
+
const RECIPE = 'docs/recipes/builder-box-lifecycle.md';
|
|
39
|
+
|
|
40
|
+
// Comments are stripped before scanning prose: each surface deliberately keeps a
|
|
41
|
+
// sentence SAYING which model it used to describe, and a naive substring check
|
|
42
|
+
// would read that history as the claim still being made.
|
|
43
|
+
const stripHtmlComments = (md) => md.replace(/<!--[\s\S]*?-->/g, '');
|
|
44
|
+
|
|
45
|
+
// The body of cmdSweepIdle, so "what the sweep does" is read from the function
|
|
46
|
+
// itself rather than from a header comment that can rot the same way the docs did.
|
|
47
|
+
function cmdSweepIdleBody() {
|
|
48
|
+
const src = read(BOX_JS);
|
|
49
|
+
const start = src.indexOf('async function cmdSweepIdle(');
|
|
50
|
+
assert.ok(start > 0, `cmdSweepIdle not found in ${BOX_JS} — this guard is reading the wrong file`);
|
|
51
|
+
const next = src.indexOf('\nasync function ', start + 1);
|
|
52
|
+
return src.slice(start, next > 0 ? next : src.length);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('the idle sweep parks — that is the fact the docs are measured against', () => {
|
|
56
|
+
const body = cmdSweepIdleBody();
|
|
57
|
+
assert.match(body, /parkOne\(/, 'cmdSweepIdle no longer calls parkOne');
|
|
58
|
+
assert.doesNotMatch(body, /deprovisionOne\(/,
|
|
59
|
+
'cmdSweepIdle calls deprovisionOne — the idle path destroys again. That is allowed, '
|
|
60
|
+
+ 'but the docs below must be rewritten to match BEFORE this guard is relaxed.');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('no box-sweep doc claims the IDLE sweep destroys, while the code parks', () => {
|
|
64
|
+
// Sentences that assert a destroy as the idle sweep's OUTCOME. Each pattern is
|
|
65
|
+
// anchored on the idle path specifically, because "destroy the droplet" is a
|
|
66
|
+
// true statement about a park (the droplet does go; the snapshot stays) and
|
|
67
|
+
// "destroy the snapshot" is a true statement about sweep-dormant.
|
|
68
|
+
const lies = [
|
|
69
|
+
/idle[- ]deprovision/i,
|
|
70
|
+
/idle box is fully DESTROYED/i,
|
|
71
|
+
/destroy the droplet \*\*AND\*\* (?:its )?snapshot/i,
|
|
72
|
+
/droplet \+ snapshot destroyed/i,
|
|
73
|
+
/idle no longer feeds it/i,
|
|
74
|
+
/no longer parks/i,
|
|
75
|
+
/there is no snapshot to wake from/i,
|
|
76
|
+
/single job is to DESTROY/i,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const file of [IDLE_SKILL, DORMANT_SKILL, RECIPE]) {
|
|
80
|
+
const prose = stripHtmlComments(read(file));
|
|
81
|
+
for (const lie of lies) {
|
|
82
|
+
assert.doesNotMatch(prose, lie,
|
|
83
|
+
`${file} still describes the pre-task-1002726 destroy model (${lie}). `
|
|
84
|
+
+ 'cmdSweepIdle calls parkOne and keeps the snapshot — see task 1003823.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('the idle skill quotes the strings the worker actually prints', () => {
|
|
90
|
+
// Step 3 of the skill orders a VERBATIM relay, so a drifted string here makes
|
|
91
|
+
// the agent either fabricate a line or stall. These are the three the worker emits.
|
|
92
|
+
const skill = read(IDLE_SKILL);
|
|
93
|
+
const body = cmdSweepIdleBody();
|
|
94
|
+
|
|
95
|
+
for (const s of ['would park', 'snapshot kept, compute stopped', 'done — parked=']) {
|
|
96
|
+
assert.ok(body.includes(s), `cmdSweepIdle no longer prints ${JSON.stringify(s)}`);
|
|
97
|
+
assert.ok(skill.includes(s),
|
|
98
|
+
`${IDLE_SKILL} does not quote ${JSON.stringify(s)}, which the worker prints and `
|
|
99
|
+
+ 'step 3 tells the agent to relay verbatim.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
assert.ok(!/done — deprovisioned=/.test(skill),
|
|
103
|
+
`${IDLE_SKILL} tells the agent to relay "done — deprovisioned=", a line the worker never emits.`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('every surface names sweep-dormant as the only automated snapshot delete', () => {
|
|
107
|
+
// The safety property box.js implements in prose: full reclaim warns first, so
|
|
108
|
+
// no automated path takes uncommitted work silently. A reader who misses this
|
|
109
|
+
// is the reader who reaches for deprovision.
|
|
110
|
+
for (const file of [IDLE_SKILL, DORMANT_SKILL, RECIPE]) {
|
|
111
|
+
assert.match(stripHtmlComments(read(file)), /only automated path that destroys a snapshot|only automated snapshot delete/i,
|
|
112
|
+
`${file} never tells the reader that sweep-dormant is the only automated path that `
|
|
113
|
+
+ 'destroys a snapshot (and that it warns first) — the safety property this subsystem rests on.');
|
|
114
|
+
}
|
|
115
|
+
});
|
package/tests/rank_gate.mjs
CHANGED
|
@@ -323,12 +323,41 @@ const __dir = dirname(fileURLToPath(import.meta.url));
|
|
|
323
323
|
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
324
324
|
const dbSrc = lifecycleDbSource();
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
// Isolate the listClaimableTasks function body so we don't match CASE expressions
|
|
327
|
+
// elsewhere in the lifecycle data layer.
|
|
328
|
+
//
|
|
329
|
+
// ANCHORED TO THE NEXT TOP-LEVEL FUNCTION, not to a byte count (task 1004015; the
|
|
330
|
+
// same fix task 1004002 landed in tests/reward_gate.mjs). The window used to be
|
|
331
|
+
// `start + 4000`, and listClaimableTasks measures 12019 chars — so the slice carried
|
|
332
|
+
// a THIRD of the function it claims to scan.
|
|
333
|
+
//
|
|
334
|
+
// That is a false RED for the positive assertion below (the last WHEN clause it needs
|
|
335
|
+
// sits at +3400, leaving 600 characters of margin before an unrelated feature pushes
|
|
336
|
+
// it out of frame — reward_gate was widened four times for exactly that). It is a
|
|
337
|
+
// false GREEN for the NEGATIVE assertions in the next test, which is the worse half:
|
|
338
|
+
// a stale pre-thetes mapping living anywhere past +4000 — two thirds of the body —
|
|
339
|
+
// passes a test whose entire purpose is to catch it.
|
|
340
|
+
//
|
|
341
|
+
// The boundary lookup is ASSERTED rather than defaulted: if listActiveClaims is ever
|
|
342
|
+
// renamed or moved, this fails loudly and names the fix, instead of silently falling
|
|
343
|
+
// back to a byte window and reinstating the bug.
|
|
344
|
+
// LAZY, and called from inside each t() — deliberately not an IIFE evaluated at module
|
|
345
|
+
// load. Every assertion in this file lives inside a t() closure so a failure is caught,
|
|
346
|
+
// named and counted while the other ~30 tests below still run. An assert at top level
|
|
347
|
+
// would throw at IMPORT time in exactly the case it guards (listActiveClaims renamed),
|
|
348
|
+
// aborting the file before any downstream test registers — trading a false green for a
|
|
349
|
+
// silent loss of the rest of the suite.
|
|
350
|
+
const claimableBody = () => {
|
|
329
351
|
const start = dbSrc.indexOf('async function listClaimableTasks');
|
|
330
|
-
assert.ok(start >= 0, 'listClaimableTasks not found in
|
|
331
|
-
const
|
|
352
|
+
assert.ok(start >= 0, 'listClaimableTasks not found in the lifecycle data layer');
|
|
353
|
+
const end = dbSrc.indexOf('async function listActiveClaims', start);
|
|
354
|
+
assert.ok(end > start,
|
|
355
|
+
'listActiveClaims boundary not found — re-anchor this slice to whatever function now follows listClaimableTasks');
|
|
356
|
+
return dbSrc.slice(start, end);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value', () => {
|
|
360
|
+
const body = claimableBody();
|
|
332
361
|
for (const [rank, tier] of Object.entries(db.REQUIRES_RANK_TIER)) {
|
|
333
362
|
const re = new RegExp(`WHEN '${rank}' THEN ${tier}\\b`);
|
|
334
363
|
assert.ok(re.test(body),
|
|
@@ -338,8 +367,7 @@ t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value
|
|
|
338
367
|
});
|
|
339
368
|
|
|
340
369
|
t('the claimable CASE no longer carries the pre-thetes archon=3/metic=2 scale', () => {
|
|
341
|
-
const
|
|
342
|
-
const body = dbSrc.slice(start, start + 4000);
|
|
370
|
+
const body = claimableBody();
|
|
343
371
|
assert.ok(!/WHEN 'archon' THEN 3\b/.test(body),
|
|
344
372
|
"stale archon=3 mapping present — CASE not migrated to the 4-rank scale");
|
|
345
373
|
assert.ok(!/WHEN 'metic' THEN 2\b/.test(body),
|