instar 0.28.86 → 0.28.87

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.
Files changed (33) hide show
  1. package/dist/core/InitiativeTracker.d.ts +1 -0
  2. package/dist/core/InitiativeTracker.d.ts.map +1 -1
  3. package/dist/core/InitiativeTracker.js +8 -1
  4. package/dist/core/InitiativeTracker.js.map +1 -1
  5. package/dist/core/PlanDocParser.d.ts +62 -0
  6. package/dist/core/PlanDocParser.d.ts.map +1 -0
  7. package/dist/core/PlanDocParser.js +328 -0
  8. package/dist/core/PlanDocParser.js.map +1 -0
  9. package/dist/core/SafeYaml.d.ts +45 -0
  10. package/dist/core/SafeYaml.d.ts.map +1 -0
  11. package/dist/core/SafeYaml.js +230 -0
  12. package/dist/core/SafeYaml.js.map +1 -0
  13. package/dist/core/SessionManager.d.ts +12 -0
  14. package/dist/core/SessionManager.d.ts.map +1 -1
  15. package/dist/core/SessionManager.js +60 -0
  16. package/dist/core/SessionManager.js.map +1 -1
  17. package/dist/core/StageTransitionValidator.d.ts +97 -0
  18. package/dist/core/StageTransitionValidator.d.ts.map +1 -0
  19. package/dist/core/StageTransitionValidator.js +338 -0
  20. package/dist/core/StageTransitionValidator.js.map +1 -0
  21. package/dist/monitoring/StallTriageNurse.d.ts +11 -0
  22. package/dist/monitoring/StallTriageNurse.d.ts.map +1 -1
  23. package/dist/monitoring/StallTriageNurse.js +66 -0
  24. package/dist/monitoring/StallTriageNurse.js.map +1 -1
  25. package/dist/server/routes.d.ts.map +1 -1
  26. package/dist/server/routes.js +312 -0
  27. package/dist/server/routes.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/data/builtin-manifest.json +47 -47
  30. package/upgrades/0.28.87.md +153 -0
  31. package/upgrades/side-effects/0.28.86.md +13 -0
  32. package/upgrades/side-effects/project-scope-phase1a-pr2.md +377 -0
  33. package/upgrades/side-effects/verify-injection-stuck-input.md +107 -0
@@ -0,0 +1,377 @@
1
+ # Side-Effects Review — project-scope Phase 1a PR 2 (Stage validator + /projects API)
2
+
3
+ **Version / slug:** `project-scope-phase1a-pr2`
4
+ **Date:** `2026-05-11`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `required (new decision points + new external surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ Second of three PRs scaffolding the project-scope feature. PR 1 added
11
+ the type-level surface; this PR adds the persistence-layer artifact
12
+ validators and the HTTP routes that expose project records.
13
+
14
+ Spec source: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ Phase 1.2, 1.3, 1.6,
15
+ 1.10, 1.12.
16
+
17
+ New files (persistence/validator layer):
18
+ - `src/core/SafeYaml.ts` (234 lines) — minimal, safe YAML-frontmatter
19
+ extractor. Hand-rolled subset (scalars + flow arrays + line lists),
20
+ bounded depth/length to neutralise YAML-bomb classes of input.
21
+ - `src/core/PlanDocParser.ts` (370 lines) — parses a plan-doc markdown
22
+ file into `{project, children, errors}`. Validates required fields,
23
+ slug shape, source/effort/scope tags, child uniqueness, round
24
+ membership.
25
+ - `src/core/StageTransitionValidator.ts` (397 lines) — deterministic
26
+ validator for `pipelineStage` edges (outline → approved → building
27
+ → merged, plus skipped/regressed). Every check is an artifact
28
+ assertion (frontmatter equals literal, `gh pr view` reports
29
+ `MERGED`, `git merge-base --is-ancestor` succeeds, file exists,
30
+ realpath jailed under `targetRepoPath`).
31
+
32
+ Modified files:
33
+ - `src/core/InitiativeTracker.ts` (+11/-4) — `list()` accepts an
34
+ optional `kind` filter (records without explicit `kind` are treated
35
+ as `'task'` for back-compat).
36
+ - `src/server/routes.ts` (+332) — new `/projects/*` route group plus
37
+ two helpers (`hashAuthHeader`, `checkAndIncrementProjectsRate`) and
38
+ a small create-orchestrator (`createProjectAndChildren`).
39
+
40
+ HTTP endpoints (the only ones in PR 2; the rest of the spec's Phase
41
+ 1.3 endpoint set lands in Phase 1b):
42
+
43
+ - `GET /projects` — list projects (joins via `kind: 'project'`).
44
+ - `GET /projects/:id` — fetch a project plus its children (via
45
+ `parentProjectId`). Supports `?fields=<csv>` projection (Phase
46
+ 1.10).
47
+ - `GET /projects/:id/next` — returns 501 with a placeholder body;
48
+ full implementation is Phase 1b's `ProjectRoundRunner`.
49
+ - `POST /projects` — create from a plan doc. Rate-limited 5/hour per
50
+ auth-token hash; persists counter at
51
+ `.instar/local/projects-rate.json` (gitignored, per-machine).
52
+ - `POST /projects/validate` — dry-run plan-doc parse; no side effects.
53
+ - `DELETE /projects/:id` — archive (status → `'archived'`). Requires
54
+ `If-Match: <version>`; refuses while any round is `in-progress`.
55
+
56
+ Tests (49 new, all passing):
57
+ - `tests/unit/PlanDocParser.test.ts` — 9
58
+ - `tests/unit/StageTransitionValidator.test.ts` — 26
59
+ - `tests/integration/projects-api.test.ts` — 14
60
+
61
+ ## Decision-point inventory
62
+
63
+ - **Stage transition gate** (`StageTransitionValidator`) — **add** —
64
+ rejects requested edges that fail their artifact preconditions
65
+ (e.g. `building → merged` without a `gh pr view → MERGED`).
66
+ Reconciler-only edges (`*-> regressed`) require explicit
67
+ `bypassMode: 'reconciler'`.
68
+ - **Plan-doc parse gate** (`PlanDocParser`) — **add** — rejects plan
69
+ docs missing required frontmatter or with malformed child shapes;
70
+ fed by `POST /projects` and `POST /projects/validate`.
71
+ - **`POST /projects` rate-limit** — **add** — 5 creates/hour per
72
+ hashed auth token. Best-effort: fails open on disk error.
73
+ - **`DELETE /projects/:id` archive guard** — **add** — refuses
74
+ archive while any `round.status === 'in-progress'`; requires
75
+ `If-Match`.
76
+ - **`GET/DELETE /projects/:id` kind guard** — **add** — returns 404
77
+ if the named id resolves to a non-project initiative.
78
+
79
+ All decision points are structural — they accept/reject on type,
80
+ reference integrity, and artifact presence, not on conversational
81
+ context.
82
+
83
+ ---
84
+
85
+ ## 1. Over-block
86
+
87
+ **What legitimate inputs does this change reject that it shouldn't?**
88
+
89
+ The validators are artifact-bound: each rejection corresponds to a
90
+ missing or contradictory artifact (frontmatter field absent, PR not
91
+ merged, file outside `targetRepoPath`, plan-doc child id not unique).
92
+ The structure of every check is "does the named artifact exist /
93
+ equal the expected literal?" — none of them inspect free-form text or
94
+ make a judgement call.
95
+
96
+ - `StageTransitionValidator` only fires on `pipelineStage` transitions
97
+ for child initiatives. Project-kind records and pre-project-scope
98
+ task records (no `pipelineStage`) bypass it entirely.
99
+ - `PlanDocParser` errors are returned as structured `errors[]` from
100
+ `POST /projects/validate` rather than turned into HTTP failures, so
101
+ the user can iterate on their plan doc before committing.
102
+ - The `POST /projects` rate limiter is 5/hour per auth-token hash —
103
+ generous for the documented usage shape (one project per session)
104
+ and fails open if the counter file can't be written, so a disk
105
+ hiccup doesn't refuse legitimate creates.
106
+ - The `DELETE /projects/:id` in-progress guard rejects only while a
107
+ round is *literally* `in-progress`. Pending/complete/halted/paused
108
+ rounds do not block archive.
109
+
110
+ No legitimate input shape is rejected by these new checks.
111
+
112
+ ---
113
+
114
+ ## 2. Under-block
115
+
116
+ **What failure modes does this still miss?**
117
+
118
+ - `StageTransitionValidator` only validates the named edge. A caller
119
+ that issues two transitions in sequence (`outline → approved`,
120
+ `approved → building`) gets each edge validated independently;
121
+ there is no "history" check. This is intentional — the artifacts
122
+ carry the history (frontmatter `approved-at`, TaskFlow record id).
123
+ - The reconciler-only edges (`*-> regressed`) are bypassable when the
124
+ caller passes `bypassMode: 'reconciler'`. This is documented in the
125
+ module header (P1, signal-vs-authority section) and the flag is
126
+ only honored when callers pass it explicitly. User-initiated HTTP
127
+ calls in this PR never set it.
128
+ - The `POST /projects` rate-limiter counter is per-machine. A user
129
+ with multiple machines under the same auth token could create
130
+ 5×N projects/hour. The spec calls this acceptable (per-agent
131
+ semantics) and `.instar/local/` is intentionally gitignored.
132
+ - `POST /projects` does not validate that referenced spec files
133
+ actually exist on disk at create time — only the validator does,
134
+ at transition time. This is intentional: child specs are often
135
+ written after the project is registered.
136
+
137
+ ---
138
+
139
+ ## 3. Level-of-abstraction fit
140
+
141
+ **Is this at the right layer?**
142
+
143
+ Yes.
144
+
145
+ - `StageTransitionValidator` lives at the persistence-layer edge
146
+ alongside `InitiativeTracker`. It is the deterministic *authority*
147
+ for stage transitions (P1: there is exactly one). The drift
148
+ checker (Phase 1.4) and dashboard surfaces will *signal* into it,
149
+ not duplicate it.
150
+ - `PlanDocParser` is a pure parser with no I/O beyond a single file
151
+ read; it returns structured data that the route handler decides
152
+ what to do with. Re-usable from the `/project` skill (PR 3) and
153
+ the dashboard's plan-doc preview without HTTP plumbing.
154
+ - `SafeYaml` is a deliberately narrow YAML-subset extractor. We
155
+ rejected the `js-yaml` dependency for this surface because (a) it
156
+ brings YAML-bomb / billion-laughs surface area that we don't need
157
+ on a publicly-accessible API, and (b) the plan-doc grammar is
158
+ small enough to handle with a 234-line bespoke parser.
159
+ - HTTP routes in `routes.ts` are thin: parse params, look up via
160
+ the tracker, translate domain errors to HTTP codes, return JSON.
161
+ No business logic moves into the route layer.
162
+
163
+ ---
164
+
165
+ ## 4. Signal vs authority compliance
166
+
167
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
168
+
169
+ **Does this change hold blocking authority with brittle logic?**
170
+
171
+ - [x] No — these are structural validators, explicitly within the
172
+ doc's "When this principle does NOT apply" carve-out.
173
+
174
+ The doc names two carve-outs that apply here directly:
175
+
176
+ 1. **"Hard-invariant validation"** — typing and structural validators
177
+ at the boundary of the system. `StageTransitionValidator` checks
178
+ "does the frontmatter `state:` field equal `approved`?", "does
179
+ `gh pr view` report `MERGED`?", "is this realpath under
180
+ `targetRepoPath`?". `PlanDocParser` checks "is the slug a valid
181
+ shape?", "are all referenced child ids unique?". These are
182
+ structural checks against literals, not judgements about
183
+ meaning. None of them inspects conversational content.
184
+
185
+ 2. **"Safety guards on irreversible actions"** — the
186
+ `DELETE /projects/:id` in-progress guard and the `If-Match`
187
+ requirement on archive are precisely the kind of hard-block the
188
+ doc endorses: the cost of a false pass is losing an active round;
189
+ the cost of a false block is "try again after the round
190
+ finishes."
191
+
192
+ No LLM judgment lives anywhere in this PR. There is no detector
193
+ emitting signals into a higher authority; the entire PR is
194
+ deterministic precondition logic. The drift checker (Phase 1.4 / PR
195
+ in Phase 1b) is the *signal* counterpart to this validator's
196
+ *authority* — it observes and reports, the validator decides.
197
+
198
+ ---
199
+
200
+ ## 5. Interactions
201
+
202
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
203
+
204
+ - **Shadowing.** The new `/projects/*` routes are a fresh path
205
+ prefix. They do not shadow `/initiatives/*`. Both can resolve to
206
+ the same `Initiative` record through the tracker, but the new
207
+ routes filter on `kind: 'project'` and return 404 for non-project
208
+ hits, so a `task`-kind initiative is never accidentally exposed as
209
+ a project.
210
+ - **Auth.** The `AgentServer` mounts `authMiddleware` *before* the
211
+ router, so every `/projects/*` request gets the same bearer-token
212
+ check as the rest of the API. The handlers do not re-implement
213
+ auth.
214
+ - **Rate limiter.** The 5/hour counter is a counter file under
215
+ `.instar/local/projects-rate.json`. The `.instar/local/` directory
216
+ is gitignored (Phase 1.12 of the spec), so the counter never syncs
217
+ across machines — matching the per-agent semantics the spec calls
218
+ for. The reset cadence (60-minute sliding window) is implemented
219
+ but not surfaced as a config knob; if a future user requests
220
+ configurability, that is a Phase 1b polish.
221
+ - **OCC.** `DELETE /projects/:id` requires `If-Match` and translates
222
+ `OccVersionMismatchError` (from PR 1) into a 409 with body
223
+ `{error: 'version mismatch', currentVersion}`. The mapping is
224
+ symmetric with the spec's wire format.
225
+ - **Backfill.** `list({kind: 'project'})` falls back to `kind ?? 'task'`
226
+ for in-memory records that haven't yet been persisted, so an
227
+ in-progress backfill never accidentally surfaces a pre-1a task as
228
+ a project.
229
+ - **Routes.ts size.** Routes file grows from ~5400 → ~5750 lines.
230
+ Still a single mountable router; no architectural change to the
231
+ HTTP surface.
232
+ - **Races.** The rate-limit counter is read-modify-written within a
233
+ single handler tick (synchronous fs). Two near-simultaneous
234
+ creates from the same token can over-count by 1, but cannot
235
+ under-count, so the limit holds. The plan-doc parse and the
236
+ `tracker.create` calls run in sequence — there is no concurrent
237
+ create path.
238
+ - **Feedback loops.** None. The validator is a request/response
239
+ function; it does not emit signals back to anything.
240
+
241
+ ---
242
+
243
+ ## 6. External surfaces
244
+
245
+ **Does this change anything visible outside the immediate code path?**
246
+
247
+ - **Other agents on the same machine.** New HTTP endpoints under
248
+ `/projects/*`. No agent currently calls them — they are new
249
+ surface. Existing `/initiatives/*` endpoints are untouched.
250
+ - **Other users of the install base.** Same: new endpoints are
251
+ additive; nothing existing is changed in shape. The
252
+ `InitiativeTracker.list()` signature change adds an optional
253
+ field and is fully backward-compatible.
254
+ - **External systems.** `StageTransitionValidator` shells out to
255
+ `gh pr view` and `git merge-base --is-ancestor` (via injectable
256
+ helpers). These run only when a caller triggers a stage
257
+ transition; PR 2 does not include any code path that fires them
258
+ yet (advance/halt/ack endpoints land in Phase 1b). The validator
259
+ is exercised by unit tests with mocked helpers.
260
+ - **Persistent state.** New file:
261
+ `.instar/local/projects-rate.json`. Created on first
262
+ `POST /projects`; size is bounded (one entry per active token in
263
+ the last hour, GC'd on every write). Format:
264
+ `{ "<sha256-prefix>": { count: number, windowStart: epochMs } }`.
265
+ - **Timing / runtime conditions.** `POST /projects` reads the plan
266
+ doc from disk and parses it. Bounded by plan-doc size; in
267
+ practice <100ms. No background workers.
268
+
269
+ ---
270
+
271
+ ## 7. Rollback cost
272
+
273
+ **If this turns out wrong in production, what's the back-out?**
274
+
275
+ - **Hot-fix release.** Revert the commit. The three new files
276
+ (`PlanDocParser.ts`, `SafeYaml.ts`, `StageTransitionValidator.ts`)
277
+ are deleted; the additive hunk in `routes.ts` reverts cleanly;
278
+ the `InitiativeTracker.list()` `kind` filter is backward-compatible
279
+ (omitted filter returns all kinds, matching the prior behavior).
280
+ - **Data migration.** None required. The counter file
281
+ `.instar/local/projects-rate.json` is per-machine and can be
282
+ deleted (or left in place — unused files don't hurt). No
283
+ `Initiative` records are written by PR 2 alone outside of normal
284
+ `tracker.create()` writes that already round-trip.
285
+ - **Agent state repair.** Not needed. Any project records created
286
+ via `POST /projects` before rollback continue to exist as
287
+ `kind: 'project'` initiatives. Old code that reads them via
288
+ `/initiatives/*` will see `kind: 'project'` and ignore it (it's
289
+ an unknown extra field to pre-PR-1 code, and PR 1 has already
290
+ shipped so that path is closed in practice).
291
+ - **User visibility.** Rollback removes the `/projects/*` endpoints.
292
+ Any UI surfaces that started calling them get 404s; the
293
+ dashboard's projects tab will gracefully degrade (the PR 3
294
+ surface is conditional on these endpoints existing).
295
+
296
+ Rollback is a single `git revert` with no follow-up. Phase 1b code
297
+ that depends on the validators would need to be reverted in the
298
+ same revert if it had already shipped, but in the current sequence
299
+ PR 2 ships first.
300
+
301
+ ---
302
+
303
+ ## Conclusion
304
+
305
+ PR 2 of 3 for the project-scope Phase 1a build. New decision points
306
+ (stage transitions, plan-doc parsing, rate limit, archive guard) are
307
+ all structural validators or hard-invariant safety guards — they fit
308
+ within the explicit signal-vs-authority carve-outs and the doc's
309
+ guidance on what brittle/deterministic checks are allowed to do.
310
+ HTTP routes are thin translations; business logic lives in the
311
+ persistence layer alongside `InitiativeTracker`. No LLM judgment, no
312
+ new authority for a conversational decision point. Tests: 49 new
313
+ across 3 suites, all passing. Typecheck clean. Clear to ship pending
314
+ second-pass review.
315
+
316
+ ---
317
+
318
+ ## Second-pass review (required)
319
+
320
+ **Reviewer:** independent audit pass
321
+ **Date:** `2026-05-11`
322
+
323
+ Independently re-read the changes and the three review axes the brief
324
+ flagged:
325
+
326
+ 1. **Signal-vs-authority compliance.** Confirmed: `StageTransitionValidator`
327
+ is a deterministic precondition function over artifact assertions —
328
+ literal equality on `review-convergence`, `approved`, `approved-by`,
329
+ `approved-date` frontmatter; `view.state === 'MERGED'` string compare;
330
+ `/^[0-9a-f]{7,64}$/i` sha regex; `gitMergeBaseIsAncestor(oid, 'origin/main')`
331
+ ancestry check via injected helper. No LLM, no judgement,
332
+ no scored similarity. Reconciler-only edges (`*-> regressed`) require
333
+ explicit `bypassMode: 'reconciler'` and are otherwise rejected with
334
+ code `REGRESSED_RECONCILER_ONLY`. The `PlanDocParser` performs
335
+ schema/shape checks (slug regex, required keys, child uniqueness,
336
+ round membership); same category. Both fit cleanly under
337
+ `docs/signal-vs-authority.md` § "Hard-invariant validation" and
338
+ § "Safety guards on irreversible actions".
339
+
340
+ 2. **No smuggled decision points.** Reviewed the +332 diff in
341
+ `routes.ts`. Six handlers, all thin: parse → tracker call → status
342
+ code translation. The `pickFields` helper is a Set-based allowlist
343
+ projection (deterministic). The rate limiter is a per-token counter
344
+ against a flat threshold (5/hour), not a content judgement; it
345
+ fails *open* on disk error so a flaky volume can't accidentally
346
+ become a block. The `DELETE` in-progress guard is hard-invariant
347
+ ("any round.status === 'in-progress'"). No new sentinel, gate, or
348
+ watchdog surface.
349
+
350
+ 3. **Interactions audit.** Auth is enforced upstream by the existing
351
+ `authMiddleware` mount in `AgentServer.ts` — the new handlers do
352
+ not re-implement it. The rate-limit file path
353
+ `.instar/local/projects-rate.json` is under the gitignored
354
+ `.instar/local/` per Phase 1.12 of the spec (verified). The handler
355
+ acquires no lock around its read-modify-write; this is acceptable
356
+ because (a) Node's event loop serialises handlers per process, (b)
357
+ the comment explicitly classes the limiter as best-effort, and (c)
358
+ the worst case under contention is an off-by-one count, never a
359
+ leak past the threshold in the steady state. `list({kind: 'project'})`
360
+ correctly falls back to `kind ?? 'task'` for not-yet-persisted
361
+ in-memory records, matching the backfill semantics from PR 1.
362
+ `OccVersionMismatchError` is caught by name (`err.name`) which is
363
+ the same pattern the rest of the routes file uses, and is correctly
364
+ translated to a 409 with `{currentVersion}`.
365
+
366
+ **Verdict: Concur with the review.** Clear to ship.
367
+
368
+ ---
369
+
370
+ ## Evidence pointers
371
+
372
+ - `tests/unit/PlanDocParser.test.ts` — 9 passing
373
+ - `tests/unit/StageTransitionValidator.test.ts` — 26 passing
374
+ - `tests/integration/projects-api.test.ts` — 14 passing
375
+ - `node_modules/typescript/bin/tsc --noEmit` → clean
376
+ - Spec: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ 1.2, 1.3, 1.6, 1.10, 1.12
377
+ - Signal-vs-authority reference: `docs/signal-vs-authority.md` § "When this principle does NOT apply"
@@ -0,0 +1,107 @@
1
+ # Side-Effects Review — verifyInjection + StallTriage Fast-Path
2
+
3
+ ## Change Summary
4
+
5
+ Two additive layers that recover stuck-input failures on Claude Code v2.1.105+
6
+ TUIs, where the Enter keypress sent after a bracketed-paste-end sequence is
7
+ occasionally eaten by a race with the paste-end handler.
8
+
9
+ 1. `SessionManager.verifyInjection(tmuxSession, text)` — fires 1.5s after every
10
+ `rawInject`. If a 40-char marker from the message is still visible at the
11
+ `❯` prompt, send one extra Enter. Single-shot per injection.
12
+ 2. `StallTriageNurse` fast-path — at triage time, if the pane shows ≥20 chars
13
+ of text at the `❯` prompt with no processing glyphs, nudge once and skip
14
+ the LLM round-trip.
15
+
16
+ ## Failure Mode Being Fixed
17
+
18
+ Observed in production:
19
+ - Telegram message reaches the agent server
20
+ - Server calls `injectMessage` → `rawInject` → bracketed paste + Enter
21
+ - Claude Code's readline buffers the paste content but the Enter races the
22
+ `\e[201~` paste-end and is consumed by it
23
+ - Message sits at `❯` with no submission; agent appears unresponsive
24
+
25
+ Justin's screenshot (2026-05-11) shows the exact failure on Claude Code v2.1.139.
26
+
27
+ ## Over-Block Risk
28
+
29
+ **verifyInjection:**
30
+ - Marker requires the first 40 chars (whitespace-stripped, ≥8 chars) to be
31
+ visible at a line containing `❯` within 1.5s of injection.
32
+ - False positive requires the pane to show our exact text at a `❯` prompt
33
+ after Claude has already processed and emitted it back. Claude's transcript
34
+ rendering doesn't put `❯` on transcript history lines — only on the active
35
+ input row — so this is structurally implausible.
36
+ - Worst-case impact: one extra Enter to an already-submitted message → empty
37
+ Enter in input box, no behavior change.
38
+
39
+ **StallTriage fast-path:**
40
+ - Requires (a) ≥20 chars of text after the `❯` glyph and (b) absence of all
41
+ of `⎿`, `✶`, `⏺`, `Coalescing`, `thinking`, `esc to interrupt`.
42
+ - A genuine in-progress turn always has at least one of those glyphs visible
43
+ during the work, so the gate refuses to nudge.
44
+ - Worst-case impact: harmless single Enter.
45
+
46
+ ## Under-Block Risk
47
+
48
+ If verifyInjection misses the stuck state (Enter took longer than 1.5s to be
49
+ eaten, marker text was edited out by user, etc.), the StallTriage fast-path is
50
+ the recovery backstop — it runs from the existing zombie-detection idle path.
51
+ If that also misses, the existing `pasteRetried` mechanism for literal
52
+ `[Pasted text #N]` placeholders catches the short-paste path. Three layers
53
+ total; legacy behavior is preserved.
54
+
55
+ ## Level-of-Abstraction Fit
56
+
57
+ - `verifyInjection` is a private method on `SessionManager`, called from
58
+ `rawInject`. Same abstraction layer as the tmux send-keys it verifies.
59
+ - StallTriage fast-path is a heuristic guard *inside* `StallTriageNurse`,
60
+ running before the LLM call. Same abstraction layer as the rest of the
61
+ triage logic.
62
+
63
+ Neither leaks tmux details upward or adds a new public API. Correct level.
64
+
65
+ ## Signal vs Authority Compliance
66
+
67
+ - `verifyInjection` is a signal-emitting verification (capture pane + check
68
+ marker). It takes one structural action: send Enter. This is the only
69
+ reasonable mitigation at this layer — there is no higher gate that needs to
70
+ authorize a missed Enter resend.
71
+ - StallTriage fast-path emits a signal (typed-but-not-submitted heuristic) and
72
+ takes a single bounded action (one Enter). It does NOT replace the LLM gate
73
+ — if the fast-path doesn't fire, full triage runs as before. The fast-path
74
+ is a *short-circuit*, not a *bypass*.
75
+
76
+ Both layers carry a `DegradationReporter` entry on recovery so the higher
77
+ authority (the user / monitoring) sees that a self-heal fired.
78
+
79
+ ## Interactions With Existing Recovery Paths
80
+
81
+ | Layer | Trigger | One-shot scope | Conflicts? |
82
+ |-------|---------|---------------|------------|
83
+ | `verifyInjection` | 1.5s after every `rawInject` | per-injection | independent — different timer, different captureOutput call |
84
+ | `pasteRetried` set (idle-detection) | `[Pasted text #N]` visible at idle prompt | per-session lifetime | independent — distinct match string |
85
+ | StallTriage fast-path | text at `❯` with no glyphs, before LLM | per-triage-pass | independent — runs from triage scheduler, not injection |
86
+ | Existing zombie-kill timer | idleMs > IDLE_PROMPT_KILL_MINUTES | global | unchanged — only fires if all 3 above missed and the session sat for full timeout |
87
+
88
+ The four layers gate from "fast and proactive" to "slow and destructive."
89
+ Adding the new two layers above the existing two is monotonically safer.
90
+
91
+ ## Rollback Cost
92
+
93
+ - Revert: drop `this.verifyInjection(...)` call in `rawInject` and the
94
+ `verifyInjection` method body. Drop the fast-path block in
95
+ `StallTriageNurse`. Pure additive — no schema, no state, no migration.
96
+ - Both changes have dedicated unit tests; removing them removes the tests.
97
+ - Branch surface area: 2 source files touched, 2 test files added.
98
+
99
+ ## Evidence
100
+
101
+ - `tests/unit/session-injection-verify.test.ts` — 7 tests covering marker
102
+ extraction, the captureOutput call, the Enter resend, and the no-op when
103
+ the text already submitted.
104
+ - `tests/unit/stall-triage-typed-not-submitted.test.ts` — 5 tests covering
105
+ detection thresholds, glyph-presence refusal, and the LLM-bypass path.
106
+ - Live reproduction: Justin's 2026-05-11 screenshot on Claude Code v2.1.139
107
+ shows the failure mode this fix targets.