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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SessionManager.d.ts +15 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +26 -15
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/claudeReadinessProbe.d.ts +137 -0
- package/dist/core/claudeReadinessProbe.d.ts.map +1 -0
- package/dist/core/claudeReadinessProbe.js +181 -0
- package/dist/core/claudeReadinessProbe.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.986.md +153 -0
- package/upgrades/side-effects/booting-pane-read-as-ready.md +222 -0
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
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": "
|
|
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.
|