instar 1.3.772 → 1.3.774

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.772",
3
+ "version": "1.3.774",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,9 +28,10 @@
28
28
  "test:contract": "node scripts/run-contract-tests.js",
29
29
  "test:contract:raw": "vitest run --config vitest.contract.config.ts",
30
30
  "test:all": "vitest run && vitest run --config vitest.integration.config.ts && vitest run --config vitest.e2e.config.ts",
31
- "lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unregistered-self-action.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs && node scripts/lint-no-unreachable-messaging-gate.js",
31
+ "lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unregistered-self-action.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs && node scripts/lint-no-unreachable-messaging-gate.js && node scripts/lint-nature-chains.mjs",
32
32
  "lint:routing-registry": "node scripts/lint-routing-registry-freshness.js",
33
33
  "lint:no-opus-claude-cli-gating": "node scripts/lint-no-opus-claude-cli-gating.js",
34
+ "lint:nature-chains": "node scripts/lint-nature-chains.mjs",
34
35
  "lint:model-freshness": "node scripts/lint-model-registry-freshness.mjs",
35
36
  "lint:model-freshness:strict": "INSTAR_MODEL_FRESHNESS_STRICT=1 node scripts/lint-model-registry-freshness.mjs",
36
37
  "lint:no-unbounded-llm-spawn": "node scripts/lint-no-unbounded-llm-spawn.js",
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-nature-chains.mjs — the FD4 HARNESS-DOOR BAN, enforced at BUILD time.
4
+ *
5
+ * Spec: docs/specs/nature-axis-routing.md — FD4 (§188-232, "The harness-door ban"),
6
+ * FD4.1 (§205, the pinned-concrete-id reserve), FD8 (§393, no-Fable).
7
+ *
8
+ * THE INVARIANT (structurally unbreakable, enforced in THREE places — §202):
9
+ * No FAST/SORT/JUDGE (bounded/gating) chain position may resolve to a NON-reserve
10
+ * model on the `claude-code` harness door. The ONE permitted claude-code position in
11
+ * a bounded/gating chain is the single sanctioned reserve — pinned to a CONCRETE model
12
+ * id (via ROUTING_LABEL_TO_MODEL_ID['claude-code']), NEVER a bare tier label (a label
13
+ * could resolve differently under a future CLI alias/tier remap — the Adv3 class). It is
14
+ * a deny-by-default ALLOWLIST, not a denylist: any claude-code id that is not the reserve
15
+ * fails, so a future/unrecognized capable Claude id can never slip past. WRITE is exempt —
16
+ * open-ended writing is the sole legitimate Opus-via-CLI lane. Additionally (FD8 §393):
17
+ * NO chain position (incl. WRITE) may resolve to a Fable model.
18
+ *
19
+ * THIS is the compile-time place of the three (§202). The other two are RUNTIME, in
20
+ * src/core/IntelligenceRouter.ts: the resolve-time/config-load validator
21
+ * (`validateNatureRoutingChains`) and the per-position allowlist clamp
22
+ * (`clampToReserveOnCleanDoor` + A1's always-on `clampClaudeCliSwapModel`). The companion
23
+ * ratchet test (tests/unit/llm-routing-nature-ratchet.test.ts) pins this SAME invariant
24
+ * over the real imported TS symbols and asserts this lint agrees with the TS validator
25
+ * (drift guard) — Structure > Willpower.
26
+ *
27
+ * This is a build RATCHET: it exits 1 on any violation (the authored v3 defaults are clean,
28
+ * so it passes today). It reads SOURCE TEXT (the house pattern — lints run pre-compile), and
29
+ * it hard-codes NOTHING about which id is "right": it derives the reserve id and the label→id
30
+ * map from src/data/llmBenchCoverage.ts, so swapping the sanctioned reserve is a data edit
31
+ * there, never a change here.
32
+ *
33
+ * Exit codes: 0 — every chain position clean; 1 — at least one ban violation, or a
34
+ * parse/read error (fail-closed: an unparseable map is a build failure, not a silent pass).
35
+ *
36
+ * Usage:
37
+ * node scripts/lint-nature-chains.mjs
38
+ */
39
+
40
+ import fs from 'node:fs';
41
+ import path from 'node:path';
42
+ import { fileURLToPath } from 'node:url';
43
+
44
+ const __filename = fileURLToPath(import.meta.url);
45
+ const __dirname = path.dirname(__filename);
46
+ const ROOT = path.resolve(__dirname, '..');
47
+ const COVERAGE_SRC = path.join(ROOT, 'src', 'data', 'llmBenchCoverage.ts');
48
+
49
+ const BOUNDED_GATING_CHAINS = new Set(['FAST', 'SORT', 'JUDGE']);
50
+ const KNOWN_TIER_LABELS = new Set(['fast', 'balanced', 'capable', 'ultra', 'reasoning']);
51
+
52
+ /** Slice a top-level `export const NAME ...= {` … `\n};` block body from source. */
53
+ function sliceConstBlock(src, name) {
54
+ const start = src.indexOf(`export const ${name}`);
55
+ if (start < 0) throw new Error(`${name} not found in ${path.basename(COVERAGE_SRC)}`);
56
+ const braceOpen = src.indexOf('{', start);
57
+ if (braceOpen < 0) throw new Error(`${name}: opening brace not found`);
58
+ const end = src.indexOf('\n};', braceOpen);
59
+ if (end < 0) throw new Error(`${name}: terminating '\\n};' not found`);
60
+ return src.slice(braceOpen, end);
61
+ }
62
+
63
+ /**
64
+ * Extract ROUTING_LABEL_TO_MODEL_ID as { [door]: { [label]: concreteId } }.
65
+ * Each door entry is `'door': { 'label': 'id', label2: 'id2' }`.
66
+ */
67
+ export function extractLabelMap(src) {
68
+ const body = sliceConstBlock(src, 'ROUTING_LABEL_TO_MODEL_ID');
69
+ const map = {};
70
+ const doorRe = /'([^']+)':\s*\{([^}]*)\}/g;
71
+ let dm;
72
+ while ((dm = doorRe.exec(body)) !== null) {
73
+ const door = dm[1];
74
+ const inner = dm[2];
75
+ const pairRe = /(?:'([^']+)'|([A-Za-z][\w.\-]*))\s*:\s*'([^']+)'/g;
76
+ const labels = {};
77
+ let pm;
78
+ while ((pm = pairRe.exec(inner)) !== null) {
79
+ const label = pm[1] || pm[2];
80
+ labels[label] = pm[3];
81
+ }
82
+ map[door] = labels;
83
+ }
84
+ if (Object.keys(map).length === 0) throw new Error('ROUTING_LABEL_TO_MODEL_ID parsed empty');
85
+ return map;
86
+ }
87
+
88
+ /**
89
+ * Extract NATURE_ROUTING_DEFAULT_CHAINS as { FAST:[{door,model}], SORT:[...], ... }.
90
+ * Positions are `{ door: 'x', model: 'y', ...flags }` inside each chain's `[ ... ]`.
91
+ */
92
+ export function extractChains(src) {
93
+ const body = sliceConstBlock(src, 'NATURE_ROUTING_DEFAULT_CHAINS');
94
+ const chains = {};
95
+ for (const chain of ['FAST', 'SORT', 'JUDGE', 'WRITE']) {
96
+ const chainRe = new RegExp(`${chain}:\\s*\\[([\\s\\S]*?)\\],`);
97
+ const m = chainRe.exec(body);
98
+ if (!m) throw new Error(`chain ${chain} not found in NATURE_ROUTING_DEFAULT_CHAINS`);
99
+ const posRe = /\{\s*door:\s*'([^']+)'\s*,\s*model:\s*'([^']+)'/g;
100
+ const positions = [];
101
+ let pm;
102
+ while ((pm = posRe.exec(m[1])) !== null) positions.push({ door: pm[1], model: pm[2] });
103
+ chains[chain] = positions;
104
+ }
105
+ return chains;
106
+ }
107
+
108
+ const isFableModel = (s) => /fable/i.test(s);
109
+
110
+ /**
111
+ * The FD4 ban predicate over one authored position — MIRRORS
112
+ * IntelligenceRouter.validateChainPosition. Returns a violation object or null.
113
+ */
114
+ export function banViolationForPosition(chain, pos, index, labelMap, reserveId) {
115
+ const resolvedModelId = labelMap[pos.door]?.[pos.model] ?? pos.model;
116
+ if (isFableModel(resolvedModelId) || isFableModel(pos.model)) {
117
+ return {
118
+ chain,
119
+ index,
120
+ door: pos.door,
121
+ model: pos.model,
122
+ resolvedModelId,
123
+ rule: 'fable-banned',
124
+ detail: `${chain}[${index}] ${pos.door}/'${pos.model}' resolves to a Fable model ('${resolvedModelId}') — no nature chain may emit Fable (FD8 §393).`,
125
+ };
126
+ }
127
+ if (!BOUNDED_GATING_CHAINS.has(chain)) return null; // WRITE exempt
128
+ if (pos.door !== 'claude-code') return null;
129
+ if (resolvedModelId === reserveId) return null; // allowlisted — the sanctioned pinned reserve
130
+ const pinnedInRegistry = labelMap['claude-code']?.[pos.model] !== undefined;
131
+ if (!pinnedInRegistry && KNOWN_TIER_LABELS.has(pos.model)) {
132
+ return {
133
+ chain,
134
+ index,
135
+ door: pos.door,
136
+ model: pos.model,
137
+ resolvedModelId,
138
+ rule: 'claude-code-tier-label',
139
+ detail: `${chain}[${index}] is claude-code/'${pos.model}' — an UNPINNED tier label; the one permitted claude-code position must be the PINNED concrete reserve id '${reserveId}' (FD4 §205).`,
140
+ };
141
+ }
142
+ return {
143
+ chain,
144
+ index,
145
+ door: pos.door,
146
+ model: pos.model,
147
+ resolvedModelId,
148
+ rule: 'claude-code-non-reserve',
149
+ detail: `${chain}[${index}] resolves claude-code → '${resolvedModelId}', which is NOT the sanctioned reserve '${reserveId}' (deny-by-default allowlist ban, FD4 §202-217).`,
150
+ };
151
+ }
152
+
153
+ /** Run the full lint over source text. Returns { violations, reserveId, chains, labelMap }. */
154
+ export function runNatureChainsLint(src) {
155
+ const labelMap = extractLabelMap(src);
156
+ const reserveId = labelMap['claude-code']?.balanced;
157
+ if (!reserveId) throw new Error("ROUTING_LABEL_TO_MODEL_ID['claude-code'].balanced (the reserve id) not found");
158
+ const chains = extractChains(src);
159
+ const violations = [];
160
+ for (const chain of ['FAST', 'SORT', 'JUDGE', 'WRITE']) {
161
+ chains[chain].forEach((pos, i) => {
162
+ const v = banViolationForPosition(chain, pos, i, labelMap, reserveId);
163
+ if (v) violations.push(v);
164
+ });
165
+ }
166
+ return { violations, reserveId, chains, labelMap };
167
+ }
168
+
169
+ function main() {
170
+ let src;
171
+ try {
172
+ src = fs.readFileSync(COVERAGE_SRC, 'utf8');
173
+ } catch (err) {
174
+ console.error(`lint-nature-chains: cannot read ${COVERAGE_SRC} — ${err.message}`);
175
+ process.exit(1);
176
+ }
177
+ let result;
178
+ try {
179
+ result = runNatureChainsLint(src);
180
+ } catch (err) {
181
+ console.error(`lint-nature-chains: parse error (fail-closed — an unparseable map is a build failure) — ${err.message}`);
182
+ process.exit(1);
183
+ }
184
+ if (result.violations.length === 0) {
185
+ console.log(
186
+ `lint-nature-chains: OK — every FAST/SORT/JUDGE claude-code position is the pinned reserve ` +
187
+ `'${result.reserveId}', and no chain emits Fable (FD4 harness-door ban clean).`,
188
+ );
189
+ process.exit(0);
190
+ }
191
+ console.error(
192
+ 'lint-nature-chains: FD4 HARNESS-DOOR BAN VIOLATION(S) in NATURE_ROUTING_DEFAULT_CHAINS ' +
193
+ '(src/data/llmBenchCoverage.ts):\n' +
194
+ result.violations.map((v) => ` - [${v.rule}] ${v.detail}`).join('\n') +
195
+ `\n\nThe one permitted claude-code FAST/SORT/JUDGE position is the pinned reserve id ` +
196
+ `'${result.reserveId}' (author it as the registry-pinned 'balanced' label or the concrete id). ` +
197
+ `WRITE is exempt for Opus-via-CLI, but NO chain may emit Fable. Spec: docs/specs/nature-axis-routing.md FD4/FD8.`,
198
+ );
199
+ process.exit(1);
200
+ }
201
+
202
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === __filename;
203
+ if (isDirectRun) main();
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-05T06:15:12.528Z",
5
- "instarVersion": "1.3.772",
4
+ "generatedAt": "2026-07-05T07:42:29.724Z",
5
+ "instarVersion": "1.3.774",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1538,7 +1538,7 @@
1538
1538
  "type": "subsystem",
1539
1539
  "domain": "sessions",
1540
1540
  "sourcePath": "src/core/SessionManager.ts",
1541
- "contentHash": "fa7570cfad6454faed57c41435475136946b7801a6e1d32b01d2b34fcea8b66f",
1541
+ "contentHash": "2f186c89e86c60d54d6c786a66d7370ed926e30a806a5643ceacd4a88211f867",
1542
1542
  "since": "2025-01-01"
1543
1543
  },
1544
1544
  "subsystem:auto-updater": {
@@ -0,0 +1,27 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Completes two of the three enforcement places for the FD4 "harness-door ban" in the (still-dark) nature-axis router (spec: docs/specs/nature-axis-routing.md). The third place — the always-on runtime clamp — already shipped in A1 (#1386) and A2.1 (#1387).
9
+
10
+ - **Build-time lint** (scripts/lint-nature-chains.mjs, wired into `npm run lint`): fails the build if any FAST/SORT/JUDGE chain position resolves to a non-reserve model on the claude-code harness door, if the one permitted claude-code position is an unpinned tier label instead of the pinned concrete reserve id, or if any chain (including WRITE) resolves to a Fable model. Deny-by-default allowlist; the reserve id and label map are derived from src/data/llmBenchCoverage.ts, so nothing is hard-coded.
11
+ - **Resolve-time + config-load validator** (validateNatureRoutingChains, a pure predicate in src/core/IntelligenceRouter.ts): the same rule run on live config. It rejects a banned chain both when config is loaded (mergeNatureRoutingChains) and when a route is resolved (resolveRoute), falling back to the built-in defaults and warning once — so an operator config edit can never open the banned route.
12
+
13
+ Dev-gated / dark: both new checks only run when sessions.natureRouting is enabled; when it is unset/off the resolve path is byte-identical to before (asserted in tests). This is enforcement hardening only — NOT the metered-door Increment B, and NOT the go-live flip.
14
+
15
+ ## What to Tell Your User
16
+
17
+ This is internal safety plumbing for how I pick which model answers my own background checks — there is nothing for you to turn on or change, and nothing about our conversations changes. In plain terms: I added two guards that make it structurally impossible for one of my quick safety checks to be routed to an expensive model in a way that a bench found makes it easier to fool. One guard runs when my code is built; the other runs if the routing feature is ever switched on. The routing feature itself is still off by default, so today this is invisible — it just means that when it does get turned on, the unsafe route is already sealed shut in two more places.
18
+
19
+ ## Summary of New Capabilities
20
+
21
+ - **FD4 harness-door ban — build-lint**: a new build check refuses any routing chain that would send a bounded safety check to a non-reserve model on the Claude CLI, or emit a Fable model.
22
+ - **FD4 harness-door ban — runtime validator**: a live config/resolve-time guard rejects a banned routing chain and falls back to the safe built-in defaults, so a config edit can never re-open the banned route.
23
+
24
+ ## Evidence
25
+
26
+ - scripts/lint-nature-chains.mjs runs clean over the real chain map; full `npm run lint` is green.
27
+ - tests/unit/llm-routing-nature-ratchet.test.ts and tests/unit/nature-routing-resolver.test.ts (54 tests) green, covering positive/negative lint cases, config-load and resolve-time rejection, the build-lint/validator drift guard, and byte-identical-when-off with a banned override present.
@@ -0,0 +1,39 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed a retention bug in the idle-zombie veto-backoff (the follow-up to the
9
+ session-respawn-thrash-elimination work). On a standby machine, the pre-check that decides
10
+ "have I already backed off cleaning up this session?" keyed the cooldown on the reap-guard's
11
+ reason, while the record step stored the terminate path's reason (`not-lease-holder`). The two
12
+ never matched, so the ledger entry was deleted and rebuilt every tick and the 30-minute cooldown
13
+ never held — the cleanup re-fired every 5 seconds (2,523 consecutive warnings observed live). The
14
+ pre-check now mirrors the terminate path's skip-reason precedence (`protected` → lease gate →
15
+ reap-guard), so both halves key on the same reason and the cooldown actually holds.
16
+
17
+ ## What to Tell Your User
18
+
19
+ Nothing user-facing. This is an internal reliability fix — it stops a wasteful background loop on
20
+ multi-machine setups. No behavior a user interacts with changes.
21
+
22
+ ## Summary of New Capabilities
23
+
24
+ None — this is a correctness fix to existing behavior (the idle-zombie veto-backoff introduced in
25
+ `session-respawn-thrash-elimination`). It ships behind the same `monitoring.idleKillVetoBackoff`
26
+ config knob and is a strict no-op on a single-machine agent.
27
+
28
+ ## Evidence
29
+
30
+ - Root cause confirmed against source (`SessionManager.computeIdleZombieReapVerdict` keyed on the
31
+ reap-guard reason; `terminateSessionInternal` stores `not-lease-holder` on standby) and proven by
32
+ a live 5-second-cadence WARN flood (2,523 lines).
33
+ - 3 new Tier-1 tests in `tests/unit/session-manager-idle-veto-backoff.test.ts`: a load-bearing
34
+ standby-spin repro (fails on the buggy code, passes on the fix — cooldown holds, one terminate
35
+ attempt across 12 ticks), an equivalence PROPERTY test using the REAL `terminateSessionInternal`
36
+ as the oracle (the structural guard against future drift), and an `isAwakeMachine`-unset guard.
37
+ - Full suite: 18/18 in the veto-backoff file, 31/31 across the reaper/veto family, `tsc --noEmit`
38
+ clean. Review-converged (5 internal reviewers + a codex GPT-5.5 external pass) — see
39
+ `docs/specs/reports/idle-zombie-veto-key-consistency-convergence.md`.
@@ -0,0 +1,67 @@
1
+ # Side-Effects Review — Idle-Zombie Veto-Backoff Key Consistency
2
+
3
+ Spec: `docs/specs/idle-zombie-veto-key-consistency.md` (converged, approved)
4
+ Change: `src/core/SessionManager.ts` — `computeIdleZombieReapVerdict` now mirrors
5
+ `terminateSessionInternal`'s skip-reason precedence (`protected` → lease-gate `not-lease-holder`
6
+ → reapGuard) so the veto-backoff pre-check keys the cooldown on the SAME reason `recordVeto`
7
+ stores. Plus 3 Tier-1 tests in `tests/unit/session-manager-idle-veto-backoff.test.ts` (a
8
+ load-bearing standby-spin repro, a real-`terminateSessionInternal`-as-oracle equivalence property
9
+ test, and an `isAwakeMachine`-unset guard test).
10
+
11
+ **Decision point?** Yes — this touches the idle-zombie kill-eligibility DECISION path. It is a
12
+ SIGNAL fix: it corrects which reason KEY the veto-backoff ledger uses; it does NOT change the kill
13
+ AUTHORITY (that stays entirely in `terminateSessionInternal`, which independently re-applies the
14
+ lease gate). The pre-check only decides "have I already backed off?" — never "may I kill?".
15
+
16
+ 1. **Over-block** — Does it reject a legitimate kill it shouldn't? No. On the awake machine the
17
+ standby branch is skipped (`!this.isAwakeMachine()` is false) and behavior is byte-identical to
18
+ today; a session with no keep-reason is still killed on the first attempt (existing test 2 still
19
+ green). The fix only makes a STANDBY machine's cooldown actually HOLD — it was already declining
20
+ to kill (a standby never reaps another machine's sessions); the fix stops the wasteful re-attempt
21
+ spin, not a legitimate kill.
22
+
23
+ 2. **Under-block** — Failure modes it still misses? The residual transient reasons `in-flight` and
24
+ `already-<status>` are deliberately NOT mirrored (they are non-recurring by construction — a
25
+ one-tick mismatch re-evaluates once, never a sustained spin; both are absent from
26
+ `IDLE_ZOMBIE_ESCALATION_REASONS`, so no false P19 escalation). The `in-flight`-flap bound is
27
+ asserted by a Tier-1 test. A future terminate skip-reason added BEFORE the lease gate is caught
28
+ by the equivalence property test's classification-completeness assertion (it forces a
29
+ mirrored-or-residual decision rather than silently going green).
30
+
31
+ 3. **Level-of-abstraction fit** — Right layer? Yes. The fix lives in the pre-check that already owns
32
+ the reason-key derivation. A "single source of truth" shared helper (extract
33
+ `computeAutonomousSkipReason` and have both paths call it) was considered in convergence and
34
+ rejected as an unclean pure extraction — `terminateSessionInternal`'s skip logic interleaves with
35
+ `origin` bypass, a five-flag bypass set, `knownDead`, CAS `already-*`, and the live
36
+ `this.terminating` Set — a wide refactor of the most safety-critical method for a cooldown-key
37
+ fix. The equivalence property test gives the shared-helper's drift-guarantee without the blast
38
+ radius.
39
+
40
+ 4. **Signal vs authority** — Compliant. The change produces/uses a SIGNAL (the cooldown key); the
41
+ blocking AUTHORITY (kill vs skip) remains in `terminateSessionInternal`, unchanged and
42
+ independently re-evaluated. `blocked` stays the raw reapGuard verdict; only `reasonKey` is
43
+ overridden, preserving the C2/R4-2 single-guard-eval threading.
44
+
45
+ 5. **Interactions** — Does it shadow/double-fire/race? No new race (the monitor tick is
46
+ single-threaded; the fix adds a synchronous branch before the existing await point). It does NOT
47
+ change the age-gate `VetoedKillBackoff` instance (that keys via its own 2-arg callsites). The
48
+ escalation gate keys on `vetoKey` (= the stored terminate reason), same source as `recordVeto` —
49
+ no divergence introduced.
50
+
51
+ 6. **External surfaces** — None. No API route, no config schema change, no dashboard surface, no
52
+ agent-facing message. The only observable behavior change is FEWER log lines / terminate attempts
53
+ on a standby machine (the spin stops).
54
+
55
+ 7. **Multi-machine posture (Cross-Machine Coherence)** — `machine-local BY DESIGN`
56
+ (`machine-local-justification: hardware-bound-resource`). The `VetoedKillBackoff` ledger is a
57
+ per-process in-memory Map guarding THIS machine's own tmux/session inventory (hardware-bound); no
58
+ replication, no proxied read, no cross-machine surface. On a single-machine agent
59
+ `isAwakeMachine()` returns awake → the standby branch is skipped → strict no-op.
60
+
61
+ 8. **Rollback cost** — Trivial. Revert the one method (three-branch → one-line). No data migration,
62
+ no state repair. The parent config knob `monitoring.idleKillVetoBackoff.enabled` still gates the
63
+ whole ledger; when disabled the corrected key is never consulted. No hot-fix coupling.
64
+
65
+ **Migration Parity:** N/A — pure runtime code fix to one private method; changes no installed file
66
+ (no `.claude/settings.json`, config default, CLAUDE.md template, hook, or skill). Existing agents
67
+ receive it via the normal version bump.
@@ -0,0 +1,75 @@
1
+ # Side-Effects Review — FD4 harness-door ban: enforcement lints (build-lint + resolve-time/config-load validator)
2
+
3
+ **Version / slug:** `nature-axis-routing-fd4-lints`
4
+ **Date:** `2026-07-04`
5
+ **Author:** `echo (build hand, 24h autonomous run, topic 29723)`
6
+ **Second-pass reviewer:** `not required (Tier 1 — deterministic, dark/dev-gated, byte-identical when off)`
7
+
8
+ ## Summary of the change
9
+
10
+ Completes the FD4 "harness-door ban" from `docs/specs/nature-axis-routing.md` by adding the two enforcement places that were still prose (the third place — the always-on runtime clamp — already shipped in A1 #1386 / A2.1 #1387). Files touched: `src/core/IntelligenceRouter.ts` (pure validator predicate `validateNatureRoutingChains`/`validateChainPosition`/`isNatureRoutingChainsValid` + wiring into `mergeNatureRoutingChains` config-load and `resolveRoute` resolve-time + a deduped rejection warning), `scripts/lint-nature-chains.mjs` (new build-lint), `package.json` (wire the lint into `npm run lint` + a `lint:nature-chains` alias), and two unit test files (`tests/unit/llm-routing-nature-ratchet.test.ts`, `tests/unit/nature-routing-resolver.test.ts`). The change interacts with the nature-axis routing decision surface only; it adds NO new routing behavior when the feature is off.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `FD4 harness-door ban (compile-time place)` — add — the build-lint `lint-nature-chains.mjs` fails the build on a banned authored chain position.
15
+ - `FD4.3 resolve-time / config-load validator` — add — the pure predicate that rejects a banned live chain → built-in defaults + warn-once, consulted in `resolveRoute` and `mergeNatureRoutingChains`.
16
+ - `FD4 place-3 runtime clamp (clampToReserveOnCleanDoor / clampClaudeCliSwapModel)` — pass-through — unchanged; the new validator is belt-and-suspenders ahead of it.
17
+ - `sessions.natureRouting enable/dryRun gate` — pass-through — the whole nature block (and therefore the new validator) still runs ONLY when enabled.
18
+
19
+ ## 1. Over-block
20
+
21
+ **What legitimate inputs does this change reject that it shouldn't?**
22
+
23
+ The build-lint could in principle reject a legitimate authored chain. It does not: the registry-pinned `balanced` label on claude-code (which resolves to the concrete reserve id) is accepted in FAST/SORT/JUDGE, the literal concrete reserve id is accepted, WRITE's `capable`/`fast` (Opus/Haiku) are accepted, and clean non-claude-code doors (incl. the openrouter Opus API door) are accepted. Verified: the real authored v3 defaults pass with zero violations (positive ratchet test). The runtime validator only rejects a chain that violates the static ban, and it falls back to the built-in defaults rather than failing the call, so a rejected operator override degrades to the safe default, never to a hard denial.
24
+
25
+ ## 2. Under-block
26
+
27
+ **What failure modes does this still miss?**
28
+
29
+ The lint's model-id resolution is scoped to `ROUTING_LABEL_TO_MODEL_ID` + a literal Fable substring scan — it does not follow a claude-code *tier label* through the downstream per-adapter tier map (e.g. `frameworkDefaultModels.claude-code`). That is deliberate: the claude-code FAST/SORT/JUDGE allowlist forces the pinned reserve id regardless, and WRITE tier resolution is the FD8 companion config concern (a tracked, restart-gated remainder), not this slice. The FD4.2 R-rule lints (R3–R8: doc-tree Claude-ban, Flash-Lite pin, injection-exposed JUDGE bans) are NOT built here — they depend on an injection-exposure map that does not yet exist; tracked as a remainder.
30
+
31
+ ## 3. Level-of-abstraction fit
32
+
33
+ The validator is a pure, side-effect-free predicate over the static chain data (mirrors `validateChainPosition`), placed beside the existing FD4 functions in `IntelligenceRouter.ts`. The build-lint follows the house pattern (`lint-routing-registry-freshness.js` / `lint-no-opus-claude-cli-gating.js`): read source text pre-compile, export a pure function, exit 1 on violation. Correct altitude — no new engine, no new config language.
34
+
35
+ ## 4. Signal vs authority compliance
36
+
37
+ The build-lint is a CI gate (a build ratchet — its natural authority is to fail the build on a real violation; the authored defaults are clean so it passes today). The resolve-time/config-load validator does exercise authority — it REJECTS a banned chain → built-in defaults — but this is a safety clamp on a MODEL-ROUTING decision, not a principal/operator authority decision: it never blocks a user message, never grants or credits anyone, and only ever narrows toward the sanctioned-safe default (the same safe direction as the existing runtime clamp). It emits a deduped warn-once so the rejection is never silent. No principal identity is read or asserted anywhere.
38
+
39
+ ## 5. Interactions
40
+
41
+ Interacts with `resolveRoute` (adds a validation step before the availability walk) and `mergeNatureRoutingChains` (rejects a banned override chain at load). Both run only under `natureRouting.enabled`. Existing resolver tests (which pass valid default chains / valid overrides) are unaffected — validation returns empty and the positions are unchanged. The place-3 clamp still runs downstream; the two layers coexist as designed (three-place enforcement). The A1 `clampClaudeCliSwapModel` degrade-path clamp is untouched (its byte-identical-when-off behavior is preserved).
42
+
43
+ ## 6. External surfaces
44
+
45
+ No external surface. No new HTTP route, no CLI command, no MCP tool, no Telegram/Slack path, no network egress. The build-lint reads a local source file; the validator reads in-memory config already loaded by the router.
46
+
47
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
48
+
49
+ No operator-facing surface added or changed. The warn-once rejection message names the offending chain + the human-readable violation detail (which position, which rule, the sanctioned reserve id) so an operator who mis-edits a chain via `PATCH /config` sees exactly why it was rejected. No raw secrets, tokens, or file paths are emitted.
50
+
51
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
52
+
53
+ No cross-machine state. The validator is a pure function over per-call config; the build-lint is a repo build gate. The chain config is read live per call on each machine independently (existing `resolveConfig()` behavior). No replication, no shared ledger, no lease interaction. A per-machine operator override that is banned is rejected identically and independently on each machine.
54
+
55
+ ## 8. Rollback cost
56
+
57
+ Low and clean. Revert the PR: the build-lint line drops out of `npm run lint`, the validator functions are removed, and `resolveRoute`/`mergeNatureRoutingChains` return to passing chains through unvalidated (the place-3 runtime clamp still guarantees safety). No migration, no persisted state, no config schema change, no data-format change. Because the validator only runs when `natureRouting.enabled`, a rollback is invisible to every fleet agent (feature dark).
58
+
59
+ ## Conclusion
60
+
61
+ A deterministic, dark/dev-gated, byte-identical-when-off hardening slice that closes two of the three FD4 enforcement places. No block/allow surface on user traffic, no external surface, no multi-machine state, trivial rollback. Tier 1.
62
+
63
+ ## Second-pass review (if required)
64
+
65
+ Not required — Tier 1 (deterministic enforcement lints; byte-identical when off; no operator/user-facing surface). Full `npm run lint` green and both affected test files green (54 tests) at authoring time.
66
+
67
+ ## Evidence pointers
68
+
69
+ - `scripts/lint-nature-chains.mjs` runs clean over the real chain map (`lint-nature-chains: OK`).
70
+ - `tests/unit/llm-routing-nature-ratchet.test.ts` — positive (real chains pass) + negative (Opus / tier-label / Fable fail) + drift guard (lint predicate agrees with the TS validator).
71
+ - `tests/unit/nature-routing-resolver.test.ts` — validator both-sides, config-load rejection, resolve-time rejection, and byte-identical-when-off with a banned override present.
72
+
73
+ ## Class-Closure Declaration (display-only mirror)
74
+
75
+ This closes the compile-time + resolve-time/config-load places of the FD4 ban as a class over the whole chain map (all four chains, every position), not a single spot fix. Tracked remainders (out of this slice by design): the FD4.2 R-rule lints (need the injection-exposure map), the full FD6 aggregated attention-item on rejection (warn-once here), and the FD8 `frameworkDefaultModels` companion config change (restart-gated).