instar 1.2.83 → 1.3.0

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.
@@ -0,0 +1,27 @@
1
+ # Upgrade Guide — NEXT
2
+
3
+ <!-- bump: patch -->
4
+ <!-- Valid values: patch, minor, major -->
5
+ <!-- patch = bug fixes, refactors, test additions, doc updates -->
6
+ <!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
7
+ <!-- major = breaking changes to existing APIs or behavior -->
8
+
9
+ ## What Changed
10
+
11
+ **The `/build` stop-hook is now session-scoped — it only nags the session that actually owns the build.** Before, the hook that keeps a build from quitting half-done had no idea *which* of your concurrent sessions started the build, so it fired its "keep working" block into every session — trapping unrelated ones and, worse, spending the owning build's reinforcement budget on each misfire (when that budget hit its cap, the hook stopped protecting the real builder too).
12
+
13
+ Now `build-state.py` stamps the owning session (its tmux session name, and optionally the Claude session UUID) at build start, and the hook blocks **only** the proven owner. Every other session approve-exits without touching the owner's budget. A build with no owner stamp (legacy state) gets a conservative no-adopt: the hook goes quiet rather than guessing — it never traps a session and never claims ownership.
14
+
15
+ The hook ships via the always-overwrite path (the inline `getBuildStopHook()` twin in `PostUpdateMigrator`, kept byte-identical to `src/templates/hooks/build-stop-hook.sh` and asserted by a drift test), so every agent gets it on update.
16
+
17
+ ## What to Tell Your User
18
+
19
+ - **No action needed; this just stops cross-talk between your sessions.** If you run more than one session at once, a build in one of them will no longer pester the others or drain its own "keep going" budget. You won't notice anything unless you run concurrent sessions, in which case it gets quieter.
20
+
21
+ ## Summary of New Capabilities
22
+
23
+ | Capability | How to Use |
24
+ |-----------|-----------|
25
+ | Session-scoped build stop-hook | Automatic. `build-state.py init` stamps `owner.{tmux,session,stampedAt}`; the hook blocks only the owner. |
26
+ | Owner-stamp flags on `build-state.py init` | `--owner-session "$CLAUDE_CODE_SESSION_ID"` (precision; SKILL wiring is a fast-follow), `--owner-tmux` (override seam). Tmux name is auto-resolved by default. |
27
+ | Conservative no-adopt for un-stamped builds | Automatic. No owner stamp → hook approves without claiming ownership (never traps, never drains). |
@@ -0,0 +1,133 @@
1
+ # Side-Effects Review — Build Stop-Hook Session-Scoping
2
+
3
+ **Slug:** `build-stop-hook-session-scoping`
4
+ **Date:** `2026-05-26`
5
+ **Author:** Echo
6
+ **Spec:** `docs/specs/BUILD-STOP-HOOK-SESSION-SCOPING-SPEC.md` (converged round 1, approved by Justin via Telegram topic 13352)
7
+ **Second-pass reviewer:** independent general-purpose review agent (3 findings, all incorporated — see spec §"Review Findings Incorporated")
8
+
9
+ ## Summary of the change
10
+
11
+ The `/build` Stop hook had no notion of which session owns a build. With one
12
+ shared `build-state.json` and one hook in a checkout, a build started by session
13
+ A fired its "keep working" block into every concurrent session of the same agent
14
+ — trapping unrelated sessions and, on every misfire, incrementing the shared
15
+ `reinforcementsUsed` counter, which drains the owning build's protection budget
16
+ to zero.
17
+
18
+ This change stamps the owning session at `/build` start (`build-state.py init`
19
+ writes `owner.{tmux,session,stampedAt}`) and teaches the hook to block **only**
20
+ the proven owner. Any other session approve-exits **without** incrementing the
21
+ counter. A build with no owner stamp gets a conservative no-adopt (approve,
22
+ never claim ownership) — it never traps a session and never inverts ownership.
23
+
24
+ ## Files changed (in gate scope = behavior)
25
+
26
+ - `src/core/PostUpdateMigrator.ts` — the inline `getBuildStopHook()` (the
27
+ shipping artifact; written to `.instar/hooks/instar/build-stop-hook.sh` on every
28
+ migration via always-overwrite, and by `init.ts` via `getHookContent`). Added
29
+ the ownership block between the terminal-phase early-exit and the counter
30
+ mutation.
31
+ - `src/templates/hooks/build-stop-hook.sh` — the canonical reference template +
32
+ builtin-manifest fingerprint. Kept byte-identical to the inline twin (asserted
33
+ by a new drift test).
34
+
35
+ Out of gate scope but part of the change:
36
+ - `playbook-scripts/build-state.py` — `cmd_init` stamps `owner`; new
37
+ `--owner-session` / `--owner-tmux` flags; `resolve_owner_tmux()` helper.
38
+ - `tests/unit/build-stop-hook-session-scoping.test.ts` (new, 12 tests),
39
+ `tests/unit/PostUpdateMigrator-buildStopHook.test.ts` (+1 drift test).
40
+ - `docs/specs/*`, `upgrades/NEXT.md` (docs / release note).
41
+
42
+ ## Decision-point inventory
43
+
44
+ - **Added**: hook ownership gate — between terminal-phase exit and counter
45
+ mutation. Decides block (owner) vs approve-no-increment (non-owner / unknown /
46
+ un-stamped). This is the new decision boundary.
47
+ - **Added**: `build-state.py` owner stamp at init (records identity; no runtime
48
+ decision, pure data).
49
+ - **Unchanged**: no-state-file exit, terminal-phase exit, and the
50
+ counter/reinforcement block logic itself (the owner path falls through to the
51
+ exact pre-existing code).
52
+
53
+ ## Over-block / under-block analysis
54
+
55
+ - **Over-block risk (trapping a non-owner):** eliminated. A non-owner returns
56
+ `approve` before reaching the counter. The only block path requires a positive
57
+ owner match (tmux or session). Tested: non-owner tmux, non-owner session,
58
+ identity-unknown, and legacy/un-stamped all return approve.
59
+ - **Under-block risk (owner not protected):** bounded and acceptable. The owner
60
+ is protected whenever `owner.tmux` matches the live tmux (the load-bearing
61
+ path, proven live) or `owner.session` matches stdin `session_id`. The only
62
+ under-protection case is an **un-stamped** build (legacy state, or an
63
+ environment where stamping didn't run) — by deliberate design the hook goes
64
+ quiet there rather than guess. Forfeiting protection for a stale build is the
65
+ correct trade vs. trapping the wrong session (spec §"Why conservative-no-adopt").
66
+ - **Bootstrap inversion (the rejected alternative):** an earlier draft let the
67
+ first session to Stop adopt ownership. The independent review showed this
68
+ inverts ownership in the real incident pattern (busy owner never stops first).
69
+ Removed entirely; replaced with conservative no-adopt. Tested: un-stamped state
70
+ yields approve with `owner` NOT written.
71
+
72
+ ## Level-of-abstraction fit
73
+
74
+ The fix lives at the same layer as the bug: the Stop hook and the state writer.
75
+ It mirrors the already-shipped autonomous stop-hook's session-scoping ladder
76
+ (tmux-name primary, session-UUID backstop, fail-open) without merging the two
77
+ (explicit non-goal — bash hooks don't share code cleanly; premature abstraction
78
+ avoided). No new module, no new service.
79
+
80
+ ## Signal-vs-authority compliance
81
+
82
+ The hook is a low-context filter making a binary ownership decision from
83
+ locally-verifiable identifiers (tmux `#S`, stdin `session_id`). It does not
84
+ arrogate higher-level judgment — it only declines to block a session it cannot
85
+ prove it owns. Conservative-by-construction: every ambiguous case resolves to
86
+ `approve` (release), never to `block` (trap). It emits no user-facing messages.
87
+
88
+ ## Interactions
89
+
90
+ - **Reinforcement counter:** the owner path is byte-for-byte the prior logic, so
91
+ graduated protection (3/5/10) is unchanged for the owner. Non-owners no longer
92
+ touch the counter at all.
93
+ - **Restart reconcile:** writes `owner.session` ONLY on a confirmed tmux-owner
94
+ match with a rotated UUID. Gated strictly behind the tmux match — a non-owner
95
+ can never clobber `owner.session` (tested explicitly).
96
+ - **stdin consumption:** the hook now reads stdin (`cat`). Stop hooks deliver and
97
+ close stdin in production (the autonomous hook relies on this), so no hang.
98
+ Even with no stdin/session, tmux-scoping alone is sufficient (proven live).
99
+ - **Worktree topology:** ownership is keyed on the cwd-independent tmux name, so
100
+ it is correct whether the owner launched at the main root and `cd`'d into a
101
+ worktree or launched rooted inside the worktree. The old, fragile `$PWD`-based
102
+ stopgap is NOT carried forward.
103
+ - **Migration parity:** inline twin is always-overwritten → every agent gets the
104
+ new hook on update. `build-state.py` rides the repo checkout (the only place
105
+ the bug occurs — see spec §Migration Parity 2). Drift test prevents
106
+ template/inline divergence.
107
+
108
+ ## Rollback cost
109
+
110
+ Low and clean. Revert the two src files (and optionally build-state.py); the
111
+ always-overwrite migration restores the prior hook on next update. The added
112
+ `owner` block in state is additive JSON ignored by the old hook — no destructive
113
+ schema migration. No data loss path.
114
+
115
+ ## Tracked deferral
116
+
117
+ The SKILL change to pass `--owner-session "$CLAUDE_CODE_SESSION_ID"` (session-UUID
118
+ precision; + its dedicated PostUpdateMigrator migration per Migration Parity §5)
119
+ is a deliberate fast-follow per the approved phasing (Justin approved one-PR-now +
120
+ tiny-follow-up). The flag is already plumbed and tested in `build-state.py`; only
121
+ the SKILL invocation + migration remain. This is tracked, not orphaned.
122
+
123
+ ## Verification
124
+
125
+ - 3-tier behavior tests (12) drive the **real shipping hook** (from
126
+ `getHookContent`) against the **real `build-state.py`** with real stdin/tmux
127
+ seams — covering owner-block, non-owner-no-drain, repeated-non-owner,
128
+ session-only owner, identity-unknown fail-open, legacy no-adopt, restart
129
+ reconcile, anti-clobber, terminal-phase. Plus build-state stamp tests (3) and
130
+ the template/inline drift test (1).
131
+ - Live test-as-self: ran the shipping hook with **real** `tmux display-message`
132
+ resolution (no seam) in this session (`echo-build-stop-hook-session-scoping`) —
133
+ confirmed owner→block, non-owner→approve with zero counter drain.
@@ -0,0 +1,35 @@
1
+ # Side-effects review — fresh-session stop-gate shadow wiring
2
+
3
+ **Scope**: Complete the conservative first rollout for the fresh-session stop-gate: wire the existing server authority/database, install a Stop-hook router, and default to observe-only shadow mode when the gate is healthy.
4
+
5
+ **Files touched**:
6
+ - `src/commands/server.ts` — constructs `StopGateDb` and `UnjustifiedStopGate`, persists mode state, passes both into `AgentServer`.
7
+ - `src/server/stopGate.ts` — persists mode flips to `server-data/stop-gate-mode.json`.
8
+ - `src/server/routes.ts` — records SessionStart rows in `StopGateDb` when hook events arrive.
9
+ - `src/core/PostUpdateMigrator.ts` — installs `stop-gate-router.js` and patches `.claude/settings.json` Stop hooks.
10
+ - `src/commands/init.ts` and `src/templates/hooks/settings-template.json` — include the router on fresh installs.
11
+ - `src/core/installCodexHooks.ts` — mirrors the Stop router into Codex hook config.
12
+ - Focused tests cover route mode persistence, hook behavior, Codex registration, and update migration.
13
+
14
+ **Under-block**: Intentional for this PR. Default mode is `shadow` only when the authority and SQLite log initialize successfully; otherwise the gate is `off`. In shadow mode the hook submits evaluations but always lets the agent exit. Enforcement is reserved for a later explicit operator flip.
15
+
16
+ **Over-block**: Minimal. The router only emits `{decision:"block"}` when the server is already in `enforce` mode and the server authority returns `continue` with a reminder. All network errors, malformed hook payloads, missing config, hot-path failures, compaction-in-flight, kill-switch, and degraded initialization paths fail open.
17
+
18
+ **Signal vs authority**: Compliant. The hook collects evidence metadata and simple signals, but never decides whether a Stop is unjustified. The server-side `UnjustifiedStopGate` remains the sole authority for `continue`; the hook is a transport/router.
19
+
20
+ **External surfaces**:
21
+ - New installed hook file: `.instar/hooks/instar/stop-gate-router.js`.
22
+ - Existing routes become live for real Stop events: `GET /internal/stop-gate/hot-path` and `POST /internal/stop-gate/evaluate`.
23
+ - New persisted mode file: `server-data/stop-gate-mode.json`.
24
+ - Existing SQLite event log: `server-data/stop-gate.db`.
25
+
26
+ **Migration parity**:
27
+ - Post-update migration writes the router and patches existing Claude settings.
28
+ - Fresh `instar init` installs the router template.
29
+ - Codex hook installer places the router first in the Stop chain before the existing review trio.
30
+
31
+ **Rollback cost**: Revert this change set. Existing `stop-gate-mode.json` and `stop-gate.db` files can remain on disk; without the router/server wiring they are inert. Emergency runtime rollback without code revert: `instar gate mode off` or set the kill-switch.
32
+
33
+ **Tests**:
34
+ - `npm test -- --run tests/unit/routes-stopGate.test.ts tests/unit/stop-gate-router-hook.test.ts tests/unit/installCodexHooks.test.ts tests/unit/PostUpdateMigrator-codexHooks.test.ts`
35
+ - `npx tsc --noEmit`