instar 1.3.663 → 1.3.665

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.
Files changed (38) hide show
  1. package/dist/core/CoherenceGate.d.ts +30 -0
  2. package/dist/core/CoherenceGate.d.ts.map +1 -1
  3. package/dist/core/CoherenceGate.js +81 -9
  4. package/dist/core/CoherenceGate.js.map +1 -1
  5. package/dist/core/CoherenceReviewer.d.ts +21 -0
  6. package/dist/core/CoherenceReviewer.d.ts.map +1 -1
  7. package/dist/core/CoherenceReviewer.js +52 -5
  8. package/dist/core/CoherenceReviewer.js.map +1 -1
  9. package/dist/core/IdleErrorClassifier.d.ts +44 -0
  10. package/dist/core/IdleErrorClassifier.d.ts.map +1 -0
  11. package/dist/core/IdleErrorClassifier.js +87 -0
  12. package/dist/core/IdleErrorClassifier.js.map +1 -0
  13. package/dist/core/SendGateway.d.ts.map +1 -1
  14. package/dist/core/SendGateway.js +21 -2
  15. package/dist/core/SendGateway.js.map +1 -1
  16. package/dist/core/SessionManager.d.ts +10 -0
  17. package/dist/core/SessionManager.d.ts.map +1 -1
  18. package/dist/core/SessionManager.js +40 -3
  19. package/dist/core/SessionManager.js.map +1 -1
  20. package/dist/core/paneTail.d.ts +29 -0
  21. package/dist/core/paneTail.d.ts.map +1 -0
  22. package/dist/core/paneTail.js +52 -0
  23. package/dist/core/paneTail.js.map +1 -0
  24. package/dist/core/reviewers/escalation-resolution.d.ts.map +1 -1
  25. package/dist/core/reviewers/escalation-resolution.js +8 -0
  26. package/dist/core/reviewers/escalation-resolution.js.map +1 -1
  27. package/dist/core/types.d.ts +10 -0
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/core/types.js.map +1 -1
  30. package/dist/monitoring/StuckSignatureClassifier.d.ts.map +1 -1
  31. package/dist/monitoring/StuckSignatureClassifier.js +5 -3
  32. package/dist/monitoring/StuckSignatureClassifier.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/data/builtin-manifest.json +3 -3
  35. package/upgrades/1.3.664.md +34 -0
  36. package/upgrades/1.3.665.md +68 -0
  37. package/upgrades/side-effects/idle-error-tailgate-corroboration.md +65 -0
  38. package/upgrades/side-effects/reviewer-fail-closed-on-abstain.md +70 -0
@@ -30,6 +30,7 @@
30
30
  * incident, and adds tail-gated rate-limit + context-too-long detection.
31
31
  */
32
32
  import { classifyWedgeTail } from './ContextWedgeSentinel.js';
33
+ import { liveTail as sharedLiveTail } from '../core/paneTail.js';
33
34
  /** Rate-limit / usage-limit pane signatures (the Claude usage-limit form the
34
35
  * RateLimitSentinel does NOT auto-handle — it only owns the capacity throttle).
35
36
  * Anchored on the STATIVE/blocking forms Claude Code prints when it actually
@@ -57,10 +58,11 @@ const NORMAL_COMPACTION_TAIL_PATTERNS = [
57
58
  /compaction[^.\n]{0,20}resumed/i,
58
59
  /compaction recovery/i,
59
60
  ];
60
- /** The last `tailLines` non-empty, trimmed lines of a capture, joined. */
61
+ /** The last `tailLines` non-empty, trimmed lines of a capture, joined.
62
+ * Delegates to the shared paneTail.liveTail (CMT-1785: one definition of the live
63
+ * tail) and joins to preserve this module's byte-identical string-matching behavior. */
61
64
  function liveTail(text, tailLines) {
62
- const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
63
- return lines.slice(-tailLines).join('\n');
65
+ return sharedLiveTail(text, tailLines).join('\n');
64
66
  }
65
67
  function anyMatch(text, patterns) {
66
68
  for (const p of patterns)
@@ -1 +1 @@
1
- {"version":3,"file":"StuckSignatureClassifier.js","sourceRoot":"","sources":["../../src/monitoring/StuckSignatureClassifier.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"StuckSignatureClassifier.js","sourceRoot":"","sources":["../../src/monitoring/StuckSignatureClassifier.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.663",
3
+ "version": "1.3.665",
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",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-25T08:31:55.420Z",
5
- "instarVersion": "1.3.663",
4
+ "generatedAt": "2026-06-25T18:41:44.708Z",
5
+ "instarVersion": "1.3.665",
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": "7ea591c3a48b752c066a2fdf0179804a6319177b3664bde1bcc20d9fc0a0d508",
1541
+ "contentHash": "d6bacb543ff87514a6a650bce3269c4d6377b1b411ef0f651fdb88d196d11717",
1542
1542
  "since": "2025-01-01"
1543
1543
  },
1544
1544
  "subsystem:auto-updater": {
@@ -0,0 +1,34 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The CMT-1794 fail-open fix (v1) — the systemic "No Silent Degradation" gap the convergent audit found. The coherence reviewers that screen outbound messages (leaked credentials/PII to the wrong recipient, org-constraint violations, fabricated claims, hallucinated URLs) were SILENTLY PASSING the message whenever their own LLM call errored, timed out, or returned unparseable output: the base `CoherenceReviewer.review()` resolved with `pass:true` and `CoherenceGate` counted that errored reviewer as a GENUINE PASS (it only abstain-counted a promise rejection, and `review()` catches internally). So on any LLM blip, the highest-stakes outbound checks were skipped and the message went out unchecked.
9
+
10
+ v1 (the substance):
11
+ - **Abstain, not silent pass.** `CoherenceReviewer` (and the `escalation-resolution` reviewer's `review()` OVERRIDE — a gap the new ratchet caught) now tag an errored/timeout/unparseable result as `abstained:true` with a STRUCTURED cause (typed-timeout `.code`, never a string-match of the error text). `CoherenceGate` counts an abstain as an abstain (excluded from the pass/block tallies + `passCount`), not a genuine pass.
12
+ - **High-criticality abstain fails CLOSED external.** A floor reviewer (`information-leakage`, `value-alignment`, `claim-provenance`, `url-validity`) that abstains on an EXTERNAL channel routes into the gate's EXISTING `highCritTimeout` fail-closed path (reuse, not a new tier — the convergence caught that the draft's parallel `critical` tier was a no-op). Internal channels stay fail-open-with-report (Decision D — blocking the agent's own control loop on a blip is worse). The floor is a hardcoded, multi-machine-uniform set that config can RAISE but not silently downgrade.
13
+ - **Provider-swap before abstain (§8).** Reviewer calls are now `gating:true`, so the router swaps harness/account before a reviewer abstains — a single-provider blip keeps the review alive; fail-closed engages only on a true multi-provider outage.
14
+ - **Coverage fix (Dec B).** `information-leakage` was skipped for the DEFAULT `primary-user` recipient, so the headline protection never ran on the common path; it now stays enabled for primary-user on EXTERNAL channels (a message to the operator can still leak a third party's PII), keyed on the resolved external flag.
15
+ - **SendGateway Stage-4** thrown-gate-error catch now fails CLOSED on external (was swallow-and-send).
16
+ - **Operator kill-switch** `responseReview.failClosedOnCriticalAbstain` (default true) reverts the fail-closed behavior live, no deploy.
17
+ - **CI ratchet** (`tests/unit/reviewer-fail-closed-ratchet.test.ts`) drives every reviewer subclass through a forced LLM error and fails the build if any returns a verdict without the abstain tag — so a future reviewer/override can't silently reintroduce the fail-open.
18
+
19
+ v2 refinements are tracked (CMT-1801), all bounded-not-blocking: Dec C short-message critical-only coverage (narrowed by PEL catching credential patterns deterministically), the breaker/hold optimization (the amplification it optimizes is already bounded by the spawn-cap + maxRetries), the kill-switch's true-live server wiring (v1 uses snapshot + safe default), and the integration/e2e tier.
20
+
21
+ ## What to Tell Your User
22
+
23
+ If your agent ever sent an outbound message during an LLM hiccup that should have been screened (for a leaked credential, a third party's PII, an org-constraint violation, a fabricated claim/link) — that gap is closed. When one of those checks can't run because its own LLM call errors out, the agent now HOLDS the message for a moment (and retries) instead of sending it unchecked — but only for messages going to the outside world, and only on a genuine outage (a one-off blip just swaps to a backup and keeps going). Messages to you directly aren't held (that would risk freezing the agent). If a sustained outage ever made this too cautious, there's a one-flip off-switch that takes effect with no restart.
24
+
25
+ ## Summary of New Capabilities
26
+
27
+ - `responseReview.failClosedOnCriticalAbstain` (config, default true) — operator kill-switch: revert the new fail-closed-on-critical-reviewer-abstain behavior live, without a deploy. Mirrors `messaging.toneGate.failClosedOnExhaustion`.
28
+
29
+ ## Evidence
30
+
31
+ - `tests/unit/reviewer-fail-closed-on-abstain.test.ts` (3) — external+all-abstain → fail-closed (the exact bug, previously `pass:true`); internal → pass-with-report; abstain ≠ clean pass.
32
+ - `tests/unit/reviewer-fail-closed-ratchet.test.ts` (10) — every reviewer abstains on a forced LLM error; the review()-override set is locked.
33
+ - 164 tests green across CoherenceReviewer + CoherenceGate + MessageSentinel + spawn-cap + the new files; `npm run build` + `tsc --noEmit` clean.
34
+ - Driven by the converged + approved spec (2-round spec-converge + codex cross-model); the convergence caught a no-op tier and a false rides-retry premise before any code.
@@ -0,0 +1,68 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ CMT-1785: the idle-error detector that decides "did this paused session stall on a transient API
9
+ error (nudge it) or stop normally (leave it)?" is migrated from a brittle bare-substring scan to a
10
+ tail-gated, frame-discriminated SIGNAL.
11
+
12
+ - New pure modules `src/core/paneTail.ts` (shared `liveTail`/`stripLineLead`/`wasGlyphLed` — ONE
13
+ definition of "the live tail", extracted so it is no longer copied per consumer) and
14
+ `src/core/IdleErrorClassifier.ts` (`classifyIdleError`).
15
+ - The old gate `TERMINAL_ERROR_PATTERNS.some(p => recentOutput.includes(p))` (a bare `.includes()`
16
+ over the whole capture) is replaced by `classifyIdleError`, which fires only when a terminal-error
17
+ token sits in the live tail (last 20 non-empty lines) on a Claude-EMITTED error frame — a two-tier
18
+ begins-with rule: **Tier A** the line begins with `API Error:`; **Tier B** the line is glyph-led
19
+ (`⏺`/`⎿`/…) and begins with one of the 11 patterns. The match set is a strict subset of the old
20
+ buffer-wide match, so the change can only SUPPRESS spurious fires, never add one.
21
+ - The idle-error capture is widened 30→45 rows (`RATE_LIMIT_SETTLED_CAPTURE_LINES`): Claude Code's
22
+ input box + footer render 15-25 rows BELOW the error, so a 30-row capture could miss it entirely.
23
+ - `StuckSignatureClassifier` is migrated onto the shared `liveTail` (behavior-preserving via
24
+ `.join('\n')`; its existing test corpus is the characterization guard).
25
+ - A once-per-idle-episode structured record (`{event:'idle-error-classify', result:'fired'|'suppressed', …}`)
26
+ lands in `logs/server.log` so a wave of suppressions on genuine errors (the under-fire risk) is
27
+ observable. Cleared in both the re-arm block and `sessionComplete` (no leak on the death path).
28
+
29
+ The signal still feeds the existing `apiErrorAtIdle` → `rateLimitSentinel` recovery actuator and gains
30
+ no blocking authority (Signal vs Authority).
31
+
32
+ ## What to Tell Your User
33
+
34
+ Nothing you need to do. Under the hood, the agent got more reliable at telling a session that genuinely
35
+ stalled on an API error (which it nudges back to life) apart from one that's merely *showing* an old or
36
+ quoted error on screen. The old check was a blunt text search that sometimes kicked off a needless
37
+ recovery on a healthy session, and — more importantly — sometimes read too little of the screen and
38
+ missed a real stall. The new check looks at the live bottom of the screen, reads enough of it to clear
39
+ the input box, and only counts a line the tool actually emitted as an error. Net: fewer false recoveries
40
+ AND fewer missed real ones, so paused sessions get the right treatment faster. No new setting; behavior
41
+ is otherwise unchanged, and it's a one-line revert if ever needed.
42
+
43
+ ## Summary of New Capabilities
44
+
45
+ - `paneTail` + `IdleErrorClassifier` — a precise, tail-gated, frame-discriminated idle-error signal
46
+ replacing the bare buffer-wide substring scan; widened capture (45 rows) so the input-box chrome can
47
+ no longer hide a real error; structured once-per-episode observability of fired-vs-suppressed
48
+ decisions. Internal robustness improvement to session-stall recovery; no user-facing surface to
49
+ configure.
50
+
51
+ ## Evidence
52
+
53
+ - `tests/unit/IdleErrorClassifier.test.ts` (28) — both sides of every boundary, pinned to REAL captured
54
+ render fixtures: fires on `⏺ API Error:` / ` ⎿ API Error:` / glyph-led network tokens / wrapped
55
+ errors; suppresses stale-scrollback, prose mentions, quoted source literals (the self-collision case),
56
+ a tool's own `Error: …ECONNREFUSED`; the 45-vs-30 input-box-chrome capture test; parametrized over all
57
+ 11 patterns; bounded audit fields; paneTail helpers.
58
+ - `tests/integration/idle-error-classifier-production-wiring.test.ts` (7) — the REAL exported
59
+ `TERMINAL_ERROR_PATTERNS` produce correct decisions on real panes; the SessionManager call-site is
60
+ wired to the classifier, the 45-row capture, the dual once-per-episode clears, and the structured record.
61
+ - `tests/e2e/idle-error-classifier-lifecycle.test.ts` (2) — the real SessionManager constructs and the
62
+ `apiErrorAtIdle` recovery handoff is attachable; the production-pattern live decision is correct.
63
+ - `tests/unit/monitoring/StuckSignatureClassifier.test.ts` (13) — unchanged, the characterization guard
64
+ proving the shared-`liveTail` migration is behavior-preserving.
65
+ - `npm run build` + `tsc --noEmit` clean; full unit suite green.
66
+ - Driven by the converged + approved spec; convergence report at
67
+ `docs/specs/reports/idle-error-tailgate-corroboration-convergence.md` (4 rounds; the review caught a
68
+ CRITICAL under-fire-on-the-common-error-form flaw before any code).
@@ -0,0 +1,65 @@
1
+ # Side-Effects Review — Idle-Error Detection: Tail-Gated, Frame-Discriminated Signal (CMT-1785)
2
+
3
+ Spec: `docs/specs/idle-error-tailgate-corroboration.md` (converged 4 rounds, approved).
4
+ Change: replace the bare `TERMINAL_ERROR_PATTERNS.some(includes)` idle-error detector at the
5
+ `SessionManager` idle path with a tail-gated, frame-discriminated classifier (`IdleErrorClassifier`)
6
+ fed by a shared `paneTail` helper; widen the idle-error capture 30→45 rows; add a once-per-episode
7
+ structured observability record.
8
+
9
+ ## 1. Over-block (what legitimate inputs does this reject that it shouldn't?)
10
+ The classifier can SUPPRESS a fire the old bare match would have made — that is the whole point —
11
+ but the risk is suppressing a GENUINE error (under-fire). Mitigated: (a) the two-tier begins-with
12
+ grammar fires on real Claude render forms (`⏺ API Error:` / ` ⎿ API Error:`), pinned by tests using
13
+ the actual captured fixtures; (b) the 45-row capture clears the input-box chrome that pushes the
14
+ error up; (c) the 20-line non-empty tail clears realistic chrome with margin; (d) a missed error
15
+ falls through to the pre-existing 15m idle-kill (slower recovery, never silent loss); (e) the
16
+ `result:'suppressed'` structured record makes a wave of real misses observable.
17
+
18
+ ## 2. Under-block (what failure modes does it still miss?)
19
+ - A genuine Claude error rendered in a form that is neither `API Error:`-led nor a glyph-led
20
+ begins-with-pattern line (unknown future render) → missed → 15m idle-kill fallback. The drift
21
+ test fails loudly if the known render fixtures stop matching.
22
+ - A residual false-POSITIVE (accepted): a tool result rendered glyph-led whose line begins with a
23
+ terminal token (e.g. `⎿ fetch failed`) fires. Bounded: the actuator is non-destructive (one wasted
24
+ nudge the verify step proves a no-op). Documented in the spec §5.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+ Correct layer: this is a deterministic SIGNAL-emitter (Signal vs Authority), feeding the existing
28
+ `apiErrorAtIdle` → `rateLimitSentinel` recovery actuator. It gains no blocking authority. The shared
29
+ `paneTail` helper is the right home for the tail-gating logic (was being copied per consumer). The
30
+ recovery actuator it feeds is honestly named a deterministic Tier-0 non-destructive self-heal (nudge
31
+ → verify → escalate-to-notice + zombie-kill veto; no respawn).
32
+
33
+ ## 4. Signal vs authority compliance
34
+ Compliant. The classifier returns a boolean SIGNAL; the actuator decides and verifies before any
35
+ action. The match set is a strict subset of the old buffer-wide match (tail + frame ⊂ buffer-wide),
36
+ so the change is monotonic-suppressing — it can only reduce spurious actuations, never add one.
37
+
38
+ ## 5. Interactions
39
+ - The `detectRateLimited` branch runs FIRST and is untouched — a server throttle still routes to
40
+ `rateLimitedAtIdle` and never reaches the classifier (pinned by the wiring test's intent; the
41
+ branch order is preserved). The wider 45-row capture is a no-op for `detectRateLimited` (it slices
42
+ its own last ~20 lines).
43
+ - `errorNudgedSessions` (the nudge episode guard) is unchanged; the new `idleErrorClassified` set
44
+ mirrors its full lifecycle (armed once/episode; cleared in BOTH the re-arm block AND
45
+ `sessionComplete` — no leak on the session-death path).
46
+ - `StuckSignatureClassifier` migrates to the shared `liveTail` via `.join('\n')` — behavior-preserving
47
+ (its existing 13-test corpus passes unchanged = the characterization guard).
48
+
49
+ ## 6. External surfaces
50
+ None new. No HTTP route, no config, no agent-installed file, no user-facing message. The only new
51
+ output is structured `console.log` records to `logs/server.log` (operator log), once per idle episode,
52
+ credential-free (matchedPattern is a fixed token; no raw pane content is logged).
53
+
54
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
55
+ Machine-local BY DESIGN. The classifier reads one local tmux pane on the host running the session —
56
+ no cross-machine state, no generated URL, no replicated surface. The downstream `rateLimitSentinel`
57
+ has a Telegram-notify surface, but the change is monotonic-suppressing, so it can only REDUCE
58
+ user-facing cross-machine notices, never add one. Correct posture, declared explicitly.
59
+
60
+ ## 8. Rollback cost
61
+ Single-commit revert: restore the bare `.some(includes)` gate + the 30-row capture, delete
62
+ `IdleErrorClassifier.ts` + `paneTail.ts` + tests + the `idleErrorClassified` field/clears + the
63
+ structured record, and revert the `StuckSignatureClassifier` import (behavior-preserving, mechanical).
64
+ No migration, no durable state, no config. The revert IS the immediate operator action if a future
65
+ Claude render change is ever suspected of under-firing.
@@ -0,0 +1,70 @@
1
+ # Side-Effects Review — Coherence reviewers fail CLOSED on abstain (CMT-1794, v1)
2
+
3
+ **Version / slug:** `reviewer-fail-closed-on-abstain`
4
+ **Date:** `2026-06-25`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `the spec's 2-round convergence (6 internal reviewers + codex cross-model) IS the multi-angle review; see docs/specs/reports/reviewer-fail-closed-on-abstain-convergence.md`
7
+
8
+ ## Summary of the change
9
+
10
+ Implements the converged+approved `reviewer-fail-closed-on-abstain` spec (the CMT-1794 fail-open fix), v1 = the substance. The audit found that when a coherence reviewer's LLM call errors/times-out/returns-unparseable, the base `CoherenceReviewer.review()` resolved with a permissive `pass:true` and `CoherenceGate` counted it as a GENUINE PASS (it only abstain-counted a promise REJECTION; review() catches internally) — so on an LLM blip the highest-stakes outbound checks (leak/constraint/provenance/url) silently passed. Files: `src/core/CoherenceReviewer.ts` (tag abstains with a structured cause), `src/core/CoherenceGate.ts` (count abstains as abstains → route to the existing `highCritTimeout` floor path + the kill-switch + Dec B coverage), `src/core/reviewers/escalation-resolution.ts` (its `review()` OVERRIDE tags abstains too — a gap the §9 ratchet caught), `src/core/types.ts` (the kill-switch config field), + 2 test files (13 tests). `src/core/SendGateway.ts` Stage-4 catch now fails closed on external.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `CoherenceGate` reviewer abstain handling — **modify** — an errored/unparseable reviewer is now an ABSTAIN (not a counted pass); a high-criticality abstain on an external channel fails the turn CLOSED via the EXISTING `highCritTimeout` path (reuse, not a new tier).
15
+ - `CoherenceReviewer.review()` / `parseResponse()` / escalation-resolution override — **modify** — tag abstains (host-set, injection-proof).
16
+ - `SendGateway` Stage-4 catch — **modify** — external fails closed (held) instead of swallow-and-send.
17
+ - `responseReview.failClosedOnCriticalAbstain` kill-switch — **add** — live-readable revert.
18
+
19
+ ## 1. Over-block
20
+
21
+ A reviewer that genuinely PASSES still passes — only a reviewer that COULD NOT FORM AN OPINION (errored) now abstains. The over-block surface is: during a real multi-provider LLM outage, external outbound needing a critical review is HELD (not lost — it rides the existing retry path). That is the safe direction the standard mandates, it is operator-revertable (the kill-switch), and the critical set is narrow (4 floor reviewers). §8 ensures a single-provider blip SWAPS (review stays alive) rather than abstaining, so a held outbound requires a true outage, not a transient flake.
22
+
23
+ ## 2. Under-block
24
+
25
+ Bounded coverage deferred to v2 (tracked CMT-1801): short URL-free external messages still skip the gate (Dec C) — but PEL catches credential PATTERNS deterministically regardless of length, so the residual is a short non-pattern PII string; the breaker/hold optimization (§3) is deferred, but the amplification it optimizes is ALREADY bounded by the host spawn-cap + `maxRetries` (3 turns), so v1 is bounded, not unbounded. MessageSentinel inbound stays fail-open-except-capacity by deliberate decision (it leaks nothing on fail-open; the emergency-stop fast-path runs first).
26
+
27
+ ## 3. Level-of-abstraction fit
28
+
29
+ Correct — Signal-vs-Authority: the abstain is a HOST-set signal (trusted catch/parse code, never model output), the gate is the authority. The fix REUSES the gate's existing `'high'` criticality + `highCritTimeout` machinery (the convergence's headline correction: the draft's parallel `critical` tier was a no-op the code never read).
30
+
31
+ ## 4. Signal vs authority compliance
32
+
33
+ - [x] No — produces a SIGNAL (the abstain tag) consumed by the existing smart gate; removes a silent fail-open. The structured `abstainCause` is derived from typed error classes (typed-timeout `.code`), NOT a string-match of the error text — the very standard this work enforces, applied to itself.
34
+
35
+ ## 5. Interactions
36
+
37
+ - **Shadowing:** none — the abstain branch is the unified rejected-or-abstain-tagged path; capacity-shed (`capacityUnavailable`) keeps precedence over abstain over genuine pass/block (explicit tri-state).
38
+ - **Double-fire:** none — one abstain per reviewer per evaluation.
39
+ - **Kill-switch scope:** governs ONLY the NEW abstain-tag-driven `highCritTimeout`; a promise REJECTION keeps its pre-existing unconditional fail-closed (no behavior change there).
40
+ - **passCount:** an abstain no longer inflates `passCount` (else a degraded reviewer reads as healthy during an outage).
41
+
42
+ ## 6. External surfaces
43
+
44
+ - A new config key `responseReview.failClosedOnCriticalAbstain` (default true; live-readable via the optional `liveConfig` getter — v1 falls back to snapshot/safe-default, true no-restart wiring is CMT-1801). No new HTTP route. The held disposition rides the existing retry/feedback path. No operator-facing action added (the held/escalation rides the existing attention/DegradationReporter surface).
45
+
46
+ ## 6b. Operator-surface quality
47
+
48
+ No operator surface touched (no dashboard/approval/grant file). Not applicable.
49
+
50
+ ## 7. Multi-machine posture
51
+
52
+ **replicated/uniform by construction** — the floor set + abstain logic + ratchet are compiled from source (identical across machines); the `reviewerCriticality` config can only RAISE (the hardcoded floor holds on every machine regardless of config), so the leak/constraint protection cannot silently diverge. Held drafts are machine-local by design (Decision E). No user-facing notice, no cross-topic durable state, no generated URL.
53
+
54
+ ## 8. Rollback cost
55
+
56
+ Pure code change + one additive config key. Back-out = revert the commit (reviewers return to the prior fail-open) OR flip `failClosedOnCriticalAbstain:false` (live, no deploy) to revert just the fail-closed behavior. No data migration, no agent-state repair.
57
+
58
+ ## Conclusion
59
+
60
+ v1 ships the SUBSTANCE of CMT-1794: no reviewer silently fails open (the §9 ratchet locks it across every subclass + caught the escalation-resolution override gap), a high-criticality abstain fails closed external (proven by a dedicated test), the leak-review coverage bypass (Dec B) is closed, the provider-swap (§8) keeps reviews alive on a single-provider blip, and a live kill-switch reverts it. The v2 refinements (Dec C, breaker/hold, kill-switch live-wiring, integration/e2e) are bounded-not-blocking and tracked under CMT-1801.
61
+
62
+ ## Second-pass review
63
+
64
+ The spec's 2-round convergence (6 internal reviewers across security/adversarial/scalability/integration/decision-completeness/lessons + a codex GPT-tier cross-model pass + the conformance gate) IS the multi-angle review for this change — it reshaped the design (caught the no-op tier + the false rides-retry premise) before any code. See the convergence report. The implementation was verified against that converged design with 164 passing tests across every touched area.
65
+
66
+ ## Evidence pointers
67
+
68
+ - `tests/unit/reviewer-fail-closed-on-abstain.test.ts` (3): external+all-abstain→fail-closed (the exact bug, was pass:true); internal→pass-with-report (Decision D); abstain≠clean-pass.
69
+ - `tests/unit/reviewer-fail-closed-ratchet.test.ts` (10): every reviewer subclass abstains on a forced LLM error (never a silent pass); the review()-override set is the known {information-leakage (delegates to super), escalation-resolution (tags in its own catch)} — a NEW override trips the ratchet.
70
+ - Regression: CoherenceReviewer (47) + CoherenceGate (27) + MessageSentinel + spawn-cap-fail-closed-gates + all the above = 164 tests green. `npm run build` + `tsc --noEmit` clean.