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,137 @@
1
+ /**
2
+ * Readiness classification for a framework TUI pane tail.
3
+ *
4
+ * WHY THIS EXISTS AS ITS OWN MODULE
5
+ * ---------------------------------
6
+ * On 2026-07-26 a user's message was injected into a freshly-spawned Claude Code
7
+ * pane 15 seconds before that pane could accept input. The paste was swallowed by
8
+ * the TUI still painting itself, and the delivery was logged as a SUCCESS
9
+ * (`Injected initial message into "…" (915 chars, after stabilization delay)`).
10
+ * The message reached the agent only because an unrelated start-hook backstop
11
+ * re-read recent history.
12
+ *
13
+ * The cause was the readiness probe, not the injector. It accepted a bare
14
+ * slash-command mention anywhere in the captured tail:
15
+ *
16
+ * if (/\/(effort|model|fast)/.test(tail)) return true;
17
+ *
18
+ * Claude Code's startup banner advertises slash commands IN PROSE — the pane that
19
+ * fooled it carried "…Run /model and select Fable to use it." So the probe matched
20
+ * a promotional line and declared a still-booting pane ready. A string matcher
21
+ * fooled by its own subject: the banner that proves the app is NOT yet accepting
22
+ * input was read as proof that it is.
23
+ *
24
+ * THOSE CLAUSES ARE DELETED, NOT TUNED. The first fix attempt kept them and
25
+ * required "status-bar shape" (line-start, or directly after an interpunct).
26
+ * Second-pass review falsified that:
27
+ * - The banner uses the same shape. `⚠ CLAUDE.md is over the … limit · /memory
28
+ * to free up context` and `+1 more · /status` are BANNER lines in interpunct
29
+ * shape. The only thing separating them from a status bar was that `memory`
30
+ * and `status` fall outside the three-word set — i.e. still vocabulary, and a
31
+ * plausible `· /model to opt in` promo line matched outright.
32
+ * - The line-start branch reinstated the Anthropic-copy dependency it claimed to
33
+ * remove: at a fixed pane width an edit shifting the wrap by ~14 characters
34
+ * puts `/model` at column 0 and the original defect returns.
35
+ * A signal that cannot separate an ADVERT for a command from a STATUS BAR showing
36
+ * one carries no information about readiness. Widening the genuinely structural
37
+ * markers below costs nothing and covers the same panes.
38
+ *
39
+ * WHY THE ANSWER IS NOT A BOOLEAN
40
+ * -------------------------------
41
+ * There are three consumers and they want OPPOSITE responses to the two ways a
42
+ * pane can fail to be ready:
43
+ * - the spawn/inject path — must not type into a pane that is still painting,
44
+ * and must not type into a MENU (see below);
45
+ * - `waitForClaudeReadyWithRetry` — waits, then falls back;
46
+ * - the Slack stuck-session path (`server.ts`) — calls this on an ALREADY-LIVE
47
+ * session and KILLS it when the answer is not-ready.
48
+ * A bare `false` tells that third caller "kill this" for a pane that is merely
49
+ * mid-boot or politely waiting on a question. So the probe reports WHICH state it
50
+ * saw and each caller applies its own policy. `isReadyPromptTail` is retained as
51
+ * the boolean façade for callers that genuinely only want "can I type now".
52
+ *
53
+ * THE MENU CASE (found by the operator, 2026-07-26)
54
+ * -------------------------------------------------
55
+ * Claude Code paints the SAME `❯` glyph on a menu's focused option as it uses for
56
+ * the input box (`PermissionPromptAutoResolver.SELECTOR_GLYPH`). So a session
57
+ * sitting on a startup question read as READY. This is strictly worse than the
58
+ * banner case: text typed at a banner is lost, but text typed at a menu is not —
59
+ * Enter SELECTS AN OPTION, so an arriving message can answer a permission question
60
+ * on the operator's behalf. A menu is therefore classified as its own state, never
61
+ * as ready, no matter which glyphs it carries.
62
+ *
63
+ * Kept pure (text in, verdict out) so it is testable without tmux, and so the
64
+ * regression fixtures are the literal pane text that caused each finding.
65
+ *
66
+ * RULE 3.1 RATIONALE (state-detection; registry:
67
+ * `specs/provider-portability/06-state-detector-registry.md`)
68
+ * ------------------------------------------------------------------------------
69
+ * - **Criticality:** silent-corruption-if-wrong — the worst class, demonstrated
70
+ * twice. A false `ready` on a painting pane loses a user message AND writes a
71
+ * success line for it; a false `ready` on a menu is worse still, because Enter
72
+ * SELECTS an option, so an arriving message can answer a permission question on
73
+ * the operator's behalf.
74
+ * - **Frequency:** per-spawn and per-inject, plus per-stuck-check on the Slack
75
+ * path.
76
+ * - **Stability:** UNSTABLE. Claude Code's TUI is a private surface. This is not a
77
+ * theoretical rating — the 2026-07-26 incident WAS an upstream copy change (a
78
+ * Fable-5 promo line) turning a passing detector into a failing one, and the
79
+ * menu case is an upstream glyph reuse.
80
+ * - **Fallback:** partial and asymmetric. The spawn path has an extended wait and
81
+ * a blind-inject-if-alive fallback; the loss on 2026-07-26 was caught only by
82
+ * the start hook's unanswered-message backstop, which is an unrelated path and
83
+ * cannot be relied on as this detector's safety net. The Slack path's not-ready
84
+ * branch is DESTRUCTIVE (kill + respawn), so a wrong answer there costs a live
85
+ * session, not a delay.
86
+ * - **→ Verdict: deterministic + canary REQUIRED.** This module ships the
87
+ * deterministic half plus regression fixtures carrying the literal pane text of
88
+ * both known failures. It does NOT ship a canary, so it detects the two shapes
89
+ * that already bit us and not the next drift. Canary tracked as CMT-1044.
90
+ * <!-- tracked: CMT-1044 -->
91
+ */
92
+ /**
93
+ * What a captured pane tail shows.
94
+ *
95
+ * - `ready` — an input surface is drawn and accepting input.
96
+ * - `menu` — a numbered selection menu is focused. NOT ready, and NOT a
97
+ * stuck session: something is waiting on an answer. A caller
98
+ * that kills on not-ready must not kill on this.
99
+ * - `not-ready` — no input surface found (typically still painting).
100
+ */
101
+ export type PaneReadiness = 'ready' | 'menu' | 'not-ready';
102
+ /**
103
+ * True when the tail shows a FOCUSED selection menu.
104
+ *
105
+ * Requires the selector glyph to sit ON a numbered option line, AND at least two
106
+ * numbered option lines overall. Both halves are load-bearing:
107
+ *
108
+ * - "glyph on the option line" separates a menu from the input box. The box
109
+ * renders `❯` alone on its own line; a menu renders `❯ 1. Yes`. Testing only
110
+ * "the tail contains ❯ somewhere" would classify ordinary assistant output
111
+ * that happens to list `1.` and `2.` above the prompt as a menu, stalling a
112
+ * genuinely ready session — an over-block on the inject path.
113
+ * - "at least two options" separates a menu from a single numbered line of
114
+ * ordinary output.
115
+ *
116
+ * This mirrors `PermissionPromptAutoResolver`'s requirement (a line whose lead
117
+ * glyphs contain ❯ whose text is `N.` + a label) rather than inventing a second,
118
+ * divergent notion of what a menu looks like.
119
+ */
120
+ export declare function tailShowsMenu(tail: string): boolean;
121
+ /**
122
+ * Classify a captured pane tail.
123
+ *
124
+ * `tail` is the joined last-N non-blank lines of a `capture-pane` read. Never pass
125
+ * the whole scrollback: a banner that has scrolled far above the prompt is not
126
+ * evidence either way, and older content only adds false-positive surface.
127
+ */
128
+ export declare function classifyPaneReadiness(tail: string): PaneReadiness;
129
+ /**
130
+ * Boolean façade: can text be typed into this pane right now?
131
+ *
132
+ * A menu answers `false` — correct for every caller that is about to type. A
133
+ * caller whose not-ready branch is DESTRUCTIVE (kill/respawn) must use
134
+ * `classifyPaneReadiness` instead and leave a `menu` pane alone.
135
+ */
136
+ export declare function isReadyPromptTail(tail: string): boolean;
137
+ //# sourceMappingURL=claudeReadinessProbe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claudeReadinessProbe.d.ts","sourceRoot":"","sources":["../../src/core/claudeReadinessProbe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC;AA6B3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUnD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAejE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Readiness classification for a framework TUI pane tail.
3
+ *
4
+ * WHY THIS EXISTS AS ITS OWN MODULE
5
+ * ---------------------------------
6
+ * On 2026-07-26 a user's message was injected into a freshly-spawned Claude Code
7
+ * pane 15 seconds before that pane could accept input. The paste was swallowed by
8
+ * the TUI still painting itself, and the delivery was logged as a SUCCESS
9
+ * (`Injected initial message into "…" (915 chars, after stabilization delay)`).
10
+ * The message reached the agent only because an unrelated start-hook backstop
11
+ * re-read recent history.
12
+ *
13
+ * The cause was the readiness probe, not the injector. It accepted a bare
14
+ * slash-command mention anywhere in the captured tail:
15
+ *
16
+ * if (/\/(effort|model|fast)/.test(tail)) return true;
17
+ *
18
+ * Claude Code's startup banner advertises slash commands IN PROSE — the pane that
19
+ * fooled it carried "…Run /model and select Fable to use it." So the probe matched
20
+ * a promotional line and declared a still-booting pane ready. A string matcher
21
+ * fooled by its own subject: the banner that proves the app is NOT yet accepting
22
+ * input was read as proof that it is.
23
+ *
24
+ * THOSE CLAUSES ARE DELETED, NOT TUNED. The first fix attempt kept them and
25
+ * required "status-bar shape" (line-start, or directly after an interpunct).
26
+ * Second-pass review falsified that:
27
+ * - The banner uses the same shape. `⚠ CLAUDE.md is over the … limit · /memory
28
+ * to free up context` and `+1 more · /status` are BANNER lines in interpunct
29
+ * shape. The only thing separating them from a status bar was that `memory`
30
+ * and `status` fall outside the three-word set — i.e. still vocabulary, and a
31
+ * plausible `· /model to opt in` promo line matched outright.
32
+ * - The line-start branch reinstated the Anthropic-copy dependency it claimed to
33
+ * remove: at a fixed pane width an edit shifting the wrap by ~14 characters
34
+ * puts `/model` at column 0 and the original defect returns.
35
+ * A signal that cannot separate an ADVERT for a command from a STATUS BAR showing
36
+ * one carries no information about readiness. Widening the genuinely structural
37
+ * markers below costs nothing and covers the same panes.
38
+ *
39
+ * WHY THE ANSWER IS NOT A BOOLEAN
40
+ * -------------------------------
41
+ * There are three consumers and they want OPPOSITE responses to the two ways a
42
+ * pane can fail to be ready:
43
+ * - the spawn/inject path — must not type into a pane that is still painting,
44
+ * and must not type into a MENU (see below);
45
+ * - `waitForClaudeReadyWithRetry` — waits, then falls back;
46
+ * - the Slack stuck-session path (`server.ts`) — calls this on an ALREADY-LIVE
47
+ * session and KILLS it when the answer is not-ready.
48
+ * A bare `false` tells that third caller "kill this" for a pane that is merely
49
+ * mid-boot or politely waiting on a question. So the probe reports WHICH state it
50
+ * saw and each caller applies its own policy. `isReadyPromptTail` is retained as
51
+ * the boolean façade for callers that genuinely only want "can I type now".
52
+ *
53
+ * THE MENU CASE (found by the operator, 2026-07-26)
54
+ * -------------------------------------------------
55
+ * Claude Code paints the SAME `❯` glyph on a menu's focused option as it uses for
56
+ * the input box (`PermissionPromptAutoResolver.SELECTOR_GLYPH`). So a session
57
+ * sitting on a startup question read as READY. This is strictly worse than the
58
+ * banner case: text typed at a banner is lost, but text typed at a menu is not —
59
+ * Enter SELECTS AN OPTION, so an arriving message can answer a permission question
60
+ * on the operator's behalf. A menu is therefore classified as its own state, never
61
+ * as ready, no matter which glyphs it carries.
62
+ *
63
+ * Kept pure (text in, verdict out) so it is testable without tmux, and so the
64
+ * regression fixtures are the literal pane text that caused each finding.
65
+ *
66
+ * RULE 3.1 RATIONALE (state-detection; registry:
67
+ * `specs/provider-portability/06-state-detector-registry.md`)
68
+ * ------------------------------------------------------------------------------
69
+ * - **Criticality:** silent-corruption-if-wrong — the worst class, demonstrated
70
+ * twice. A false `ready` on a painting pane loses a user message AND writes a
71
+ * success line for it; a false `ready` on a menu is worse still, because Enter
72
+ * SELECTS an option, so an arriving message can answer a permission question on
73
+ * the operator's behalf.
74
+ * - **Frequency:** per-spawn and per-inject, plus per-stuck-check on the Slack
75
+ * path.
76
+ * - **Stability:** UNSTABLE. Claude Code's TUI is a private surface. This is not a
77
+ * theoretical rating — the 2026-07-26 incident WAS an upstream copy change (a
78
+ * Fable-5 promo line) turning a passing detector into a failing one, and the
79
+ * menu case is an upstream glyph reuse.
80
+ * - **Fallback:** partial and asymmetric. The spawn path has an extended wait and
81
+ * a blind-inject-if-alive fallback; the loss on 2026-07-26 was caught only by
82
+ * the start hook's unanswered-message backstop, which is an unrelated path and
83
+ * cannot be relied on as this detector's safety net. The Slack path's not-ready
84
+ * branch is DESTRUCTIVE (kill + respawn), so a wrong answer there costs a live
85
+ * session, not a delay.
86
+ * - **→ Verdict: deterministic + canary REQUIRED.** This module ships the
87
+ * deterministic half plus regression fixtures carrying the literal pane text of
88
+ * both known failures. It does NOT ship a canary, so it detects the two shapes
89
+ * that already bit us and not the next drift. Canary tracked as CMT-1044.
90
+ * <!-- tracked: CMT-1044 -->
91
+ */
92
+ /**
93
+ * Status-bar / footer strings that only exist once the TUI has finished starting.
94
+ *
95
+ * Sourced from the probes already trusted for this elsewhere in the tree
96
+ * (`SessionReaper`, the interactive-pool adapter config) rather than invented
97
+ * here, so the readiness question gets one vocabulary instead of four. Verified
98
+ * absent from the boot banner. `bypass permissions` alone covered only ONE of
99
+ * Claude Code's permission-mode footers — a session in auto-accept mode shows
100
+ * none of it — which is why the `❯`-is-always-present assumption was load-bearing
101
+ * before and is not now.
102
+ */
103
+ const AT_PROMPT_FOOTERS = [
104
+ 'bypass permissions',
105
+ '? for shortcuts',
106
+ 'shift+tab to cycle',
107
+ 'auto-accept edits',
108
+ ];
109
+ /**
110
+ * A menu option line: an optional glyph run, then `N.` or `N)` and a label.
111
+ * Matches the shape Claude Code uses for approval / consent / trust prompts.
112
+ */
113
+ const MENU_OPTION_RE = /^[^\w\n]{0,4}\s*\d+[.)]\s+\S/;
114
+ /** The selector cursor Claude Code paints on the focused option — and on nothing else. */
115
+ const SELECTOR_GLYPH = '❯';
116
+ /**
117
+ * True when the tail shows a FOCUSED selection menu.
118
+ *
119
+ * Requires the selector glyph to sit ON a numbered option line, AND at least two
120
+ * numbered option lines overall. Both halves are load-bearing:
121
+ *
122
+ * - "glyph on the option line" separates a menu from the input box. The box
123
+ * renders `❯` alone on its own line; a menu renders `❯ 1. Yes`. Testing only
124
+ * "the tail contains ❯ somewhere" would classify ordinary assistant output
125
+ * that happens to list `1.` and `2.` above the prompt as a menu, stalling a
126
+ * genuinely ready session — an over-block on the inject path.
127
+ * - "at least two options" separates a menu from a single numbered line of
128
+ * ordinary output.
129
+ *
130
+ * This mirrors `PermissionPromptAutoResolver`'s requirement (a line whose lead
131
+ * glyphs contain ❯ whose text is `N.` + a label) rather than inventing a second,
132
+ * divergent notion of what a menu looks like.
133
+ */
134
+ export function tailShowsMenu(tail) {
135
+ if (!tail || !tail.includes(SELECTOR_GLYPH))
136
+ return false;
137
+ let options = 0;
138
+ let glyphOnOption = false;
139
+ for (const line of tail.split('\n')) {
140
+ if (!MENU_OPTION_RE.test(line))
141
+ continue;
142
+ options++;
143
+ if (line.includes(SELECTOR_GLYPH))
144
+ glyphOnOption = true;
145
+ }
146
+ return glyphOnOption && options >= 2;
147
+ }
148
+ /**
149
+ * Classify a captured pane tail.
150
+ *
151
+ * `tail` is the joined last-N non-blank lines of a `capture-pane` read. Never pass
152
+ * the whole scrollback: a banner that has scrolled far above the prompt is not
153
+ * evidence either way, and older content only adds false-positive surface.
154
+ */
155
+ export function classifyPaneReadiness(tail) {
156
+ if (!tail)
157
+ return 'not-ready';
158
+ // A menu is checked FIRST and wins over every positive marker below, because it
159
+ // carries the same `❯` the input box does. Typing here selects an option.
160
+ if (tailShowsMenu(tail))
161
+ return 'menu';
162
+ // The framework prompt character. Codex uses ›; keeping this probe Claude-only
163
+ // previously delayed continuation bootstraps to the full timeout.
164
+ if (tail.includes(SELECTOR_GLYPH) || tail.includes('›'))
165
+ return 'ready';
166
+ // Footer/status-bar strings that only render once the TUI is up.
167
+ if (AT_PROMPT_FOOTERS.some(marker => tail.includes(marker)))
168
+ return 'ready';
169
+ return 'not-ready';
170
+ }
171
+ /**
172
+ * Boolean façade: can text be typed into this pane right now?
173
+ *
174
+ * A menu answers `false` — correct for every caller that is about to type. A
175
+ * caller whose not-ready branch is DESTRUCTIVE (kill/respawn) must use
176
+ * `classifyPaneReadiness` instead and leave a `menu` pane alone.
177
+ */
178
+ export function isReadyPromptTail(tail) {
179
+ return classifyPaneReadiness(tail) === 'ready';
180
+ }
181
+ //# sourceMappingURL=claudeReadinessProbe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claudeReadinessProbe.js","sourceRoot":"","sources":["../../src/core/claudeReadinessProbe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAaH;;;;;;;;;;GAUG;AACH,MAAM,iBAAiB,GAAG;IACxB,oBAAoB;IACpB,iBAAiB;IACjB,oBAAoB;IACpB,mBAAmB;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,cAAc,GAAG,8BAA8B,CAAC;AAEtD,0FAA0F;AAC1F,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1D,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACzC,OAAO,EAAE,CAAC;QACV,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,aAAa,GAAG,IAAI,CAAC;IAC1D,CAAC;IACD,OAAO,aAAa,IAAI,OAAO,IAAI,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,IAAI,CAAC,IAAI;QAAE,OAAO,WAAW,CAAC;IAE9B,gFAAgF;IAChF,0EAA0E;IAC1E,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IAEvC,+EAA+E;IAC/E,kEAAkE;IAClE,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IAExE,iEAAiE;IACjE,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC;IAE5E,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,qBAAqB,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC;AACjD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.985",
3
+ "version": "1.3.986",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-26T17:59:55.648Z",
5
- "instarVersion": "1.3.985",
4
+ "generatedAt": "2026-07-26T18:49:10.482Z",
5
+ "instarVersion": "1.3.986",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1538,7 +1538,7 @@
1538
1538
  "type": "subsystem",
1539
1539
  "domain": "sessions",
1540
1540
  "sourcePath": "src/core/SessionManager.ts",
1541
- "contentHash": "4fb46f8df9bd47f899f51c13d25057a158d21b00bacec8bbecd75b3414c64b95",
1541
+ "contentHash": "3f88cbed25892776b9eaa258ff68b7eb7b8b676713ea01280899ee8f5b282e96",
1542
1542
  "since": "2025-01-01"
1543
1543
  },
1544
1544
  "subsystem:auto-updater": {
@@ -0,0 +1,153 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ **A session's startup banner — and a session's startup QUESTION — were read as an input prompt, so
9
+ a message could be typed into a pane that could not receive it, and the delivery was logged as a
10
+ success.**
11
+
12
+ `SessionManager.detectClaudePrompt` decided a pane was accepting input if the last six
13
+ non-blank lines contained `❯`, `›`, `bypass permissions`, or a mention of `/effort`,
14
+ `/model` or `/fast`. That last clause matched a bare slash-command mention **anywhere, in
15
+ any context, including prose**.
16
+
17
+ Claude Code's startup banner advertises slash commands in prose. The pane that caused the
18
+ incident carried, verbatim:
19
+
20
+ > `…Fable 5 draws down usage faster than Opus 4.8. Run /model and`
21
+ > `select Fable to use it. Learn more: https://support.claude.com/…`
22
+
23
+ so the probe matched a promotional line and declared a still-painting pane ready.
24
+
25
+ **Observed (topic 29723, 2026-07-26).** Inbound message at `17:44:49Z`, session respawned,
26
+ pointer prompt injected at `17:45:00Z` — but that session's own start hook did not complete
27
+ until `17:45:15Z`. The paste landed fifteen seconds early, was swallowed by the TUI redraw,
28
+ and the injector wrote `Injected initial message into "…" (915 chars, after stabilization
29
+ delay)`. A success line for a delivery that did not happen. The message reached the agent
30
+ only because the start hook's unanswered-message backstop re-reads recent history — an
31
+ unrelated path that happens to cover this hole and cannot be relied on to.
32
+
33
+ **The slash-command clauses are deleted, not tuned.** A first attempt kept them and required
34
+ "status-bar shape" (line-start or directly after an interpunct). Second-pass review falsified that:
35
+ the banner uses the same shape — `· /memory to free up context` and `+1 more · /status` are BANNER
36
+ lines — so the discrimination was still vocabulary, and `· /model to opt in` matched outright. The
37
+ line-start branch also reinstated the very dependency it claimed to remove: at a fixed pane width, a
38
+ copy edit shifting the wrap by ~14 characters puts the command at column 0 and the defect returns. A
39
+ signal that cannot separate an ADVERT for a command from a STATUS BAR showing one carries no
40
+ readiness information. The structural markers were widened instead — `? for shortcuts`,
41
+ `shift+tab to cycle` and `auto-accept edits` join `bypass permissions`, so a session in auto-accept
42
+ mode no longer depends on the prompt glyph being visible.
43
+
44
+ **Second defect, found by the operator: a startup question read as ready.** Claude Code paints the
45
+ same `❯` on a menu's focused option that it uses for the input box. This is strictly worse than the
46
+ banner — text typed at a banner is lost, but Enter at a menu SELECTS AN OPTION, so an arriving
47
+ message can answer a permission question on the operator's behalf. A focused menu is now its own
48
+ state, never ready.
49
+
50
+ **The verdict is no longer a boolean.** Three consumers ask this question and one of them KILLS a
51
+ live session when the answer is not-ready. "Still painting" and "waiting on an answer" want opposite
52
+ responses from it, so the probe now names which state it saw and each caller applies its own policy;
53
+ the destructive caller leaves a menu alone, waits a bounded moment for the always-on auto-resolver to
54
+ clear it, and only then treats the session as stuck.
55
+
56
+ Classification moved into a small pure module so the incident's literal pane text is a
57
+ regression fixture rather than a paraphrase.
58
+
59
+ ## What to Tell Your User
60
+
61
+ If you ever messaged me, watched a session start, and then saw it sit there doing nothing with
62
+ your message apparently lost — this was one way that happened. I was deciding I was awake by
63
+ looking for certain command names on screen, and my own startup banner mentions those command
64
+ names while advertising a feature. So I read "still starting up" as "ready", typed your message
65
+ into a screen that was still drawing itself, and recorded it as delivered.
66
+
67
+ There is a second case, sharper than the first. Sessions sometimes start by asking a question,
68
+ and the marker drawn beside the selected answer is the same one drawn for the input box. So a
69
+ session waiting on a question also read as ready. At a banner, a message typed too early is
70
+ simply lost. At a question it is not lost — pressing Enter picks an answer, so an arriving
71
+ message could have answered a permission question on your behalf. That can no longer happen.
72
+
73
+ I now wait for the actual input box, or for a footer that only exists once the app is running,
74
+ and I treat a question on screen as somewhere not to type.
75
+
76
+ One honest limit: this makes typing too early far less likely, but I still report a message as
77
+ delivered on the basis of having typed it, not on the basis of it appearing. Confirming that
78
+ typed text actually arrived is a separate change, recorded and not bundled here.
79
+
80
+ ## Summary of New Capabilities
81
+
82
+ - A pane showing only a startup banner is no longer classified as ready for input, so a message
83
+ is not injected before the session can receive it.
84
+ - A focused selection menu is recognised as its own state and is never ready, so an arriving
85
+ message cannot select an option in a question meant for the operator.
86
+ - Slash-command mentions no longer count as a readiness signal in any form — the discrimination
87
+ moved to markers that only exist once the TUI is running.
88
+ - All three permission-mode footers are recognised (`bypass permissions`, `? for shortcuts`,
89
+ `auto-accept edits`, `shift+tab to cycle`), where previously only one was — so a session in
90
+ auto-accept mode no longer depends on the prompt glyph being visible.
91
+ - The readiness verdict names which state it saw rather than answering yes/no, so the caller that
92
+ kills a stuck session no longer kills one that is merely waiting on a question.
93
+ - The readiness decision is a pure, separately-testable function, so the pane text from a real
94
+ incident is carried as a regression fixture instead of being described.
95
+
96
+ ## Evidence
97
+
98
+ **Reproduction (before),** real pane text against the pre-fix clause:
99
+
100
+ | input | pre-fix verdict | correct verdict |
101
+ |---|---|---|
102
+ | the 2026-07-26 boot banner (contains `Run /model and`) | `ready` | not ready |
103
+ | `Do you want to proceed?` + `❯ 1. Yes` / `2. No` | `ready` | menu (never ready) |
104
+ | `Do you trust the files in this folder?` + two options | `ready` | menu (never ready) |
105
+ | `Run /fast to enable faster output on this plan.` | `ready` | not ready |
106
+
107
+ **Also falsified — the intermediate "status-bar shape" attempt,** kept here because it is the
108
+ reason the clause was deleted rather than narrowed:
109
+
110
+ | input | "shape" verdict | correct verdict |
111
+ |---|---|---|
112
+ | `⚠ Fable 5 promotional access ends soon · /model to opt in` | `ready` | not ready |
113
+ | ` · /model to switch between Opus and Fable` | `ready` | not ready |
114
+ | soft-wrapped banner putting `/model` at column 0 | `ready` | not ready |
115
+ | `opus 5 • medium • /effort` (bullet separator) | not ready | ready |
116
+
117
+ **Observed after:**
118
+
119
+ | input | before | after |
120
+ |---|---|---|
121
+ | the 2026-07-26 boot banner | ready | **not ready** |
122
+ | any prose or interpunct mention of `/model`, `/fast`, `/effort` | ready | **not ready** |
123
+ | a focused startup question (3 real shapes) | ready | **menu — never typed into** |
124
+ | ordinary output listing `1.` / `2.` above the input box | ready | ready (not mistaken for a menu) |
125
+ | a single numbered line beside the prompt | ready | ready |
126
+ | drawn input box (`❯`) / codex prompt (`›`) | ready | ready |
127
+ | `⏵⏵ bypass permissions on` | ready | ready |
128
+ | `? for shortcuts` / `auto-accept edits` / `shift+tab to cycle` | **not ready** | **ready** (newly recognised) |
129
+ | banner that has since grown an input box | ready | ready |
130
+ | empty / whitespace pane | not ready | not ready |
131
+
132
+ **Both guards refuse.** Restoring the old slash-command clause fails six assertions; removing the
133
+ menu classification fails three. Both include the discrimination guard, whose only job is to prove
134
+ the probe still tells its three verdicts apart:
135
+
136
+ ```
137
+ # restoring the slash-command clause
138
+ × REGRESSION: the 2026-07-26 startup banner does NOT read as ready
139
+ × REGRESSION: a banner line in interpunct "status-bar shape" does NOT read as ready
140
+ × REGRESSION: a soft-wrapped banner putting /model at line start does NOT read as ready
141
+ × an agent echoing a slash command in its own output does NOT read as ready
142
+ × the probe discriminates > produces all three verdicts and is not stuck on one
143
+ Tests 6 failed | 12 passed (18)
144
+
145
+ # removing the menu classification
146
+ × REGRESSION: a startup question does NOT read as ready
147
+ × a menu is not ready to be typed into
148
+ × the probe discriminates > produces all three verdicts and is not stuck on one
149
+ Tests 3 failed | 15 passed (18)
150
+ ```
151
+
152
+ **Scope run:** `npx tsc --noEmit` clean; every tree-scanning test under `tests/unit` plus every
153
+ test that mocks `capture-pane` (the fixtures that feed this probe) — counts in the PR body.