instar 1.3.985 → 1.3.986

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,222 @@
1
+ # Side-Effects Review — a startup banner, and a startup menu, were read as an input prompt
2
+
3
+ **Version / slug:** `booting-pane-read-as-ready`
4
+ **Date:** `2026-07-26`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `independent reviewer subagent — VERDICT: CONCERN. Design changed in response; see Phase 5.`
7
+
8
+ ## Summary of the change
9
+
10
+ `SessionManager.detectClaudePrompt` decided a pane was accepting input if the last
11
+ six non-blank lines contained `❯`, `›`, `bypass permissions`, `/(effort|model|fast)`,
12
+ or `(low|medium|high) · /effort`. Two independent defects:
13
+
14
+ **1 — the banner.** The slash-command clause matched a bare mention **anywhere, in
15
+ any context, including prose**. Claude Code's startup banner advertises slash
16
+ commands in prose; the incident pane carried:
17
+
18
+ > `…Fable 5 draws down usage faster than Opus 4.8. Run /model and`
19
+ > `select Fable to use it. Learn more: https://support.claude.com/…`
20
+
21
+ **2 — the menu (found by the operator).** Claude Code paints the **same `❯`** on a
22
+ menu's focused option that it uses for the input box. A session sitting on a
23
+ startup question therefore read as READY. This is strictly worse: text typed at a
24
+ banner is lost, but **Enter at a menu SELECTS AN OPTION**, so an arriving message
25
+ can answer a permission question on the operator's behalf.
26
+
27
+ **Observed (topic 29723, 2026-07-26).** Inbound `17:44:49Z` → respawn → pointer
28
+ injected `17:45:00Z` → that session's start hook completed `17:45:15Z`. The paste
29
+ landed 15s early, was swallowed by the redraw, and the injector logged
30
+ `Injected initial message into "…" (915 chars, after stabilization delay)` — a
31
+ success line for a delivery that did not happen. The message reached the agent only
32
+ via the start hook's unanswered-message backstop, an unrelated path.
33
+
34
+ **The slash-command clauses are DELETED, not tuned.** The first attempt kept them
35
+ and required "status-bar shape" (line-start or post-interpunct). Second-pass review
36
+ falsified that, and I verified each claim before accepting it:
37
+
38
+ | falsifying input | verdict under the "shape" fix |
39
+ |---|---|
40
+ | `⚠ Fable 5 promotional access ends soon · /model to opt in` | `ready` (false positive) |
41
+ | ` · /model to switch between Opus and Fable` | `ready` (false positive) |
42
+ | soft-wrapped banner putting `/model` at column 0 | `ready` (false positive) |
43
+ | `opus 5 • medium • /effort` (bullet separator) | not ready (false negative) |
44
+
45
+ The artifact's own fixture contains `· /memory to free up context` and
46
+ `+1 more · /status` — **banner** lines in the exact shape it claimed prose never
47
+ uses. So discrimination was still vocabulary wearing structure's clothes, and the
48
+ line-start branch reinstated the Anthropic-copy dependency it claimed to remove
49
+ (at a fixed pane width, a copy edit shifting the wrap ~14 chars recreates the
50
+ original defect). A signal that cannot separate an *advert for* a command from a
51
+ *status bar showing* one carries no readiness information. Deleted, and the
52
+ genuinely structural markers widened instead.
53
+
54
+ **The answer is no longer a boolean.** See §1 — one of three consumers KILLS a live
55
+ session on not-ready. `classifyPaneReadiness` returns `ready | menu | not-ready`;
56
+ `isReadyPromptTail` is retained as the façade for callers that only want "can I
57
+ type now".
58
+
59
+ ## Decision-point inventory
60
+
61
+ | point | classification | note |
62
+ |---|---|---|
63
+ | `❯` / `›` present (and not on an option line) → ready | `invariant` | Deterministic substring test. |
64
+ | permission-mode / shortcut footer present → ready | `invariant` | Widened from one marker to four, sourced from the probes already trusted for this (`SessionReaper`, interactive-pool config) rather than invented here. |
65
+ | selector glyph ON a numbered option line + ≥2 options → menu | `invariant` | Deterministic shape test, mirroring `PermissionPromptAutoResolver`'s existing definition rather than a second divergent one. |
66
+ | slash-command mention → ready | **REMOVED** | Could not distinguish advert from status bar in any formulation tried. |
67
+ | consent-dialog auto-accept | `invariant` | Unchanged, and deliberately left in `SessionManager` — it has a side effect (`send-keys`), so it does not belong in a pure classifier. Verified still running BEFORE the classifier and over all 20 captured lines. |
68
+
69
+ No judgment points. No model call.
70
+
71
+ ## 1. Over-block
72
+
73
+ **The risk is not uniform across callers, and the first draft of this section was
74
+ wrong about that.** There are three consumers:
75
+
76
+ 1. `waitForClaudeReadyWithRetry` → `handleReadyAndInject` (spawn/inject). Failure
77
+ direction: waits, then the pre-existing extended-wait and blind-inject-if-alive
78
+ fallbacks apply. Safe.
79
+ 2. `waitForClaudeReady` direct, in the Slack stuck-session path (`server.ts:8395`).
80
+ **Failure direction: it KILLS the live session and respawns.** A tightened probe
81
+ that false-negatives here destroys a live conversation. `docs/signal-vs-authority.md`
82
+ names session lifecycle as explicitly high-risk.
83
+ 3. `classifyPaneState` (new), used by (2) to tell `menu` from `not-ready`.
84
+
85
+ **Mitigations, both required by the above:**
86
+ - The positive marker set was **widened**, not just narrowed: `? for shortcuts`,
87
+ `shift+tab to cycle`, `auto-accept edits` join `bypass permissions`. Previously
88
+ only one of Claude Code's three permission-mode footers was recognised, so a
89
+ session in auto-accept mode depended entirely on `❯` being visible. That
90
+ assumption is no longer load-bearing. All four are asserted absent from the
91
+ banner fixture, so the widening costs no false positives.
92
+ - The Slack path no longer kills on `menu`. It gives the always-on auto-resolver a
93
+ bounded second window and re-checks; a menu that never clears falls through to
94
+ the pre-existing stuck path, so no message is lost.
95
+
96
+ **Residual, stated honestly:** a status bar using a separator other than U+00B7 (`•`,
97
+ box-drawing) and showing none of the four footers and no `❯` would now wait. I have
98
+ not enumerated every Claude Code rendering across widths and themes. On caller (1)
99
+ that costs a delay; on caller (2) the menu carve-out plus the widened footers are
100
+ what keep it from costing a session.
101
+
102
+ ## 2. Under-block
103
+
104
+ **Injection still reports success on having TYPED, not on arrival.**
105
+ `verifyInjection` recovers a swallowed *submit*, not a swallowed *paste*. A pane
106
+ that becomes ready and then stalls mid-paste still produces a success log for a lost
107
+ message. Deliberately **not bundled** — it changes the injector's success criterion
108
+ at every inject callsite, and folding it in would make this PR's refusal evidence
109
+ ambiguous. Recorded as a candidate, not silently deferred.
110
+
111
+ Also untouched: the probe reads the last 6 of 20 captured non-blank lines. A banner
112
+ longer than that window pushing a real prompt out of view still reads not-ready.
113
+ Unchanged behaviour; fails toward waiting on callers (1) and (3).
114
+
115
+ The menu detector requires ≥2 numbered options. A single-option prompt, or a
116
+ free-text startup question with no numbered options, is not detected as a menu.
117
+
118
+ ## 3. Level-of-abstraction fit
119
+
120
+ One pure function, no state, no I/O, below `SessionManager`. The consent-dialog
121
+ branch stayed behind because it presses keys — a classifier that mutates the pane is
122
+ not a classifier.
123
+
124
+ **A smarter component already exists and was consulted rather than duplicated.**
125
+ `PermissionPromptAutoResolver` owns approval-prompt handling (matching, auto-answer,
126
+ audit). This probe deliberately reuses its notion of a menu (selector glyph on a
127
+ numbered option line) instead of inventing a second one, and does not attempt to
128
+ answer prompts — it only declines to call a menu an input surface.
129
+
130
+ Callers of the removed clause: exactly one, verified independently by grep. No
131
+ duplicate site left holding the old semantics.
132
+
133
+ ## 4. Signal vs authority compliance
134
+
135
+ **The first draft's answer here was materially incomplete and is corrected.** It
136
+ claimed the probe "cannot block a message, refuse an action, or reach a user". False
137
+ via §1(2): a not-ready verdict at `server.ts` refuses an action and kills a session.
138
+
139
+ The *principle* is not violated — `docs/signal-vs-authority.md` scopes it to
140
+ brittle logic making judgments about **meaning**, and exempts deterministic
141
+ invariants. This is mechanics (is a glyph on screen), not meaning. But the honest
142
+ statement is: **this probe feeds a destructive authority**, which is exactly why the
143
+ verdict was widened from a boolean to a named state, so that authority can
144
+ distinguish "wedged" from "waiting on an answer" instead of treating both as kill.
145
+
146
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
147
+
148
+ No judgment points introduced. The change moves *away* from a vocabulary match whose
149
+ behaviour drifts as Anthropic edits its banner copy, toward structural markers. Note
150
+ the correction to the first draft: the intermediate "status-bar shape" design did
151
+ **not** achieve that and claimed it anyway — the claim is retracted above rather
152
+ than quietly restated.
153
+
154
+ ## 5. Interactions
155
+
156
+ - **`waitForClaudeReadyWithRetry` / `handleReadyAndInject`** — unchanged; both now
157
+ consult the corrected probe. A pane that used to pass early on the banner now
158
+ passes on the input box instead: later, and correctly.
159
+ - **`server.ts` Slack stuck-session path** — changed, see §1.
160
+ - **`PermissionPromptAutoResolver`** — complementary, not shadowed. It clears
161
+ prompts; this declines to type at them. The Slack carve-out explicitly depends on
162
+ it running (it is an always-on floor with no enable flag).
163
+ - **`StuckInputSentinel`, `SessionWatchdog`, `PromptGate`, `PresenceProxy`,
164
+ `SessionReaper`, `TriageOrchestrator`, `anthropic-interactive-pool`** — each keeps
165
+ its own independent markers for its own question ("is it stuck?", "is it idle?").
166
+ None imports `detectClaudePrompt`; none carried the removed clause. Their marker
167
+ vocabulary is now the *source* for this probe's footers rather than a fourth
168
+ divergent set.
169
+ - **`ModelSwapService`** injects literal `/model <id>` into live panes — a real
170
+ in-pane source of the removed token. Another reason the clause had to go.
171
+ - **Codex / Gemini / pi panes** — the `›` clause is unchanged; they do not print the
172
+ Claude banner.
173
+
174
+ ## 6. External surfaces
175
+
176
+ None. No route, no config key, no CLI flag, no message text, no state file. Two new
177
+ exported functions and one new public method on `SessionManager`, all internal.
178
+
179
+ ## 6b. Operator-surface quality
180
+
181
+ One new log line when the Slack path declines to kill a session on a menu, naming
182
+ the session and the reason. The pre-existing failure lines are unchanged.
183
+
184
+ Worth recording for the follow-up: the *success* line still reports typing, not
185
+ arrival — the asymmetry that hid this incident for seven hours is untouched here.
186
+
187
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
188
+
189
+ Machine-local by construction — the probe reads a tmux pane on the machine that owns
190
+ it. No replication, no lease interaction, no shared state, no generated URL. Two
191
+ machines on different versions apply their own probe to their own panes; there is no
192
+ cross-machine invariant to violate.
193
+
194
+ ## 8. Rollback cost
195
+
196
+ Low. Delete the module, restore the inline clauses, revert one `server.ts` block. No
197
+ migration, no persisted state, no config default, nothing written to an agent home.
198
+ The test file would fail on revert, which is the desired property.
199
+
200
+ ## Phase 5 — Second-pass review (independent reviewer subagent)
201
+
202
+ **VERDICT: CONCERN** — one blocking, four should-fix, one note. Every finding was
203
+ independently verified before being accepted; all were real.
204
+
205
+ | # | finding | disposition |
206
+ |---|---|---|
207
+ | 1 (blocking) | Unenumerated third consumer at `server.ts:8395` kills a live session on not-ready, so §1's "the failure direction is waiting, which is safe" and §4's "cannot refuse an action" are false for it. | **Design changed.** Verdict widened to `ready \| menu \| not-ready`; Slack path given a menu carve-out + bounded re-check; §1 and §4 rewritten. |
208
+ | 2 (should-fix) | The `^` line-start branch reinstates the Anthropic-copy dependency via wrap geometry, and carries no test weight. | **Clause deleted entirely** (both branches). |
209
+ | 3 (should-fix) | "Shape, not vocabulary" is falsified by the artifact's own fixture; `· /model to opt in` matches. | **Verified and accepted.** Clause deleted; the claim is retracted in §4b rather than restated. |
210
+ | 4 (should-fix) | "A ready pane always shows `❯`" is contradicted by four sibling probes; only one of three permission-mode footers was covered. | **Marker set widened** to four footers, sourced from those siblings. This is also the mitigation for #1. |
211
+ | 5 (should-fix) | §4 Q4 materially incomplete. | **Rewritten** above. |
212
+ | 6 (note) | Interpunct is U+00B7 only; `•` and box-drawing are false negatives. `ModelSwapService` injects `/model` into live panes. | Moot for the separator (clause deleted); the `ModelSwapService` point is recorded in §5 as further justification. |
213
+
214
+ **Independently found by the operator, in parallel:** the menu case (§ Summary 2).
215
+ Verified against three realistic startup questions, all three of which read as ready
216
+ before this change.
217
+
218
+ **Reviewer concurrence on the revised design was not re-sought.** The revision was
219
+ driven by the reviewer's own findings plus a verified operator report, and every
220
+ change is pinned by a test asserting both sides of its boundary. That is a
221
+ disclosed reduction in independence for the second iteration, not a claim of
222
+ concurrence.