instar 1.3.723 → 1.3.724

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,188 @@
1
+ # Side-Effects Review — Slack files.info self-verify fix + F2 ghost-text exclusion in StuckInputSentinel
2
+
3
+ **Version / slug:** `p05-filesinfo-ghosttext`
4
+ **Date:** `2026-07-02`
5
+ **Author:** Echo (autonomous, roadmap 0.5)
6
+ **Second-pass reviewer:** self, second pass over the final diff (Tier 1)
7
+
8
+ ## Summary of the change
9
+
10
+ Two code halves of roadmap item 0.5, both grounded in 2026-07-02 live evidence:
11
+
12
+ 1. **Slack files.info self-verify** (`src/messaging/slack/SlackAdapter.ts`):
13
+ the startup self-verify probed `files.info` with the malformed synthetic id
14
+ `F000SELFTEST`; Slack rejects that id at server-side ARGUMENT VALIDATION
15
+ (`invalid_arguments`) before lookup, and the old classifier counted that as
16
+ an unexpected FAILURE — a healthy adapter red-flagged itself at every boot.
17
+ Fix: (a) probe with a WELL-FORMED synthetic id (`F0000000000`) so a healthy
18
+ workspace answers `file_not_found`; (b) extract the classification into an
19
+ exported pure function `classifyFilesInfoSelfTest()` that treats
20
+ `invalid_arguments` as what it proves (endpoint + auth + transport all
21
+ answered) while `missing_scope` and unknown errors remain failures; (c) use
22
+ `SlackApiError.slackError` (the structured code) when available instead of
23
+ substring-matching the whole message.
24
+
25
+ 2. **F2 ghost-text exclusion** (`src/core/StuckInputSentinel.ts` +
26
+ `src/core/SessionManager.ts`): the sentinel treated Claude Code's DIM
27
+ model-generated composer suggestion (SGR 2, `ESC[0;2m`) as stuck real input
28
+ and fired 4 Enter presses at it (live stuck-input-events.jsonl). Fix: before
29
+ any keypress on the generic `❯`-prompt path, re-capture the pane WITH ANSI
30
+ escapes (new `SessionManager.captureOutputAnsi`, `capture-pane -e`) and
31
+ classify the stuck text's presentation: `real` → recover as before; `ghost`
32
+ (entirely dim) → never press keys, sticky-exhaust until the text changes;
33
+ `inconclusive` (capture failed / frames raced / mixed styling) → LOG-ONLY
34
+ this tick, re-assess next tick. One observability event per stuck text
35
+ (`ghost-text-skip` / `ghost-check-inconclusive`, outcome `skipped`).
36
+
37
+ ## Decision-point inventory
38
+
39
+ - **Added** (`StuckInputSentinel.evaluateSession`): the ghost-text gate — a new
40
+ refusal branch between "fire-eligible" and "fire keypress" on the generic
41
+ prompt path only. The codex marker path is exempt by construction (it only
42
+ fires at the exact text we ourselves injected). Escalation (tier C) is
43
+ unreachable for ghost text: attempts only accrue through this gate.
44
+ - **Added** (`classifyPromptTextPresentation` + `parseAnsiDimLines`): pure
45
+ classification logic. Only the SGR dim attribute (2 / 22 / reset) is the
46
+ ghost tell; extended-color params (`38;5;n`, `38;2;r;g;b`, colon forms) are
47
+ skipped so a truecolor component `2` is never misread as dim. The ANSI
48
+ frame's own prompt extraction must reproduce the plain frame's text EXACTLY,
49
+ or the verdict is `inconclusive` — the styling verdict is always about the
50
+ same characters the detection saw.
51
+ - **Modified** (`SlackAdapter._selfVerify` check 2): outcome classification
52
+ moved to `classifyFilesInfoSelfTest()`; `invalid_arguments` reclassified
53
+ fail → pass. `missing_scope` and unknown errors keep failing.
54
+
55
+ ## FAIL-DIRECTION (the load-bearing property)
56
+
57
+ **The sentinel change fails toward LOG-ONLY, never toward Enter.** Every
58
+ uncertain path — ANSI capture unsupported, capture returned null, capture
59
+ threw, the two frames raced (text mismatch), unparseable escapes, mixed
60
+ styling — resolves to `inconclusive`, which withholds the keypress for that
61
+ tick and logs one event. `inconclusive` is deliberately NOT sticky: a raced
62
+ capture self-heals next tick, so a genuinely stuck message is recovered at most
63
+ a few ticks late; a persistent failure keeps failing toward not pressing.
64
+ Pressing Enter at model-fabricated text is the unsafe direction; a delayed
65
+ recovery is the safe cost. The invariant encoded: the sentinel never
66
+ auto-submits text the user (or an authorized injector) did not actually type.
67
+
68
+ ## Roll-up across the seven review dimensions
69
+
70
+ 1. **Over-block**: the ghost gate could suppress recovery of REAL stuck input
71
+ if the classifier misfires. Mitigations: color is deliberately NOT a tell
72
+ (only SGR 2); a plain no-SGR frame classifies `real` (legacy behavior
73
+ preserved — proven by the pre-existing suite passing with the default stub);
74
+ `inconclusive` is transient, re-assessed every fire-eligible tick; truecolor
75
+ and 256-color params are param-skipped. Both sides tested.
76
+ 2. **Under-block**: ghost text that Claude Code someday renders at NORMAL
77
+ intensity would not be caught — but then it is visually indistinguishable
78
+ from typed text and no capture-based tell exists; the dim attribute is the
79
+ entire live-evidenced signature. The codex-marker exemption cannot leak: it
80
+ requires a marker WE injected.
81
+ 3. **Silent failure**: none added. Every skip logs exactly one
82
+ `stuck-input-events.jsonl` row (bounded: one per stuck text, not per tick).
83
+ `captureOutputAnsi` returns null on failure exactly like `captureOutput`.
84
+ 4. **Authority creep**: none. The sentinel LOSES authority (a new refusal
85
+ branch); nothing gains a new write path. The Slack change alters only a
86
+ boot-time diagnostic verdict, no message flow.
87
+ 5. **Fleet blast radius**: both changes are always-on but strictly narrowing.
88
+ Slack: only installs with a Slack adapter + `files:read` scope run check 2;
89
+ the reclassification can only flip a false-red to green. Sentinel: one extra
90
+ `tmux capture-pane -e` per fire-eligible tick per stuck session (rare;
91
+ bounded by the same tick cadence that already captures panes).
92
+ 6. **Rollback**: single-commit revert. No config keys, no migrations, no state
93
+ format changes. The new JSONL `action` values are additive to an existing
94
+ log consumers already treat as free-form.
95
+ 7. **Observability**: new `ghost-text-skip` / `ghost-check-inconclusive`
96
+ events in `stuck-input-events.jsonl`; Slack self-verify detail strings name
97
+ the exact outcome (`argument validation` vs `file_not_found`).
98
+
99
+ ## 3. Level-of-abstraction fit
100
+
101
+ Right layer, both halves. The ghost gate lives INSIDE the sentinel's own
102
+ fire path (the only place a keypress originates), not as a parallel filter in
103
+ front of it — the same layer that already hosts the activity-indicator refusal
104
+ and the codex-marker strategy choice. The ANSI capture primitive lives in
105
+ `SessionManager` beside `captureOutput` (its styled twin) rather than being
106
+ re-implemented in the sentinel, so future consumers that need rendering (not
107
+ just bytes) reuse it. The Slack classification is a pure exported function at
108
+ adapter level — the self-verify is a boot diagnostic owned by the adapter; no
109
+ higher-layer gate exists (or is needed) for a boot-time health verdict.
110
+
111
+ ## 4. Signal vs authority compliance
112
+
113
+ **Checkbox: No — this change has no NEW block/allow surface over agent or user
114
+ information flow.** The ghost gate is deterministic (brittle-by-nature) logic,
115
+ but the only "authority" it holds is the power to make the sentinel DO NOTHING
116
+ — it withholds the sentinel's own recovery keypress; it cannot block a message,
117
+ gate information, or constrain the user or agent. The dangerous authority
118
+ (pressing keys into a live session) is what the sentinel already held; this
119
+ change narrows it, fail-safe toward inaction, exactly like the pre-existing
120
+ activity-indicator refusal at the same layer. The Slack half changes a
121
+ boot-diagnostic verdict string; no flow authority at all.
122
+
123
+ ## 5. Interactions
124
+
125
+ - **Shadowing:** the gate runs AFTER stuck-detection and the min-ticks window
126
+ and BEFORE `fireStuckInputRecovery` — it cannot shadow detection, escalation
127
+ handling (which runs earlier in the flow but later in the attempt ladder), or
128
+ the verifyInjection race-window skip. Nothing runs after it that expects a
129
+ keypress to have happened (the event log records `skipped` honestly).
130
+ - **Double-fire:** none. The gate only ever REMOVES a fire. The one-event-per-
131
+ stuck-text latch (`ghostSkipLogged`) prevents log double-fire.
132
+ - **Races:** the plain and ANSI captures are two separate tmux calls; a pane
133
+ repaint between them is detected by exact text-match and resolves to
134
+ `inconclusive` (no press) — the race is handled by construction, not by
135
+ timing assumptions.
136
+ - **Feedback loops:** none; the gate writes no state the detector reads.
137
+ - **Escalation interaction:** tier-C escalation is unreachable for ghost text —
138
+ attempts only accrue through the gate, so four `real` verdicts must precede
139
+ any escalation request.
140
+
141
+ ## 6. External surfaces
142
+
143
+ - **Other agents / install base:** behavior narrows only (fewer keypresses;
144
+ a false-red boot check goes green). No API, route, config, or template
145
+ changes. No new dependency.
146
+ - **External systems:** the Slack probe now sends `F0000000000` instead of
147
+ `F000SELFTEST` to `files.info` — same call count, same scope, still a
148
+ synthetic id that cannot match a real file.
149
+ - **Persistent state:** two additive `action` values in the existing
150
+ `stuck-input-events.jsonl` free-form log. No schema, no migration.
151
+ - **Timing/runtime conditions:** tmux `capture-pane -e` support (present in
152
+ every tmux version instar supports; a failure degrades to `inconclusive`,
153
+ i.e. log-only).
154
+ - **Operator surface (Mobile-Complete Operator Actions):** no operator-facing
155
+ actions added or touched.
156
+
157
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
158
+
159
+ **Machine-local BY DESIGN, both halves.** The sentinel observes and recovers
160
+ tmux panes that exist only on THIS machine's disk/terminal — the pane, the
161
+ capture, and the keypress are all physically local, like the rest of the
162
+ stuck-input/watchdog family. The Slack self-verify diagnoses THIS machine's
163
+ adapter boot. Neither emits user-facing notices (the sentinel's events are
164
+ housekeeping JSONL; the self-verify writes to the boot log), neither holds
165
+ durable state that could strand on topic transfer, and neither generates URLs.
166
+
167
+ ## 8. Rollback cost
168
+
169
+ Single-commit revert, shipped as the next patch. No config keys, no
170
+ migrations, no persistent-state cleanup, no agent state repair. During a
171
+ rollback window the only regression users could see is the OLD behavior
172
+ returning (false-red Slack check line; watchdog pressing Enter at ghost text).
173
+
174
+ ## Test coverage
175
+
176
+ - `tests/unit/StuckInputSentinel-ghost-text.test.ts` (21 tests): the exact live
177
+ F2 frame refused across 10 ticks; genuine input still recovered; null-capture
178
+ / throwing-capture / raced-frame / mixed-styling all fail toward not
179
+ pressing; truecolor + 256-color params not misread; SGR 22 cancels dim;
180
+ cross-line dim state; wrapped-line ghost; sticky-ghost reset on text change;
181
+ codex path ungated; exactly one observability event.
182
+ - `tests/unit/slack-files-info-selfverify.test.ts` (12 tests): classifier both
183
+ sides (ok / file_not_found / invalid_arguments pass; missing_scope /
184
+ invalid_auth / unknown fail); probe id shape; `_selfVerify` wiring-level test
185
+ proving the live regression goes green and failures still fail.
186
+ - `tests/unit/StuckInputSentinel.test.ts`: pre-existing suite green with the
187
+ gate in place (default ANSI stub = plain frame = `real`), proving legacy
188
+ recovery behavior is preserved.