@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 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.852",
6
- "core_contract": "1.19.852",
7
- "source_commit": "bf625696dfb7d52ffce6a01a498a5f9bb7fdfa75",
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:07:34.757Z",
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": 2278,
14
+ "functional_verbatim": 2279,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2801,
20
- "tree_sha256": "d02b8c530b080946a1eeca76fc4a81f1cee5c8d893f9f41639fb880495862380",
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": "75cf389b149eecb2feb08521e846ec486059c4b2b56f01ec3d6d46f62e89ecf9"
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": "67d76943773d604d5534b2b8abb5b6aff1d64c7928e4d19a995d4cead2f406f6"
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": "197f6af6f83b766818989ee6f6fb71723dcf8420ddd190c18703e8c26b5aa889"
7915
+ "sha256": "79f7b3c7784020460b50dfa7da87eb875e7b16fe577054c32c66c6aac23ba9b3"
7916
7916
  },
7917
7917
  {
7918
7918
  "path": "package.json",
7919
7919
  "mode": "0000644",
7920
- "sha256": "065f7ae763810587db17bc9522c11f8672ef9b9689a1cb8630debc9e12ae586e"
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": "82b3a923d967a361a85d409528a785f2e70434289ee10cf25f3fd1e580eddacb"
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": "78626dc42e6d31685352e43e1e8e094a904f59d9d0562caf6b6e126934df973f"
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
- idle > BOX_IDLE_MINUTES (auto, every 15m) task 804:
11
- none ─provision─▶ active ──────────────────────────────▶ destroyed destroys
12
- ▲ │ ▲ droplet
13
- wake(manual) │ park(manual) │ + snapshot
14
- │ ▼ │
15
- parked ── parked > BOX_DORMANT_DAYS ──────┘
16
- (auto, daily)
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 deprovision** (auto, every 15 min) — when an `active` box is idle > `BOX_IDLE_MINUTES`, **destroy the droplet AND its snapshot** ([#804](https://example.com/builders#/task/804)). Compute billing stops; nothing is kept. **Not reversible** — the next connect re-provisions a fresh box, and any uncommitted work on the old box is lost (spike [#837](https://example.com/builders#/task/837)).
21
- - **park** (manual Archon CLI only) — snapshot the droplet then **destroy** it. 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. **No longer automatic** — the idle sweep deprovisions instead of parking ([#804](https://example.com/builders#/task/804)).
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`. Since idle no longer parks, this only catches boxes an Archon **manually** parked.
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 **deprovisions** 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 destroy it.
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 **destroy** a box someone is actively using. Because the idle path is now a full deprovision (no park to wake from), that wrongly-reaped box also loses uncommitted work (spike [#837](https://example.com/builders#/task/837)).
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` | every 15 min | **deprovision** `active` boxes with no activity — destroy droplet **and** snapshot ([#804](https://example.com/builders#/task/804)); not a park | `BOX_IDLE_MINUTES` (default 10; **currently 120 live**) |
38
- | `box-dormant-reclaim` | daily | destroy the snapshot of long-`parked` boxes (pings builder). Since idle no longer parks, only catches **manual** Archon parks | `BOX_DORMANT_DAYS` (14) |
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; [#804](https://example.com/builders#/task/804) simplified the idle path to a direct deprovision — the snapshot/wake machinery now serves only the manual `park`/`wake` commands.)
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-**deprovision** threshold (minutes). **Currently overridden to `120` live** as a soak margin — see "How idle is detected". |
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.852",
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.852",
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.852",
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.852'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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
+ });
@@ -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
- t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value', () => {
327
- // Isolate the listClaimableTasks function body so we don't match CASE
328
- // expressions elsewhere in db.js.
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 db.js');
331
- const body = dbSrc.slice(start, start + 4000);
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 start = dbSrc.indexOf('async function listClaimableTasks');
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),