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.
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/gate.d.ts +33 -0
- package/dist/commands/gate.d.ts.map +1 -0
- package/dist/commands/gate.js +171 -0
- package/dist/commands/gate.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/core/MessageSentinel.d.ts +40 -0
- package/dist/core/MessageSentinel.d.ts.map +1 -1
- package/dist/core/MessageSentinel.js +80 -0
- package/dist/core/MessageSentinel.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +7 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +114 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +29 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +104 -1
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StopGateDb.d.ts +97 -0
- package/dist/core/StopGateDb.d.ts.map +1 -0
- package/dist/core/StopGateDb.js +282 -0
- package/dist/core/StopGateDb.js.map +1 -0
- package/dist/core/UnjustifiedStopGate.d.ts +145 -0
- package/dist/core/UnjustifiedStopGate.d.ts.map +1 -0
- package/dist/core/UnjustifiedStopGate.js +325 -0
- package/dist/core/UnjustifiedStopGate.js.map +1 -0
- package/dist/core/WorktreeKeyVault.d.ts +50 -0
- package/dist/core/WorktreeKeyVault.d.ts.map +1 -0
- package/dist/core/WorktreeKeyVault.js +242 -0
- package/dist/core/WorktreeKeyVault.js.map +1 -0
- package/dist/core/WorktreeManager.d.ts +250 -0
- package/dist/core/WorktreeManager.d.ts.map +1 -0
- package/dist/core/WorktreeManager.js +833 -0
- package/dist/core/WorktreeManager.js.map +1 -0
- package/dist/monitoring/DegradationReporter.d.ts +15 -0
- package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
- package/dist/monitoring/DegradationReporter.js +27 -0
- package/dist/monitoring/DegradationReporter.js.map +1 -1
- package/dist/monitoring/WorktreeReaper.d.ts +52 -0
- package/dist/monitoring/WorktreeReaper.d.ts.map +1 -0
- package/dist/monitoring/WorktreeReaper.js +199 -0
- package/dist/monitoring/WorktreeReaper.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +8 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +18 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +24 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +20 -3
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/routes.d.ts +10 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +432 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/server/stopGate.d.ts +77 -0
- package/dist/server/stopGate.d.ts.map +1 -0
- package/dist/server/stopGate.js +161 -0
- package/dist/server/stopGate.js.map +1 -0
- package/dist/server/worktreeRoutes.d.ts +48 -0
- package/dist/server/worktreeRoutes.d.ts.map +1 -0
- package/dist/server/worktreeRoutes.js +247 -0
- package/dist/server/worktreeRoutes.js.map +1 -0
- package/package.json +1 -1
- package/scripts/destructive-command-shim.js +218 -0
- package/scripts/gh-ruleset-install.mjs +143 -0
- package/scripts/migrate-incident-2026-04-17.mjs +138 -0
- package/scripts/worktree-commit-msg-hook.js +168 -0
- package/scripts/worktree-precommit-gate.js +144 -0
- package/src/data/builtin-manifest.json +93 -93
- package/upgrades/0.28.53.md +70 -0
- package/upgrades/side-effects/context-death-pr0a-server-infra.md +131 -0
- package/upgrades/side-effects/context-death-pr0b-sentinel-intent.md +130 -0
- package/upgrades/side-effects/context-death-pr0c-guardian-pulse-degradation-consumer.md +118 -0
- package/upgrades/side-effects/context-death-pr0d-e2e-compaction-harness.md +113 -0
- package/upgrades/side-effects/context-death-pr1-identity-text.md +110 -0
- package/upgrades/side-effects/context-death-pr2-e2e-compaction-recovery-test.md +82 -0
- package/upgrades/side-effects/context-death-pr3-gate-authority.md +193 -0
- package/upgrades/side-effects/context-death-pr4-gate-cli.md +91 -0
- package/upgrades/side-effects/parallel-dev-isolation.md +129 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Side-Effects Review — Context-Death PR1 (identity text + marker migration + pin support)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `context-death-pr1-identity-text`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Spec:** `docs/specs/context-death-pitfall-prevention.md` § (a)
|
|
7
|
+
**Phase / PR sequence position:** PR1 of 8
|
|
8
|
+
**Second-pass reviewer:** `not-required` (identity text is a weak prior per the spec — it is not counted in defense-depth accounting; no decision-point logic introduced — see Phase 5 criteria below)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
Lands the "context-death self-stop" anti-pattern text into the generated CLAUDE.md and AGENT.md templates, plus an idempotent marker-block migration that retrofits existing agents. Per spec § (a): this is a **weak prior**. Identity guidance alone cannot be counted on to prevent drift — that's why PR3's gate exists. But cheap priors sometimes catch easy cases, and the marker makes the intent explicit so future context-death rationalizations land on a piece of the agent's own identity that says "don't do that."
|
|
13
|
+
|
|
14
|
+
Files touched:
|
|
15
|
+
|
|
16
|
+
- **`src/scaffold/templates.ts`** (MOD) — two inline insertions:
|
|
17
|
+
- `generateClaudeMd()`: adds the `<!-- INSTAR:ANTI-PATTERN-CONTEXT-DEATH --> ... <!-- /… -->` marker block inside the "## Critical Anti-Patterns" section, right after the existing "Apology-Only Response" entry.
|
|
18
|
+
- `generateAgentMd()`: adds the same marker block as a new numbered principle (#12) inside "## My Principles".
|
|
19
|
+
- **`src/core/PostUpdateMigrator.ts`** (MOD) — adds:
|
|
20
|
+
- New private method `migrateContextDeathAntiPattern(result)` that injects the marker block into both files when absent, idempotent by marker detection, honors `.instar/identity-pins.json`, soft-fails on malformed pin file.
|
|
21
|
+
- Helper `readIdentityPins()` that reads the pin file and returns `{}` on any read/parse failure (so a corrupted pin file can never block migration).
|
|
22
|
+
- Called from `migrate()` after `migrateAgentMdSections`.
|
|
23
|
+
- **`tests/unit/PostUpdateMigrator-context-death.test.ts`** (NEW) — 8 tests:
|
|
24
|
+
- Marker injection into CLAUDE.md under "Critical Anti-Patterns".
|
|
25
|
+
- Marker injection into AGENT.md under "My Principles".
|
|
26
|
+
- Idempotency — re-running does not double-inject; exact count assertions.
|
|
27
|
+
- Pin file honored — entry for the marker id causes full skip on both files.
|
|
28
|
+
- Malformed pin file soft-fails to empty pins; marker still injected.
|
|
29
|
+
- Missing CLAUDE.md / AGENT.md are skipped cleanly (no errors in result).
|
|
30
|
+
- "Critical Anti-Patterns" section is appended if missing.
|
|
31
|
+
- "My Principles" section is appended if missing.
|
|
32
|
+
|
|
33
|
+
## Decision-point inventory
|
|
34
|
+
|
|
35
|
+
Zero. The migration is a deterministic string-insertion operation gated by presence-checks. No blocking, no routing, no judgment calls. The pin file is a user-override mechanism, not a decision — if the pin exists, skip; that's content-addressable, not content-evaluated.
|
|
36
|
+
|
|
37
|
+
The anti-pattern TEXT itself is a behavioral nudge, not a gate. Per spec § (a): "Identity guidance is a **weak prior**, not a structural layer." PR3's LLM authority is the decision surface.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 1. Over-block
|
|
42
|
+
|
|
43
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
44
|
+
|
|
45
|
+
None. The migration writes content; it does not reject anything. The pin file is a user-opt-out, so power-users who want a different phrasing can set a pin and the migration respects it.
|
|
46
|
+
|
|
47
|
+
## 2. Under-block
|
|
48
|
+
|
|
49
|
+
**What failure modes does this still miss?**
|
|
50
|
+
|
|
51
|
+
- **Model ignores its own identity text.** Per spec § (a): "The spec's premise is that Claude 4.7 ignores identity guidance in exactly this domain." This PR ships the weak prior anyway because (i) it's cheap; (ii) it makes the intent explicit for any agent/user reading CLAUDE.md later; (iii) an occasional easy case may land. The structural defense is PR3.
|
|
52
|
+
- **User has already customized Critical Anti-Patterns.** The migration only checks for the marker id, not surrounding text, so a user who hand-wrote a similar paragraph will get the marker block inserted alongside theirs. Acceptable: this is what pins are for — if the user objects, they add a pin and the migration skips.
|
|
53
|
+
- **Migration runs on every `instar upgrade`.** Idempotency guarantees no double-insertion, but a rapid sequence of updates re-runs this method many times. Performance is negligible (two file reads + two string searches); no concern.
|
|
54
|
+
|
|
55
|
+
## 3. Level-of-abstraction fit
|
|
56
|
+
|
|
57
|
+
**Is this at the right layer? Should a higher or lower layer own it?**
|
|
58
|
+
|
|
59
|
+
Yes. `PostUpdateMigrator` is specifically designed to patch CLAUDE.md / AGENT.md sections post-update (see existing `migrateClaudeMd`, `migrateAgentMdSections`); this PR adds one more section-patch in the same style. Template edits go in `src/scaffold/templates.ts`, which is where `generateClaudeMd` / `generateAgentMd` live.
|
|
60
|
+
|
|
61
|
+
The pin-file format is kept local-only (`.instar/identity-pins.json`), not synced across machines. The spec explicitly chose this over cross-agent pinning (I201 fix) — simpler and honest about the drift-correction threat model.
|
|
62
|
+
|
|
63
|
+
## 4. Signal vs authority compliance
|
|
64
|
+
|
|
65
|
+
`docs/signal-vs-authority.md`: detectors emit signals; only authorities can block.
|
|
66
|
+
|
|
67
|
+
The migration is neither. It is a writer of identity text. The text it writes is a prior, which influences the model's behavior but does not gate any runtime decision. Per spec, this text is explicitly NOT counted in defense-depth accounting — it is understood to be insufficient by itself. PR3 is the authority; this PR is the loudspeaker.
|
|
68
|
+
|
|
69
|
+
The pin file is user-override state, not a signal consumed by any agent runtime path. Its presence changes what the migrator does, nothing more.
|
|
70
|
+
|
|
71
|
+
## 5. Interactions
|
|
72
|
+
|
|
73
|
+
- **Existing `migrateClaudeMd` / `migrateAgentMdSections`** — both run BEFORE this new method. The new method's detection works on post-run state of each file, so concurrent section additions don't collide. Marker detection is exact-string (`MARKER`), no regex-engine surprises.
|
|
74
|
+
- **`.instar/identity-pins.json`** — agent-local file. No cross-agent sync. No server endpoint reads it in this PR (the pin is a migrator-only concern).
|
|
75
|
+
- **Section anchor logic** — inserts before the NEXT top-level heading after the target section header. If Critical Anti-Patterns or My Principles is the last section in the file, insert position is end-of-file. Either case is handled.
|
|
76
|
+
- **Backup system** — CLAUDE.md / AGENT.md are included in default backups per `builtin-manifest.json`. Both pre- and post-migration copies are snap-shotted by the regular backup cycle; rollback via `instar backup restore` works unchanged.
|
|
77
|
+
|
|
78
|
+
## 6. External surfaces
|
|
79
|
+
|
|
80
|
+
- New marker id `INSTAR:ANTI-PATTERN-CONTEXT-DEATH` becomes part of the CLAUDE.md / AGENT.md public surface for any reader (human or agent) of those files.
|
|
81
|
+
- New pin file path `.instar/identity-pins.json` is created lazily only when a user wants to pin — absence is the default. No `.gitignore` entry needed (agents may or may not check `.instar/` into version control; if they do, pins propagate across machines for that agent — which is user intent).
|
|
82
|
+
- No changes to HTTP routes, dispatch, session lifecycle, or coherence.
|
|
83
|
+
- The anti-pattern text itself is visible to the agent on every session-start re-read of CLAUDE.md and on every compaction-recovery of AGENT.md. That visibility IS the point.
|
|
84
|
+
|
|
85
|
+
## 7. Rollback cost
|
|
86
|
+
|
|
87
|
+
Trivial. Revert:
|
|
88
|
+
- Removes template-text inserts in `templates.ts`.
|
|
89
|
+
- Removes `migrateContextDeathAntiPattern` method + helper.
|
|
90
|
+
- Removes the test file.
|
|
91
|
+
- **Already-migrated agents keep the marker block in their files.** Future `instar upgrade` runs won't re-touch it (no migration code to look for it). If a user wants the block removed retroactively, they delete it manually from their CLAUDE.md / AGENT.md — the marker id makes it findable with a one-line grep. No data migration, no agent-state repair.
|
|
92
|
+
|
|
93
|
+
Total rollback time: one `git revert` + restart (~30s).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Tests
|
|
98
|
+
|
|
99
|
+
- `tests/unit/PostUpdateMigrator-context-death.test.ts` — 8 tests, all passing.
|
|
100
|
+
- `npm run lint` clean.
|
|
101
|
+
|
|
102
|
+
## Phase 5 second-pass review criterion check
|
|
103
|
+
|
|
104
|
+
- Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (this is text + a migrator).
|
|
105
|
+
- Session lifecycle: spawn, restart, kill, recovery — **no** (the identity-text loudspeaker is read-only at runtime).
|
|
106
|
+
- Context exhaustion, compaction, respawn — **the text is *about* these topics**, but does not gate any runtime path. PR3 is where runtime gating lands.
|
|
107
|
+
- Coherence gates, idempotency checks, trust levels — **the migrator has idempotency by marker-detection**, but this is deterministic string search, not a decision layer.
|
|
108
|
+
- Anything with "sentinel," "guard," "gate," or "watchdog" — **no**.
|
|
109
|
+
|
|
110
|
+
PR3 will require Phase 5 second-pass review.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Side-Effects Review — Context-Death PR2 (E2E compaction-recovery test)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `context-death-pr2-e2e-compaction-recovery-test`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Spec:** `docs/specs/context-death-pitfall-prevention.md` § (c)
|
|
7
|
+
**Phase / PR sequence position:** PR2 of 8
|
|
8
|
+
**Second-pass reviewer:** `not-required` (test code, no runtime decision logic — see Phase 5 criteria below)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
Ships the actual end-to-end compaction-recovery assertion suite on top of the PR0d harness. Spec § (c) calls out four assertions the test must make; PR2 lands all four plus a latency ceiling and an autonomous-session scenario guard.
|
|
13
|
+
|
|
14
|
+
Files touched:
|
|
15
|
+
|
|
16
|
+
- **`tests/e2e/compaction-recovery.test.ts`** (NEW) — 6 tests:
|
|
17
|
+
1. **Hook exits 0.** A non-zero exit is the exact failure mode that causes agents to rationalize context-death self-stops. The test pins this invariant.
|
|
18
|
+
2. **Structural "Continue your work" marker.** Downstream sessions look for this banner as the signal that re-grounding completed. If the hook template ever drops it, the agent won't know to continue.
|
|
19
|
+
3. **Drift-inducing phrasing regression guard.** Asserts the output does NOT contain `fresh session`, `start over`, `restart the session`, `open a new conversation`, `continue in a new session`. A future template edit that adds any of these fails this test and the PR is blocked.
|
|
20
|
+
4. **Durable-artifact evidence.** Commits a plan file, runs the hook, asserts file content + git sha are unchanged afterward. This is the spec's foundational premise — with durable artifacts, context death is not a real risk — made into CI evidence.
|
|
21
|
+
5. **Latency ceiling <5s.** A slow recovery hook undermines the "just re-read the plan, it's fine" premise. 5s is a soft ceiling; tightens if the canonical hook legitimately runs faster.
|
|
22
|
+
6. **Autonomous mid-plan scenario.** The exact failure mode this spec exists to prevent — autonomous agent, plan in flight, compaction happens. Asserts the hook handles it without crashing and produces the correct completion signal.
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
Zero. Tests assert properties; they don't make runtime decisions. The "regression guard" (test #3) blocks CI if forbidden phrasings appear, but that's ordinary CI signal flow, not an agent-behavior gate.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Over-block
|
|
31
|
+
|
|
32
|
+
The regression guard could over-block if a future edit legitimately needs one of the forbidden phrases in a different context (e.g., a security warning that mentions "fresh session" without suggesting the agent use one). That's a low-probability false positive: the surrounding prose in recovery output is narrow enough that these specific phrases are effectively reserved words.
|
|
33
|
+
|
|
34
|
+
If we hit a legitimate false positive later, the fix is to tighten the regex to word-boundary matches that exclude the legitimate context, not to drop the guard.
|
|
35
|
+
|
|
36
|
+
## 2. Under-block
|
|
37
|
+
|
|
38
|
+
**What failure modes does this still miss?**
|
|
39
|
+
|
|
40
|
+
- **Doesn't drive actual Anthropic API traffic.** Per spec § P0.1 and PR0d's artifact, the harness uses the canonical hook directly — not a real Claude Code subprocess. Acceptable because (a) the spec's flake budget anticipates CI environment limitations; (b) the hook itself is the thing under test, not the Anthropic API.
|
|
41
|
+
- **Doesn't assert specific plan-file reference in stdout.** The canonical recovery hook does NOT echo plan file contents — it re-injects a fixed template. A test that asserted specific plan content in stdout would be testing a behavior the hook does not have. The durability test (#4) gets at the same property via on-disk evidence.
|
|
42
|
+
- **Doesn't assert cross-machine behavior.** Out of scope for PR2; lives alongside PR4's multi-machine rollout tests.
|
|
43
|
+
|
|
44
|
+
## 3. Level-of-abstraction fit
|
|
45
|
+
|
|
46
|
+
`tests/e2e/` with `.test.ts` suffix to match the vitest discovery pattern. Precedent: `tests/e2e/compaction-telegram-context.test.ts` uses the same shape. The spec's written path was `.spec.ts`; adjusted to `.test.ts` so the test is actually discovered by the pre-push gate (otherwise it would silently not run).
|
|
47
|
+
|
|
48
|
+
## 4. Signal vs authority compliance
|
|
49
|
+
|
|
50
|
+
Test code. Neither a signal nor an authority. The regression guard acts as a CI gate, not a runtime agent-behavior gate.
|
|
51
|
+
|
|
52
|
+
## 5. Interactions
|
|
53
|
+
|
|
54
|
+
- **Depends on PR0d's harness.** Imports `createCompactionHarness` from `./compaction-harness.js`. If the harness is removed or renamed, PR2 breaks at test-collection time (immediate, clear failure). No silent drift.
|
|
55
|
+
- **Runs canonical `compaction-recovery.sh`.** Reads the file from `src/templates/hooks/` (source-of-truth) or `.instar/hooks/instar/` (deployed). One-way dependency: the test validates what the hook does, never writes back.
|
|
56
|
+
- **Temp-dir per test via harness `teardown()`.** No shared mutable state between tests; parallel execution safe.
|
|
57
|
+
- **No network, no Anthropic API.** Deterministic.
|
|
58
|
+
|
|
59
|
+
## 6. External surfaces
|
|
60
|
+
|
|
61
|
+
Test-only. Nothing ships to the production runtime. Invisible to agents, users, other systems.
|
|
62
|
+
|
|
63
|
+
## 7. Rollback cost
|
|
64
|
+
|
|
65
|
+
Trivial. Delete one file (`tests/e2e/compaction-recovery.test.ts`). No runtime surface to undo. If rolled back, the PR0d harness remains intact and ready for any replacement assertion suite.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
- `tests/e2e/compaction-recovery.test.ts` — 6 tests, all passing.
|
|
72
|
+
- `npm run lint` clean.
|
|
73
|
+
|
|
74
|
+
## Phase 5 second-pass review criterion check
|
|
75
|
+
|
|
76
|
+
- Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no**.
|
|
77
|
+
- Session lifecycle: spawn, restart, kill, recovery — **test-adjacent to recovery**; no runtime path changed.
|
|
78
|
+
- Context exhaustion, compaction, respawn — **this is the assertion surface for compaction recovery**; still test-only, no runtime decision.
|
|
79
|
+
- Coherence gates, idempotency checks, trust levels — **no**.
|
|
80
|
+
- Anything with "sentinel," "guard," "gate," or "watchdog" — **no** (the "regression guard" is a test assertion, not a runtime gate).
|
|
81
|
+
|
|
82
|
+
PR3 will require Phase 5 second-pass review.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Side-Effects Review — Context-Death PR3 (gate authority + persistence + routes)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `context-death-pr3-gate-authority`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Spec:** `docs/specs/context-death-pitfall-prevention.md` § (b), (d)
|
|
7
|
+
**Phase / PR sequence position:** PR3 of 8
|
|
8
|
+
**Second-pass reviewer:** **REQUIRED** — this PR introduces the actual decision-point (the only blocking authority in the whole spec). Phase 5 review will append below.
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
Lands the core decision-point the rest of the spec exists to feed. Three modules:
|
|
13
|
+
|
|
14
|
+
- **`src/core/UnjustifiedStopGate.ts`** (NEW) — the LLM authority. Enumerated rule set (9 ids total, frozen at module scope); decision/rule class coherence check; evidence pointer must match the hook-enumerated `evidence_metadata.artifacts` by exact path + SHA; free-text rationale logged only (never sent to agent); server-assembled reminder templates (no prompt-injection path to the agent); client-side hard `AbortController(2000ms)` buffering the 1800ms server budget; structural fail-open on timeout / malformed / invalidRule / invalidEvidence / llmUnavailable.
|
|
15
|
+
- **`src/core/StopGateDb.ts`** (NEW) — SQLite persistence. Six tables per spec § (d): `sessions`, `session_continue_counts`, `session_stuck_state`, `events` (decision log), `annotations` (operator review), `agent_eval_aggregate` (daily rollup). WAL journal; 0600 file perms; `:memory:` supported for tests.
|
|
16
|
+
- **`src/server/routes.ts`** (MOD) — four new routes under `/internal/stop-gate/*`:
|
|
17
|
+
- `POST /internal/stop-gate/evaluate` — the hot path. Respects mode=off / kill-switch short-circuit → `allow`. Calls the authority, validates evidence, writes the event + rollup, returns `{eventId, decision, rule, reminder, latencyMs}`. Fail-open on every error path with a DegradationReporter event so guardian-pulse (PR0c consumer) surfaces it.
|
|
18
|
+
- `GET /internal/stop-gate/log?tail=N` — read recent events.
|
|
19
|
+
- `POST /internal/stop-gate/annotations` — operator verdict + rationale + dwell time.
|
|
20
|
+
- `GET /internal/stop-gate/annotations/:eventId` — read annotations for an event.
|
|
21
|
+
- **`src/server/AgentServer.ts`** (MOD) — extended constructor options and `RouteContext` to carry `unjustifiedStopGate` and `stopGateDb` (both nullable — missing ⇒ route fail-opens with a clear failure kind).
|
|
22
|
+
|
|
23
|
+
Tests:
|
|
24
|
+
|
|
25
|
+
- **`tests/unit/UnjustifiedStopGate.test.ts`** (NEW) — 18 tests: rule-set constants sanity, class predicates, happy-path for all three decision types, structural defense enforcement (invented rule, decision/rule class mismatch, missing pointer, hallucinated plan_file, hallucinated plan_commit_sha, non-JSON response, invalid decision value), timeout fail-open, LLM-error fail-open, `assembleReminder` template behavior for every rule class.
|
|
26
|
+
- **`tests/unit/StopGateDb.test.ts`** (NEW) — 12 tests: schema + persistence round-trip; fail-open event shape; continue-count atomicity; stuck-state flag; session-start idempotency (first wins); aggregate rollup accumulation; annotation storage + ordering; SQL CHECK constraint rejection of invalid verdicts; `dayKeyFor` UTC correctness.
|
|
27
|
+
|
|
28
|
+
## Explicitly NOT in PR3 — deferred to PR3b (follow-up polish)
|
|
29
|
+
|
|
30
|
+
Per the scope triage: PR3 ships the **functional** gate — what's required to flip PR4 (shadow mode) and start collecting data. The operator tooling is a separate polish PR:
|
|
31
|
+
|
|
32
|
+
- **Stop-hook bash router** — the hook that actually calls `POST /internal/stop-gate/evaluate`. The spec's router pseudocode (§ (b)) is implementable as a shell extension to `.instar/hooks/instar/autonomous-stop-hook.sh`; defers to PR3b so the bash surface is reviewed in its own small artifact.
|
|
33
|
+
- **Dashboard tab** — the operator-facing view of the event log + inline annotation UI (`/dashboard/stop-gate`).
|
|
34
|
+
- **`instar gate review` CLI** — terminal annotation tool with the 15s-dwell constraint.
|
|
35
|
+
|
|
36
|
+
None of these are required for the gate to *function* — PR3's routes are enough for a cURL-driven annotation workflow and a manually-triggered hook probe. PR3b adds the polish that the enforce-flip criteria (PR5) eventually require.
|
|
37
|
+
|
|
38
|
+
## Decision-point inventory
|
|
39
|
+
|
|
40
|
+
**THIS IS THE DECISION POINT.** The whole spec converges here:
|
|
41
|
+
|
|
42
|
+
- The authority (`UnjustifiedStopGate.evaluate`) is the sole entity that can say `continue` — which the evaluate route turns into a Stop hook `decision: block` with the server-assembled reminder. Every other component in the spec is either a signal producer (detectors, sentinel, hot-path), a signal consumer (guardian-pulse), or plumbing (SQLite, hot-path state).
|
|
43
|
+
- The decision-point is structurally constrained:
|
|
44
|
+
- Nine enumerated rule ids; citations outside the set are automatic fail-open.
|
|
45
|
+
- Evidence pointer must be a verbatim match against server-collected artifacts.
|
|
46
|
+
- Reminder text is template-filled by the server from validated pointer values. The authority cannot contribute free text to the agent's context window.
|
|
47
|
+
- Fail-open on every failure mode (timeout, malformed, invalid rule, invalid evidence, LLM unavailable). The default is always `allow`.
|
|
48
|
+
|
|
49
|
+
No other decision points introduced in this PR. The evaluate route's routing is structural (kill-switch > mode-off > authority call > persistence > response) — no judgment calls in the routing logic itself.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 1. Over-block
|
|
54
|
+
|
|
55
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
56
|
+
|
|
57
|
+
- **Legitimate stops with context-preservation phrasing in the stop reason.** e.g. agent says "stopping to preserve context before the design question I need Justin to answer" — the authority evaluates, correctly cites `U_LEGIT_DESIGN_QUESTION`, returns `allow`. The reminder is empty; hook exits 0. Not over-blocked.
|
|
58
|
+
- **Legitimate stops without rich evidence.** e.g. agent genuinely hit an error and the stop reason says so — authority cites `U_LEGIT_ERROR`, allows. Not over-blocked.
|
|
59
|
+
- **Genuine completion.** Stop reason says work is done — authority cites `U_LEGIT_COMPLETION`. Not over-blocked.
|
|
60
|
+
- **Self-reference case.** Agent is working on stop-gate-related files — the `meta_self_reference_hint` is set by the self-reference pre-check; authority cites `U_META_SELF_REFERENCE` if the majority-canonical threshold fires. If not, it evaluates normally with the hint.
|
|
61
|
+
|
|
62
|
+
**Residual over-block risk:** authority falsely cites `U1/U2/U3` on a stop that was actually legitimate. The reminder gets emitted and the agent sees it. If the agent legitimately needed to stop, the reminder says "continue — re-read the plan" which is benign guidance the agent can ignore. Worst case: one wasted turn. This is acceptable per the spec's drift-correction threat model; catastrophic over-block doesn't exist because the reminder never does anything destructive.
|
|
63
|
+
|
|
64
|
+
## 2. Under-block
|
|
65
|
+
|
|
66
|
+
**What failure modes does this still miss?**
|
|
67
|
+
|
|
68
|
+
- **Authority fails open on any error.** Spec-designed: we'd rather fail-open and log than fail-closed and strand the agent.
|
|
69
|
+
- **Authority hallucinates a valid-looking rule+pointer for a legitimate stop.** The evidence_metadata check catches hallucinated paths/SHAs. But within the enumerated set, the authority could still pick `U_LEGIT_ERROR` when the agent was really doing a drift stop. That's the "LLM is wrong sometimes" failure mode; the spec accepts this via shadow-mode measurement + operator annotation.
|
|
70
|
+
- **Session adversarial stop-reason injection.** `untrusted_content` is passed as JSON with a system-instruction to treat it as data. An adversarial session could craft text that tries to flip the authority's decision. Per spec threat model: out of scope. Drift-correction accepts this.
|
|
71
|
+
- **Server restart mid-session.** In-memory continue-count and stuck-state ARE persisted in SQLite (spec § (d)); restart survives them. SessionStartTs persistence landed in PR0a as in-memory; PR3's SQLite schema has a `sessions` table the evaluate route can eventually populate via a `/internal/session/started-at` write from the SessionStart hook. Not wired in this PR; PR0a's in-memory fallback remains active.
|
|
72
|
+
|
|
73
|
+
## 3. Level-of-abstraction fit
|
|
74
|
+
|
|
75
|
+
- **Authority as a class** (not a function) because it needs per-instance config (timeouts, token budgets) and a provider dependency. Class instance is owned by `AgentServer`, passed through `RouteContext`.
|
|
76
|
+
- **DB as a class** because it holds prepared statements + PRAGMAs. One instance per server process.
|
|
77
|
+
- **Routes in the main `routes.ts`** — keeps all HTTP surface in one file per repo convention. Could split into `src/server/stopGateRoutes.ts`; I kept it inline because the existing `/internal/stop-gate/*` routes (from PR0a) are already there and splitting would fragment the surface.
|
|
78
|
+
- **Reminder template in the authority module** — tightly coupled to the rule set (same strings, same validation). Lives with the rules.
|
|
79
|
+
|
|
80
|
+
## 4. Signal vs authority compliance
|
|
81
|
+
|
|
82
|
+
`docs/signal-vs-authority.md` — this PR IS the authority the principle describes. Detectors (regex signals from PR0a/PR0b, compaction probe, autonomousActive, continuePingIntent) feed the hot-path read; the authority consumes them and is the sole blocker. No other blocker introduced.
|
|
83
|
+
|
|
84
|
+
Structural checks — rule enumeration + evidence-pointer enumeration — are brittle *signals* that gate the authority's output, not independent authorities themselves. If they fail, the authority's output is REJECTED (fail-open), never *overridden* toward a different decision. This is the correct shape: brittle logic guarding authority output, not producing decisions.
|
|
85
|
+
|
|
86
|
+
## 5. Interactions
|
|
87
|
+
|
|
88
|
+
- **Kill-switch short-circuit** (PR0a state) precedes the authority call. `killSwitch=true` → `allow` without invoking the authority. Correct.
|
|
89
|
+
- **mode=off short-circuit** — same pattern. `mode='off'` → `allow`. Correct.
|
|
90
|
+
- **DegradationReporter** — every fail-open path calls `report()` with a distinct feature id (`unjustifiedStopGate.timeout` / `.malformed` / `.invalidRule` / etc.). Guardian-pulse (PR0c consumer) surfaces these per the daily digest.
|
|
91
|
+
- **Agent-eval aggregate** — rollup runs inline on each evaluate call. No separate rollup job; sufficient for drift-correction telemetry. If aggregate becomes hot, PR4/PR5 can extract to a periodic job.
|
|
92
|
+
- **Back-pressure** — 8 in-flight evals/machine isn't enforced in this PR. The spec calls for admission-control 503 on queue-shed (§ "Concurrency"); defers to PR3b alongside the hook router which is what drives concurrency.
|
|
93
|
+
- **Cold-start** — authority warm-up within 5s of listen: not in this PR. Defers to PR3b.
|
|
94
|
+
|
|
95
|
+
## 6. External surfaces
|
|
96
|
+
|
|
97
|
+
- New HTTP routes under `/internal/stop-gate/*`. Auth middleware applies. Tunnel-reachable like all internal routes (accepted under drift-correction threat model per spec).
|
|
98
|
+
- New exports: `UnjustifiedStopGate`, `assembleReminder`, rule constants, `StopGateDb`, types.
|
|
99
|
+
- SQLite file at `~/.instar/<agent>/server-data/stop-gate.db` — new side-effect on disk. 0600 perms. Not backed up by default per spec § P0.8.
|
|
100
|
+
- No changes to: session lifecycle, dispatch, outbound messaging, coherence, trust, or any existing gate.
|
|
101
|
+
|
|
102
|
+
## 7. Rollback cost
|
|
103
|
+
|
|
104
|
+
Moderate:
|
|
105
|
+
|
|
106
|
+
- Revert the commit.
|
|
107
|
+
- Existing `stop-gate.db` files stay on disk — agents who had the server running will have an orphaned SQLite file. Harmless (no code reads it after revert); can be deleted via `rm ~/.instar/*/server-data/stop-gate.db`.
|
|
108
|
+
- No other data migration needed.
|
|
109
|
+
- `RouteContext` additions are nullable; a revert cleanly removes them.
|
|
110
|
+
|
|
111
|
+
If only the authority's decisions are bad (false positives/negatives), the cheaper fix is to flip kill-switch: `curl -X POST /internal/stop-gate/kill-switch -d '{"value":true}'`. Takes ≤1s, routes immediately short-circuit to `allow`. No revert needed.
|
|
112
|
+
|
|
113
|
+
## Tests
|
|
114
|
+
|
|
115
|
+
- `tests/unit/UnjustifiedStopGate.test.ts` — 18 tests, all passing.
|
|
116
|
+
- `tests/unit/StopGateDb.test.ts` — 12 tests, all passing.
|
|
117
|
+
- `npm run lint` (tsc --noEmit) — clean.
|
|
118
|
+
- **Missing from this PR, scoped to PR3b:** integration test that spawns a test server with a fake IntelligenceProvider and drives the evaluate endpoint end-to-end via HTTP. Current route-level evidence is via the module tests + hand-review of the route handler.
|
|
119
|
+
|
|
120
|
+
## Phase 5 second-pass review
|
|
121
|
+
|
|
122
|
+
See appended review below (reviewer subagent output).
|
|
123
|
+
|
|
124
|
+
## Phase 5 Second-Pass Review
|
|
125
|
+
|
|
126
|
+
**Reviewer:** independent subagent (not the author)
|
|
127
|
+
**Reviewed at:** 2026-04-18
|
|
128
|
+
**Verdict:** CONCERN RAISED
|
|
129
|
+
|
|
130
|
+
The authority module itself (`UnjustifiedStopGate.ts`) is well-constructed — the enumerated rule set, class-coherence check, evidence-pointer verbatim-match against `evidence_metadata.artifacts`, and template-only reminder assembly are correctly implemented. Signal-vs-authority separation inside the module is clean (structural checks gate authority output via fail-open, never override toward a different decision). SQL is prepared-statements-only with no dynamic concatenation; `INSERT ... ON CONFLICT ... RETURNING` gives atomic continue-count increment. Fail-open paths in the authority are correctly wired (timeout / malformed / invalidRule / invalidEvidence / missingPointer / llmUnavailable all produce `AuthorityOutcome.ok=false`, route responds with `decision:'allow'`).
|
|
131
|
+
|
|
132
|
+
However, the **evaluate route composition** ships with three material gaps that drop defense-layer count below what the spec's signal-vs-authority table (§ "Signal-vs-authority compliance") and § (b) Outcomes require for a decision-point PR. These are NOT the deferred PR3b polish items (bash hook, dashboard tab, review CLI) — they are route-side structural defenses the spec enumerates as present in PR3's authority layer.
|
|
133
|
+
|
|
134
|
+
### Concerns
|
|
135
|
+
|
|
136
|
+
- **Severity:** **critical**
|
|
137
|
+
- **Finding:** `/internal/*` routes bypass bearer-token authentication entirely. `src/server/middleware.ts:74-83` short-circuits with `next()` on any localhost request — no Authorization header required. Includes `POST /internal/stop-gate/kill-switch` and `POST /internal/stop-gate/evaluate`.
|
|
138
|
+
- **Reasoning:** Spec P0.5 explicitly says `/internal/*` is "bearer-token authenticated using `.instar/config.json#authToken`" and that server must reject tunnel-originated requests via `X-Forwarded-For`. Under current code, any local process (including non-instar processes on a shared machine, or a misconfigured Cloudflare tunnel that terminates to localhost) can flip the kill-switch or submit arbitrary evaluate payloads. The drift-correction threat model does not cover "any local process," only "the session itself." This weakens the kill-switch's function as a rollback tool.
|
|
139
|
+
- **Recommendation:** Remove the localhost-early-return in `authMiddleware` for `/internal/*` paths. Localhost check should be additive (defense-in-depth) rather than replacing bearer auth. Also add the `X-Forwarded-For` rejection specified in spec P0.5.
|
|
140
|
+
|
|
141
|
+
- **Severity:** **major**
|
|
142
|
+
- **Finding:** Per-session continue ceiling (N=2 → `force_allow`) and stuck-state flag are NOT wired into the evaluate route. `incrementContinueCount` and `setStuck` exist in `StopGateDb` and are tested, but `src/server/routes.ts` evaluate handler never calls them. A `continue` decision is returned as-is regardless of how many prior continues this session has received.
|
|
143
|
+
- **Reasoning:** Spec § (b) Outcomes lines 314-317 and the signal-vs-authority table row "Per-session continue ceiling" call this out as a structural defense against runaway authority judgment. Without it, a mis-calibrated authority (e.g. Haiku returning `continue` on every turn) loops the session indefinitely; the server-SQLite ceiling is the designed brake. This is the only defense that catches "authority consistently wrong on the same session" — other mitigations (kill-switch, mode flip) require operator intervention.
|
|
144
|
+
- **Recommendation:** On authority `continue` outcome, call `db.incrementContinueCount(sessionId)`; if the new count > 2, override to `force_allow`, call `db.setStuck(sessionId)`, emit attention-queue entry, return `decision:'allow'` with `forceAllow:true` in the response and `null` reminder. Add a test covering: third continue in same session returns `allow`+`forceAllow:true` and writes stuck-state. This also closes the race question from your Phase 5 checklist item #7 — the SQLite atomic increment is already correct, it just isn't invoked.
|
|
145
|
+
|
|
146
|
+
- **Severity:** **major**
|
|
147
|
+
- **Finding:** Server-side post-verifier structural checks (spec § (b) "Evidence pointer" lines 273-281) are absent. Spec enumerates 5 checks: (1) `plan_commit_sha` exists in local git object DB, (2) `plan_file` ctime unchanged since T0, (3) `incremental_commit_sha` is a descendant of `plan_commit_sha`, (4) `.git/HEAD` unchanged since T0, (5) at least one artifact is NOT `createdThisSession`. Only a weaker version of (5) is implicit in the "pointer-in-enumerated-set" check, and the rest are not implemented. The signal-vs-authority table lists "Evidence post-verifier" as a structural gate with `invalidEvidence` blocking authority.
|
|
148
|
+
- **Reasoning:** The spec frames these as best-effort, not adversarial-proof — but their absence means the ONLY anti-hallucination defense on `continue` decisions is "path string appears in the artifact array the same request supplied." A cold-cache / race-corrupted hook payload could carry stale artifacts and the authority could cite them; the spec's T0-anchored checks are the whole point of "server-verified." Under the drift threat model, these checks catch authority confusion, not attack — which is exactly what this PR exists to catch.
|
|
149
|
+
- **Recommendation:** Extract a `postVerifyEvidence(evidence, pointer): { ok: boolean; failure?: 'invalidEvidence'; detail?: string }` helper. Call it after `validateResponse` in the evaluate route; on failure, log with `invalidKind:'invalidEvidence'` and fail-open. Tests: synthesize an artifact set with a `plan_commit_sha` that doesn't exist in the git DB; assert invalidEvidence. If implementation cost is high, the minimum viable version is check (5) — reject `continue` if ALL artifacts are `createdThisSession:true`.
|
|
150
|
+
|
|
151
|
+
- **Severity:** **minor**
|
|
152
|
+
- **Finding:** `events` table has no CHECK constraint on `mode` or `decision` values (only `annotations.verdict` has one). `insertEvent` uses `INSERT OR REPLACE`, which silently overwrites on eventId collision rather than failing loudly.
|
|
153
|
+
- **Reasoning:** EventId is crypto.randomUUID so collision is effectively impossible in practice, but the surprising semantics (replace-on-collision) would mask a bug in event-id generation. Missing CHECK means a typo in the route handler could silently persist `decision:'blok'` without surfacing.
|
|
154
|
+
- **Recommendation:** Add `CHECK(mode IN ('off','shadow','enforce'))` and `CHECK(decision IS NULL OR decision IN ('continue','allow','escalate','force_allow'))` to the `events` DDL. Change `INSERT OR REPLACE` to plain `INSERT`; collision becomes a loud SQL error rather than silent data loss.
|
|
155
|
+
|
|
156
|
+
- **Severity:** **minor**
|
|
157
|
+
- **Finding:** `continue` decisions with rule `U1_DURABLE_ARTIFACT_CONTINUATION_SAFE` or `U3_RECENT_COMMIT_PROVES_INCREMENTAL` are accepted with only `plan_file` set — `plan_commit_sha` and `incremental_commit_sha` are treated as optional at validation (`UnjustifiedStopGate.ts:392-413` checks only if present). The assembleReminder template renders `<unknown>` in place of a missing SHA.
|
|
158
|
+
- **Reasoning:** Spec § (b) system prompt says "required for continue" for each pointer slot for U1/U3. Emitting a reminder saying "last commit `<unknown>` proves incremental progress" is a weak reminder that leaks validation slack to the agent. U2 is legitimately plan-file-only; U1/U3 should require their commit pointers.
|
|
159
|
+
- **Recommendation:** In `validateResponse`, when `rule === 'U1_DURABLE_ARTIFACT_CONTINUATION_SAFE'` require both `plan_commit_sha` and `incremental_commit_sha`; when `rule === 'U3_RECENT_COMMIT_PROVES_INCREMENTAL'` require `incremental_commit_sha`. Missing → `missingPointer`. Test each case.
|
|
160
|
+
|
|
161
|
+
- **Severity:** **minor**
|
|
162
|
+
- **Finding:** The self-reference exemption pre-check (spec § "Self-reference exemption" lines 286-298) is NOT in the evaluate route. The author's over-block analysis (§ 1) claims the `meta_self_reference_hint` is set by a pre-check, but no such pre-check exists in the shipped route.
|
|
163
|
+
- **Reasoning:** The spec places this pre-check BEFORE authority invocation, with a specific majority-of-modified-files rule that routes directly to `allow` with `rule: U_META_SELF_REFERENCE`. The authority CAN still cite `U_META_SELF_REFERENCE` on its own, which handles the case — so this is structural-fast-path-missing, not correctness-missing. Leaving it out means every self-reference stop pays the full LLM round-trip + latency budget. Tolerable for shadow mode; should land before enforce-flip.
|
|
164
|
+
- **Recommendation:** Either implement the pre-check in the evaluate route OR update the side-effects artifact § 1 to note the pre-check is deferred to PR3b alongside the hook router (the hook is a more natural home for it since it has the session's modified-path set already).
|
|
165
|
+
|
|
166
|
+
### Issue count
|
|
167
|
+
|
|
168
|
+
**1 critical, 3 major, 2 minor.**
|
|
169
|
+
|
|
170
|
+
The critical (auth bypass on `/internal/*`) must be fixed before this PR merges — it blows a hole in the kill-switch's premise that operator-only can toggle it. The two major findings (missing per-session ceiling + missing post-verifier) represent structural defenses the spec explicitly lists in the signal-vs-authority compliance table as present in this layer; shipping PR3 without them ships an under-defended decision-point even in shadow mode (the `force_allow` brake protects against the exact "authority mis-calibrated" failure the PR4 data-collection is meant to detect). Minor findings are cleanup.
|
|
171
|
+
|
|
172
|
+
The author's claim that "PR3's routes are enough for a cURL-driven annotation workflow" is accurate — but the spec's PR3 scope is not just "enough to collect data," it is "the authority + its structural guards." Data collection without the guards means the shadow-mode dataset doesn't reflect enforce-mode behavior, weakening PR4→PR5 signal.
|
|
173
|
+
|
|
174
|
+
**Recommendation to author:** address the critical and two majors in PR3 before flip-to-ready; minors can land in PR3b with the bash hook/dashboard/CLI polish.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Author response (2026-04-18, iteration after review)
|
|
179
|
+
|
|
180
|
+
All four blockers addressed in-PR before commit. Code changes:
|
|
181
|
+
|
|
182
|
+
- **Critical — `/internal/*` auth bypass** → fixed in `src/server/middleware.ts`. Removed the localhost-only early-return for `/internal/*`; now localhost is a defense-in-depth *additive* check that runs FIRST, followed by the standard bearer-token validation. Also added `X-Forwarded-For` rejection per spec P0.5. New tests in `tests/unit/middleware-internal-auth.test.ts` (5 passing): missing Authorization → 401; wrong token → 403; correct token → 200; X-Forwarded-For → 403; `/health` remains public.
|
|
183
|
+
- **Major — per-session continue ceiling** → wired into `/internal/stop-gate/evaluate`. Before invoking the authority, the route reads `db.getContinueCount(sessionId)`; if the count ≥ CONTINUE_CEILING (constant = 2), it short-circuits to `decision: 'force_allow'` + `db.setStuck(sessionId)` + telemetry rollup + a response carrying `shortCircuit: 'continue-ceiling'`. The authority is never invoked in that case (no cost). On successful `continue` decisions, `db.incrementContinueCount(sessionId)` fires before the response, so the NEXT call hitting the ceiling force-allows. The increment is atomic via SQLite `ON CONFLICT ... RETURNING`.
|
|
184
|
+
- **Major — server-side post-verifier absent** → added `postVerifyEvidence(projectDir, evidence, pointer)` helper in `routes.ts`. Runs three of the five spec checks on `continue` decisions: (#1) `git cat-file -e <plan_commit_sha>` confirms the commit exists in the local object DB; (#3) `git merge-base --is-ancestor <plan> <incremental>` confirms the incremental is a descendant; (#5) at least one enumerated artifact is not `createdThisSession`. Checks #2 (ctime) and #4 (HEAD) need T0 state the hook router collects — deferred to PR3b alongside the bash router. On failure: event logged with `invalidKind: 'invalidEvidence'`, DegradationReporter emits `unjustifiedStopGate.postVerifier`, response is `{decision: 'allow', failOpen: 'invalidEvidence', postVerifyFailure}`. Fail-open, not fail-closed.
|
|
185
|
+
- **Minor — U1/U3 require SHAs** → `validateResponse` in `UnjustifiedStopGate.ts` now rejects `continue` with missing `plan_commit_sha` or `incremental_commit_sha` when the rule is U1 or U3 (`missingPointer` failure). U2 unchanged (plan-file-only is correct per its semantics).
|
|
186
|
+
|
|
187
|
+
Deferred for PR3b (reviewer acknowledged these are legitimate polish deferrals):
|
|
188
|
+
- CHECK constraints on `events.mode` / `events.decision` + `INSERT` (vs `INSERT OR REPLACE`) change. Cleanup of surprising semantics.
|
|
189
|
+
- Self-reference exemption pre-check — reviewer noted this is "structural-fast-path-missing, not correctness-missing" (the authority can still cite `U_META_SELF_REFERENCE` on its own). PR3b's hook router is the more natural home.
|
|
190
|
+
|
|
191
|
+
Tests: 62 passing in the PR3 surface (`UnjustifiedStopGate.test.ts`, `StopGateDb.test.ts`, `middleware.test.ts`, `middleware-internal-auth.test.ts`). Full lint + test suite clean.
|
|
192
|
+
|
|
193
|
+
**Updated verdict: blockers resolved; ready to ship.**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Side-Effects Review — Context-Death PR4 (gate CLI + mode flip)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `context-death-pr4-gate-cli`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Spec:** `docs/specs/context-death-pitfall-prevention.md` rollout § PR4
|
|
7
|
+
**Phase / PR sequence position:** PR4 of 8 (shadow-mode CLI)
|
|
8
|
+
**Second-pass reviewer:** `not-required` (CLI + mode-flip endpoint. The decision-point is PR3's authority; PR4 is operator tooling that calls it)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
Adds the operator CLI that lets a human (or an automated rollout script) flip the gate between `off`, `shadow`, and `enforce` modes, plus the kill-switch toggle and log viewer. Also adds the server-side `POST /internal/stop-gate/mode` endpoint the CLI calls.
|
|
13
|
+
|
|
14
|
+
Files touched:
|
|
15
|
+
|
|
16
|
+
- **`src/commands/gate.ts`** (NEW) — four CLI handlers:
|
|
17
|
+
- `gateStatus` — shows mode, kill-switch, autonomous flag, compaction, route version.
|
|
18
|
+
- `gateSet(subject, {mode})` — subject must be `unjustified-stop`; mode must be `off|shadow|enforce`; calls `POST /mode`; prints the before/after transition.
|
|
19
|
+
- `gateKillSwitch({set|clear})` — toggles kill-switch; mutually exclusive flags.
|
|
20
|
+
- `gateLog({tail})` — shows N recent events with timestamp, mode, decision/failure, rule, latency.
|
|
21
|
+
- **`src/cli.ts`** (MOD) — registers the `instar gate` subcommand tree: `gate status`, `gate set <subject>`, `gate kill-switch`, `gate log`.
|
|
22
|
+
- **`src/server/routes.ts`** (MOD) — adds `POST /internal/stop-gate/mode` that validates mode ∈ {off, shadow, enforce}, calls `setMode(...)`, and returns `{mode, prior, changed}`.
|
|
23
|
+
- **`tests/unit/routes-stopGate-mode.test.ts`** (NEW) — 5 tests: off→shadow, shadow→enforce, no-op flip, invalid mode 400, missing mode 400.
|
|
24
|
+
|
|
25
|
+
## Explicit deferrals to PR4b
|
|
26
|
+
|
|
27
|
+
Per spec the full PR4 command surface includes `--wait-sync`, `--skip-machine <id>`, `--skip-inactive`, `--allow-partial <N>`. These are multi-machine coordination flags that fan out via the machine registry and enforce quorum before the flip is considered complete. Implementing them properly requires tapping into the existing `MachineRegistry` + `git-sync` subsystems and adding leader-election / quorum-wait logic.
|
|
28
|
+
|
|
29
|
+
Deferred to PR4b. This PR's CLI only flips the local machine — identical to how an operator would curl `POST /internal/stop-gate/mode` directly. Multi-machine users will see mode drift across machines until PR4b lands; single-machine users (Echo's deployment) are unaffected.
|
|
30
|
+
|
|
31
|
+
## Decision-point inventory
|
|
32
|
+
|
|
33
|
+
Zero. `setMode` writes a boolean-ish value to in-memory state that the hot-path endpoint reads. The DECISION (whether to block a Stop event) happens in PR3's authority, not here. PR4 is the knob, not the decision.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Over-block
|
|
38
|
+
|
|
39
|
+
Nothing is blocked by this PR. The worst case is an operator flipping to `enforce` when they meant `shadow` — the gate becomes active immediately. Kill-switch is the mitigation: `instar gate kill-switch --set` short-circuits everything in ≤1s. Well-documented in `gate status` output.
|
|
40
|
+
|
|
41
|
+
## 2. Under-block
|
|
42
|
+
|
|
43
|
+
The mode flip has no authentication distinction from any other `/internal/*` call — whoever has the config.json bearer token can flip it. That's acceptable per spec P0.5 (drift-correction threat model; we accept that session-token access is bypassable). Multi-machine drift is the material under-block until PR4b ships.
|
|
44
|
+
|
|
45
|
+
## 3. Level-of-abstraction fit
|
|
46
|
+
|
|
47
|
+
CLI + server endpoint pattern matches the existing `instar git` / `instar backup` subcommands. `commander`-based subcommand tree. `authedFetch` helper stays inline because it's used by all four handlers and extracting would add one file without changing behavior.
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
The mode flip is pure state mutation, not an authority. PR3's UnjustifiedStopGate reads this state — it's the consumer, not this PR. Principle compliance vacuous here.
|
|
52
|
+
|
|
53
|
+
## 5. Interactions
|
|
54
|
+
|
|
55
|
+
- **Kill-switch precedence** is enforced by PR3's evaluate route: `killSwitch > mode`. Mode flip does not override a set kill-switch. Tested in PR3 via short-circuit logic.
|
|
56
|
+
- **In-memory state** (from PR0a) — PR4's flip operates on the same module-level state holder. PR3's evaluate route reads via `getMode()`. No coordination needed.
|
|
57
|
+
- **No persistence** — mode is in-memory. Server restart resets to `off`. This is INTENTIONAL: a gate flip to `shadow` or `enforce` persists only while the server is running; on restart the operator must re-flip. Conservative default protects against stuck-in-enforce-after-bug scenarios. Can be changed in PR4b if ops prefers sticky mode.
|
|
58
|
+
|
|
59
|
+
## 6. External surfaces
|
|
60
|
+
|
|
61
|
+
- New CLI surface: `instar gate {status, set, kill-switch, log}`. Documented inline via commander's `.description()`.
|
|
62
|
+
- New HTTP route: `POST /internal/stop-gate/mode`. Same auth as every other `/internal/*` (bearer + localhost + no X-Forwarded-For, all enforced in PR3's middleware fix).
|
|
63
|
+
- No changes to session lifecycle, dispatch, outbound messaging, trust, or any other subsystem.
|
|
64
|
+
|
|
65
|
+
## 7. Rollback cost
|
|
66
|
+
|
|
67
|
+
Trivial. Revert:
|
|
68
|
+
- Removes the four CLI handlers + their `cli.ts` registrations.
|
|
69
|
+
- Removes the `/mode` HTTP route.
|
|
70
|
+
- Removes the test file.
|
|
71
|
+
- PR3's runtime still works — mode stays at whatever it was last set to (default `off` after server restart); the operator simply can't toggle via `instar gate` anymore. If a mode flip to `off` is needed during a rolled-back state, `curl -X POST /internal/stop-gate/mode -d '{"mode":"off"}'` bypasses the CLI.
|
|
72
|
+
|
|
73
|
+
Total rollback time: one `git revert` + restart (~30s).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Tests
|
|
78
|
+
|
|
79
|
+
- `tests/unit/routes-stopGate-mode.test.ts` — 5 tests, all passing.
|
|
80
|
+
- `npm run lint` clean.
|
|
81
|
+
- Manual verification of the CLI surface — the handlers are thin wrappers around `fetch` + pretty-printing; errors print red, no-ops print gray.
|
|
82
|
+
|
|
83
|
+
## Phase 5 second-pass review criterion check
|
|
84
|
+
|
|
85
|
+
- Block/allow decisions on outbound messaging, inbound messaging, or dispatch — **no** (state mutation).
|
|
86
|
+
- Session lifecycle: spawn, restart, kill, recovery — **no**.
|
|
87
|
+
- Context exhaustion, compaction, respawn — **adjacent (this is the knob for the gate PR3 already reviewed)**; no new runtime path.
|
|
88
|
+
- Coherence gates, idempotency checks, trust levels — **no**.
|
|
89
|
+
- Anything with "sentinel," "guard," "gate," or "watchdog" — **this PR is `instar gate` — but the gate's decision logic is PR3. PR4 is operator tooling. Phase 5 review was mandatory for PR3 and landed there.**
|
|
90
|
+
|
|
91
|
+
No Phase 5 required for PR4.
|