instar 1.3.464 → 1.3.466

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,72 @@
1
+ # Side-Effects Review — deferral-detector merge-deferral category
2
+
3
+ **Version / slug:** `deferral-detector-merge-deferral`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `independent reviewer subagent — see below`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds a fifth detection category — **merge-deferral** — to the `deferral-detector` hook (source-of-truth: `getDeferralDetectorHook()` in `src/core/PostUpdateMigrator.ts`; deployed copy mirrors it via the existing always-overwrite hook migration). New `mergeDeferralPatterns` array (7 patterns) catches two shapes of handing the merge of a **self-authored** PR back to the operator: (a) explicitly assigning the call — "the merge call is yours", "your merge call", "your call to merge", "leave the merge to you", "up to you to merge", "merge is your call to make"; and (b) asking permission to merge one's own PR — "want me to merge?", "should I merge?", "ready to merge?". Like the time/fatigue category, `mergeDeferralMatches` is NOT gated by the `isInfrastructureBacked` anti-trigger — having tracked the PR does not legitimize handing its merge back. A new checklist section instructs the agent to merge a self-authored green PR itself (`scripts/safe-merge.mjs … --squash --admin` / `gh pr merge`), states the operator directed this must never be a blocker (2026-06-09), and carves out the only legitimate non-merges (CI genuinely red on this change; someone else's PR). Still signal-only (`decision: 'approve'`, additionalContext only — never blocks). Files: `src/core/PostUpdateMigrator.ts` (hook template), `tests/unit/deferral-detector-orphan-todo.test.ts` (+11 cases).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `deferral-detector` hook outbound-message scan — **modify (additive)** — adds a new signal category. Does NOT change the existing inability/orphan/time-fatigue categories or the never-block contract.
15
+
16
+ ## 1. Over-block
17
+
18
+ No block surface — the hook is signal-only (injects additionalContext, never blocks/denies). "Over-block" → over-FLAG. Two over-flag classes, both bounded:
19
+
20
+ - **Permission-seeking on someone else's PR.** "want me to merge your PR?" about a PR the *user* authored is a legitimate question, but it still matches `merge_permission_seeking`. The hook can't see PR authorship from message text, so it flags and lets the agent decide — the checklist explicitly states "it is SOMEONE ELSE's PR (then asking is fine)", so the injected context tells the agent to disregard when that applies. Cost is one extra context block, never a withheld message.
21
+ - **Discussing this very feature.** A message *about* merge-deferral (like this one) contains the trigger words and will self-flag — annoying but harmless (signal-only), identical in kind to the orphan/time-fatigue categories self-flagging when discussed.
22
+
23
+ Patterns are scoped to the comm-command gate (only telegram-reply/send-message/etc. are scanned), bounding noise to outbound human messages.
24
+
25
+ ## 2. Under-block
26
+
27
+ Pattern set is finite regex; it will miss novel phrasings ("I'll let you make the merge decision on this", "the green light to merge is yours"). Acceptable for a signal layer — patterns cover the observed incident shape ("the merge call is yours") plus the common permission-seeking variants the operator's directive named. Residual (stated, not deferred): the detector does not attempt to verify PR authorship or CI state — it is a framing detector, not a merge executor. The actual auto-merge-on-green behavior lives in instar-dev Phase 7 (the skill flow); this hook is the cross-cutting catch for when an outbound message hands the merge back regardless of flow.
28
+
29
+ ## 3. Level-of-abstraction fit
30
+
31
+ Correct layer. This is a brittle keyword detector → it belongs as a SIGNAL (the deferral-detector is exactly that), not as blocking authority. It composes with the existing four categories in the same hook at the same altitude, reusing the comm-command gate, the JSON output contract, and the test harness. A complementary structural surface already exists at a different layer: instar-dev Phase 7 ("Auto-merge on green — never pause to ask") governs the build flow. This hook covers the gap Phase 7 doesn't: a self-authored-PR merge handed back in *any* outbound message, not only inside an instar-dev build. The two are layered (flow-level prescription + message-level detector), not parallel duplicates.
32
+
33
+ ## 4. Signal vs authority compliance
34
+
35
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
36
+
37
+ - [x] No — this change has no block/allow surface; it produces a signal (additionalContext) consumed by the agent, exactly per the hook's "SIGNAL ONLY" contract.
38
+
39
+ A brittle regex detector must NOT hold blocking authority; this addition stays signal-only. If hard enforcement is ever wanted, the right path is the instar-dev pre-commit/merge gates (which already exist) or feeding the smart gate (MessagingToneGate), never making this regex block. Compliant.
40
+
41
+ ## 5. Interactions
42
+
43
+ - **Shadowing:** the new category is independent of inability/orphan/time-fatigue; `allMatches` concatenates all categories. The orphan category's infrastructure-backed suppression is unchanged; the new category intentionally bypasses it (documented, mirrors time/fatigue). No existing category is shadowed.
44
+ - **Double-fire:** a message can now trigger multiple sections (e.g. permission-seeking inability + merge-deferral) — intended; each section is additive context. No double-send.
45
+ - **Races:** none — pure stdin→stdout, no shared state.
46
+ - **Migration parity:** `deferral-detector.js` is already in the always-overwrite migration set (`result.upgraded.push('hooks/instar/deferral-detector.js …')`), so the new content redeploys to every agent on update with no new migration entry needed.
47
+
48
+ ## 6. External surfaces
49
+
50
+ - **Agents/users:** ships to the whole install base via the always-overwrite hook migration. Effect: more frequent (signal-only) checklists when an agent hands a self-authored merge back or asks permission to merge. No user-visible message change, no API change.
51
+ - **Persistent state:** none.
52
+ - **Timing/runtime:** depends only on the outbound message text (already available to the hook). No new external calls.
53
+
54
+ ## 7. Rollback cost
55
+
56
+ Pure additive change to a signal hook template + tests. Back-out = revert the commit; on next update the prior hook content redeploys. No persistent state, no migration to unwind. An individual agent can also neutralize it locally by editing its deployed `.instar/hooks/instar/deferral-detector.js` (until next update). Low.
57
+
58
+ ## Conclusion
59
+
60
+ An additive, signal-only change that closes a real, freshly-corrected behavior gap (handing a self-authored green PR's merge back to the operator — incident 2026-06-09, PR #1040 presented as "the merge call is yours") with code rather than willpower — directly per the Structure > Willpower standard, and reinforcing the operator directive that a self-authored merge must never be a blocker. No block surface added; signal-vs-authority compliant; layered with (not duplicating) instar-dev Phase 7. Because it modifies a behavioral guard hook, a Phase-5 second-pass review is requested before commit.
61
+
62
+ ## Second-pass review (if required)
63
+
64
+ **Reviewer:** independent reviewer subagent (general-purpose)
65
+ **Independent read of the artifact: concur**
66
+
67
+ Concur — no must-fix bugs found. The reviewer independently rendered the deployed hook (`getHookContent('deferral-detector')` → `node -c` valid JS) and confirmed: (1) **escaping correct** — every `\\b` → `\b` in the deployed regex, both `\\'` → valid `\'` in the single-quoted checklist strings, no stray backtick / broken escape / silently-never-matching regex (all 7 patterns matched real inputs); (2) **the laundering invariant holds** — `mergeDeferralMatches` is computed by a plain `.filter()` OUTSIDE the `isInfrastructureBacked ? [] : …` gate, verified empirically that "tracked commitment + follow-up PR … but the merge call is yours" STILL fires; (3) **signal-only intact** — exactly one output path (`decision:'approve'`, additionalContext only), six `process.exit(0)`, no block/deny/ask path; (4) **no regression** — the only source deletion is the single `allMatches` line re-spread to append `...mergeDeferralMatches`; inability/orphan/time-fatigue arrays + gates + sections byte-for-byte unchanged; (5) **false-positive profile acceptable** — all must-NOT-fire cases ("I merged it myself", "I'll merge on green", "merging now", "Both fixes verified live") plus 10 more benign merge-mentioning messages produced zero firings; full file 35/35. Bounded over-fire noted (permission-seeking pattern (b) can fire on non-PR "safe to merge these branches?" uses) — over-FLAG not over-block, anticipated in §1, judged not-must-fix for a signal layer (anchoring it to own-PR context would risk under-firing the real incident shape). Under-fire on novel framings noted as expected residual.
68
+
69
+ ## Evidence pointers
70
+
71
+ - Live incident: 2026-06-09, topic 2169 — the agent built PR #1040 (auto-heal ladder) and presented it as "CI is running now … the merge call is yours," prompting the operator's correction: "the merge call should never be mine, at least not for PRs you authored. Please change this permanently moving forward so it is never a blocker."
72
+ - Tests: `tests/unit/deferral-detector-orphan-todo.test.ts` merge-deferral block (11 cases) incl. the exact incident phrasing, the permission-seeking variants, the "tracked PR still fires" laundering case, and the must-NOT-fire cases ("I merged it myself", "I'll merge on green", "merging now"). Generated-hook node syntax check passed; `tsc --noEmit` exit 0; 35/35 deferral-detector tests green.
@@ -0,0 +1,104 @@
1
+ # Side-Effects Review — Reaper active-process relaxation parity (terminate honors the reaper's relaxation)
2
+
3
+ **Version / slug:** `reaper-active-process-relaxation-parity`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `reviewer subagent (required — touches a KEEP-guard / kill decision)`
7
+
8
+ ## Summary of the change
9
+
10
+ `SessionReaper.evaluate()` already relaxes the `active-process` KEEP-veto for a session it has proven idle — either `cpuFlat` under pressure (`cpuAwareActiveProcessKeep`) or 8h-stale-idle whose only blocker is its own idle children, e.g. the standing MCP stack (`reapStaleIdleWithActiveChildren`, default ON). It then layers transcript-flat + positive-idle + render-stasis-through-grace before reaching `reap-pending` and calling `terminate()`. But the terminate authority (`SessionManager.terminateSession`) re-runs the **shared, un-relaxed `ReapGuard`**, which returns `keep('active-process')` again — so it re-vetoes the very reap the reaper authorized. The reap is attempted and skipped every tick forever (observed live on dist v1.3.448: **1,532× `skipped:active-process`**, 0 idle reaps; idle sessions accumulated and over-subscribed the host). This change plumbs the reaper's already-made relaxation through to the authority via a new `bypassActiveProcessKeep` opt — mirroring the existing `bypassRecoveryFlag` pattern. It lifts ONLY the `active-process` keep-reason and only when the reaper sets it (on a reap whose veto it relaxed). Files: `src/core/SessionManager.ts` (opt + bypass), `src/monitoring/SessionReaper.ts` (dep signature + `performReap` forwards the flag), `src/commands/server.ts` (dep wiring passes the opt through), plus unit/integration/e2e tests.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `SessionManager.terminateSession` autonomous KEEP-guard (`src/core/SessionManager.ts:~793`) — **modify** — add a scoped bypass of the `active-process` reason when `opts.bypassActiveProcessKeep` is set; every other KEEP-guard is re-checked and still vetoes.
15
+ - `SessionReaper.performReap` (`src/monitoring/SessionReaper.ts`) — **modify** — forwards `bypassActiveProcessKeep: <reap relaxed active-process>` to `terminate()`. The flag is `evaln.cpuTightened || evaln.staleIdleRelaxed`.
16
+ - `SessionReaperDeps.terminate` signature — **modify** — accepts an optional `{ bypassActiveProcessKeep }` third arg.
17
+ - `ReapGuard` itself — **pass-through** — unchanged; the relaxation stays in the reaper (the component holding the per-tick idle proof), and the authority is told the decision rather than re-deriving it.
18
+
19
+ ---
20
+
21
+ ## 1. Over-block
22
+
23
+ **What legitimate inputs does this change reject that it shouldn't?**
24
+
25
+ None new — this change *removes* an over-block (the reaper's own authorized reap was being rejected). It does not reject anything that was previously allowed: the bypass only ever lifts a veto, never adds one.
26
+
27
+ ---
28
+
29
+ ## 2. Under-block
30
+
31
+ **What failure modes does this still miss / could the bypass let a working session through?**
32
+
33
+ The risk is over-reaping a session that *looks* idle but is doing real work via a child process. This is bounded by the conditions that must ALL hold before `performReap` sets the flag: (a) the reaper relaxed active-process only via `cpuFlat` (descendants below the CPU-active floor under pressure) or `staleIdle` (no user message in 8h); AND (b) the stateful proofs still cleared — transcript did not grow this tick, the pane is positively idle (ready prompt, no working footer), and the frame was byte-static across the full grace window (a frame change aborts the reap). A session genuinely working would grow its transcript or render a working frame and be KEPT. And the worst case is recoverable: a reaped session's conversation persists in its transcript and resumes via `claude --resume` on the next message. Residual miss: a child doing real work that produces NO pane render and NO transcript growth for 8h while the user is silent — extremely narrow, and resumable if it ever occurs.
34
+
35
+ ---
36
+
37
+ ## 3. Level-of-abstraction fit
38
+
39
+ **Is this at the right layer?**
40
+
41
+ Yes. The relaxation DECISION needs the per-tick observation state (CPU delta, transcript-growth, frame-stasis, candidacy clock) that only `SessionReaper` holds — so it correctly stays there. The terminate AUTHORITY (`SessionManager`) is the right place to enforce the guard for *other* killers, but it has no per-tick state and cannot re-derive the relaxation; so the reaper TELLS it the decision via an explicit, scoped opt. This is exactly the existing `bypassRecoveryFlag` contract (the recovery engine tells the authority "I already set the recovery flag; honor my kill"). The alternative — making `hasActiveProcesses` MCP-aware with a process-name allowlist — was rejected: it is brittle (allowlist drift), changes a shared primitive used by `McpProcessReaper`, and is unnecessary because the existing `staleIdle`/`cpuFlat` relaxations already identify the case correctly.
42
+
43
+ ---
44
+
45
+ ## 4. Signal vs authority compliance
46
+
47
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
48
+
49
+ - [x] No — this change has no NEW block/allow surface; it lifts an existing veto in a scoped, explicit case.
50
+
51
+ The change does NOT add brittle blocking authority. It removes an over-broad veto in a specific, well-evidenced case. The default (no flag) leaves the `active-process` veto fully intact — fail-safe toward KEEP. The bypass is opt-in per call and only the reaper (which has already produced strong idle evidence) sets it. An arbitrary killer calling `terminate()` without the flag still gets the full veto. No new heuristic owns a kill decision; the existing reaper pipeline (which already had the relaxation) simply has its decision honored end-to-end.
52
+
53
+ ---
54
+
55
+ ## 5. Interactions
56
+
57
+ - **Shadowing:** The bypass is evaluated alongside `bypassRecoveryFlag` in the same `bypassThis` expression; they are independent (different keep-reasons). No shadowing — each only lifts its own reason.
58
+ - **Double-fire:** None. `performReap` is the single idle-reap call site; the topic-moved-closeout path keeps its plain 2-arg `terminate()` call (no bypass) and is unaffected.
59
+ - **Races:** The reaper's two-phase (mark-reaping → grace → terminate) and the in-flight lock in `terminateSession` are unchanged. The bypass is read inside the existing guarded section; protected/lease/CAS/in-flight all still apply.
60
+ - **Feedback loops:** None. Reaping an idle session frees resources; it does not feed back into the reaper's inputs for other sessions.
61
+
62
+ ---
63
+
64
+ ## 6. External surfaces
65
+
66
+ - **Other agents / users:** Behavior change is internal to one agent's session lifecycle. Across the install base, idle sessions whose only blocker was their standing MCP stack will now actually be reaped once 8h-stale (or CPU-flat under pressure) — the intended fix. Conversations are preserved (resume on next message), so the user-visible effect is "idle topics get cleaned up; they resume seamlessly when next messaged."
67
+ - **External systems:** None (no Telegram/Slack/GitHub/Cloudflare surface).
68
+ - **Persistent state:** None added. The reap-log already records reaped/skipped; entries will now show `reaped` where they showed `skipped:active-process`.
69
+ - **Timing/runtime:** The reap still requires the full hysteresis + grace window; no timing assumption changes.
70
+
71
+ ---
72
+
73
+ ## 7. Rollback cost
74
+
75
+ Pure code change across three source files; revert and ship as the next patch. No persistent-state migration, no agent-state repair, no user-visible regression during the rollback window (reverting simply restores the prior "idle sessions kept" behavior). The new config-free behavior rides the already-shipped `reapStaleIdleWithActiveChildren`/`cpuAwareActiveProcessKeep` flags, so a precise rollback can also be achieved by toggling those without reverting code.
76
+
77
+ ---
78
+
79
+ ## Conclusion
80
+
81
+ The review confirmed the fix is the *minimal correct* one: the relaxation already existed and was correct; the only defect was that the terminate authority re-vetoed it. Plumbing the decision through (mirroring `bypassRecoveryFlag`) fixes the stalemate without adding any brittle heuristic or broad primitive change, and fails safe (default keeps the veto; over-reap is bounded by strong idle proofs and is recoverable via `--resume`). Clear to ship pending second-pass concurrence.
82
+
83
+ ---
84
+
85
+ ## Second-pass review (if required)
86
+
87
+ **Reviewer:** independent reviewer subagent (required — touches a KEEP-guard / kill decision)
88
+ **Independent read of the artifact: CONCUR**
89
+
90
+ The reviewer independently traced all five audit axes and concurred:
91
+ - **Over-reap (safe):** `relaxedActiveProcess` is set true only when `evaluate()` assigned `cpuTightened`/`staleIdleRelaxed`, which happens ONLY inside the `blocked.reason === 'active-process'` relaxation branch; the stateful idle proofs (transcript-not-grown, positive-idle-required, frame-static-through-grace) sit strictly downstream, so there is no path where the flag is true but the session is rendering or growing. Worst case (8h-silent, no render, no transcript growth, real child work) is bounded and recoverable via `--resume`.
92
+ - **Scope (correct):** because `ReapGuard.evaluate()` returns the single first-match keep reason (ordered early-return), lifting the bypass on `active-process` cannot unmask any higher-priority guard; protected/lease/CAS/in-flight all fire outside the bypassable block.
93
+ - **Signal vs authority (compliant):** no new blocking surface — it lifts an existing veto, opt-in and fail-safe; default (no flag) preserves prior behavior exactly; exact mirror of `bypassRecoveryFlag`.
94
+ - **Wiring (correct):** the server dep closure forwards the opt; the topic-moved-closeout call deliberately omits it.
95
+ - **Tests (both sides):** flag-true reaps, flag-false / other-reason kept, real reaper↔authority agreement integration-tested.
96
+
97
+ Minor doc nit raised: ensure the referenced `docs/specs/reaper-active-process-relaxation-parity.md` exists — RESOLVED, it is added as part of this change.
98
+
99
+ ---
100
+
101
+ ## Evidence pointers
102
+
103
+ - Live root-cause evidence: reap-log `skipped:active-process` ×1,532 + `skipped:open-commitment` ×173; reaper-audit `reap-pending` rows for the same sessions (dist v1.3.448).
104
+ - Tests: `tests/unit/session-reaper-cpu-aware-keep.test.ts` (reaper passes the flag iff it relaxed), `tests/unit/session-manager-terminate.test.ts` (authority honors the flag, scoped to active-process only), `tests/integration/session-lifecycle-reap-wiring.test.ts` (real reaper↔terminate agree end-to-end), `tests/e2e/session-reaper-lifecycle.test.ts` (lifecycle still green). All green: 115 tests across the 7 affected suites.