instar 1.3.727 → 1.3.729
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 +93 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +11 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +11 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/IntelligenceRouter.d.ts +57 -0
- package/dist/core/IntelligenceRouter.d.ts.map +1 -1
- package/dist/core/IntelligenceRouter.js +135 -20
- package/dist/core/IntelligenceRouter.js.map +1 -1
- package/dist/core/LocalSessionOwnershipStore.d.ts +5 -0
- package/dist/core/LocalSessionOwnershipStore.d.ts.map +1 -1
- package/dist/core/LocalSessionOwnershipStore.js +13 -0
- package/dist/core/LocalSessionOwnershipStore.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +17 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionBuildContextStore.d.ts +20 -0
- package/dist/core/SessionBuildContextStore.d.ts.map +1 -1
- package/dist/core/SessionBuildContextStore.js +20 -3
- package/dist/core/SessionBuildContextStore.js.map +1 -1
- package/dist/core/SessionManager.d.ts +5 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +3 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionOwnershipRegistry.d.ts +16 -0
- package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -1
- package/dist/core/SessionOwnershipRegistry.js +13 -0
- package/dist/core/SessionOwnershipRegistry.js.map +1 -1
- package/dist/core/StateManager.d.ts +20 -0
- package/dist/core/StateManager.d.ts.map +1 -1
- package/dist/core/StateManager.js +72 -4
- package/dist/core/StateManager.js.map +1 -1
- package/dist/core/WriteAdmission.d.ts +277 -0
- package/dist/core/WriteAdmission.d.ts.map +1 -0
- package/dist/core/WriteAdmission.js +514 -0
- package/dist/core/WriteAdmission.js.map +1 -0
- package/dist/core/WriteDomainRegistry.d.ts +132 -0
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -0
- package/dist/core/WriteDomainRegistry.js +186 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +65 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +18 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +6 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +79 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/{1.3.727.md → 1.3.728.md} +73 -1
- package/upgrades/1.3.729.md +92 -0
- package/upgrades/side-effects/per-target-swap-timeout.md +120 -0
- package/upgrades/side-effects/standby-write-reconciliation.md +62 -0
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
# Upgrade Guide — vNEXT
|
|
2
2
|
|
|
3
3
|
<!-- assembled-by: assemble-next-md -->
|
|
4
|
-
<!-- bump:
|
|
4
|
+
<!-- bump: minor -->
|
|
5
5
|
|
|
6
6
|
## What Changed
|
|
7
7
|
|
|
8
|
+
The IntelligenceRouter failure-swap loop bounded EVERY swap attempt with one
|
|
9
|
+
global cap (`intelligence.swapAttemptTimeoutMs`, default 5000ms). Measured
|
|
10
|
+
provider latency (llm-pathway-bench, N=30, uncontended) shows gemini-flash's
|
|
11
|
+
p50 is 8,538ms — ABOVE the 5s cap — so whenever gemini was a swap target it was
|
|
12
|
+
SIGTERMed before it could answer on the majority of attempts: it reliably
|
|
13
|
+
burned a full 5s swap slot and then failed, poisoning the failover tail
|
|
14
|
+
(the R4 gemini-swap-timeout finding).
|
|
15
|
+
|
|
16
|
+
Per `docs/specs/per-target-swap-timeout-spec.md` (review-converged 2026-07-01,
|
|
17
|
+
approved), the cap now resolves PER TARGET FRAMEWORK with three new optional
|
|
18
|
+
`intelligence` config fields, all defaulting UNSET (dark ship — routing/timeout
|
|
19
|
+
behavior is byte-identical to today until an operator opts in):
|
|
20
|
+
|
|
21
|
+
- `swapAttemptTimeoutMsByFramework` — per-target caps. Resolution per swap
|
|
22
|
+
target: `byFramework[target]` (valid = finite number > 0) → the global
|
|
23
|
+
`swapAttemptTimeoutMs` → no cap. An INVALID value (0, negative, NaN,
|
|
24
|
+
non-number) falls through to the global — never "no cap", never a 0ms kill
|
|
25
|
+
(FD5: per-framework config cannot express "unbounded"; only the global's
|
|
26
|
+
≤0/unset does). Recommended opt-in package:
|
|
27
|
+
`{ "claude-code": 8000, "pi-cli": 9000, "gemini-cli": 18000, "codex-cli": 45000 }`.
|
|
28
|
+
- `swapAttemptTimeoutMsMax` — clamp on any single resolved cap (invalid/unset
|
|
29
|
+
⇒ 120s) so a typo'd huge value cannot pin a host spawn-cap slot unbounded.
|
|
30
|
+
- `swapTotalBudgetMs` — wall-clock TOTAL budget over the whole swap tail
|
|
31
|
+
(recommended 40000, set together with the per-target caps). When set, each
|
|
32
|
+
attempt's effective cap is `min(resolvedCap, budgetRemaining)` on a MONOTONIC
|
|
33
|
+
clock (never Date.now()), and the loop stops and falls closed once ≤250ms
|
|
34
|
+
remains — so the worst-case swap tail is literally ≤ the budget. Unset ⇒ no
|
|
35
|
+
enforcement (today's semantics).
|
|
36
|
+
|
|
37
|
+
Also: the swap timeout timer is now cleared on settle (`withSwapTimeout`
|
|
38
|
+
helper) so a fast success no longer leaks a pending timer per call; a stray/
|
|
39
|
+
misspelled `byFramework` key warns once and is ignored (falls through to the
|
|
40
|
+
global). The `swap-attempt-timeout:` degrade-reason format is unchanged.
|
|
41
|
+
`failureSwap` ORDER remains latency-load-bearing (sequential loop) — order
|
|
42
|
+
fastest-first; keep caps ≤ the circuit breaker's failure sensitivity.
|
|
43
|
+
|
|
44
|
+
Rollback: remove the config block → instant revert to the global cap. No
|
|
45
|
+
migrateConfig entry (absence is the default state, codexExecJson precedent).
|
|
46
|
+
|
|
8
47
|
`AgentWorktreeReaper.start()` scheduled ONLY a 24h `setInterval` — no initial
|
|
9
48
|
pass. Agent servers restart far more often than daily (auto-updates, sleep/wake
|
|
10
49
|
supervisor bounces), so the interval timer reset on every restart and an
|
|
@@ -72,6 +111,16 @@ again. Dark by default — nothing activates without an explicit `testCast` bloc
|
|
|
72
111
|
|
|
73
112
|
## What to Tell Your User
|
|
74
113
|
|
|
114
|
+
<!-- audience: agent-only, maturity: preview -->
|
|
115
|
+
- **Backup AI routes now get fair deadlines (off until you opt in)**: when my
|
|
116
|
+
main AI route fails mid-decision, I try backups — but every backup shared one
|
|
117
|
+
five-second deadline, and one of my backups (gemini) usually needs about nine
|
|
118
|
+
seconds, so it was being cut off before it could ever answer. I can now give
|
|
119
|
+
each backup its own deadline matched to how fast it really is, plus an
|
|
120
|
+
overall time limit so a chain of backups can never keep you waiting too long.
|
|
121
|
+
Nothing changes until we turn it on — if you'd like, I can enable the
|
|
122
|
+
recommended settings for you and undo it just as easily.
|
|
123
|
+
|
|
75
124
|
<!-- audience: user, maturity: stable -->
|
|
76
125
|
|
|
77
126
|
If you switched on the stale-worktree auto-cleaner, it now genuinely runs: one
|
|
@@ -91,6 +140,12 @@ dry-run first, and it stays entirely off unless you enabled it.
|
|
|
91
140
|
|
|
92
141
|
## Summary of New Capabilities
|
|
93
142
|
|
|
143
|
+
| Capability | How to Use |
|
|
144
|
+
|-----------|-----------|
|
|
145
|
+
| Per-target swap-attempt caps | `intelligence.swapAttemptTimeoutMsByFramework` in `.instar/config.json` (unset = today's global cap) |
|
|
146
|
+
| Per-attempt clamp | `intelligence.swapAttemptTimeoutMsMax` (invalid/unset ⇒ 120s) |
|
|
147
|
+
| Total swap-tail budget | `intelligence.swapTotalBudgetMs` (unset = no budget enforcement) |
|
|
148
|
+
|
|
94
149
|
- The stale-worktree reaper actually fires on real deployments: a one-time
|
|
95
150
|
cleaning pass ~15 minutes after boot (tunable/disableable via
|
|
96
151
|
`monitoring.agentWorktreeReaper.initialPassDelayMs`), unchanged daily cadence
|
|
@@ -102,6 +157,23 @@ dry-run first, and it stays entirely off unless you enabled it.
|
|
|
102
157
|
|
|
103
158
|
## Evidence
|
|
104
159
|
|
|
160
|
+
- Live measurement (the bug): llm-pathway-bench R4 characterization, N=30
|
|
161
|
+
uncontended — gemini-flash p50 = 8,538ms, p95 = 15,726ms vs the 5,000ms flat
|
|
162
|
+
cap: every median-speed gemini swap attempt was killed at the cap before it
|
|
163
|
+
could answer (observed as `swap-attempt-timeout: gemini-cli` degrade reasons;
|
|
164
|
+
the attempt burned the full 5s slot and produced nothing). Other measured
|
|
165
|
+
targets for scale: claude ~3s p50 / ~6s p95, pi ~4.6s / ~7s, codex ~18s / ~43s
|
|
166
|
+
— no single cap fits all four.
|
|
167
|
+
- After (mechanism, exercised end-to-end in-process): with the recommended 18s
|
|
168
|
+
gemini cap, an 8.5s-latency gemini target is SERVED on swap
|
|
169
|
+
(`tests/integration/per-target-swap-timeout.test.ts`, wired with the server's
|
|
170
|
+
exact threading expressions); with no per-framework config the same target is
|
|
171
|
+
abandoned at exactly 5s and the call fails closed — byte-identical to before
|
|
172
|
+
(regression tests pin the cap value the provider subprocess receives).
|
|
173
|
+
- The field fix requires the operator opt-in (dark ship); the delivery
|
|
174
|
+
follow-through is tracked as commitment CMT-1889 (surface the recommended
|
|
175
|
+
values as a one-tap go/no-go).
|
|
176
|
+
|
|
105
177
|
- `tests/unit/agent-worktree-reaper.test.ts`: 47/47 green — 7 new tests (initial
|
|
106
178
|
pass fires exactly once at the configured delay and before the interval;
|
|
107
179
|
respects dry-run; disabled reaper sets no timers; `<= 0` restores
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The multi-machine mesh ran two contradictory write models at once: the
|
|
9
|
+
one-awake model flips the WHOLE StateManager read-only on every non-lease-holder
|
|
10
|
+
(one process-wide boolean), while the active-active session pool deliberately
|
|
11
|
+
places owned, serving sessions on those same "standby" machines. F9's sharpest
|
|
12
|
+
line: a machine can be read-only ("standby") while actively OWNING pool topics —
|
|
13
|
+
so its writes about sessions it owns and serves were blocked
|
|
14
|
+
(`[SessionManager] Failed to record build context … StateManager is read-only`),
|
|
15
|
+
and every added machine became a standby-shaped writer-that-can't-write.
|
|
16
|
+
Separately, the P2-6 family showed mutating routes (`POST /evolution/actions`,
|
|
17
|
+
`POST /attention`) hanging open-endedly instead of refusing.
|
|
18
|
+
|
|
19
|
+
Per `docs/specs/standby-write-reconciliation.md` (review-converged round 3,
|
|
20
|
+
approved), writes are now classified into a write **domain**
|
|
21
|
+
(machine-local / session-scoped / topic-scoped / cluster-shared) by a single
|
|
22
|
+
source of truth (`WriteDomainRegistry`) and admitted by ONE synchronous
|
|
23
|
+
in-memory decision point (`WriteAdmission` — no fs/network/LLM on the admission
|
|
24
|
+
path; p99 < 1ms pinned by test):
|
|
25
|
+
|
|
26
|
+
- **machine-local** writes admit EVERYWHERE — even on a read-only standby (the
|
|
27
|
+
F9 fix: the per-machine build-context write is no longer blocked).
|
|
28
|
+
- **session-/topic-scoped** writes admit iff THIS machine owns the record per
|
|
29
|
+
the session-pool FSM's single-owner guarantee (boot-warmed `OwnershipIndex`
|
|
30
|
+
+ onCommit hook on BOTH ownership substrates); a not-owner refusal NAMES the
|
|
31
|
+
owner.
|
|
32
|
+
- **cluster-shared** keeps byte-identical authority to today: holder admits,
|
|
33
|
+
standby refuses.
|
|
34
|
+
- Every inadmissible write gets a **typed, machine-readable 409 refusal**
|
|
35
|
+
(`error/code/domain/scope/owner/leaseHolder/asOf/retryable` + `Retry-After`)
|
|
36
|
+
in well under 2s — never a hang; refuse-before-touch (a refused write mutates
|
|
37
|
+
nothing). The legacy read-only message string is preserved for log scraping.
|
|
38
|
+
- `SessionBuildContextStore` re-keyed per machine (§3.3) with a one-time
|
|
39
|
+
lease-holder legacy-key cleanup; `FileClassifier` sync exclusions for
|
|
40
|
+
attention-items + evolution (the second convergence axis).
|
|
41
|
+
- Observability: `GET /write-admission` (mode, per-domain counters, recent
|
|
42
|
+
refusals, ownership-index stats) + an event-loop-lag gauge on the AUTHED
|
|
43
|
+
`/health` extension only. Refusal storms raise exactly ONE deduped attention
|
|
44
|
+
item (burst invariant pinned by test).
|
|
45
|
+
- Ships dev-gated dark (`multiMachine.writeAdmission` — enabled OMITTED →
|
|
46
|
+
resolveDevAgentGate) AND dryRun-first with a §9.14 double latch: config
|
|
47
|
+
`dryRun:false` alone grants NO refusal authority until the write-surface
|
|
48
|
+
inventory constant is flipped in code. On the fleet nothing changes; the
|
|
49
|
+
legacy blanket guard keeps enforcing until graduation. Rollback: set
|
|
50
|
+
`multiMachine.writeAdmission.enabled` to false (or leave the gate dark).
|
|
51
|
+
|
|
52
|
+
## What to Tell Your User
|
|
53
|
+
|
|
54
|
+
<!-- audience: agent-only, maturity: experimental -->
|
|
55
|
+
- **My machines no longer block their own homework (experimental, dark)**: when
|
|
56
|
+
I run on more than one machine, a machine that isn't "in charge" used to be
|
|
57
|
+
forbidden from writing ANY of its own notes — even notes about conversations
|
|
58
|
+
it was actively serving. Now each write is checked against who actually owns
|
|
59
|
+
that piece of work: notes about this machine's own work always go through,
|
|
60
|
+
notes about work another machine owns are politely declined with a clear,
|
|
61
|
+
fast "machine X owns this, retry shortly" answer instead of hanging forever.
|
|
62
|
+
Nothing changes on your setup yet — this ships dark and observation-first,
|
|
63
|
+
and I can turn it on deliberately when it has soaked.
|
|
64
|
+
|
|
65
|
+
## Summary of New Capabilities
|
|
66
|
+
|
|
67
|
+
| Capability | How to Use |
|
|
68
|
+
|-----------|-----------|
|
|
69
|
+
| Ownership-scoped write admission (dev-gated dark, dryRun-first) | `multiMachine.writeAdmission` in `.instar/config.json` (enabled omitted → live on dev agents, dark on fleet) |
|
|
70
|
+
| Typed 409 write refusal with Retry-After | Automatic on inadmissible writes when live; body carries code/domain/owner/leaseHolder |
|
|
71
|
+
| Write-admission observability | `GET /write-admission` (Bearer; 503 when dark) |
|
|
72
|
+
| Event-loop-lag gauge | Authed `GET /health` extension only |
|
|
73
|
+
|
|
74
|
+
## Evidence
|
|
75
|
+
|
|
76
|
+
- Root cause grounded live (2026-07-02, Laptop stderr): `[SessionManager]
|
|
77
|
+
Failed to record build context … StateManager is read-only (this machine is
|
|
78
|
+
on standby). Blocked: set` — a write about a session this machine OWNS,
|
|
79
|
+
blocked by the blanket lease boolean (F9).
|
|
80
|
+
- Spec converged round 3 (0 MUST-FIX / 0 SHOULD-FIX; external gemini-2.5-pro
|
|
81
|
+
CONVERGE), approved under standing Session-A preapproval (topic 29836).
|
|
82
|
+
- All three test tiers shipped and green: unit (110 tests — §3.2 verdict table
|
|
83
|
+
both sides, typed-refusal contract, ownership-index parity on BOTH
|
|
84
|
+
substrates, registry I9 story validation, StateManager one-way attach +
|
|
85
|
+
fail-toward-today, conformance ratchet + write-surface inventory baseline),
|
|
86
|
+
integration (13 tests — P2-6 family 201s on a standby-that-owns, typed 409 +
|
|
87
|
+
Retry-After through the real HTTP pipeline in <2s, refuse-before-touch store
|
|
88
|
+
snapshot, dark = 503), e2e lifecycle (8 tests — production init path,
|
|
89
|
+
live-on-dev 200 / fleet-dark 503, §9.14 double latch at the production
|
|
90
|
+
constant, F9 fix alive end-to-end, burst invariant ≤1 attention item).
|
|
91
|
+
- I2 hard properties pinned by test: ZERO fs on the admission path (including
|
|
92
|
+
negative lookups) and p99 < 1ms over 10k evaluate calls.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Side-Effects Review — Per-Target Failure-Swap Timeout (fixes gemini-swap-timeout)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `per-target-swap-timeout`
|
|
4
|
+
**Date:** `2026-07-02`
|
|
5
|
+
**Author:** `echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `required — the change modifies timeout resolution on the failure-swap path that serves GATING calls (the tone gate rides it)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The IntelligenceRouter failure-swap loop used ONE global per-attempt timeout (`intelligence.swapAttemptTimeoutMs`, default 5s) for every swap target. Measured provider latency (llm-pathway-bench, N=30) shows gemini's p50 is ~8.5s — the 5s cap sits BELOW gemini's median, so gemini as a swap target was reliably killed before it could answer, wasting a full swap slot ("poisoning the failover tail"). This change makes the cap resolvable PER TARGET FRAMEWORK (`intelligence.swapAttemptTimeoutMsByFramework`), adds a per-attempt clamp (`swapAttemptTimeoutMsMax`, invalid/unset ⇒ 120s), and adds an optional wall-clock TOTAL swap budget (`swapTotalBudgetMs`) that clamps each in-flight attempt to the remaining budget on a MONOTONIC clock, bounding the whole fail-closed tail to literally ≤ budget. All three fields default UNSET — routing/timeout behavior is byte-identical to today until an operator opts in (dark ship). Files touched: `src/core/IntelligenceRouter.ts` (resolveSwapCap, withSwapTimeout, per-target loop resolution, unknown-key hygiene), `src/core/types.ts` (three optional config fields), `src/commands/server.ts` (threads the three fields), `tests/unit/per-target-swap-timeout.test.ts`, `tests/integration/per-target-swap-timeout.test.ts`, spec + ELI16 + convergence report under `docs/specs/`. Spec: `docs/specs/per-target-swap-timeout-spec.md` (review-converged 2026-07-01, approved).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `IntelligenceRouter failure-swap loop` (`src/core/IntelligenceRouter.ts` `evaluate()`) — **modify** — a routing/degrade path (which fallback provider gets how long), NOT a block/allow gate. No new gate is introduced or removed; the swap still fires only for `attribution.gating`/`deferrable` calls with configured `failureSwap` targets (scope unchanged). What changed is only HOW LONG each target is given (per-target vs one flat cap) and a new optional total-budget stop that falls CLOSED (the safe direction for gating calls).
|
|
15
|
+
- `Gating ladder deadline` (`gatingLadderBudgetMs`) — **pass-through** — still checked first at each loop iteration, unchanged.
|
|
16
|
+
|
|
17
|
+
**Phase 1 principle-check answer (recorded):** the change touches a decision point only in the routing/degrade sense — it does not gate information flow by content, block actions by meaning, or filter messages. Timeout resolution is deterministic mechanics (a wall-clock bound on a subprocess attempt), in the signal-vs-authority carve-out class of structural/transport mechanics, not a judgment decision. No brittle check gains blocking authority.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
24
|
+
|
|
25
|
+
At the default (all three fields unset): none — resolution degenerates to exactly today's single global cap; the regression tests assert the 5s behavior byte-for-byte (same cap value passed through as `timeoutMs`, same fail-closed rethrow).
|
|
26
|
+
|
|
27
|
+
When an operator opts in: two deliberate "rejections" exist and both are the spec's explicit fail-SAFE choices, not accidents. (a) A `swapTotalBudgetMs` set below the 250ms floor disables swapping on the first attempt — an absurd misconfig fails CLOSED (no swap) rather than open (unbounded swap). (b) When the remaining budget is ≤ 250ms, a viable fast target later in the tail is NOT admitted — the loop falls closed. Both are unit-tested. A per-framework value that is invalid (0, negative, NaN, non-number) can never over-block: it falls through to the global, never to "no cap" and never to a 0ms instant kill (FD5, tested for every invalid class).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Under-block
|
|
32
|
+
|
|
33
|
+
**What failure modes does this still miss?**
|
|
34
|
+
|
|
35
|
+
- **The dark default does not fix the field bug.** Gemini keeps dying at 5s until an operator sets the recommended values — this is the spec's own "ships-inert" finding. Mitigation is a DELIVERY obligation (FD8): surface the recommended values as a one-tap go/no-go and track the opt-in as a commitment (registered: CMT — see Conclusion). Tracked, not silently dropped.
|
|
36
|
+
- **A provider that ignores `timeoutMs` can overrun its cap as an orphaned subprocess.** The `Promise.race` timer still resolves the swap decision at the cap (the loop advances on time), but the subprocess kill is the provider's own responsibility — unchanged by this change and already true of the existing global cap (spec: provider timeout contract).
|
|
37
|
+
- **Operator misconfig within valid ranges:** a per-target cap set above the circuit breaker's failure sensitivity could keep a chronically-slow target alive without tripping the breaker; a slow target ordered FIRST in `failureSwap` delays faster ones up to its cap (order is latency-load-bearing). Both are documented operator constraints in the spec and the types.ts JSDoc — static caps remain blind to live latency drift by design (auto-tuning is explicitly out of scope, FD3 <!-- tracked: CMT-1889 -->).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
**Is this at the right layer?**
|
|
44
|
+
|
|
45
|
+
Yes. The failure-swap loop in `IntelligenceRouter` is the ONLY place that owns swap-attempt timing, and the change modifies exactly that resolution point (cap resolution moved inside the per-target loop). It does not re-implement any primitive: it reuses the existing `Promise.race` pattern (now wrapped in `withSwapTimeout`, which only adds timer-clear-on-settle), the existing `timeoutMs` provider pass-through contract, and the existing `onDegrade` observability channel (`swap-attempt-timeout:` reason format preserved byte-for-byte so DegradationReporter/metrics consumers are unaffected). The config lives in the existing `intelligence` block alongside the global cap it extends. No higher layer (LlmQueue, circuit breaker, gating ladder) owns per-attempt timing; no lower layer duplicates it.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
52
|
+
|
|
53
|
+
- [x] No — this change has no block/allow surface.
|
|
54
|
+
|
|
55
|
+
The change has no judgment-based block/allow surface: it is timeout mechanics on a routing/degrade path (the signal-vs-authority carve-out class: structural bounds, not content judgment). The one nuance — acknowledged in the spec's foundation note — is that a static cap holds kill-authority over whether a provider ATTEMPT survives, blind to real latency; that mis-fit is the root shape of the original incident. This change REDUCES the mis-fit (caps sized per target to measured p50/p95) without adding any new authority, and the residual (static caps can drift from live latency) is explicitly flagged as a recurrence vector for the auto-tuning follow-up (FD3, out of scope <!-- tracked: CMT-1889 -->). Timed-out attempts still count toward the per-framework circuit breaker (unchanged), so the breaker — the existing authority over provider health — keeps its signal supply.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 5. Interactions
|
|
60
|
+
|
|
61
|
+
- **Shadowing:** the gating-ladder deadline (`gatingLadderBudgetMs`) is checked FIRST at each loop iteration, before the new budget check — an active ladder still wins (unchanged precedence). The new total budget only ADDS a stop; it cannot extend the ladder's deadline. Verified in code order.
|
|
62
|
+
- **Double-fire:** the per-attempt timer and the provider's own `timeoutMs` SIGTERM fire at the SAME bound (the cap is passed through as `timeoutMs`, exactly as before) — by design, not a race: the race timer decides the swap, the subprocess bound kills the process. The budget-clamped cap keeps them identical (the clamped value is what flows through).
|
|
63
|
+
- **Races:** `withSwapTimeout` preserves the shipped crash-safe `Promise.race` semantics (per-input settlement handlers; late reject swallowed, late resolve ignored — both re-covered by the existing N1 tests, which still pass unmodified). The only addition is `clearTimeout` in a `finally`, which cannot race the rejection (a fired timer's clear is a no-op).
|
|
64
|
+
- **Feedback loops:** timed-out swap attempts still count toward the target's circuit breaker (unchanged), so a chronically-timing-out target still trips its breaker and gets skipped fast on later calls — no new loop. The spec documents the operator constraint that caps SHOULD stay ≤ the breaker's failure sensitivity.
|
|
65
|
+
- **Config layering:** the three new fields ride `IntelligenceRouterOptions`, NOT `ComponentFrameworksConfig` — so the §4.6 computed-default/live-override layering in `resolveConfig` is untouched (verified: the layered-resolveConfig tests pass unmodified).
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 6. External surfaces
|
|
70
|
+
|
|
71
|
+
- **Other agents / install base:** none until opt-in — all three fields default unset and there is no `migrateConfig` entry (absence is the default state, codexExecJson precedent), so no deployed agent's config changes on update.
|
|
72
|
+
- **External systems:** none. No new network calls, no new subprocess kinds — only how long an existing swap subprocess is given.
|
|
73
|
+
- **Persistent state:** none. Per-call resolution from live opts; no ledger, no file, no store.
|
|
74
|
+
- **Timing/runtime conditions:** the budget uses a MONOTONIC clock (`performance.now()`), never `Date.now()`, so an NTP step/DST jump cannot warp it (round-3 external finding; the gating-ladder's existing `Date.now()` deadline is unchanged — out of this change's scope). Observability: a cap firing emits the same `swap-attempt-timeout:` degrade reason as today (format preserved), visible in DegradationReporter and `/metrics/features`.
|
|
75
|
+
- **Operator surface (Mobile-Complete Operator Actions):** no operator-facing action is added — the knobs are config fields. The FD8 delivery obligation (one-tap go/no-go for the recommended values) is where the operator surface will live; it is a tracked commitment (see Conclusion), deliberately outside this spec's code change per the approved spec.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
80
|
+
|
|
81
|
+
No operator surface — not applicable (no dashboard/approval/grant/revoke/secret-drop file is staged; the change is config + router internals + tests).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
86
|
+
|
|
87
|
+
**Machine-local BY DESIGN** (spec FD4). The swap cap is a per-call routing parameter read from each machine's local config at call time. It holds no durable cross-machine state (nothing to strand on topic transfer), emits no user-facing notice (no one-voice gating needed), and generates no URL (nothing to survive machine boundaries). Each machine's providers have that machine's latency characteristics, so per-machine values are the correct scope; an operator who wants uniform values sets them per machine's config. No replication path needed.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 8. Rollback cost
|
|
92
|
+
|
|
93
|
+
Pure code change with a dark default — two independent rollback levers, both cheap:
|
|
94
|
+
|
|
95
|
+
- **Operator-level (instant, no release):** remove the `swapAttemptTimeoutMsByFramework` / `swapAttemptTimeoutMsMax` / `swapTotalBudgetMs` block from `.instar/config.json` → instant revert to the global-cap behavior (spec: "Rollback: remove the config block → instant revert").
|
|
96
|
+
- **Code-level:** revert the commit, ship as next patch. No persistent state, no data migration, no agent state repair, no user-visible regression during the rollback window (the default path is byte-identical for routing/timeout semantics; the only internal difference is the timer-clear-on-settle hygiene, which is behavior-neutral for routing).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Conclusion
|
|
101
|
+
|
|
102
|
+
The review confirms the change is a localized, additive, dark-shipped modification of the failure-swap timeout resolution with no new block/allow authority, no persistent state, no external surface until opt-in, and a two-lever rollback. The design decisions that the review leaned on hardest — FD5 (invalid per-framework values fall through to the global, closing the accidental-uncap footgun), FD6 (the total budget clamps each IN-FLIGHT attempt so the tail is literally ≤ budget, and falls CLOSED at the floor), FD7 (maxCap clamp + validated knobs + timer hygiene) — are all unit-tested on both sides of every boundary (valid vs each invalid class). Two items are flagged, both already tracked: (1) the FD8 delivery obligation (the dark ship fixes nothing until the operator opts in — the recommended-values go/no-go + commitment is registered as CMT-1889 and is delivery work outside this spec's code change per the approved spec); (2) static caps remain blind to latency drift — the auto-tuning follow-up <!-- tracked: CMT-1889 --> (FD3) rides the same tracked commitment's context. Clear to ship pending second-pass concurrence (required: the change sits on the path that serves gating calls).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Second-pass review (if required)
|
|
107
|
+
|
|
108
|
+
**Reviewer:** claude (independent second-pass subagent)
|
|
109
|
+
|
|
110
|
+
**Independent read of the artifact: concur** — Verified against the working-tree diff (`src/core/IntelligenceRouter.ts`, `src/core/types.ts`, `src/commands/server.ts`) and all 56 tests (25 + 3 new; the 11 + 17 pre-existing pass unmodified): the code matches the spec's resolveCap contract exactly (isFinite && >0 validation, byFramework→global→undefined fall-through, maxCap clamp with invalid→120s default, in-flight `min(cap, remaining)` budget clamp on `performance.now()`, 250ms-floor fall-closed, budget-unset ⇒ no enforcement), no brittle check gains blocking authority (the unknown-key check is warn-once signal-only; the budget stop is a deterministic resource bound in the signal-vs-authority carve-out class), and the ladder-deadline-before-budget precedence, breaker counting, Promise.race crash-safety, finally-clause timer clear, and `swap-attempt-timeout:` prefix format are all true in the code as claimed — with one non-blocking precision note: the "byte-identical at default for ANY global cap" claim is overbroad for the exotic-but-JSON-reachable case of a pre-existing `swapAttemptTimeoutMs > 120000`, which the (spec-mandated, FD7-intended) default maxCap now clamps to 120s even with all three new fields unset — a fail-safe-direction change worth knowing in forensics, not a ship blocker.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Evidence pointers
|
|
115
|
+
|
|
116
|
+
- Unit: `tests/unit/per-target-swap-timeout.test.ts` (25 tests — resolveSwapCap contract incl. every invalid class, the gemini 8.5s fix + 5s regression, FD6 budget in-flight clamp/floor/unset/invalid/uncapped-bound, FD7 timer clear, wiring incl. server threading scan, unknown-key warn-once).
|
|
117
|
+
- Integration: `tests/integration/per-target-swap-timeout.test.ts` (3 tests — server-exact wiring: the FD8 recommended-values package serves gemini at 8.5s under an 18s cap; no-config and absent-intelligence-block regressions pin the 5s global byte-for-byte).
|
|
118
|
+
- Pre-existing behavior locked: `tests/unit/provider-fallback-swap-timeout.test.ts` (11 tests) and `tests/unit/intelligence-router.test.ts` (17 tests) pass UNMODIFIED against the new code.
|
|
119
|
+
- Tier-3 carve-out: no HTTP route is added (internal routing-timeout knob), so per the Testing-Integrity Standard's no-route carve-out there is no "feature-alive/200" e2e; Tiers 1 + 2 apply (stated in the spec's Testing section).
|
|
120
|
+
- Measured basis: llm-pathway-bench N=30 (gemini p50 8,538ms / p95 15,726ms; claude ~3s/~6s; pi ~4.6s/~7s; codex ~18s/~43s).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Side-Effects Review — Standby-Write Reconciliation + Typed Refusal (P2-6 / F9)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/standby-write-reconciliation.md (converged round-3 + approved — operator standing preapproval, topic 29836). **Parent:** A Refusal Stays a Refusal — a write the server cannot complete returns an immediate typed refusal or a bounded error, never an open-ended hang.
|
|
4
|
+
**Ships dev-gated DARK** (`multiMachine.writeAdmission.enabled` OMITTED from ConfigDefaults → resolveDevAgentGate: live on a development agent, dark on the fleet) **and dry-run FIRST** (`dryRun: true` even on dev — FD-7 telemetry pattern). Refusal authority is DOUBLE-latched: `dryRun:false` AND the compiled `WRITE_SURFACE_INVENTORY_COMPLETE` constant (§9.14), which ships `false` and is flipped only by the wave-2 PR that lands the reviewed write-surface inventory — config alone can never grant authority.
|
|
5
|
+
**Files:** src/core/WriteAdmission.ts (new), src/core/WriteDomainRegistry.ts (new), src/core/StateManager.ts, src/core/SessionBuildContextStore.ts, src/core/SessionManager.ts, src/core/LocalSessionOwnershipStore.ts, src/core/SessionOwnershipRegistry.ts, src/core/FileClassifier.ts, src/core/devGatedFeatures.ts, src/core/types.ts, src/core/PostUpdateMigrator.ts, src/config/ConfigDefaults.ts, src/monitoring/guardManifest.ts, src/scaffold/templates.ts, src/server/AgentServer.ts, src/server/routes.ts, src/commands/server.ts
|
|
6
|
+
|
|
7
|
+
## What changed
|
|
8
|
+
|
|
9
|
+
1. **WriteAdmission.ts (new):** the single synchronous, in-memory admission decision point (I2 — zero fs/network/LLM/await on the admission path). Implements the §3.2 decision table split by domain: `machine-local` admits everywhere; `cluster-shared` rides the legacy lease boolean verbatim; `session-scoped` admits unbound/absent/released (today-equivalent to the pool-active carve-out — M2 reachability) and refuses only a record positively naming another machine; `topic-scoped` absent/released collapses to the legacy lease boolean (§9.18 — I4 by construction) unless an I9-audited absent-window story opts in. Owns the boot-warmed `OwnershipIndex` (one synchronous `store.all()` scan at construction + the interface-level `onCommit` hook, hook-registered BEFORE the warm so no commit can interleave) with ingest validation (round-2 L1: malformed ⇒ `ownership-unresolved` fail-closed, never `not-owner` with `owner:null`). Typed refusals per §3.4 (409 + Retry-After, refusal bodies from LOCAL knowledge only — I7). Route-seam throw splits by domain (§9.16: machine-local proceeds, scoped/shared refuse typed `admission-error`); store-seam throw falls back to the legacy verdict and is recorded (`noteStoreSeamError`). §6 observability: per-domain counters, ≤50-entry refusal ring, bounded+rotated `logs/write-admission.jsonl` (buffered — flushed OFF the admission path), the `monitorEventLoopDelay` gauge (the P2-6 hang-attribution instrument), and the ≥N-per-window ONE-deduped-attention-item aggregate (flood lesson).
|
|
10
|
+
2. **WriteDomainRegistry.ts (new):** the single source of truth (ops / EXACT kv keys / route prefixes → domain + two-axis convergence story). I9 enforced structurally: a machine-local entry with no story, or on a git-synced shared path with no file-level arm, is REFUSED the classification and downgraded to cluster-shared (recorded in `refusedClassifications`). Exports `WRITE_SURFACE_INVENTORY_COMPLETE=false` (the §9.14 latch), `sessionBuildContextKeyFor` (charset-jailed per-machine key), and `buildWriteDomainRegistry` — the ONE builder the server wires and the tests read (PR-#334 registry↔wiring identity).
|
|
11
|
+
3. **StateManager.ts:** `guardWrite` gains `key`/`scope` opts and delegates to the attached admission layer; `'legacy'` verdicts fall through to the blanket boolean verbatim; refusals throw `WriteRefusedError` (legacy message string preserved for log-scraping continuity). `attachWriteAdmission` is ONE-WAY (second attach throws) and lands before routes are wired (§3.2 pre-construction window); pre-attach behavior is byte-identical to today. New `sessionPoolActive` getter.
|
|
12
|
+
4. **SessionBuildContextStore.ts + SessionManager.ts + server.ts:** the store is re-keyed PER MACHINE (`session-build-context-<jailed-machine-id>`, literal `local` on identity-less installs — round-2 L3: coordinator/mesh identity, never the caller-less `StateManager.setMachineId`), via a late-resolving getter threaded through SessionManagerOptions. This is the F9 fix (the 2026-07-02 "Failed to record build context … read-only" stderr line). The LEGACY shared key stays unclassified (cluster-shared, I8), self-drains (6h max age), and gets a one-time lease-holder cleanup at boot.
|
|
13
|
+
5. **LocalSessionOwnershipStore.ts + SessionOwnershipRegistry.ts:** `onCommit` added to the `SessionOwnershipStore` INTERFACE (round-2 S4) and fired at each substrate's own mutation point (Local: `persist()` after `cache.set`; InMemory: `casWrite()` at `recs.set`) — both mutation paths (`registry.cas()`, `OwnershipApplier`) funnel through `casWrite`, so the index can never miss a local transition. A listener throw never fails the CAS (guarded).
|
|
14
|
+
6. **FileClassifier.ts:** `.instar/state/attention-items.json` + `.instar/state/evolution/` added to the git-sync exclusions (the I9 second-axis wave-1 build item, round-2 S1) — deliberately NOT the whole `.instar/state/` tree.
|
|
15
|
+
7. **routes.ts:** the route seam `refuseInadmissibleWrite` — admission is the FIRST await-free statement after body validation (I1) on the wave-1 P2-6 families (`POST/PATCH /evolution/*`, `POST/PATCH /attention*`), BEFORE the tone gate on `POST /attention` (I3 refuse-before-touch). `GET /write-admission` (503 when dark) + the event-loop gauge on the AUTHED `/health` extension ONLY (§6 — a load oracle is never handed to an unauthenticated caller).
|
|
16
|
+
8. **server.ts:** production wiring — gate → construct (registry + real ownership store boot-warm + attention hook + logDir) → one-way attach → AgentServer/routes. Init failure is non-fatal (layer stays null → route 503s, legacy guard keeps enforcing).
|
|
17
|
+
9. **guardManifest.ts / devGatedFeatures.ts / ConfigDefaults.ts / types.ts:** the `writeAdmission` posture row (`loadBearing:false` while the legacy guard enforces — re-reviewed at fleet graduation), the DEV_GATED_FEATURES registration, the SHARED_DEFAULTS block (no `enabled:` literal — the #1001 anti-pattern avoided), the `WriteAdmissionConfig` type.
|
|
18
|
+
10. **PostUpdateMigrator.ts + templates.ts:** the Write Admission CLAUDE.md section (Agent Awareness), shipped to new agents via `generateClaudeMd` and to existing agents via the content-sniffed idempotent `migrateClaudeMd` patch. Honestly tagged experimental/dry-run (Maturity Honesty).
|
|
19
|
+
|
|
20
|
+
## Blast radius
|
|
21
|
+
|
|
22
|
+
- **Dry-run carries ZERO authority.** In every shipped configuration (fleet dark; dev live+dryRun) the legacy blanket guard keeps enforcing byte-identically; the new layer only evaluates and logs would-verdicts. The only execution-path deltas that are live everywhere are (a) the SessionBuildContextStore per-machine RE-KEY (single-writer-per-file by construction; the legacy key self-drains) and (b) the FileClassifier sync exclusions (both stores already wrote locally on every machine — the exclusion closes a PRE-EXISTING git-sync fork surface, §3.1).
|
|
23
|
+
- **Fork-safety never weakens (I4).** cluster-shared refuses on every non-holder in EVERY mode; topic-scoped absent/released IS the legacy boolean; session-scoped only ever TIGHTENS relative to the existing pool-active carve-out. Single-machine agents: strict no-op (I6).
|
|
24
|
+
- **A broken guard fails toward today.** Store seam: any admission throw → legacy verdict + recorded occurrence. Route seam: machine-local proceeds (refusing would create a NEW outage for writes safe everywhere), scoped/shared refuse typed `admission-error` (a broken guard must not enable a fork). Construction failure at boot: layer null, route 503, legacy guard enforcing.
|
|
25
|
+
- **No egress, no spend, no new HTTP mutation surface.** The only new route is the read-only `GET /write-admission`. The aggregate attention item is throttled to ONE per (surface, code, direction) window.
|
|
26
|
+
|
|
27
|
+
## Risk + mitigation
|
|
28
|
+
|
|
29
|
+
- **Risk:** the admission path regresses request latency or does I/O. **Mitigation:** I2 enforced by test (fs spied on every admission call including negative lookups; p99 <1ms over 10k calls); log rows are buffered and flushed on a timer OFF the admission path.
|
|
30
|
+
- **Risk:** the ownership index diverges from the store. **Mitigation:** hook-then-warm ordering inside a synchronous constructor; parity tests run the SAME cas/applier sequences against BOTH shipped substrates and assert index≡store.
|
|
31
|
+
- **Risk:** `dryRun:false` leaks refusal authority before the wave-2 inventory is reviewed. **Mitigation:** the compiled `WRITE_SURFACE_INVENTORY_COMPLETE=false` latch — config cannot flip it; proven by the e2e double-latch test at the production constant.
|
|
32
|
+
- **Risk:** a refusal storm floods Telegram topics. **Mitigation:** aggregate-only alerting (≥N per window ⇒ ONE deduped item), proven by the e2e burst-invariant test (40 refusals ⇒ exactly 1 item).
|
|
33
|
+
- **Risk:** the re-key strands existing build-context entries. **Mitigation:** entries carry a 6h max age (self-drain); restore is inherently machine-local (worktree paths are per-disk), so no cross-machine read ever needed the shared key; one-time lease-holder cleanup removes the inert file.
|
|
34
|
+
|
|
35
|
+
## Migration parity
|
|
36
|
+
|
|
37
|
+
- Config: the `multiMachine.writeAdmission` block lives in SHARED_DEFAULTS, which feeds BOTH `getInitDefaults` (new agents) and `getMigrationDefaults` → `applyDefaults` (existing agents, existence-checked, `_instar_noMigrate`-respecting, idempotent). No `enabled:` literal (dev-gate rides resolveDevAgentGate).
|
|
38
|
+
- CLAUDE.md: `WRITE_ADMISSION_CLAUDEMD_SECTION` shared by `generateClaudeMd` (new) and the content-sniffed `migrateClaudeMd` patch (existing) — single source, can't drift.
|
|
39
|
+
- No hook/skill/wire-protocol changes; old-version PEERS need nothing (admission is machine-local, no cross-machine dependency — the F4 mixed-pair row in §5).
|
|
40
|
+
|
|
41
|
+
## Dark-gate line-map
|
|
42
|
+
|
|
43
|
+
- The new `multiMachine.writeAdmission` block (11 lines, no `enabled:` literal) shifts every multiMachine-onward `enabled: false` attribution DOWN by +11 — the EXPECTED map in tests/unit/lint-dev-agent-dark-gate.test.ts is updated and re-verified via the attributor (866→877, 870→881, 877→888, 887→898, 1124→1135, 1150→1161, 1160→1171, 1189→1200, 1377→1388, 1518→1529, 1563→1574, 1588→1599). `writeAdmission` registered in DEV_GATED_FEATURES.
|
|
44
|
+
|
|
45
|
+
## Rollback
|
|
46
|
+
|
|
47
|
+
- Fleet: already dark (gate resolves off) — zero presence, route 503s. Dev: set `multiMachine.writeAdmission.enabled: false` (force-dark even on a dev agent) → the layer is never constructed, StateManager keeps the legacy blanket verdict. The re-key rollback is inert by design: reverting to the legacy key merely resumes the old shared-file behavior; per-machine files self-drain in 6h.
|
|
48
|
+
|
|
49
|
+
## Tests (all three tiers)
|
|
50
|
+
|
|
51
|
+
- **Tier 1 (unit):** tests/unit/write-admission.test.ts (37 — the full §3.2 verdict table: every domain × role × ownership state × pool-active × dryRun, both sides of every boundary; I2 fs-spy + p99 timing; refusal schema + Retry-After; route/store throw splits), tests/unit/write-admission-ownership-index.test.ts (10 — parity across arbitrary cas/applier sequences against BOTH substrates; ingest validation; boot-warm completeness), tests/unit/write-domain-registry.test.ts (16 — I9 both axes; exact-key kv; legacy key stays cluster-shared), tests/unit/state-manager-write-admission.test.ts (11 — delegation, one-way attach, legacy fallback on throw, journal path jail survives), tests/unit/write-domain-conformance-ratchet.test.ts (6 — the §3.5 ratchet over routes.ts + the recorded TODO-classify baseline in tests/fixtures/write-surface-inventory.json, down-only), tests/unit/session-build-context-store.test.ts (re-key + per-machine isolation), tests/unit/lint-dev-agent-dark-gate.test.ts (line-map).
|
|
52
|
+
- **Tier 2 (integration):** tests/integration/write-admission-routes.test.ts (13 — the wave-1 families 201 on a standby-that-owns-topics; cluster-shared 409 typed + Retry-After + zero store mutation (I3 store snapshot); dryRun legacy behavior + would-verdict rows; admission-throw per-seam fail directions).
|
|
53
|
+
- **Tier 3 (e2e):** tests/e2e/write-admission-lifecycle.test.ts (8 — production init path with the REAL gate/registry/on-disk ownership store; live-on-dev 200 / fleet-dark 503 / explicit-false force-dark; the §9.14 double latch at the PRODUCTION constant; the F9 fix end-to-end on a read-only standby; burst invariant ≤1 attention item; GUARD_MANIFEST row + GET /guards posture row over real disk config).
|
|
54
|
+
|
|
55
|
+
## CI follow-up (2026-07-03): discoverability + delivery-completeness classification
|
|
56
|
+
|
|
57
|
+
Two conformance ratchets (unit shards 1 + 4) required deliberate classifications the feature commit had not made — both are metadata-only, zero behavior change:
|
|
58
|
+
|
|
59
|
+
- **`/write-admission` route prefix → `INTERNAL_PREFIXES`** (src/server/CapabilityIndex.ts): the read-only status surface is dev-gated dark (503 on the fleet) and agent-surfaced via the CLAUDE.md "Write Admission" awareness section rather than `/capabilities` discovery — same class as `/mesh-selfheal` and `/worktrees/agent-reaper`. No route, verdict, or admission behavior changes.
|
|
60
|
+
- **`'Write Admission'` migrator section → tracked in `legacyMigratorSections`** (tests/unit/feature-delivery-completeness.test.ts): records the template+migrator parity already implemented via the shared `WRITE_ADMISSION_CLAUDEMD_SECTION` (dark/dry-run, not framework-shadowed — same class as 'Mesh Self-Healing').
|
|
61
|
+
|
|
62
|
+
Side effects: none (a capability-index classification entry + a test allowlist entry). Rollback: revert the two hunks.
|