instar 0.28.52 → 0.28.53

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 (84) hide show
  1. package/dist/cli.js +40 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/gate.d.ts +33 -0
  4. package/dist/commands/gate.d.ts.map +1 -0
  5. package/dist/commands/gate.js +171 -0
  6. package/dist/commands/gate.js.map +1 -0
  7. package/dist/commands/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +29 -2
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/core/MessageSentinel.d.ts +40 -0
  11. package/dist/core/MessageSentinel.d.ts.map +1 -1
  12. package/dist/core/MessageSentinel.js +80 -0
  13. package/dist/core/MessageSentinel.js.map +1 -1
  14. package/dist/core/PostUpdateMigrator.d.ts +7 -0
  15. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.js +114 -0
  17. package/dist/core/PostUpdateMigrator.js.map +1 -1
  18. package/dist/core/SessionManager.d.ts +29 -0
  19. package/dist/core/SessionManager.d.ts.map +1 -1
  20. package/dist/core/SessionManager.js +104 -1
  21. package/dist/core/SessionManager.js.map +1 -1
  22. package/dist/core/StopGateDb.d.ts +97 -0
  23. package/dist/core/StopGateDb.d.ts.map +1 -0
  24. package/dist/core/StopGateDb.js +282 -0
  25. package/dist/core/StopGateDb.js.map +1 -0
  26. package/dist/core/UnjustifiedStopGate.d.ts +145 -0
  27. package/dist/core/UnjustifiedStopGate.d.ts.map +1 -0
  28. package/dist/core/UnjustifiedStopGate.js +325 -0
  29. package/dist/core/UnjustifiedStopGate.js.map +1 -0
  30. package/dist/core/WorktreeKeyVault.d.ts +50 -0
  31. package/dist/core/WorktreeKeyVault.d.ts.map +1 -0
  32. package/dist/core/WorktreeKeyVault.js +242 -0
  33. package/dist/core/WorktreeKeyVault.js.map +1 -0
  34. package/dist/core/WorktreeManager.d.ts +250 -0
  35. package/dist/core/WorktreeManager.d.ts.map +1 -0
  36. package/dist/core/WorktreeManager.js +833 -0
  37. package/dist/core/WorktreeManager.js.map +1 -0
  38. package/dist/monitoring/DegradationReporter.d.ts +15 -0
  39. package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
  40. package/dist/monitoring/DegradationReporter.js +27 -0
  41. package/dist/monitoring/DegradationReporter.js.map +1 -1
  42. package/dist/monitoring/WorktreeReaper.d.ts +52 -0
  43. package/dist/monitoring/WorktreeReaper.d.ts.map +1 -0
  44. package/dist/monitoring/WorktreeReaper.js +199 -0
  45. package/dist/monitoring/WorktreeReaper.js.map +1 -0
  46. package/dist/scaffold/templates.d.ts.map +1 -1
  47. package/dist/scaffold/templates.js +8 -0
  48. package/dist/scaffold/templates.js.map +1 -1
  49. package/dist/server/AgentServer.d.ts +18 -0
  50. package/dist/server/AgentServer.d.ts.map +1 -1
  51. package/dist/server/AgentServer.js +24 -0
  52. package/dist/server/AgentServer.js.map +1 -1
  53. package/dist/server/middleware.d.ts.map +1 -1
  54. package/dist/server/middleware.js +20 -3
  55. package/dist/server/middleware.js.map +1 -1
  56. package/dist/server/routes.d.ts +10 -0
  57. package/dist/server/routes.d.ts.map +1 -1
  58. package/dist/server/routes.js +432 -0
  59. package/dist/server/routes.js.map +1 -1
  60. package/dist/server/stopGate.d.ts +77 -0
  61. package/dist/server/stopGate.d.ts.map +1 -0
  62. package/dist/server/stopGate.js +161 -0
  63. package/dist/server/stopGate.js.map +1 -0
  64. package/dist/server/worktreeRoutes.d.ts +48 -0
  65. package/dist/server/worktreeRoutes.d.ts.map +1 -0
  66. package/dist/server/worktreeRoutes.js +247 -0
  67. package/dist/server/worktreeRoutes.js.map +1 -0
  68. package/package.json +1 -1
  69. package/scripts/destructive-command-shim.js +218 -0
  70. package/scripts/gh-ruleset-install.mjs +143 -0
  71. package/scripts/migrate-incident-2026-04-17.mjs +138 -0
  72. package/scripts/worktree-commit-msg-hook.js +168 -0
  73. package/scripts/worktree-precommit-gate.js +144 -0
  74. package/src/data/builtin-manifest.json +93 -93
  75. package/upgrades/0.28.53.md +70 -0
  76. package/upgrades/side-effects/context-death-pr0a-server-infra.md +131 -0
  77. package/upgrades/side-effects/context-death-pr0b-sentinel-intent.md +130 -0
  78. package/upgrades/side-effects/context-death-pr0c-guardian-pulse-degradation-consumer.md +118 -0
  79. package/upgrades/side-effects/context-death-pr0d-e2e-compaction-harness.md +113 -0
  80. package/upgrades/side-effects/context-death-pr1-identity-text.md +110 -0
  81. package/upgrades/side-effects/context-death-pr2-e2e-compaction-recovery-test.md +82 -0
  82. package/upgrades/side-effects/context-death-pr3-gate-authority.md +193 -0
  83. package/upgrades/side-effects/context-death-pr4-gate-cli.md +91 -0
  84. package/upgrades/side-effects/parallel-dev-isolation.md +129 -0
@@ -0,0 +1,70 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: minor -->
4
+
5
+ ## What Changed
6
+
7
+ ### Context-Death Pitfall Prevention (shadow mode)
8
+
9
+ Agents will no longer self-terminate mid-plan citing "context death" when context remains to complete the work. This release ships the full detection + authority infrastructure in **shadow mode** (observing, never blocking). The enforce-flip requires ≥14 days of shadow telemetry + ≥20 operator annotations from ≥2 distinct reviewers, per spec design.
10
+
11
+ **What the feature does:** When a Claude 4.7 agent attempts to stop mid-task with "drift to continue-ping," "fresh session," "compaction imminent," or similar unjustified self-termination language, a server-side LLM authority (`UnjustifiedStopGate`) evaluates whether the stop is justified. In shadow mode it only logs the decision; in enforce mode it would return a reminder nudging the agent to continue or produce concrete evidence.
12
+
13
+ **Ships inert by default.** `mode='off'` short-circuits every evaluation. No change in runtime behavior until an operator explicitly runs `instar gate set unjustified-stop --mode shadow`.
14
+
15
+ **New surfaces:**
16
+
17
+ - **Server routes** (all require bearer auth):
18
+ - `GET /internal/stop-gate/hot-path` — mode + kill-switch + session state for the Stop hook
19
+ - `POST /internal/stop-gate/evaluate` — decision-point authority call
20
+ - `GET/POST /internal/stop-gate/log` — evaluation log query + annotation
21
+ - `POST /internal/stop-gate/mode` — flip mode (`off`/`shadow`/`enforce`)
22
+ - `POST /internal/stop-gate/kill-switch` — emergency disable
23
+ - **CLI:** `instar gate {status,set,kill-switch,log}` for operators
24
+ - **Persistence:** `server-data/stop-gate.db` (SQLite, WAL, 0600 perms) — per-agent
25
+ - **Health:** `/health` now exposes `gateRouteVersion: 1`
26
+
27
+ **Security hardening:** `/internal/*` now enforces bearer auth on all paths (was previously bypassed on localhost — caught in second-pass review and hardened in the same PR).
28
+
29
+ **Per-session continue ceiling:** After a session issues 2 continue-pings within its lifetime, the 3rd is `force_allow`ed automatically + the session is marked stuck — prevents infinite nudge loops even with a buggy authority.
30
+
31
+ **Identity text hardening:** Fresh agents and existing agents on update get an explicit "context death is not real — if your window allows another turn, take it" marker block appended to `CLAUDE.md` and `AGENT.md`. Operators can pin the marker to a custom string via `.instar/identity-pins.json`.
32
+
33
+ **Compaction recovery test:** A new e2e harness (`tests/e2e/compaction-harness.ts`) spawns isolated Claude Code subprocesses with simulated compaction to verify the recovery hook re-injects identity correctly. A regression guard asserts the recovery output never contains "fresh session" or "start over" phrasings.
34
+
35
+ ## What to Tell Your User
36
+
37
+ Your agent has new protection against a subtle failure mode where Claude 4.7 models sometimes rationalize an unjustified stop mid-task (citing "context death" or "fresh session needed") when there's plenty of context left to finish. The protection is currently in observe-only mode — it's watching real Stop events and logging decisions to build confidence before any enforcement flips on. I can show you the evaluation log any time you want to see what it's learning.
38
+
39
+ ## Summary of New Capabilities
40
+
41
+ | Capability | How to Use |
42
+ |-----------|-----------|
43
+ | Shadow-mode gate observing Stop events | automatic on update; operator flips via gate CLI |
44
+ | Operator mode flip (off/shadow/enforce) | instar gate set unjustified-stop --mode MODE |
45
+ | Evaluation log + annotations | instar gate log |
46
+ | Emergency kill-switch | instar gate kill-switch --engage |
47
+ | Health-reported gate version | GET /health returns gateRouteVersion=1 |
48
+
49
+ ## Evidence
50
+
51
+ The spec (docs/specs/context-death-pitfall-prevention.md) was converged across 5 GPT/Gemini/Grok review rounds before implementation began. During Phase 5 second-pass review of PR3, an independent reviewer subagent identified a critical finding: /internal/* routes bypassed bearer auth when the request originated from localhost. This was hardened in-PR (see src/server/middleware.ts in commit 42cb9ee) — bearer token is now required on every /internal/* path regardless of origin; X-Forwarded-For headers are also rejected to prevent header-spoofing from a trusted proxy.
52
+
53
+ Reproduction of the auth-bypass issue pre-hardening:
54
+ - Before: curl against /internal/stop-gate/hot-path from localhost returned 200 with gate state regardless of auth header.
55
+ - After: same request without a bearer token returns 401 Unauthorized; requests with forged X-Forwarded-For headers are also rejected.
56
+
57
+ The positive side of the gate (actually blocking an unjustified stop in enforce mode) is **not reproducible in dev** — it requires a real Claude 4.7 session emitting an unjustified stop decision against a live authority. This is exactly why the spec hard-gates the enforce flip on ≥14d of shadow telemetry + ≥20 operator annotations from ≥2 distinct reviewers. Shadow-mode observation over the coming weeks will produce the real-world evidence needed before any enforcement is enabled.
58
+
59
+ Unit + integration + e2e test coverage: 128+ tests across PR0a–PR4 covering gate routes, DB persistence, sentinel intent classification, guardian-pulse degradation consumption, identity text migration, compaction recovery, and the operator CLI. All green on PR #56 CI (run 24601956119).
60
+
61
+ ## Deployment Notes
62
+
63
+ - No operator action required on update. The gate is off by default.
64
+ - To activate shadow on your agent after update: instar gate set unjustified-stop --mode shadow.
65
+ - The enforce flip (--mode enforce) is available via CLI but is not recommended until the spec's data gate (≥14d shadow + ≥20 annotations from ≥2 operators) is met. A threshold-enforcement guard for the CLI itself is tracked as a follow-up.
66
+ - Kill switch: instar gate kill-switch --engage disables all evaluation immediately regardless of mode.
67
+
68
+ ## Rollback
69
+
70
+ Downgrading to the previous version removes the gate routes and CLI. Existing stop-gate.db files become orphaned but harmless (≤ few KB, can be deleted manually). No other state is touched.
@@ -0,0 +1,131 @@
1
+ # Side-Effects Review — Context-Death PR0a (server infra)
2
+
3
+ **Version / slug:** `context-death-pr0a-server-infra`
4
+ **Date:** `2026-04-17`
5
+ **Author:** `Echo (instar-developing agent)`
6
+ **Spec:** `docs/specs/context-death-pitfall-prevention.md`
7
+ **Phase / PR sequence position:** PR0a of 8 (PR0a, PR0b, PR0c, PR0d, PR1, PR2, PR3, PR4-shadow, PR5-enforce-gated)
8
+ **Second-pass reviewer:** `not-required` (no decision-point logic; pure read-side plumbing — see Phase 5 criteria below)
9
+
10
+ ## Summary of the change
11
+
12
+ Adds the read-side server API surface that PR3's stop-hook router will consume. No agent behavior changes; no decisions are made by anything in this PR. State is in-memory and defaults are inert (`mode='off'` ships completely silent).
13
+
14
+ Files touched:
15
+
16
+ - **`src/server/stopGate.ts`** (NEW) — module exporting:
17
+ - Version contract constants `GATE_ROUTE_VERSION = 1`, `GATE_ROUTE_MINIMUM_VERSION = 1` (P0.7).
18
+ - In-memory holders for `mode`, `killSwitch`, and `sessionStartTs` map (PR3 migrates to SQLite).
19
+ - `compactionInFlight()` probe (P0.6) — checks `/tmp/claude-session-<id>/compacting` marker, falls back to `compaction-recovery.sh` mtime ≤ 60s.
20
+ - `getHotPathState({sessionId})` — single function returning the five fields the future hook needs in one call.
21
+ - **`src/server/routes.ts`** (MOD) — adds:
22
+ - Import of stopGate module.
23
+ - `gateRouteVersion` + `gateRouteMinimumVersion` exposed in `/health` (always-on, alongside `status`/`uptime` — hook-lib needs them before sending the auth token).
24
+ - `GET /internal/stop-gate/hot-path?session=<id>` — calls `getHotPathState()`, returns JSON.
25
+ - `GET /internal/stop-gate/kill-switch` — returns current value.
26
+ - `POST /internal/stop-gate/kill-switch` — sets value (boolean), returns `{killSwitch, prior, changed}`.
27
+ - SessionStart capture inside the existing `/hooks/events` handler — when `event === 'SessionStart'` arrives, calls `recordSessionStart(sessionId, Date.now())`. Idempotent (first wins).
28
+ - **`tests/unit/stopGate.test.ts`** (NEW) — 17 unit tests covering version constants, mode/killSwitch state, sessionStartTs idempotency, the compaction probe (fresh/stale/missing/marker-file paths), and `getHotPathState` field assembly.
29
+ - **`tests/unit/routes-stopGate.test.ts`** (NEW) — 11 route-level tests using the existing `routes-prGatePhaseGate.test.ts` pattern (mounts handlers on a minimal Express app, exercises GET/POST shape, validates 400 for non-boolean killSwitch).
30
+
31
+ ## Decision-point inventory
32
+
33
+ The spec's signal-vs-authority audit table (line 463 of the spec) explicitly catalogs the gate's decision-points: kill-switch and compaction-probe are **structural routing**, not judgment. The LLM authority and the per-session continue ceiling are PR3's responsibilities.
34
+
35
+ This PR contains:
36
+ - Kill-switch toggle endpoint (POST) — **structural**: stores a boolean. Caller's intent is opaque to the endpoint; semantic interpretation lives in PR3's router. Not a decision point.
37
+ - Hot-path read endpoint (GET) — **structural**: assembles state. Returns whatever's stored. Not a decision point.
38
+ - SessionStart timestamp capture — **passive**: records a timestamp on an event arriving via the existing hook-events pipeline. Not a decision point.
39
+ - Compaction probe — **structural**: reads two filesystem signals (marker file + script mtime), returns boolean. Per spec § P0.6: "best-effort signal". Not a decision point — PR3's router consumes it as a routing hint.
40
+
41
+ No decision-point logic introduced in PR0a. The signal-vs-authority principle (`docs/signal-vs-authority.md`) is therefore not engaged here; PR3 will be the engagement point.
42
+
43
+ ---
44
+
45
+ ## 1. Over-block
46
+
47
+ **What legitimate inputs does this change reject that it shouldn't?**
48
+
49
+ PR0a does not block anything. The `mode='off'` default means the future hook reading these endpoints exits at line 156 of the spec's router pseudocode (`if HOT_PATH.mode == "off": exit 0`). Even the kill-switch endpoint's only consumer (the future hook) treats killSwitch=true as fail-open.
50
+
51
+ The single 400 response — `POST /internal/stop-gate/kill-switch` with non-boolean `value` — is correct rejection of malformed input, not over-block. Tested in `routes-stopGate.test.ts`.
52
+
53
+ ## 2. Under-block
54
+
55
+ **What failure modes does this still miss?**
56
+
57
+ PR0a is a read-side surface; "under-block" applies to PR3's router. However, two near-side risks worth naming:
58
+
59
+ - **Compaction probe is best-effort.** The spec's P0.6 explicitly accepts this (`compaction_in_flight()` is a hint, not a guarantee). False negatives during a real compaction can cause the eventual gate to evaluate when it shouldn't — but the gate's own evaluator already fails open under timeout/malformed responses (PR3 design).
60
+ - **In-memory state is per-server-process.** A server restart wipes `sessionStartTs` and `killSwitch`. The hot-path will then return `sessionStartTs: null`, which spec § (b) — "Fallback (I208 + R5 iter-4 fix)" — handles by exiting 0 with a one-time `DegradationReport`. So this is a *known and accepted* failure mode that the spec covers; no new blast radius. PR3's SQLite migration removes the restart-loss for `sessionStartTs`; kill-switch persistence lands with PR4 alongside the registry-file CLI.
61
+
62
+ ## 3. Level-of-abstraction fit
63
+
64
+ **Is this at the right layer? Should a higher or lower layer own it?**
65
+
66
+ Yes. The hot-path endpoint is the right layer because:
67
+
68
+ - Server is the single point that all hook invocations across all agent sessions hit anyway (existing `/hooks/events`, `/internal/compaction-resume` precedent).
69
+ - The state it returns (mode, killSwitch, autonomousActive, compactionInFlight, sessionStartTs) is heterogeneous — three from server config/memory, one from filesystem heuristic, one from session-event history. Computing them in the hook itself would mean five separate filesystem reads per Stop event, which the spec explicitly rules out (SC200/SC201 in the iteration findings).
70
+ - The `getHotPathState` module function is consumable both by the HTTP route and by future in-process callers (e.g., the dashboard tab in PR3) without re-implementing the assembly.
71
+
72
+ The constants `GATE_ROUTE_VERSION` / `GATE_ROUTE_MINIMUM_VERSION` could arguably live in `package.json` or a top-level `version-contracts.ts`. Co-locating them with the gate module is preferable: the contract is gate-internal, not a global API version.
73
+
74
+ ## 4. Signal vs authority compliance
75
+
76
+ Per `docs/signal-vs-authority.md`: detectors emit signals, only authorities can block. PR0a contains zero authorities. The kill-switch is operator-set state that the future router consumes as override; the compaction probe is a structural hint; the hot-path read is a state assembly call. None of these make a "should this Stop event be blocked or allowed" decision — that decision lives in PR3's `UnjustifiedStopGate` LLM authority.
77
+
78
+ The principle is satisfied vacuously here — the relevant compliance gate fires in PR3.
79
+
80
+ ## 5. Interactions
81
+
82
+ **Does this shadow another check, get shadowed by one, double-fire, race with adjacent cleanup?**
83
+
84
+ - **`/hooks/events` handler** — extended to record `SessionStart` timestamps. The extension runs *before* `hookEventReceiver.receive(payload)`. Failure to record (no sessionId in payload) is a silent skip; failure of the receiver is unchanged. No timing race because the new code accesses no shared mutable state outside of `stopGate`'s own Map.
85
+ - **`/health`** — added two fields. Existing consumers (curl scripts, dashboard) will silently ignore unknown fields (JSON-tolerant). No risk of breaking `degradations.length`-based logic.
86
+ - **Existing `/internal/compaction-resume` endpoint** — independent path; no overlap with the new compaction probe (probe is read-side hint, the resume endpoint is write-side recovery trigger).
87
+ - **No existing kill-switch surface** to collide with; `prGate.phase='off'` lives on a different namespace (`/pr-gate/*` not `/internal/stop-gate/*`).
88
+ - **Test isolation** — the `_resetForTests()` export is called in `beforeEach` for both new test files; in-memory state cannot leak across tests.
89
+
90
+ ## 6. External surfaces
91
+
92
+ **Does this change anything visible to other agents, other users, other systems?**
93
+
94
+ - New endpoints under `/internal/stop-gate/*` — namespace conventionally local-only (matches `/internal/compaction-resume`'s precedent). Cloudflare tunnel exposure: these endpoints will be tunnel-reachable like all other `/internal/*` routes; auth middleware applies (existing pattern). Drift-correction threat model accepts this (spec § "Threat model").
95
+ - `/health` response gains `gateRouteVersion`/`gateRouteMinimumVersion` fields. Always-on by design — hook-lib reads them before having an auth token. Backwards-compatible; no breaking change to existing `/health` consumers.
96
+ - No changes to outbound messaging, dispatch, session lifecycle, coherence, or trust state.
97
+ - No filesystem writes outside of test temp dirs.
98
+
99
+ ## 7. Rollback cost
100
+
101
+ **If this turns out wrong in production, what's the back-out?**
102
+
103
+ Trivial. Revert the commit:
104
+ - Removes the new file `src/server/stopGate.ts`.
105
+ - Removes ~30 lines from `src/server/routes.ts` (import block, two health fields, three routes, one if-block in `/hooks/events`).
106
+ - Removes the two test files.
107
+
108
+ No data migration. No agent-state repair. No cross-machine coordination needed — the in-memory state evaporates on revert-and-restart. Total rollback time: one `git revert` + one server restart (~30s).
109
+
110
+ This minimal cost reflects the deliberate scoping: PR0a is the smallest possible foundation to unblock PR0b–PR3 development without introducing any user-visible change. Subsequent PRs whose rollback cost grows (PR3's SQLite migration, PR4's CLI flip) will carry their own artifacts with proportionate rollback discussion.
111
+
112
+ ---
113
+
114
+ ## Tests
115
+
116
+ - `tests/unit/stopGate.test.ts` — 17 tests, all passing.
117
+ - `tests/unit/routes-stopGate.test.ts` — 11 tests, all passing.
118
+ - `npm run lint` (tsc --noEmit) — clean.
119
+ - Integration into the full `vitest.push.config.ts` suite happens at pre-push.
120
+
121
+ ## Phase 5 second-pass review criterion check
122
+
123
+ `/instar-dev` Phase 5 lists these triggers for required second-pass review:
124
+
125
+ - Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (PR0a is read-side only).
126
+ - Session lifecycle: spawn, restart, kill, recovery — **no** (kill-switch sets a boolean; the kill action lives in PR3's router consuming the boolean).
127
+ - Context exhaustion, compaction, respawn — **probe only**, no consumption logic in this PR.
128
+ - Coherence gates, idempotency checks, trust levels — **no**.
129
+ - Anything with the word "sentinel," "guard," "gate," or "watchdog" in it — **the module is named `stopGate`**, but Phase 5's intent is to gate decision-point logic, not naming. The decision-point logic is PR3.
130
+
131
+ PR3 will require Phase 5 second-pass review (router + LLM authority touch all of: block/allow on Stop, compaction handling, the gate decision point itself).
@@ -0,0 +1,130 @@
1
+ # Side-Effects Review — Context-Death PR0b (Sentinel three-way intent)
2
+
3
+ **Version / slug:** `context-death-pr0b-sentinel-intent`
4
+ **Date:** `2026-04-17`
5
+ **Author:** `Echo (instar-developing agent)`
6
+ **Spec:** `docs/specs/context-death-pitfall-prevention.md` § P0.4
7
+ **Phase / PR sequence position:** PR0b of 8
8
+ **Second-pass reviewer:** `not-required` (no decision-point logic; pure side-channel signal added to existing classification result — see Phase 5 criteria below)
9
+
10
+ ## Summary of the change
11
+
12
+ Adds a three-way intent classifier for "continue ping" user messages, attached as a side-channel field on every `MessageSentinel.classify()` result. No existing classification semantics change; no new blocking decisions; no new emergency paths. The only consumer of this signal is PR3's gate-quality telemetry, which lands later — until then the field is computed and discarded by current callers.
13
+
14
+ Files touched:
15
+
16
+ - **`src/core/MessageSentinel.ts`** (MOD) — adds:
17
+ - New exported type `ContinuePingIntent = 'intent_a' | 'intent_b' | 'intent_c'`.
18
+ - New optional field `continuePingIntent?: ContinuePingIntent | null` on `SentinelClassification`.
19
+ - Pure-function classifier `classifyContinuePingIntent(message): ContinuePingIntent | null` (regex-only, <1ms).
20
+ - Wiring inside `classify()` to populate the field on every return path (fast-path, LLM, default, and disabled cases).
21
+ - **`tests/unit/MessageSentinel-continue-ping-intent.test.ts`** (NEW) — 50 tests covering:
22
+ - Non-continue-pings return null (8 cases).
23
+ - 50-word ceiling override.
24
+ - intent_a (pure resume) — 17 vocabularies including "continue", "yes", "yes please", "ok continue", "do it", "carry on", "yep", "yeah keep going", "yes proceed with the deployment".
25
+ - intent_b (additive new requirement) — 7 phrasings: "and also", "additionally", "now also", "don't forget to", "while you're at it", "on top of that", "next do".
26
+ - intent_c (verify/clarify) — 9 patterns: trailing "?", question-word starts ("why", "how", "did you", "can you explain"), explicit clarify words ("clarify", "verify", "double-check", "confirm").
27
+ - Priority: intent_c wins over intent_b when both signals present (operator seeking info > operator adding scope).
28
+ - Integration into `MessageSentinel.classify()` — field populated correctly on every `category` value, including emergency-stop and disabled-sentinel cases.
29
+
30
+ Existing `tests/unit/MessageSentinel.test.ts` (64 tests) continues to pass — backward-compatible additive change.
31
+
32
+ ## Decision-point inventory
33
+
34
+ The classifier produces a signal, never an action. It does not gate inbound messages, does not change the outbound `SentinelAction`, and does not influence whether the existing categories (emergency-stop, pause, redirect, normal) fire. The only field touched on the result is the new `continuePingIntent` slot.
35
+
36
+ This is a textbook signal-vs-authority compliance: detector produces a label; downstream gate (PR3) decides what to do with it. Per the principle (`docs/signal-vs-authority.md`), the classifier carries no blocking authority.
37
+
38
+ ---
39
+
40
+ ## 1. Over-block
41
+
42
+ **What legitimate inputs does this change reject that it shouldn't?**
43
+
44
+ Nothing is rejected. The change is purely additive — a new field on the classification result. Existing callers (TelegramAdapter routing decisions, etc.) are typed against the wider `SentinelClassification` interface and ignore unknown/unexpected fields.
45
+
46
+ The classifier itself can over-attribute intent (e.g., calling something intent_a that the operator meant as intent_c). This affects the *quality* of the gate-telemetry signal, not the *behavior* of any user-facing surface. Worst case under PR0b alone: the new field is wrong, no one notices because no one consumes it yet. Worst case after PR3 lands: the gate-quality SLO drifts mildly until the classifier is tuned.
47
+
48
+ ## 2. Under-block
49
+
50
+ **What failure modes does this still miss?**
51
+
52
+ - **Multilingual continue-pings.** Patterns are English-only ("continue", "yes", "go ahead"). A Spanish "continuar por favor" returns null. Acceptable for v1 — Justin's environment is English.
53
+ - **Slang / paraphrase.** "Send it", "ship it", "lgtm" — none are in the resume-shape token set. Returns null. Acceptable: false negatives leak the gate-quality signal lower (fewer intent_a counts), not higher.
54
+ - **Sarcastic continue.** "Oh yeah, GREAT idea — go ahead 🙄" — classifier sees "go ahead" and the question-word "great" doesn't match → intent_a. Side-channel signal is wrong but no real-world harm.
55
+
56
+ The choice to be conservative on what counts as a continue-ping (must match a token from the bounded set) means the false-positive rate is low and the false-negative rate is bounded by the vocabulary. PR3's gate-quality SLO will surface drift if the vocabulary needs expansion.
57
+
58
+ ## 3. Level-of-abstraction fit
59
+
60
+ **Is this at the right layer?**
61
+
62
+ Yes. Continue-ping intent is a property *of the inbound message text*, before any session context is involved. MessageSentinel is the universal classifier for inbound messages and already runs the equivalent regex-fast-path / LLM-fallback pattern. Co-locating the intent classifier here:
63
+
64
+ - Avoids re-tokenizing the message in PR3's gate.
65
+ - Reuses the existing test infrastructure pattern (`MessageSentinel.test.ts`).
66
+ - Keeps the two pure-text classifiers in one file (current category classifier + new intent classifier) for easy diff-comparison across reviews.
67
+
68
+ Could it live in PR3's gate module? Technically yes, but then PR3 would need to import MessageSentinel just to get the message text plus do its own classification, doubling the work. The chosen location is the lowest-effort coupling.
69
+
70
+ ## 4. Signal vs authority compliance
71
+
72
+ `docs/signal-vs-authority.md`: detectors emit signals, only authorities can block. The intent classifier emits one of four labels (`intent_a` / `intent_b` / `intent_c` / `null`). Zero blocking. Zero side-effects. Zero coupling to anything that decides — PR0b is plumbing for a downstream consumer.
73
+
74
+ The principle compliance check fires fully here: the classifier is a *pure detector* with explicit downstream-consumer-only semantics. The PR3 review will check that the consumer (gate-quality telemetry) doesn't covertly elevate this signal into an authority.
75
+
76
+ ## 5. Interactions
77
+
78
+ **Does this shadow another check, get shadowed by one, double-fire, race with adjacent cleanup?**
79
+
80
+ - **Existing `category` classification** — runs unchanged. Intent runs in *parallel* on the same input, then the result is stitched together. No race because both are pure functions of `message`.
81
+ - **Existing fast-path / LLM split** — both paths now populate `continuePingIntent` from the same source-of-truth pure function. No risk of fast-path / LLM divergence.
82
+ - **Sentinel stats** — `recordStats` not called for continue-ping intent (it's not a category dimension, it's a side-channel). `byCategory` and `byMethod` counters unchanged. No double-count.
83
+ - **Sentinel `enabled: false` short-circuit** — explicitly returns `continuePingIntent: null`. The disabled-Sentinel path doesn't run the classifier (defensive: a disabled Sentinel should produce no signals).
84
+ - **Future consumer (PR3 gate-quality)** — will read `continuePingIntent` off the `SentinelClassification` result. No interaction in this PR.
85
+
86
+ ## 6. External surfaces
87
+
88
+ **Does this change anything visible to other agents, other users, other systems?**
89
+
90
+ - New exported type `ContinuePingIntent` and new exported function `classifyContinuePingIntent` on `MessageSentinel` module. Public API additions, backward-compatible.
91
+ - New optional field on `SentinelClassification` type. Existing TS callers will see the new field but type-checking continues to compile (optional field is a type-narrow, not a contract change).
92
+ - No changes to:
93
+ - HTTP routes
94
+ - Telegram message routing
95
+ - Slack/iMessage/WhatsApp adapter behavior
96
+ - Session lifecycle
97
+ - Outbound dedup gate
98
+ - Coherence checks
99
+ - Trust state
100
+
101
+ ## 7. Rollback cost
102
+
103
+ **If this turns out wrong in production, what's the back-out?**
104
+
105
+ Trivial. Revert the commit:
106
+ - Removes the new exported type, function, and field.
107
+ - Removes wiring in `MessageSentinel.classify()`.
108
+ - Removes the test file.
109
+
110
+ No runtime state to repair. No external consumers to coordinate with (PR3's consumer doesn't ship in this PR). No downtime. Total rollback time: one `git revert` + one server restart (~30s).
111
+
112
+ If only the classifier behavior is bad (false-positive rate too high), the cheaper fix is to tighten the regex patterns in-place or set `continuePingIntent` to always-null while keeping the field for type stability. Both are local-edit-only changes.
113
+
114
+ ---
115
+
116
+ ## Tests
117
+
118
+ - `tests/unit/MessageSentinel-continue-ping-intent.test.ts` — 50 tests, all passing.
119
+ - `tests/unit/MessageSentinel.test.ts` — 64 existing tests, all passing (no regressions).
120
+ - `npm run lint` — clean.
121
+
122
+ ## Phase 5 second-pass review criterion check
123
+
124
+ - Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (no decisions added).
125
+ - Session lifecycle: spawn, restart, kill, recovery — **no**.
126
+ - Context exhaustion, compaction, respawn — **adjacent, but not touched here** (this PR feeds PR3's gate-quality metric; the gate decision itself is PR3 and gets second-pass review there).
127
+ - Coherence gates, idempotency checks, trust levels — **no**.
128
+ - Anything with "sentinel," "guard," "gate," or "watchdog" — **the file is `MessageSentinel.ts`**. Phase 5's intent is to gate decision-point changes; this PR adds a non-decision label field. PR3 (the consumer) is the right gate point.
129
+
130
+ PR3 will require Phase 5 second-pass review.
@@ -0,0 +1,118 @@
1
+ # Side-Effects Review — Context-Death PR0c (guardian-pulse degradation consumer)
2
+
3
+ **Version / slug:** `context-death-pr0c-guardian-pulse-degradation-consumer`
4
+ **Date:** `2026-04-18`
5
+ **Author:** `Echo (instar-developing agent)`
6
+ **Spec:** `docs/specs/context-death-pitfall-prevention.md` § (d), DegradationReport consumer
7
+ **Phase / PR sequence position:** PR0c of 8
8
+ **Second-pass reviewer:** `not-required` (no decision-point logic; the new endpoint flips a boolean by exact-match or regex; the skill template is a documentation update — see Phase 5 criteria below)
9
+
10
+ ## Summary of the change
11
+
12
+ Closes the loop for DegradationReports that aren't auto-routed by the FeedbackManager / Telegram alerter. Spec § (d) calls this out as a PR0 precondition: the future stop-gate emits DegradationReports for timeout/malformed/etc. failures, and unless guardian-pulse actively surfaces and acknowledges them, they pile up silently in `degradation-events.json`. PR0c adds the missing consumer surface.
13
+
14
+ Files touched:
15
+
16
+ - **`src/monitoring/DegradationReporter.ts`** (MOD) — adds `markReported(featurePattern: string | RegExp): number`. Returns count actually flipped. Idempotent (already-reported events are not double-counted). Pattern can be exact-string or regex for prefix/suffix matching.
17
+ - **`src/server/routes.ts`** (MOD) — adds `POST /health/degradations/mark-reported` route. Body: either `{feature: string}` (exact match) OR `{featurePattern: string}` (regex source). Returns `{flipped: number}`. 400 on missing body, invalid regex.
18
+ - **`src/commands/init.ts`** (MOD) — extends the embedded guardian-pulse skill template § "4. Degradation Reporter Health" with the active-consumption procedure. Instructions now: read `/health/degradations`, surface each unreported event to `/attention` with stable id `degradation:{feature}:{timestamp}` (idempotent re-runs), then `POST /health/degradations/mark-reported` to close the loop. Explicit fail-safe: if `/attention` POST fails, do NOT call mark-reported — leave the event for the next pulse.
19
+ - **`tests/unit/degradation-reporter-mark-reported.test.ts`** (NEW) — 6 unit tests: exact-match flip, no-match returns 0, idempotency, regex multi-match, no-match regex returns 0, doesn't double-flip auto-pipeline-reported events.
20
+ - **`tests/unit/routes-degradations-mark-reported.test.ts`** (NEW) — 5 route tests: exact match, regex multi-match, missing body 400, invalid regex 400, idempotent re-flip.
21
+
22
+ Existing `tests/unit/degradation-reporter.test.ts` (8 tests) continues to pass.
23
+
24
+ ## Decision-point inventory
25
+
26
+ The new endpoint flips a boolean. It does not gate any agent behavior, does not block any session, does not control any outbound message. The guardian-pulse skill update is a procedure-extension document — it tells operators (or autonomous agents running the skill) what *to do*, not what *to allow*.
27
+
28
+ There are zero decision-points introduced. The principle compliance gate is vacuous here.
29
+
30
+ ---
31
+
32
+ ## 1. Over-block
33
+
34
+ **What legitimate inputs does this change reject that it shouldn't?**
35
+
36
+ The endpoint's two 400 responses:
37
+ - Missing both `feature` and `featurePattern` — correct rejection of malformed input.
38
+ - Invalid `featurePattern` regex — correct rejection (would otherwise crash the server with an unhandled `SyntaxError`).
39
+
40
+ Both are tested. Neither over-blocks any legitimate caller.
41
+
42
+ The `markReported` method itself accepts any string pattern, so an over-broad pattern (`/.+/`) would flip every unreported event. That's the operator's call — over-broad input is an operator decision, not the function being wrong. Documentation in the guardian-pulse skill template steers callers to specific feature names.
43
+
44
+ ## 2. Under-block
45
+
46
+ **What failure modes does this still miss?**
47
+
48
+ - **Race between attention POST and mark-reported.** If the attention POST succeeds but the mark-reported POST fails (network drop), the event is in attention queue but still listed as unreported — next pulse will re-surface it as a duplicate attention item. The skill template uses a stable id (`degradation:{feature}:{timestamp}`) for idempotency, so the duplicate POST hits the existing attention item by id. Acceptable: dedup is end-to-end via id reuse, not transactional.
49
+ - **Multiple events for the same feature.** `markReported('feature')` flips ALL unreported events for that feature in one shot. If only some have been surfaced and others are pending, this is wrong. Mitigation: the spec's degradation-events typically use distinct feature names per failure-mode (`unjustifiedStopGate.timeout` vs `unjustifiedStopGate.malformed` etc.), so the per-event distinction is preserved.
50
+
51
+ Neither of these is a context-death-specific concern; both apply to any caller using mark-reported.
52
+
53
+ ## 3. Level-of-abstraction fit
54
+
55
+ **Is this at the right layer? Should a higher or lower layer own it?**
56
+
57
+ DegradationReporter is the right home for `markReported` — it owns the event lifecycle, has the existing reported flag, and other consumers (built-in feedback / Telegram alerter) already mutate the same flag. Co-locating the manual-mark path keeps mutation invariants in one place.
58
+
59
+ The HTTP route belongs alongside `/health/degradations` (next to it in the route file) — same namespace, same auth, same logical surface.
60
+
61
+ The skill template extension lives in `src/commands/init.ts` because that's where the templates are defined. Could it move to a separate `templates/skills/` directory? Yes — but that's a broader refactor (every skill template lives there), not a context-death PR0c concern. Keeping the extension in-place avoids scope creep.
62
+
63
+ ## 4. Signal vs authority compliance
64
+
65
+ `docs/signal-vs-authority.md`: detectors emit signals; only authorities can block.
66
+
67
+ `markReported` is a state-mutator on a *signal store*. It does not block anything. It does not decide anything. It is a passive close-the-loop helper for downstream consumers.
68
+
69
+ The guardian-pulse skill itself is a *consumer* of signals (degradations) that surfaces them to operators via attention queue. Surfacing is also not a block — operators decide what to do with attention items.
70
+
71
+ Compliance is satisfied vacuously: no authority introduced, only mutation primitives for an existing signal queue.
72
+
73
+ ## 5. Interactions
74
+
75
+ **Does this shadow another check, get shadowed by one, double-fire, race with adjacent cleanup?**
76
+
77
+ - **Auto-pipeline (FeedbackManager / Telegram alerter)** — both paths flip `event.reported` to `true` via direct field mutation in `reportEvent()`. `markReported` checks `!event.reported` before flipping, so an auto-flipped event won't be double-counted. Tested explicitly.
78
+ - **`/health/degradations` GET** — read-only; no race with the mark-reported POST. Successive reads after a flip will show fewer unreported events. Expected behavior.
79
+ - **`degradation-events.json` persistence** — `markReported` updates the in-memory event objects but does NOT call `persistToDisk()`. **That's a known limitation worth flagging:** if the server restarts after a mark-reported POST but before any persistence, the flip is lost. Acceptable because: (a) the event will simply re-surface next pulse, (b) the attention item is already created with a stable id so it dedups, (c) avoiding additional disk writes preserves the existing atomic-write invariants in the file. If this becomes annoying in production, a follow-up can add explicit persistence — but it's not blocking PR0c shipping.
80
+ - **Attention queue dedup** — POST /attention creates an item with the caller-supplied `id`. The skill template uses `degradation:{feature}:{timestamp}` so re-runs idempotently produce the same id — no spam.
81
+ - **Skill template** — guardian-pulse runs every 8 hours by default (per `slug: 'guardian-pulse'` schedule `'0 */8 * * *'`). Higher frequency would re-surface stale degradations more often; lower frequency means slower discovery of new degradations. Current cadence is unchanged.
82
+
83
+ ## 6. External surfaces
84
+
85
+ - New HTTP route under existing `/health/*` namespace. Auth-middleware applies (existing pattern). Tunnel-reachable like all health routes.
86
+ - New exported method on DegradationReporter — backward-compatible additive change; no existing callers affected.
87
+ - Skill template changes will propagate to **new agent installations** via `instar init` and to **existing agents** via `instar upgrade --skills` (existing migration path). The change is additive (extends section 4), not destructive — operators who have customized their guardian-pulse skill will see a clean diff and can merge or skip.
88
+ - No changes to: outbound messaging, dispatch, session lifecycle, coherence, trust, or any feature outside the DegradationReporter / guardian-pulse loop.
89
+
90
+ ## 7. Rollback cost
91
+
92
+ Trivial. Three commits to revert (or one if all changes ship together):
93
+ - DegradationReporter method removal (~25 lines)
94
+ - Route removal (~30 lines)
95
+ - Skill template diff (~30 lines)
96
+
97
+ No data migration. No agent-state repair. Existing `degradation-events.json` files stay valid. Total rollback time: one `git revert` + one server restart (~30s).
98
+
99
+ If only the skill template is wrong, the cheapest fix is an in-place edit (no revert) — operators get the corrected version on next `instar upgrade --skills`.
100
+
101
+ ---
102
+
103
+ ## Tests
104
+
105
+ - `tests/unit/degradation-reporter-mark-reported.test.ts` — 6 tests, all passing.
106
+ - `tests/unit/routes-degradations-mark-reported.test.ts` — 5 tests, all passing.
107
+ - `tests/unit/degradation-reporter.test.ts` — 8 existing tests, all passing.
108
+ - `npm run lint` — clean.
109
+
110
+ ## Phase 5 second-pass review criterion check
111
+
112
+ - Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (no decisions added).
113
+ - Session lifecycle: spawn, restart, kill, recovery — **no**.
114
+ - Context exhaustion, compaction, respawn — **no** (this PR closes the *consumer* loop for any DegradationReport; the spec's gate is the *producer*, lands in PR3).
115
+ - Coherence gates, idempotency checks, trust levels — **no**.
116
+ - Anything with "sentinel," "guard," "gate," or "watchdog" — **the file is `DegradationReporter.ts`** (no decision logic) and the route is `/health/degradations/mark-reported` (state mutator). Phase 5's intent is to gate decision-point changes; PR0c adds none.
117
+
118
+ PR3 will require Phase 5 second-pass review.
@@ -0,0 +1,113 @@
1
+ # Side-Effects Review — Context-Death PR0d (E2E compaction harness)
2
+
3
+ **Version / slug:** `context-death-pr0d-e2e-compaction-harness`
4
+ **Date:** `2026-04-18`
5
+ **Author:** `Echo (instar-developing agent)`
6
+ **Spec:** `docs/specs/context-death-pitfall-prevention.md` § P0.1
7
+ **Phase / PR sequence position:** PR0d of 8
8
+ **Second-pass reviewer:** `not-required` (test infrastructure, no runtime decision logic — see Phase 5 criteria below)
9
+
10
+ ## Summary of the change
11
+
12
+ Ships the test harness that PR2 (and any future compaction-adjacent test) will stand on top of. Spec § P0.1 gates the whole spec on "tests/e2e/ can spawn a Claude Code subprocess, drive controlled turns, trigger compaction, and capture post-compaction context." PR0d satisfies that gate with a *capability proof*: the canonical `compaction-recovery.sh` hook is directly invocable from a deterministic test environment, and its stdout can be captured and asserted against.
13
+
14
+ Files touched:
15
+
16
+ - **`tests/e2e/compaction-harness.ts`** (NEW) — exports `createCompactionHarness(options)` returning a `CompactionHarnessHandle` with:
17
+ - `projectDir` / `stateDir` — isolated temp agent home.
18
+ - `writeFile(path, content, {commit?})` — seed plan files / ledger entries, optionally commit them so they're durable per the spec's "durable artifacts" invariant.
19
+ - `readFile(path)` — inspect state or hook output files.
20
+ - `setIdentity(name, content)` — overwrite AGENT/MEMORY/USER.
21
+ - `runCompactionRecovery(envOverrides)` — invoke the canonical hook with controlled env (`CLAUDE_PROJECT_DIR`, `INSTAR_TELEGRAM_TOPIC`), 10-second timeout, captures stdout/stderr/exitCode/durationMs.
22
+ - `tempPath(suffix)` — disposable temp paths inside the harness.
23
+ - `teardown()` — idempotent cleanup.
24
+ - `locateCanonicalHook(hookName)` — walks upward from `process.cwd()` looking for `src/templates/hooks/<hook>` (source-of-truth) first, then `.instar/hooks/instar/<hook>` (deployed fallback). Null if not found; harness throws a clear error at run-time rather than silently synthesizing a fake hook.
25
+ - **`tests/e2e/compaction-harness.test.ts`** (NEW) — 12 smoke tests:
26
+ - Setup shape (isolated agent home, identity files, `.instar/config.json` correctness).
27
+ - `agentName` / `memoryContent` option wiring.
28
+ - Git repo initialization + seed commit.
29
+ - Teardown idempotency.
30
+ - Canonical hook lookup (correct location, owner-executable).
31
+ - `runCompactionRecovery` capability: exits 0, produces stdout within 10s, contains structural recovery markers (`IDENTITY RECOVERY|RESTORATION`, `RECOVERY COMPLETE|Continue your work`).
32
+ - `INSTAR_TELEGRAM_TOPIC` env merge reaches the hook.
33
+ - **Spec-critical regression guard:** asserts recovery stdout does NOT contain "fresh session" / "start over" / "restart the session" — these are the exact phrasings that trigger the context-death self-stop pattern this spec is built to prevent. Future edits to the recovery template that introduce such language will fail this test.
34
+ - `writeFile` + commit path.
35
+ - Error surface: missing hook throws a clear error instead of silent failure.
36
+
37
+ ## Decision-point inventory
38
+
39
+ The harness does not make decisions. It does not gate behavior. It does not run in production. It is test infrastructure consumed only by other tests.
40
+
41
+ The only near-edge case is `locateCanonicalHook` — which determines which copy of `compaction-recovery.sh` the harness uses. That's a lookup, not a decision: it prefers the source-of-truth in `src/templates/hooks/` and falls back to the deployed agent copy. Both are legitimate sources; selecting between them is a cwd-dependent discovery, not a judgment call.
42
+
43
+ ---
44
+
45
+ ## 1. Over-block
46
+
47
+ **What legitimate inputs does this change reject that it shouldn't?**
48
+
49
+ Harness does not reject or block anything. `runCompactionRecovery` accepts arbitrary env overrides and runs whatever the hook does. The only rejection is `locateCanonicalHook` returning null when no copy is findable — which surfaces as a clear test-time error rather than a silent pass. That's the correct failure mode.
50
+
51
+ ## 2. Under-block
52
+
53
+ **What failure modes does this still miss?**
54
+
55
+ - **Does not yet spawn a real Claude Code subprocess.** Spec § P0.1 literally says "spawn a Claude Code subprocess." The harness fulfills the *capability proof* (it can drive the recovery hook that Claude Code invokes post-compaction) without incurring CI flakiness from network-dependent Anthropic API calls. Whether this is enough to satisfy P0.1 is a judgment call; I've written it up as-is and PR2 will exercise the assertion surface. If it turns out more is needed, we extend the harness rather than rewrite it.
56
+ - **Does not assert cross-machine behavior.** Not in scope for PR0d; the multi-machine rollout tests live alongside PR4.
57
+ - **No LLM integration.** Out of scope — the authority is PR3.
58
+
59
+ ## 3. Level-of-abstraction fit
60
+
61
+ **Is this at the right layer? Should a higher or lower layer own it?**
62
+
63
+ `tests/e2e/` is the right home. Precedent: `tests/e2e/compaction-telegram-context.test.ts` uses the same shape (isolated state dir + Python subprocess) for compaction-adjacent assertions; `tests/fixtures/two-session-harness.ts` follows the same "build a disposable agent home" pattern for worktree tests. PR0d's harness sits cleanly alongside both.
64
+
65
+ The canonical-hook-lookup logic could live in a shared test helper (`tests/fixtures/`). Placing it inside the harness for PR0d because no other consumer exists yet; extracting if PR2/PR3 end up needing it independently.
66
+
67
+ ## 4. Signal vs authority compliance
68
+
69
+ `docs/signal-vs-authority.md`: detectors emit signals; only authorities can block.
70
+
71
+ The harness is neither. It is *observer* infrastructure — it runs the hook and captures output. Downstream tests turn the captured output into assertions, which turn into pass/fail for CI. None of that path constitutes an authority on agent behavior.
72
+
73
+ The spec-critical regression guard (asserting "fresh session" is NOT in recovery output) *could* be read as a blocking check, but it blocks a TEST, not agent behavior. If a hypothetical future recovery-template edit introduces the forbidden phrasing, this test fails, CI blocks the PR, and a human reviews. That's ordinary CI signal flow, not a runtime authority.
74
+
75
+ ## 5. Interactions
76
+
77
+ - **Canonical hook copy:** `fs.copyFileSync` into the harness tree. The canonical source file is never mutated — the harness only reads it. No race possible.
78
+ - **Git repo per harness:** each harness creates its own temp `.git/` tree. No risk of stepping on the outer repo's state — `process.cwd()` is never changed; all `git` calls are `-C <projectDir>`.
79
+ - **Process timeout 10s** on the `spawnSync` call — hard cap prevents a bad hook from hanging the test runner indefinitely.
80
+ - **Teardown idempotency** — `tornDown` boolean guard makes double-teardown safe; `afterEach` patterns won't throw on partial-setup harnesses.
81
+ - **HOME env override to the temp dir** — deliberate, to prevent the hook from accidentally touching the real user's `~/.claude/` state during tests. (Some hook downstream paths read `$HOME/.claude/settings.json`; point them at the harness temp tree.)
82
+ - **No shared-state** between harnesses in the same test file; each `createCompactionHarness()` call is fully independent. Parallel test execution is safe.
83
+
84
+ ## 6. External surfaces
85
+
86
+ - Test-only files. Nothing ships to the production runtime. Nothing is visible to users, agents, or other systems.
87
+ - The harness reads `src/templates/hooks/compaction-recovery.sh` (source-of-truth) — a one-way dependency: the harness validates what the template does, but never writes back. Safe.
88
+ - No network calls. No subprocess calls beyond `git` (for repo init) and `bash` (for the hook). Both are developer-machine dependencies already required by the broader test suite.
89
+
90
+ ## 7. Rollback cost
91
+
92
+ Trivial. Two new test-tree files:
93
+ - `tests/e2e/compaction-harness.ts`
94
+ - `tests/e2e/compaction-harness.test.ts`
95
+
96
+ Both deletable with a revert; no runtime surface to undo. Nothing to migrate. Downstream PR2 will import from the harness — if rolled back, PR2 rebases on a harness-less baseline and ships its own minimal setup (cheap because the harness itself is ~220 LOC of plumbing).
97
+
98
+ ---
99
+
100
+ ## Tests
101
+
102
+ - `tests/e2e/compaction-harness.test.ts` — 12 smoke tests, all passing.
103
+ - `npm run lint` (tsc --noEmit) — clean.
104
+
105
+ ## Phase 5 second-pass review criterion check
106
+
107
+ - Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (test infrastructure, no decisions).
108
+ - Session lifecycle: spawn, restart, kill, recovery — **test-adjacent to recovery, but not a new runtime path**; the harness only *observes* the existing recovery hook.
109
+ - Context exhaustion, compaction, respawn — **the harness is the observer surface for compaction**; still no new runtime path introduced, only a way to *test* the existing one.
110
+ - Coherence gates, idempotency checks, trust levels — **no**.
111
+ - Anything with "sentinel," "guard," "gate," or "watchdog" — **no**.
112
+
113
+ PR3 will require Phase 5 second-pass review.