feat(panic): Panic Response Layer — behavioral destabilization detection#83
Draft
laurentftech wants to merge 22 commits into
Draft
feat(panic): Panic Response Layer — behavioral destabilization detection#83laurentftech wants to merge 22 commits into
laurentftech wants to merge 22 commits into
Conversation
Behavioral destabilization detection orthogonal to EpistemicLease. Panic = oscillation + trajectory bursts + stale-depth persistence. EpistemicLease = freshness/authority decay. Neither implies the other. Architecture: - panic-response.ts: PanicState I/O (atomic writes), hysteresis engine (separate up/down thresholds L0-L4), buildPanicCheckOutput, signal text - epistemic-lease.ts: EpistemicTracker extended with panicScore, panicLevel, localityConfidence, recentOrientCount, interventionCountSinceStable; updatePanic() called on every tool call; orient spam protection in resetTracker() (-40 normal / -15 rapid / 0 at 3+ rapid orients); trackerToPanicState() export - mcp.ts: writes panic-state.json after every updateTracker; appends panic signal as separate content item (never corrupts JSON result body); increments interventionCountSinceStable on injection - panic-check.ts: new CLI command for PreToolUse hook consumers; reads panic-state.json, applies cooldown, always exits 0 - index.ts: registers panic-check command Telemetry (panic.jsonl, gated by OPENLORE_TELEMETRY=1): - panic_level_change: every L0-L4 transition with trigger (score/ceiling) - panic_orient_reset: orient kind (normal/rapid/spam), delta, from/to level - panic_signal_injected: tool, agent, directive_mode flag - hook_intervention: channel, severity, directive_mode - panic_level + panic_score added to every mcp tool_call event Tests: 101 passing (26 new in panic-response.test.ts, 26 new in epistemic-lease.test.ts covering hysteresis, state I/O, fail-open, cooldowns, directive mode, orient spam protection, trackerToPanicState)
connect() + openTable() now called once per dbPath, not every search. BM25 full table scan eliminated on cache hit — cached rows reused. Both caches invalidated by build() when index is rebuilt.
- gryph-bridge: optional CLI bridge for safedep/gryph observability; queryGryphSignals() + applyGryphDelta() with fail-open absence semantics - epistemic-lease: replace proxy signals with spec-correct panic weights: trajectory burst (density≥0.60→+15), oscillation spike (osc≥0.50→+10), staleDepth≥3→+25 per call; add passive decay (−5/min wall-clock) and locality recovery (−3/call when stable); add lastPanicUpdateAt and panicTriggers fields; expose triggers via trackerToPanicState() - panic-check: add --format claude|kilo|codex; query Gryph and apply delta before building hook output; emit gryph_enriched in telemetry - telemetry: add 50MB rotation (keep 5 rotated files); add panic section to telemetry command (episodes, avg recovery latency, hook intercepts, orient spam, Gryph enrichment rate, trigger frequency) - setup: add --hooks claude|kilo|codex to install PreToolUse panic-check hook into .claude/settings.json independently of --tools
- gryph-bridge: rename largePatchWhileActive → largePatchWhileStale; add OPENLORE_GRYPH_TIMEOUT_MS env var (default 150ms, min 50ms) - mcp.ts: add invariant comment excluding panic-check/telemetry from panic computation loop (CLI procs read state, never call updateTracker) - spec: add Runtime Safety Invariants section (fail-open guarantees for all subsystems); clarify L4 advisory-only enforcement model; add Shannon entropy formula for commandEntropy; add locality recovery philosophical framing; document trajectory tracking continues during stale; document depth-3 saturation intent; add OPENLORE_GRYPH_TIMEOUT_MS to Gryph config table; add interventionCountSinceStable reset conditions to formal invariants
Refractory period: - Add panicRecoverySuppressionUntil to EpistemicTracker and PanicState - orient() sets 45s suppression window on any score-reducing call - Upward signals (trajectory_burst, oscillation_spike, stale_depth_3) skip during refractory; decay and locality recovery still apply - Written to state file so hook can apply guard without MCP round-trip Provenance trace: - updatePanic() now emits panic_score_delta on every score change with full per-trigger attribution: name, delta, evidence (measured values) - Separates trigger labels from evidence so threshold tuning is possible - in_refractory flag on events where suppression was active - panic_level_change events include full provenance array - tool name propagated from updateTracker to updatePanic for attribution Spec: - Document refractory period rationale and semantics - Add panic provenance trace format with trigger/evidence separation - Known Limitations section: oscillation fragility, productive chaos, goal coherence absence, hook non-mandatory invariant - V2 non-goals: convergence signals, productive refactor mode detection, goal coherence / task scope tracking
…tion localityConfidence now uses both density and oscillation (not just density): localityConfidence = (1 - min(1, density×2)) × (1 - min(1, oscillation)) Locality gates two signals: - stale_depth_3 (+25/call): only fires when localityConfidence < 0.5 - burst escalation (depth → 3): only fires when localityConfidence < 0.5 A stale agent doing focused local work (high confidence) is not in the same risk category as a stale agent drifting cross-module. Suppressing these signals prevents the panic layer from treating coherent deep work as a crisis. Spec: add Behavioral Space section documenting the five independent dimensions and the interpretation matrix (8 situations). Add Locality Confidence Modulation subsection with formula, gating table, and rationale for appropriate vs maximum tool utilization.
Export computePanicStats, computeRecovery, computeObstinacy, and their types for direct testing. 24 tests covering: - computePanicStats: episode counting, avg_recovery_ms (completed only), failed_recovery_rate, hook_intercepts, mcp_injections, orient spam/rapid, gryph_enriched_intercepts, trigger frequency + sort order, out-of-order chronological events, non-level-change event isolation - computeRecovery: stale→orient latency avg, multi-pair avg, null when no subsequent orient, recovery half-life, recurrence rate, edge cases - computeObstinacy: tool call counting per episode, depth tracking, open episode handling, multiple episode separation
… tests Also fix computeOscillationScore to operate over transition sequence — A→A→A→A was scoring 1.0 (same as A→B→A→B confusion loop) because the old formula checked bigrams across the full window. Focused single-module work should produce oscillation=0.
Add PanicResponseMode ('off'|'telemetry'|'advisory'|'experimental_blocking')
to OpenLoreConfig. Default: 'off'. Existing users unaffected.
Architecture:
- Policy lives in mcp.ts only — never stored in EpistemicTracker or panic-state.json
- updatePanic() extracted from updateTracker() and exported; mcp.ts calls it
conditionally based on policy mode
- localityConfidence moved into updateTracker() — it's navigation state, not panic
- tracker.density stored after each updateTracker() so mcp.ts can pass it to updatePanic()
Mode semantics:
- off: zero panic overhead (no scoring, no file writes, no Gryph)
- telemetry: scoring + state file, no agent impact
- advisory: + injection into responses at L2+, hook exits 0 always
- experimental_blocking: + hook emits {"decision":"block"} at L4, still exits 0
panic-check always exits 0 — fail-open invariant. Runtime decides enforcement.
setup --panic <mode> sets config; --hooks remains independent.
Separates instrumentation from policy. 'off' already disabled persistence and intervention; 'privacy' cuts deeper — skips updateTracker() entirely so no behavioral profiling occurs in memory. Mode ladder (bottom to top): privacy → no instrumentation, no persistence, no intervention off (default) → in-memory tracking only telemetry → tracking + persistence advisory → tracking + persistence + response injection experimental_blocking → + block signal to hook runtime
'off' now means zero instrumentation (updateTracker skipped). Removed the intermediate 'off' state that ran tracking without consumers — it was computationally dead. Inner updatePanic gate drops; anything past 'off' runs the full pipeline. Final ladder: off | telemetry | advisory | experimental_blocking
Restores updateTracker() to always run — freshness/epistemic tracking is not panic instrumentation. 'off' disables the panic subsystem only; the epistemic engine remains active regardless of panic mode. Renames 'telemetry' → 'observe': the mode observes the panic engine without intervening, not just moves telemetry data. Final ladder: off | observe | advisory | experimental_blocking
…emantics localityConfidence is shared behavioral state (freshness + panic), not purely freshness state. Refractory deadline is replaced on each orient() recovery, not extended — document both explicitly in-source.
…lock advisory - localityConfidence interface gets explicit WARNING: affects both freshness and panic — blast-radius comment locks the shared contract - experimental_blocking payload adds advisory:true — protocol now matches the documented "runtime decides" semantics; no implicit authority - OpenLoreConfig docstring enumerates exactly what 'off' disables vs. what continues (freshness metrics) — eliminates the apparent contradiction
Single source of truth for all numeric thresholds, weights, cooldowns, and timing values. Both panic-response.ts and epistemic-lease.ts now import from it. Tests reference constants directly — no more behavioral snapshot drift when a threshold changes. New exports: PANIC_UP/DOWN_THRESHOLD, PANIC_TRAJECTORY_DENSITY/DELTA, PANIC_OSCILLATION_THRESHOLD/DELTA, PANIC_STALE_D3_LOCALITY_GATE/DELTA, PANIC_LOCALITY_RECOVERY, PANIC_DECAY_PER_MIN, PANIC_REFRACTORY_MS, PANIC_SESSION_EXPIRY_MS, HOOK_COOLDOWN_MS, SEVERITY_MAP.
…MCP blind spot Promotes Gryph from optional enrichment to first-class behavioral source. Background poll loop (default 15s) updates panic state independently of MCP tool calls: agents working purely via Bash/Edit/Read are now observable. Architecture: RuntimeBehaviorProvider (interface) + GryphBehaviorProvider (impl) startGryphPolling: async, single-flight, syncs in-memory tracker to prevent MCP path from overwriting Gryph-elevated scores on next call. New constants: GRYPH_POLL_INTERVAL_MS, GRYPH_POLL_INTERVAL_MIN_MS, GRYPH_RETRY_BURST_DELTA, GRYPH_LARGE_PATCH_*_DELTA, GRYPH_*_THRESHOLD. New env: OPENLORE_GRYPH_POLL_INTERVAL_MS (min 5000ms, default 15000ms). Provenance carries source:'gryph' on all Gryph-originated deltas. Backward-compat: queryGryphSignals/applyGryphDelta preserved for hook path. Fail-open invariant: all Gryph failures resolve to null, zero impact. 19 new tests: single-flight, timeout, null stability, score accumulation, stale gating, provenance attribution, tracker sync, exception safety.
Three architectural fixes from post-Gryph review:
Gryph polling decoupled from MCP session (option B):
- New `openlore gryph-watch` command: standalone observer process, lifetime
independent of MCP server. Singleton via PID file, auto-detects project dir,
exits on SIGTERM/SIGINT/stdin-close (parent death).
- Installed via `openlore setup --hooks claude` as a UserPromptSubmit hook —
starts once per session from the first user prompt, not the first tool call.
- Closes the blind spot permanently: Gryph signals flow even when no openlore
MCP tools are called during the entire session.
- Removed Gryph polling from mcp.ts (was MCP-session-scoped, too late to start).
CAS writes prevent multi-writer score regression:
- `revision: number` added to PanicState — monotonically bumped on every write.
- `casWritePanicState()`: synchronous read-check-write (atomic within Node.js
event loop, no await between ops). Returns false on revision mismatch.
- Gryph poll uses CAS with one retry: re-reads fresh state and recomputes delta
if MCP wrote between poll read and poll write.
- MCP path uses `writePanicState()` (returns new revision) and syncs
`tracker.panicRevision` — prevents MCP from writing stale revision on next call.
- `panicRevision` added to EpistemicTracker and `trackerToPanicState()`.
setInterval replaced with while loop:
- `while (!stopped) { await sleep(intervalMs); if (!stopped) await poll(); }`
- Eliminates timer drift, stop lifecycle races, and orphan timer edge cases.
- Sequential await guarantees no overlap (isPolling guard kept as defense-in-depth).
Workspace registry in startGryphPolling:
- Module-level `_pollerRegistry: Map<string, () => void>` keyed by directory.
- `startGryphPolling` stops any existing poller for the same directory before
starting a new one — enforces one-per-workspace at the call site.
Hook environments (nohup, UserPromptSubmit) often strip PATH so `which gryph` fails even when the binary exists at ~/.local/bin/gryph. Falls back to checking ~/.local/bin, ~/go/bin, /usr/local/bin, /opt/homebrew/bin before giving up. Stores resolved path in _gryphBin so spawns use the absolute path, not 'gryph'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…chema Gryph events use PascalCase (Command, ExitCode, ResultStatus, LinesAdded) but code mapped lowercase fields — commands array was always empty, entropy always 1, repetitiveRetryBurst never fired. Two fixes: 1. Interface and mappings now use PascalCase with lowercase fallbacks 2. Burst detection adds OR path: failingCommandRate > 0.30 triggers regardless of entropy — catches mixed-window scenarios where diagnostic commands dilute the entropy signal below the 0.30 threshold Verified end-to-end: panic_score_delta events firing with correct provenance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efix panic_mode_active was written to panic-response.jsonl which telemetry never reads — silent data loss. Mode is always derivable from config. kilo format was prepending [PANIC:SEVERITY] before a message that already contained its own tag, producing double-prefix output.
…on feedback loop, panic-level statusline
1. Decay in Gryph path — applySnapshotDelta now applies passive
wall-clock decay (PANIC_DECAY_PER_MIN) before adding Gryph signals.
Score could previously only go up via Gryph while MCP was idle.
2. gryphWindowStart — new PanicState field separates the Gryph query
window from lastOrientAt (which could be hours old). panic-check
hook now queries only the ~2min window since last intervention,
eliminating replaying hours of history. Advanced on each warn write.
3. Cooldown preservation — mcp.ts now reads disk state before writing
to preserve lastHookInterventionAt and gryphWindowStart set by the
panic-check process. Fixes broken cooldown: these fields were
silently wiped on every MCP tool call.
4. Intervention feedback loop — mcp.ts detects orient() calls and
emits panic_intervention_outcome event when orient followed a hook
intervention within 5 minutes. Closes the measurement gap on
whether interventions actually produce behavioral change.
5. panic-level command — new read-only `openlore panic-level` outputs
"P:L{n}" (empty at L0) for status line integration. No side effects.
Configure: openlore setup installs hooks separately.
laurentftech
pushed a commit
to laurentftech/OpenLore
that referenced
this pull request
Jun 3, 2026
…mode perf)
Field regression: multiple dogfooding sessions report batched/delayed
tool-result delivery once the openlore MCP server is registered. Root
cause traced to the watch-mode re-index pipeline:
- --watch-auto defaults to true (mcp.ts:1610), so plain `openlore mcp`
silently arms a watcher on the first tool call.
- Every save runs an O(repo), not O(change), pipeline: full 2.1MB
llm-context.json rewrite (which then forces a cold re-parse on the
next tool call via the mtime-keyed read cache), plus a full
vector-index read+overwrite ("incremental" build still toArray()s
and createTable(overwrite)s the whole corpus).
- No cross-file coalescing, so a branch switch/formatter fires the
pipeline N times back-to-back; one stderr line per change floods the
client's drain.
Spec 13.1 keeps --watch-auto on by default but makes freshness genuinely
O(change): batch coalescing, write-behind + read-cache handoff, real
incremental LanceDB row ops, signature-freshness decoupled from
embedding-freshness, VCS-flood detection + backpressure, stderr
discipline, doc reconciliation, and a watch-mode benchmark + tests.
Slotted ahead of spec 14 in spec 13's Progress list (urgent regression
fix; pollutes any benchmark run through the MCP server). Not addressed
by PR clay-good#83 (Panic Response Layer) — independent pipeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full implementation of the Panic Response Layer — behavioral destabilization detection orthogonal to EpistemicLease (freshness ≠ panic). The core insight: observe behavioral collapse, not activity level.
Freshness vs Panic — explicit separation
An agent can be stale but calm (deep linear work in stale context), or fresh but panicking (rapid confused navigation after a recent orient()). The two dimensions are computed independently; only the panic ceiling (staleDepth floors panicLevel) couples them.
Core engine (
panic-response.ts)PanicLevelL0–L4 with hysteresis (separate up/down thresholds — prevents thrashing)panic-state.jsonwrites (POSIXrename(2)), fail-open reads, 30min session expirybuildPanicCheckOutput(): cooldown enforcement, directive mode atinterventionCountSinceStable≥3revision: numberinPanicState— monotonically bumped on every write, enables CASgryphWindowStart?: string— tracks Gryph query window for the panic-check hook path independently fromlastOrientAt(which could be hours old). Advanced on each intervention write; 2-min fallback prevents replaying hours of history.Signal model (
epistemic-lease.ts)Per-call behavioral signals:
localityConfidenceis shared behavioral state — used by both the freshness engine (burst gate) and the panic engine (stale_depth_3 gate, burst escalation gate). Computed inupdateTracker()so it's always current regardless of panic mode. Changes to this metric affect both systems — modify with full blast-radius awareness:stale_depth_3and burst escalation only fire whenlocalityConfidence < 0.5(1 − min(1, density×2)) × (1 − min(1, oscillation))Refractory period: orient() sets
panicRecoverySuppressionUntil(+45s) when score reduces — suppresses upward signals to let recovery land before re-escalating. Subsequent orient() calls during an active refractory replace the deadline (not extend): the window starts fresh from the most recent recovery. Spam orient (delta=0) does not set refractory.Provenance trace: every
panic_score_deltaevent includes aprovenance[]array with per-trigger{name, delta, evidence}entries.commandEntropyis normalized Shannon entropy over recent shell command signatures:The signal is behavioral collapse, not throughput. Raw tool frequency is never a panic signal.
Orient spam protection:
−40normal /−15rapid (<2min) /0spam (≥3 rapid).interventionCountSinceStableresets on stable recovery, orient reset, or 30min session expiry.Oscillation model fix
computeOscillationScore()now operates over the transition sequence (entries where module actually changed), not the full window. Previous formula scoredA→A→A→Aas 1.0 (same as confusion loop). Focused single-module work now correctly produces oscillation=0.Gryph runtime observability (
gryph-bridge.ts+gryph-watch.ts)The MCP blind spot: panic scoring only fires on openlore MCP tool calls. Agents working exclusively via Bash/Edit/Read are invisible to the MCP-path signal model — they can enter behavioral collapse with no panic signal generated. Worse: MCP-path Gryph polling only starts after the first tool call, so sessions that never call openlore tools have zero observability.
Closed by
openlore gryph-watch— a standalone observer process whose lifetime is fully decoupled from the MCP server session:gryph-watch lifecycle:
UserPromptSubmithook viaopenlore setup --hooks— starts on the first user prompt, not the first tool call.openlore/gryph-watch.pid) — one per workspace directorymode: 'off'Polling invariants:
while (!stopped)loop replacessetInterval— eliminates timer drift, stop lifecycle races, orphan timers; sequential await makes overlap structurally impossibleisPollingflag kept as defense-in-depth_pollerRegistry: Map<string, () => void>enforces one-poller-per-workspace at the call sitespawn(non-blocking) — isolated from MCP execution pathCAS writes prevent multi-writer score regression:
Both gryph-watch and the MCP server write to
panic-state.json. Without concurrency control, a slow Gryph query that reads stale state can overwrite the MCP server's fresher write (silent score regression).casWritePanicState(directory, expectedRevision, state): reads currentrevision, checks against expected, writes withrevision+1— all synchronous, atomic within the Node.js event loop (noawaitbetween read and write)writePanicState()returns the new revision; synced intotracker.panicRevisionso subsequent MCP writes never regress the revisionpanicRevisionadded toEpistemicTrackerandtrackerToPanicState()Gryph signals + decay:
repetitiveRetryBurstlargePatchWhileStale(>500 LOC write, low entropy)largePatchWhileStale(>500 LOC write, high entropy)state.updatedAtDecay is applied inside
applySnapshotDelta()before Gryph signal deltas so score can drop between polls when the agent is idle — not just via the MCP path.OPENLORE_GRYPH_TIMEOUT_MS— per-query budget (default 150ms, min 50ms).OPENLORE_GRYPH_POLL_INTERVAL_MS— poll interval override (default 15s, min floor 5s).Centralized constants (
panic-constants.ts)All numeric thresholds, weights, cooldowns, and timing values live in
panic-constants.tsas the single source of truth. Tests import and reference these constants directly — no hardcoded magic numbers that silently diverge when thresholds change.Dual-channel intervention
panicLevel≥1(advisory/experimental_blocking modes only)openlore panic-checkfires before every tool call — closes the tunneling blind spot (agent stops calling openlore → MCP signals can't reach it)openlore gryph-watchobserves continuously, session-lifetime independentinterventionCountSinceStable≥3experimental_blockingemits{"decision":"block","advisory":true}at L4 and exits 0 —advisory:trueis always present; runtime decides enforcement. OpenLore never mandates.Cooldown preservation
mcp.tsreads disk state before eachwritePanicState()call to preservelastHookInterventionAtandgryphWindowStartset by thepanic-checkprocess (separate OS process). Previously these fields were silently wiped on every MCP tool call, breaking cooldown enforcement.Intervention feedback loop
mcp.tsdetects orient() calls (viatracker.lastOrientResetAtchange) and emitspanic_intervention_outcomewhen orient followed a hook intervention within 5 minutes. Enables retrospective validation of whether interventions actually produced behavioral change from telemetry.Opt-in panic response (
PanicResponseMode)Panic is disabled by default. Users opt in via
.openlore/config.json:{ "panicResponse": { "mode": "observe" } }off(default)observeadvisoryexperimental_blocking{"decision":"block","advisory":true}at L4mode:'off'disables: panic scoring, panic state persistence, panic interventions, panic telemetry, panic hook output, Gryph observation. Behavioral metrics required by the freshness engine (density, oscillation, localityConfidence) continue to be computed in-memory as part of EpistemicLease.Policy is never stored in
EpistemicTrackerorpanic-state.json— config is the single source of truth.panic-checkalways exits 0 (fail-open).setup --hooksandsetup --panic <mode>are independent commands.Architecture: policy / state separation
updatePanic()extracted fromupdateTracker()and exported —mcp.tscalls it conditionally based on panic modelocalityConfidencecomputed inupdateTracker()— shared behavioral state reused by freshness and panic; changes affect both systemstracker.densityandtracker.panicRevisionstored after each call so callers can read/sync them post-updateTracker()EpistemicTrackercontains behavioral/freshness state only — no policy fieldsupdateTracker()always runs; onlyupdatePanic()+writePanicState()are gated onmode !== 'off'Status line integration
openlore panic-level— read-only command outputtingP:L{n}(empty at L0) for status line displays. No side effects, no writes.{ "statusLine": { "type": "command", "command": "openlore panic-level --directory $(pwd) 2>/dev/null" } }Trajectory tracking during stale
Trajectory density and oscillation continue accumulating while stale so post-stale burst and behavioral patterns remain observable. Stale state does not freeze the behavioral model.
Runtime safety invariants
Telemetry + observability
panic.jsonldomain:panic_level_change,panic_orient_reset,hook_intervention,panic_signal_injected,panic_score_delta(with provenance),gryph_poll,panic_intervention_outcomepanic_score_deltaevents carrysource: 'gryph'when emitted from gryph-watch; provenance entries includeevidence: { source: 'gryph' }telemetry.tsrotation: 50MB threshold, keep 5 rotated filesopenlore telemetrypanic section: episodes, avg recovery latency, failed recovery rate, hook intercepts, MCP injections, orient spam, Gryph enrichment rate, trigger frequencySetup integration
openlore setup --hooks claude|kilo|codex— installs both PreToolUse panic-check hook and UserPromptSubmit gryph-watch hookopenlore setup --panic <mode>— setspanicResponse.modein.openlore/config.json; explicit downgrade always allowed--hooksor--panicused without--tools)Test plan
panic-response.test.ts: 29 tests — hysteresis, state I/O, fail-open, buildPanicCheckOutput, getPanicSignalTextepistemic-lease.test.ts: 86 tests — score accumulation, ceiling, orient spam, signal detection, refractory, localityConfidence formula, burst escalation gate, trackerToPanicStatetelemetry.test.ts: 24 tests — computePanicStats, computeRecovery, computeObstinacy with synthetic JSONL eventsgryph-bridge.test.ts: 19 tests — single-flight, stop cleanup, null stability, no-signal no-write, retry burst score update, large patch stale gating, large patch NOT stale (no delta), provenance source:gryph, exception fail-open, null tracker still writes file, score accumulation across polls (decay-aware)🤖 Generated with Claude Code