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.
- package/dist/core/InitiativeTracker.d.ts +1 -0
- package/dist/core/InitiativeTracker.d.ts.map +1 -1
- package/dist/core/InitiativeTracker.js +8 -1
- package/dist/core/InitiativeTracker.js.map +1 -1
- package/dist/core/PlanDocParser.d.ts +62 -0
- package/dist/core/PlanDocParser.d.ts.map +1 -0
- package/dist/core/PlanDocParser.js +328 -0
- package/dist/core/PlanDocParser.js.map +1 -0
- package/dist/core/SafeYaml.d.ts +45 -0
- package/dist/core/SafeYaml.d.ts.map +1 -0
- package/dist/core/SafeYaml.js +230 -0
- package/dist/core/SafeYaml.js.map +1 -0
- package/dist/core/SessionManager.d.ts +12 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +60 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StageTransitionValidator.d.ts +97 -0
- package/dist/core/StageTransitionValidator.d.ts.map +1 -0
- package/dist/core/StageTransitionValidator.js +338 -0
- package/dist/core/StageTransitionValidator.js.map +1 -0
- package/dist/monitoring/StallTriageNurse.d.ts +11 -0
- package/dist/monitoring/StallTriageNurse.d.ts.map +1 -1
- package/dist/monitoring/StallTriageNurse.js +66 -0
- package/dist/monitoring/StallTriageNurse.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +312 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/0.28.87.md +153 -0
- package/upgrades/side-effects/0.28.86.md +13 -0
- package/upgrades/side-effects/project-scope-phase1a-pr2.md +377 -0
- 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.
|