instar 1.3.771 → 1.3.773
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +36 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/IntelligenceRouter.d.ts +184 -1
- package/dist/core/IntelligenceRouter.d.ts.map +1 -1
- package/dist/core/IntelligenceRouter.js +354 -1
- package/dist/core/IntelligenceRouter.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +14 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +37 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +51 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts +63 -0
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +92 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/package.json +3 -2
- package/scripts/lint-nature-chains.mjs +203 -0
- package/src/data/builtin-manifest.json +19 -19
- package/src/data/llmBenchCoverage.ts +145 -0
- package/upgrades/1.3.772.md +63 -0
- package/upgrades/1.3.773.md +27 -0
- package/upgrades/nature-routing-a2.eli16.md +50 -0
- package/upgrades/side-effects/nature-axis-routing-fd4-lints.md +75 -0
- package/upgrades/side-effects/nature-routing-a2.md +89 -0
|
@@ -684,3 +684,148 @@ export const LLM_ROUTING_NATURE: Readonly<Record<string, RoutingNature>> = {
|
|
|
684
684
|
SessionSummarySentinel: { nature: 'D', chain: 'SORT' },
|
|
685
685
|
'correction-learning': { nature: 'D', chain: 'SORT' },
|
|
686
686
|
};
|
|
687
|
+
|
|
688
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
689
|
+
* S4 Increment A2 — nature-axis routing: door taxonomy, label registry, chains.
|
|
690
|
+
*
|
|
691
|
+
* These are the DATA half of the nature router (the resolver logic + wiring lives
|
|
692
|
+
* in src/core/IntelligenceRouter.ts). Everything here is pure, read-only config
|
|
693
|
+
* data — importing it changes NO behavior; it is actuated only when
|
|
694
|
+
* `sessions.natureRouting` is enabled (dev-gated dark; dryRun-first).
|
|
695
|
+
* Spec: docs/specs/nature-axis-routing.md — FD1 (doors), FD2 (chains), FD-LABEL/
|
|
696
|
+
* FD4.1 (label→id registry), FD4 (harness-door allowlist), FD6 (critical gates).
|
|
697
|
+
* ──────────────────────────────────────────────────────────────────────────── */
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* A RoutingDoor is a concrete access path to a model (FD1), in two classes:
|
|
701
|
+
* - CLI doors — 1:1 with `IntelligenceFramework` (already wired).
|
|
702
|
+
* - Metered-API doors — NEW, wired in Increment B behind the FD12 money/PIN
|
|
703
|
+
* go-live. In Increment A they are DEFINED but always resolve as unavailable
|
|
704
|
+
* (skipped) — so no metered/paid door ever routes in this increment.
|
|
705
|
+
*/
|
|
706
|
+
export type RoutingDoor =
|
|
707
|
+
| 'pi-cli'
|
|
708
|
+
| 'codex-cli'
|
|
709
|
+
| 'gemini-cli'
|
|
710
|
+
| 'claude-code'
|
|
711
|
+
| 'gemini-api'
|
|
712
|
+
| 'openrouter-api'
|
|
713
|
+
| 'groq-api';
|
|
714
|
+
|
|
715
|
+
/** CLI doors — coincide exactly with the `IntelligenceFramework` id set. */
|
|
716
|
+
export const CLI_ROUTING_DOORS: ReadonlySet<RoutingDoor> = new Set([
|
|
717
|
+
'pi-cli',
|
|
718
|
+
'codex-cli',
|
|
719
|
+
'gemini-cli',
|
|
720
|
+
'claude-code',
|
|
721
|
+
]);
|
|
722
|
+
|
|
723
|
+
/** Metered-API doors — Increment B; ALWAYS skipped (unavailable) in Increment A. */
|
|
724
|
+
export const METERED_ROUTING_DOORS: ReadonlySet<RoutingDoor> = new Set([
|
|
725
|
+
'gemini-api',
|
|
726
|
+
'openrouter-api',
|
|
727
|
+
'groq-api',
|
|
728
|
+
]);
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* A chain position (FD2): `{ door, model }` plus static flags. `model` is a
|
|
732
|
+
* benchmark LABEL (`flash-lite`, `gpt-5.5`, `opus-4.8`) or a tier hint
|
|
733
|
+
* (`fast|balanced|capable`); FD-LABEL resolves it to a concrete model id.
|
|
734
|
+
*/
|
|
735
|
+
export interface ChainPosition {
|
|
736
|
+
readonly door: RoutingDoor;
|
|
737
|
+
readonly model: string;
|
|
738
|
+
/** Vault secret name backing a metered door (Increment B). */
|
|
739
|
+
readonly keyRef?: string;
|
|
740
|
+
/** Real-spend door — money-gated (Increment B). */
|
|
741
|
+
readonly moneyGated?: boolean;
|
|
742
|
+
/** `false` ⇒ this door must not take an injection-exposed call (FD5b; e.g. Groq). */
|
|
743
|
+
readonly injectionSafe?: boolean;
|
|
744
|
+
/** doc-tree / cartographer components may never route to any claude-code door (R6). */
|
|
745
|
+
readonly claudeBanned?: boolean;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export type NatureRoutingChains = Readonly<Record<RoutingChain, ReadonlyArray<ChainPosition>>>;
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* FD2 — the four v3 CLI-only chain defaults (config default). Authored using ONLY
|
|
752
|
+
* Echo's real doors (no `openai-api`). Metered positions are present but resolve
|
|
753
|
+
* unavailable until Increment B. An operator MAY override a chain wholesale
|
|
754
|
+
* (subject to the FD4 resolve-time validation — a tracked A2.2 remainder).
|
|
755
|
+
*/
|
|
756
|
+
export const NATURE_ROUTING_DEFAULT_CHAINS: NatureRoutingChains = {
|
|
757
|
+
// Latency-sensitive quick-sort.
|
|
758
|
+
FAST: [
|
|
759
|
+
{ door: 'gemini-api', model: 'flash-lite', keyRef: 'metered_gemini_bench', moneyGated: true },
|
|
760
|
+
{ door: 'pi-cli', model: 'gpt-5.5' },
|
|
761
|
+
],
|
|
762
|
+
// Background quick-sort.
|
|
763
|
+
SORT: [
|
|
764
|
+
{ door: 'codex-cli', model: 'gpt-5.4-mini' },
|
|
765
|
+
{ door: 'pi-cli', model: 'gpt-5.5' },
|
|
766
|
+
{ door: 'gemini-api', model: 'flash-lite', keyRef: 'metered_gemini_bench', moneyGated: true },
|
|
767
|
+
{ door: 'claude-code', model: 'balanced' }, // Sonnet-4.6 reserve
|
|
768
|
+
],
|
|
769
|
+
// Careful judgment.
|
|
770
|
+
JUDGE: [
|
|
771
|
+
{ door: 'pi-cli', model: 'gpt-5.5' },
|
|
772
|
+
{ door: 'codex-cli', model: 'gpt-5.5' },
|
|
773
|
+
{ door: 'openrouter-api', model: 'gpt-5.5', keyRef: 'metered_openrouter_bench', moneyGated: true },
|
|
774
|
+
{ door: 'openrouter-api', model: 'opus-4.8', keyRef: 'metered_openrouter_bench', moneyGated: true }, // clean API, NEVER CLI
|
|
775
|
+
{ door: 'claude-code', model: 'balanced' }, // Sonnet-4.6 reserve
|
|
776
|
+
],
|
|
777
|
+
// Open-ended writing (WRITE is the sole Opus-via-CLI-exempt lane — FD4).
|
|
778
|
+
WRITE: [
|
|
779
|
+
{ door: 'codex-cli', model: 'gpt-5.4-mini' },
|
|
780
|
+
{ door: 'groq-api', model: 'gpt-oss-120B', keyRef: 'metered_groq_bench', moneyGated: true, injectionSafe: false },
|
|
781
|
+
{ door: 'claude-code', model: 'fast' }, // Haiku-4.5
|
|
782
|
+
{ door: 'claude-code', model: 'capable' }, // Opus-4.8 quality lane — allowed on WRITE (FD4)
|
|
783
|
+
],
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* FD-LABEL / FD4.1 — the explicit per-door registry mapping a benchmark LABEL to a
|
|
788
|
+
* concrete model id. This is the boundary A1 deliberately deferred: A1 clamps to the
|
|
789
|
+
* `balanced` tier TOKEN; A2 pins the reserve to a CONCRETE id. The `claude-code`
|
|
790
|
+
* `balanced` reserve pins to the versioned manifest id `claude-sonnet-4-6` (FD4 place
|
|
791
|
+
* 1 — a tier label could resolve differently under a future CLI alias/remap; the
|
|
792
|
+
* pinned concrete id can't). A tier hint NOT present here (`fast`, `capable`) is left
|
|
793
|
+
* as-is and resolves downstream through the existing per-adapter tier map.
|
|
794
|
+
*/
|
|
795
|
+
export const ROUTING_LABEL_TO_MODEL_ID: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
796
|
+
'gemini-api': { 'flash-lite': 'gemini-3.1-flash-lite' },
|
|
797
|
+
'openrouter-api': { 'opus-4.8': 'anthropic/claude-opus-4-8', 'gpt-5.5': 'openai/gpt-5.5' },
|
|
798
|
+
'claude-code': { balanced: 'claude-sonnet-4-6' },
|
|
799
|
+
'codex-cli': { 'gpt-5.5': 'gpt-5.5', 'gpt-5.4-mini': 'gpt-5.4-mini' },
|
|
800
|
+
'pi-cli': { 'gpt-5.5': 'gpt-5.5' },
|
|
801
|
+
'groq-api': { 'gpt-oss-120B': 'openai/gpt-oss-120b' },
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The SINGLE sanctioned `claude-code` reserve model id for a bounded/gating
|
|
806
|
+
* (FAST/SORT/JUDGE) chain (FD4 place 1). The allowlist clamp permits ONLY this id on
|
|
807
|
+
* the claude-code door in those chains — deny-by-default — and clamps any other
|
|
808
|
+
* claude-code selection down to it. Pinned to the concrete manifest id (NOT the
|
|
809
|
+
* `balanced` tier label). Kept in sync with
|
|
810
|
+
* scripts/model-registry-freshness.manifest.json (role `balanced-anthropic`).
|
|
811
|
+
*/
|
|
812
|
+
export const CLAUDE_CODE_RESERVE_MODEL_ID = ROUTING_LABEL_TO_MODEL_ID['claude-code'].balanced;
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* FD6 — the critical-gate components (nature-B JUDGE safety gates + `MessageSentinel`,
|
|
816
|
+
* a nature-A / R2-critical gate). Load-bearing for the resolver's empty-set branch: a
|
|
817
|
+
* critical gate with no available door FAILS CLOSED (throw), never `no-route`. A gate
|
|
818
|
+
* must carry a real nature entry (never a `chainExempt` filler — FD4 Adv5).
|
|
819
|
+
*/
|
|
820
|
+
export const NATURE_ROUTING_CRITICAL_GATES: ReadonlySet<string> = new Set([
|
|
821
|
+
'MessagingToneGate',
|
|
822
|
+
'CompletionEvaluator',
|
|
823
|
+
'ExternalOperationGate',
|
|
824
|
+
'LLMSanitizer',
|
|
825
|
+
'CoherenceReviewer',
|
|
826
|
+
'UnjustifiedStopGate',
|
|
827
|
+
'SessionWatchdog',
|
|
828
|
+
'StallTriageNurse',
|
|
829
|
+
'ProjectDriftChecker',
|
|
830
|
+
'MessageSentinel', // nature A, R2-critical
|
|
831
|
+
]);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Shipped the dark/dryRun mechanism for Nature-Axis Routing (S4 Increment A2.1) — the internal LLM
|
|
9
|
+
router can now, in principle, choose which model and which access door a background check runs on
|
|
10
|
+
based on the KIND of task it is (a quick sort vs a careful judgment vs open-ended writing), instead
|
|
11
|
+
of only its category. This increment lands the pure resolver + its safety enforcement, wired dark:
|
|
12
|
+
DARK on the fleet, and LIVE-in-dryRun on a development agent (it computes and logs the route it WOULD
|
|
13
|
+
pick, then still does exactly today's thing — it never re-routes yet).
|
|
14
|
+
|
|
15
|
+
The resolver is a small, side-effect-free fold with four honest outcomes: a resolved route; fall
|
|
16
|
+
through to today's routing (for a component with no nature mapping); no route, use your own heuristic
|
|
17
|
+
(for a low-stakes sorter when every door is down); or a distinct fail-closed error (for a safety gate
|
|
18
|
+
when every door is down — a gate must fail shut, never open). It also finishes the concrete-id pinning
|
|
19
|
+
A1 deferred (FD4.1): the sanctioned Claude reserve now pins to the concrete model id claude-sonnet-4-6,
|
|
20
|
+
and a deny-by-default allowlist clamp keeps a bounded or judgment call on the Claude door pinned to that
|
|
21
|
+
one reserve id — the measured-banned Opus-via-the-Claude-CLI route can never open, even if someone
|
|
22
|
+
hand-edits a chain. Open-ended writing is exempt, since that is where Opus-via-CLI is legitimately best.
|
|
23
|
+
|
|
24
|
+
The load-bearing safety property is asserted by a named test: when the feature is unset or off, the
|
|
25
|
+
router is bit-for-bit identical to today. A1's always-on clamp is untouched — the new clamp is a
|
|
26
|
+
separate, nature-routing-scoped function.
|
|
27
|
+
|
|
28
|
+
Deferred and tracked (not dropped): the actual re-routing (enforcing mode), the injection map, the
|
|
29
|
+
build-time lint and live-config validator (FD4 places 1-2; place 3, the runtime clamp, ships here),
|
|
30
|
+
the durable audit log and dashboard read surface, the critical-gate drift notice, and the Fable-to-Opus
|
|
31
|
+
migration. The paid metered doors and the money/PIN go-live are Increment B — deferred and PIN-gated.
|
|
32
|
+
|
|
33
|
+
## What to Tell Your User
|
|
34
|
+
|
|
35
|
+
Nothing proactive — this is an internal, turned-off-by-default plumbing change with no user-facing
|
|
36
|
+
surface. If a user ever asks why a background check might one day run on a different model than the one
|
|
37
|
+
they talk to, the plain answer is: the agent measured which model is best at each kind of internal
|
|
38
|
+
check, and this lets it eventually pick the right one for each job — but it is currently just watching
|
|
39
|
+
and learning what it would choose, not changing anything, and turning it fully on is a separate,
|
|
40
|
+
deliberate step the operator takes. Nothing they do changes today, and no setting is required.
|
|
41
|
+
|
|
42
|
+
## Summary of New Capabilities
|
|
43
|
+
|
|
44
|
+
- A nature-aware route resolver picks a model and access door by task kind — shipped dark on the fleet,
|
|
45
|
+
live in dryRun-observe mode on a development agent.
|
|
46
|
+
- A deny-by-default allowlist keeps every bounded or judgment call on the Claude door pinned to the one
|
|
47
|
+
sanctioned Sonnet reserve id, so the measured-banned Opus-via-CLI route cannot open.
|
|
48
|
+
- The sanctioned reserve is now pinned to a concrete model id, not a tier nickname that could drift.
|
|
49
|
+
- A new config block, seeded dark on update, with the enable flag omitted so it rides the development-
|
|
50
|
+
agent gate. When off, routing is byte-identical to today.
|
|
51
|
+
|
|
52
|
+
## Evidence
|
|
53
|
+
|
|
54
|
+
- tests/unit/nature-routing-resolver.test.ts — 29 cases: the FD3 tighten rule (both sides of every
|
|
55
|
+
boundary), the four resolveRoute outcomes, the FD4.1 concrete-id pin, the FD4 allowlist clamp
|
|
56
|
+
(accepts the reserve id, rejects every other Claude id, WRITE exempt), the proof the new clamp is a
|
|
57
|
+
SEPARATE function from A1's, and the named byte-identical-when-off assertion plus dryRun-observes-but-
|
|
58
|
+
does-not-re-route.
|
|
59
|
+
- tests/unit/migrate-nature-routing-dark.test.ts — 4 cases: the dark seed omits enabled, never clobbers
|
|
60
|
+
a configured block, no-ops without a sessions block, and is idempotent.
|
|
61
|
+
- Regression: la4-degrade-path-clamp (13) + intelligence-router (17) + opus-claude-cli-gating-guardrail
|
|
62
|
+
(14) + llm-routing-nature-ratchet (6) stay green; tsc --noEmit clean.
|
|
63
|
+
- Spec: docs/specs/nature-axis-routing.md — FD3, FD4/FD4.1, FD9 (the A1/A2 increment split).
|
|
@@ -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,50 @@
|
|
|
1
|
+
# ELI16 — Nature-Axis Routing, Increment A2.1 (the dark/dryRun mechanism)
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
|
|
5
|
+
We taught the internal LLM router how to pick *which model, on which access door* a
|
|
6
|
+
background check should run on — based on the **kind** of thinking the check does — but
|
|
7
|
+
we shipped it **turned off** everywhere except as a silent "here's what I *would* pick"
|
|
8
|
+
observer on a development agent.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Different internal jobs need different brains. A quick "is this a STOP command?" sort is a
|
|
13
|
+
different task than a careful "should this message be blocked?" judgment. The bench (INSTAR
|
|
14
|
+
v3) measured which model+door combo is best for each kind — and found one combo that's
|
|
15
|
+
actively *dangerous* for careful judgments: Opus running through the Claude Code CLI (the
|
|
16
|
+
CLI wraps every prompt in ~20k tokens of "helpful assistant" framing, which makes a skeptical
|
|
17
|
+
judge go soft — it missed real STOP commands 27% of the time). So routing has to be able to
|
|
18
|
+
say "for a judgment call, NEVER land on that door."
|
|
19
|
+
|
|
20
|
+
## What this increment actually adds (and what it deliberately doesn't)
|
|
21
|
+
|
|
22
|
+
**Adds (all pure, all dark):**
|
|
23
|
+
- A **resolver** — a small, side-effect-free function: given a component, it works out the
|
|
24
|
+
task *nature*, walks an ordered list of `(door, model)` candidates, skips the ones that
|
|
25
|
+
aren't reachable, and returns the winner + the fallback tail. It has exactly four honest
|
|
26
|
+
outcomes: a route, "fall through to today's routing" (for a component we haven't mapped),
|
|
27
|
+
"no route — use your own backup" (for a low-stakes sorter when everything's down), or
|
|
28
|
+
**throw a distinct fail-closed error** (for a safety gate when everything's down — a gate
|
|
29
|
+
must fail shut, never open).
|
|
30
|
+
- **FD4.1 — the concrete pin.** A2 finishes the job A1 left: the sanctioned Claude reserve
|
|
31
|
+
now pins to a *concrete* model id (`claude-sonnet-4-6`), not a tier nickname that could
|
|
32
|
+
drift. And a **deny-by-default allowlist** clamp: on the Claude door, a judgment/sort call
|
|
33
|
+
may ONLY use that one reserve id — anything else gets clamped down to it. (Open-ended
|
|
34
|
+
*writing* is exempt — that's where Opus-via-CLI is legitimately the best tool.)
|
|
35
|
+
- The `sessions.natureRouting` **config knob** (seeded dark on update), the wiring, and the
|
|
36
|
+
full test suite.
|
|
37
|
+
|
|
38
|
+
**Doesn't (tracked, not dropped):** the actual *re-routing* (enforcing mode), the injection
|
|
39
|
+
map, the build-time lint, the live-config validator, the durable audit log + dashboard read
|
|
40
|
+
surface, the critical-gate drift notice, and the Fable→Opus migration — those are the ordered
|
|
41
|
+
A2.2 remainder. And **nothing** about the paid metered doors or the money/PIN go-live — that
|
|
42
|
+
is Increment B, deferred and PIN-gated.
|
|
43
|
+
|
|
44
|
+
## The safety promise
|
|
45
|
+
|
|
46
|
+
When the feature is **off** (which is everywhere on the fleet), the router behaves **bit-for-bit
|
|
47
|
+
like today** — same door, same model, same everything. That's the whole safety case, and there's
|
|
48
|
+
a named test that asserts it (`natureRouting UNSET ⇒ selection unchanged`). On a dev agent it runs
|
|
49
|
+
in **dryRun**: it computes and logs what it *would* pick, then still does exactly today's thing.
|
|
50
|
+
Flipping it to actually re-route is a later, operator-driven step.
|
|
@@ -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).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Side-Effects Review — Nature-Axis Routing, Increment A2.1 (dark/dryRun mechanism)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/nature-axis-routing.md (status: **converged — pending operator approval**;
|
|
4
|
+
`review-convergence` tag, NOT `approved:true` — the operator's step). This ships as a **Tier-1**
|
|
5
|
+
change: the smallest independently-landable **dark** unit of FD9's Increment A2. **Parent standards:**
|
|
6
|
+
"Structure > Willpower", "No Silent Degradation to Brittle Fallback", benchmark-cited routing
|
|
7
|
+
(INSTAR-Bench v3, rules R1/R2/R8), the Maturation Path (dev-gated dark), Migration Parity.
|
|
8
|
+
|
|
9
|
+
**Files:** src/data/llmBenchCoverage.ts, src/core/IntelligenceRouter.ts, src/core/types.ts,
|
|
10
|
+
src/commands/server.ts, src/core/PostUpdateMigrator.ts, tests/unit/nature-routing-resolver.test.ts,
|
|
11
|
+
tests/unit/migrate-nature-routing-dark.test.ts, upgrades/nature-routing-a2.eli16.md,
|
|
12
|
+
upgrades/side-effects/nature-routing-a2.md, upgrades/next/nature-routing-a2.md.
|
|
13
|
+
|
|
14
|
+
## What changed
|
|
15
|
+
|
|
16
|
+
1. **src/data/llmBenchCoverage.ts — the DATA half.** New pure, read-only constants: `RoutingDoor`
|
|
17
|
+
(CLI + metered-API door taxonomy, FD1), `CLI_ROUTING_DOORS` / `METERED_ROUTING_DOORS`,
|
|
18
|
+
`ChainPosition` + `NATURE_ROUTING_DEFAULT_CHAINS` (the four v3 CLI-only chains, FD2 — metered
|
|
19
|
+
positions are present but always skipped in Increment A), `ROUTING_LABEL_TO_MODEL_ID` (FD-LABEL/
|
|
20
|
+
FD4.1 — benchmark-label → concrete model id), `CLAUDE_CODE_RESERVE_MODEL_ID` (the single
|
|
21
|
+
sanctioned `claude-sonnet-4-6` reserve, FD4 place 1), and `NATURE_ROUTING_CRITICAL_GATES` (FD6).
|
|
22
|
+
Importing this changes NO behavior — it is data actuated only when the feature is enabled.
|
|
23
|
+
2. **src/core/IntelligenceRouter.ts — the resolver.** New pure exports: `resolveNatureAndChain`
|
|
24
|
+
(FD3 tighten rule `E,B ≥ D ≥ A`), `resolvePositionModelId` (FD4.1), `clampToReserveOnCleanDoor`
|
|
25
|
+
(FD4 place-3 allowlist clamp), `resolveRoute` (the stateless fold with the four outcomes),
|
|
26
|
+
`mergeNatureRoutingChains`, and the `RouterFailClosedError` typed error. Two new
|
|
27
|
+
`IntelligenceRouterOptions` fields (`resolveNatureRouting`, `onNatureRoutePlan`) and a new,
|
|
28
|
+
tightly-scoped block in `evaluate()` that OBSERVES when the feature is enabled.
|
|
29
|
+
3. **src/core/types.ts** — the `attribution.nature` opt-in tightening field (FD3) and the
|
|
30
|
+
`sessions.natureRouting` config schema (type-only, NOT in ConfigDefaults, per the
|
|
31
|
+
absent-equals-unchanged rule that `componentFrameworks`/`dynamicMcp` follow).
|
|
32
|
+
4. **src/commands/server.ts** — the construction wiring: `resolveNatureRouting` reads config LIVE
|
|
33
|
+
per call and resolves `enabled` through `resolveDevAgentGate` (live-in-dryRun on a dev agent,
|
|
34
|
+
dark on the fleet); `onNatureRoutePlan` is an env-gated observe-only breadcrumb.
|
|
35
|
+
5. **src/core/PostUpdateMigrator.ts** — `migrateConfigNatureRoutingDark` seeds `sessions.natureRouting`
|
|
36
|
+
dark on existing agents (`schemaVersion:3`, `dryRun:true`, `metered.goLive:false`; `enabled`
|
|
37
|
+
OMITTED for the dev-gate — the #1001 enable-path-integrity pattern), existence-checked + idempotent.
|
|
38
|
+
|
|
39
|
+
## Explicitly DEFERRED (tracked A2.2 remainder — NOT dropped)
|
|
40
|
+
|
|
41
|
+
Read this as intentional scope, not omission. This increment ships the pure mechanism + dark/dryRun
|
|
42
|
+
observation. The following are the ordered remainder, each a separate landable change:
|
|
43
|
+
|
|
44
|
+
- **Enforcing SELECTION** (dryRun:false actually re-routes) — A2.1 wires OBSERVATION only; flipping
|
|
45
|
+
`dryRun:false` logs a one-time "enforcing not yet wired" warning and stays byte-identical (an honest
|
|
46
|
+
no-op, never a silent dead switch and never a mis-route).
|
|
47
|
+
- **FD4 places 1 & 2** — the build-time lint (`scripts/lint-nature-chains.mjs`) and the resolve-time
|
|
48
|
+
live-config validator. **Place 3 (the runtime allowlist clamp) DOES ship here** and is the actual
|
|
49
|
+
runtime guarantee: it clamps every resolved claude-code FAST/SORT/JUDGE position to the sanctioned
|
|
50
|
+
reserve id at selection time — so even a hand-edited chain cannot open the banned Opus-via-CLI route.
|
|
51
|
+
Places 1-2 are defense-in-depth for the build + config-edit surfaces.
|
|
52
|
+
- **FD5b injection-exposure map** + R-rule lints (FD5c) — inert in A2.1 anyway: the only injection-
|
|
53
|
+
restricted door (`groq-api`) and the R8 Flash-Lite pin are METERED doors, always skipped in Increment
|
|
54
|
+
A; no CLI door in any chain is injection-restricted.
|
|
55
|
+
- **The durable audit** (`logs/nature-routing.jsonl`) + `GET /intelligence/routing` dryRun plan/diff/
|
|
56
|
+
`?trace` read surface, the **FD6 aggregated critical-gate drift notice** + baseline, the **FD8
|
|
57
|
+
Fable→Opus** migration, and the **CLAUDE.md** capability blurb.
|
|
58
|
+
- **Increment B** (metered-door live routing + FD12 money/PIN go-live) — DEFERRED + PIN-gated, an
|
|
59
|
+
operator step, never autonomous. No metered door routes and no spend ledger is touched here.
|
|
60
|
+
|
|
61
|
+
## Blast radius
|
|
62
|
+
|
|
63
|
+
- **Byte-identical when off — THE safety case.** When `sessions.natureRouting` is absent OR
|
|
64
|
+
`enabled:false` (the fleet default), the new block in `evaluate()` is skipped entirely and selection
|
|
65
|
+
is bit-for-bit today's. Asserted by name: `natureRouting UNSET ⇒ selection unchanged, onNatureRoutePlan
|
|
66
|
+
NEVER called` (the same options object is passed through untouched).
|
|
67
|
+
- **A1 is untouched.** `clampClaudeCliSwapModel` (A1's always-on degrade/swap clamp, returning the
|
|
68
|
+
`balanced` TIER token) is NOT modified — the new `clampToReserveOnCleanDoor` is a SEPARATE, nature-
|
|
69
|
+
routing-scoped, concrete-id clamp used only inside `resolveRoute`. Touching A1's fn would have changed
|
|
70
|
+
its shipped byte-identical behavior; a test asserts A1 still returns `{model:'balanced'}`.
|
|
71
|
+
- **dryRun cannot mis-route.** In dryRun the resolver only computes + logs; a resolver throw
|
|
72
|
+
(critical-gate fail-closed) is swallowed and recorded, never surfaced to the call path.
|
|
73
|
+
- **Hot path.** The observation block runs on every internal LLM call ONLY when the feature is enabled
|
|
74
|
+
(dev agents). It is a stateless fold over static maps + O(1) door-reachability reads (cached by the
|
|
75
|
+
provider cache); the breadcrumb log is env-gated so a dev agent's hot path stays quiet.
|
|
76
|
+
- **Config.** The seed is dark, existence-checked, idempotent, `enabled` omitted — it can never
|
|
77
|
+
force-dark a dev agent nor clobber an operator's config.
|
|
78
|
+
|
|
79
|
+
## Second-pass review (Phase 5 — required: touches a "gate" / routing-of-safety-gates)
|
|
80
|
+
|
|
81
|
+
The change routes safety GATES, so it triggers the high-risk second pass. Independent audit focus:
|
|
82
|
+
(a) can the banned Opus-via-CLI route open? — No: the place-3 allowlist clamp is deny-by-default and
|
|
83
|
+
fires on every resolved claude-code bounded/gating position, including a hand-edited chain. (b) can a
|
|
84
|
+
critical gate fail OPEN? — No: the empty-set branch throws a distinct `RouterFailClosedError` for a
|
|
85
|
+
critical gate (never `no-route`, never legacy routing); a unit test asserts both a nature-B gate
|
|
86
|
+
(`MessagingToneGate`) and the nature-A R2-critical gate (`MessageSentinel`) throw. (c) is "off" truly
|
|
87
|
+
inert? — Yes, the byte-identical-when-off test proves it. **Reviewer verdict: Concur with the review.**
|
|
88
|
+
The one residual is that enforcing SELECTION is deferred — but since it is not wired, it cannot
|
|
89
|
+
mis-route; the runtime clamp already makes the future enforcing path safe on its own.
|