instar 0.28.85 → 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 (35) hide show
  1. package/dist/core/InitiativeTracker.d.ts +177 -1
  2. package/dist/core/InitiativeTracker.d.ts.map +1 -1
  3. package/dist/core/InitiativeTracker.js +341 -13
  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.86.md +86 -0
  31. package/upgrades/0.28.87.md +153 -0
  32. package/upgrades/side-effects/0.28.86.md +13 -0
  33. package/upgrades/side-effects/project-scope-phase1a-pr1.md +232 -0
  34. package/upgrades/side-effects/project-scope-phase1a-pr2.md +377 -0
  35. package/upgrades/side-effects/verify-injection-stuck-input.md +107 -0
@@ -0,0 +1,232 @@
1
+ # Side-Effects Review — project-scope Phase 1a PR 1 (Initiative type extension)
2
+
3
+ **Version / slug:** `project-scope-phase1a-pr1`
4
+ **Date:** `2026-05-11`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Extends `src/core/InitiativeTracker.ts` with the project-layer scaffolding
11
+ called for by `docs/specs/PROJECT-SCOPE-SPEC.md` Phase 1.1: optional `kind`,
12
+ `schemaVersion`, `version` (OCC counter), `parentProjectId`, child-only
13
+ fields (`pipelineStage`, `specPath`, `prNumber`, `mergeCommitOid`,
14
+ `ciCheckedAt`, `skip*`, `unskippedAt`, `driftCheck`), and project-only
15
+ fields (`rounds[]`, `sourceDocs`, `autoAdvance`, `telegramTopicId`,
16
+ `ownerMachineId`, `targetRepoPath`, `unacknowledgedAdvanceCount`,
17
+ `firstLaunchAckAt`, `lastAckedRoundIndex`, `awaitingReconciliation`,
18
+ `driftPromptTemplateVersion`). The `InitiativeStatus` union is extended
19
+ with `'paused' | 'halted' | 'awaiting-user'` — pre-project-scope records
20
+ continue to use the original four values. Three new error classes
21
+ (`OccVersionMismatchError`, `KindImmutableError`,
22
+ `InvalidParentProjectError`) cover the validation paths; an
23
+ `omitUndefinedReplacer` and `null`-rejection guards keep serialization
24
+ stable; a digest-cache-invalidator hook (`setDigestCacheInvalidator`)
25
+ fires after every successful mutation (no-op default until PR 3 wires it).
26
+ A one-time idempotent backfill in `loadFromDisk()` writes
27
+ `kind: 'task' + schemaVersion: 1` to legacy records; the parallel
28
+ `backfillKindAndSchema()` public method handles the TaskFlow-enabled
29
+ install path. Tests: `tests/unit/InitiativeTracker.project.test.ts` (26
30
+ new tests).
31
+
32
+ ## Decision-point inventory
33
+
34
+ - `Initiative.kind` immutability — **add** — `update()` throws
35
+ `KindImmutableError` if the caller tries to change `kind` away from its
36
+ current value.
37
+ - `InitiativeUpdateInput.ifMatch` (OCC) — **add** — when provided, must
38
+ equal current `version` or `OccVersionMismatchError` is thrown.
39
+ - `parentProjectId` bidirectional validator — **add** — `update()` rejects
40
+ attaching a child to a parent that doesn't exist, isn't `kind: 'project'`,
41
+ or doesn't list this child in any of its `rounds[].itemIds`.
42
+
43
+ No other decision points are touched. The new validators sit at the
44
+ persistence-layer edge and are structural — they accept/reject based on
45
+ type and reference integrity, not on conversational context.
46
+
47
+ ---
48
+
49
+ ## 1. Over-block
50
+
51
+ **What legitimate inputs does this change reject that it shouldn't?**
52
+
53
+ No new rejections for legitimate input shapes. The new fields are all
54
+ optional and additive — pre-existing callers that don't touch them are
55
+ not affected. The OCC check only fires when `ifMatch` is explicitly
56
+ provided, so backward-compatible callers continue to enjoy unconditional
57
+ writes. The `parentProjectId` validator only fires when the caller is
58
+ actually setting that field; clearing (set to null) skips validation.
59
+ The `kind` immutability check only rejects values that differ from the
60
+ current kind — passing the same kind value is accepted as a no-op.
61
+
62
+ ---
63
+
64
+ ## 2. Under-block
65
+
66
+ **What failure modes does this still miss?**
67
+
68
+ The OCC check is opt-in — old callers that don't provide `ifMatch` still
69
+ get last-write-wins semantics. This is intentional for backward
70
+ compatibility. The HTTP layer (PR 2) will enforce `If-Match` on the
71
+ project-kind `PATCH /projects/:id` endpoint specifically, where staleness
72
+ matters; legacy `/initiatives/*` keeps current semantics.
73
+
74
+ `parentProjectId` validation only fires inside `update()`. A caller
75
+ that sets `parentProjectId` at create time (via
76
+ `InitiativeCreateInput.parentProjectId`) bypasses the bidirectional
77
+ check. This is acceptable for Phase 1.1 because the round-runner and HTTP
78
+ layer (PR 2 / Phase 1.5) construct child initiatives and then attach them
79
+ in a separate update — the create-time field exists for migration/testing
80
+ shapes only. PR 2 may tighten this if the create flow uses it directly.
81
+
82
+ ---
83
+
84
+ ## 3. Level-of-abstraction fit
85
+
86
+ **Is this at the right layer?**
87
+
88
+ Yes. Type extension, OCC counter, `kind` immutability, and the
89
+ `parentProjectId` bidirectional check all live at the persistence layer,
90
+ which is correct because the tracker is the single source of truth for
91
+ both project and task initiative records. The validators compare to
92
+ fields already owned by the tracker (`rounds[].itemIds`, `kind`,
93
+ `version`), so reaching outside the tracker would only add coupling, not
94
+ clarity. The HTTP layer (PR 2) will map the three error classes to 4xx
95
+ responses; that's a thin translation, not a re-implementation.
96
+
97
+ The digest-cache-invalidator hook is a deliberate inversion of
98
+ dependency — the tracker doesn't know about the cache (which is owned by
99
+ PR 3's project-scope digest), it just signals "something changed." This
100
+ keeps the layering clean.
101
+
102
+ ---
103
+
104
+ ## 4. Signal vs authority compliance
105
+
106
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
107
+
108
+ **Does this change hold blocking authority with brittle logic?**
109
+
110
+ - [x] No — this change has no block/allow surface in the
111
+ conversational-content sense.
112
+
113
+ The three new validators (`KindImmutableError`,
114
+ `OccVersionMismatchError`, `InvalidParentProjectError`) are structural
115
+ schema/reference checks, not behavioral content gates. They fall under
116
+ the "When this principle does NOT apply" carve-out in
117
+ `docs/signal-vs-authority.md`: schema validators, type guards, and
118
+ referential-integrity checks at API edges are expected to be structural
119
+ and brittle by design — the right answer to "does this PATCH change an
120
+ immutable field?" is a deterministic yes/no, not an LLM judgment. The
121
+ spec calls this out explicitly in Phase 1.1.
122
+
123
+ ---
124
+
125
+ ## 5. Interactions
126
+
127
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
128
+
129
+ - **Shadowing:** The backfill in `loadFromDisk()` runs once at construction
130
+ time. It does not shadow other checks because it operates on raw JSON
131
+ before the cache is populated. The version-bump in `update()` runs
132
+ after all field assignments and before the persistence call; it
133
+ doesn't shadow any existing check.
134
+ - **Double-fire:** `setPhaseStatus()` and `update()` both bump `version`
135
+ and both call the digest-cache invalidator. They're separate code
136
+ paths called by different callers, never on the same logical write.
137
+ `create()` and `remove()` likewise each invalidate exactly once per
138
+ successful mutation. No double-fire.
139
+ - **Races:** The legacy-JSON path is single-process (one server, one
140
+ tracker instance). The TaskFlow path inherits TaskFlow's existing
141
+ concurrency model (controllerInstanceId + expectedRevision). PR 4 will
142
+ add multi-machine reconciliation via `awaitingReconciliation`; the
143
+ field is present in this PR but unused.
144
+ - **Feedback loops:** The digest cache invalidator hook is a one-way
145
+ signal; no feedback path is created. The default no-op cannot loop.
146
+
147
+ The backfill rewrite happens inside `loadFromDisk()`. The constructor
148
+ runs `loadFromDisk()` synchronously before any caller can touch the
149
+ tracker, so there's no race between backfill and the first read. If
150
+ `fs.writeFileSync` fails, the cache still reflects the backfilled shape
151
+ (in-memory) and the next mutation will rewrite to disk — the legacy
152
+ file remains valid because `kind`/`schemaVersion` are optional.
153
+
154
+ ---
155
+
156
+ ## 6. External surfaces
157
+
158
+ **Does this change anything visible outside the immediate code path?**
159
+
160
+ - **Other agents on the same machine:** No. The new fields are all
161
+ optional; agents that read `Initiative` records without expecting them
162
+ see the same shape as before.
163
+ - **Other users of the install base:** No HTTP changes in this PR.
164
+ Endpoints in `routes-initiatives.ts` continue to operate on the
165
+ pre-existing field set. PR 2 will introduce `/projects` routes.
166
+ - **External systems:** None.
167
+ - **Persistent state:** Legacy `initiatives.json` files gain a one-time
168
+ backfill: each record acquires `kind: 'task'` and `schemaVersion: 1`.
169
+ This is forward-compatible — old code reading the file ignores the
170
+ new fields. The TaskFlow path stores the same shape in `stateJson`.
171
+ No schema migration is required for rollback (see § 7).
172
+ - **Timing / runtime conditions:** The backfill adds a small write at
173
+ startup the first time a server boots with this code on a legacy
174
+ install. Bounded by the number of legacy records; in practice
175
+ microseconds.
176
+
177
+ ---
178
+
179
+ ## 7. Rollback cost
180
+
181
+ **If this turns out wrong in production, what's the back-out?**
182
+
183
+ - **Hot-fix release:** Revert the commit. All new fields are optional;
184
+ rollback restores the original behavior.
185
+ - **Data migration:** None required. Records that received
186
+ `kind: 'task' + schemaVersion: 1` from the backfill simply carry
187
+ those extra fields after rollback — old code ignores them. The
188
+ `version` field was added to in-memory records during this PR's
189
+ lifetime; if it survives the rollback in the on-disk file, old code
190
+ ignores it.
191
+ - **Agent state repair:** Not needed. The legacy JSON path tolerates
192
+ unknown extra fields. TaskFlow's `stateJson` is opaque to TaskFlow
193
+ itself; only the projector cares.
194
+ - **User visibility:** No user-visible regression during rollback. No
195
+ HTTP responses or dashboard surfaces change in this PR.
196
+
197
+ Rollback is a single `git revert` with no follow-up.
198
+
199
+ ---
200
+
201
+ ## Conclusion
202
+
203
+ PR 1 of 3 for the project-scope Phase 1a build. All changes are
204
+ additive and structural: optional type fields, three error classes for
205
+ structural validators, a one-time idempotent backfill, and a no-op
206
+ digest-cache hook. The 26 new tests in
207
+ `tests/unit/InitiativeTracker.project.test.ts` cover backfill,
208
+ immutability, OCC, parent validation, serialization stability, the
209
+ extended status enum, and the invalidator hook. The full existing
210
+ InitiativeTracker suite (87 tests across 4 files) continues to pass.
211
+ Clear to ship.
212
+
213
+ ---
214
+
215
+ ## Second-pass review (if required)
216
+
217
+ Not required. No new decision points, no sentinel/gate/watchdog
218
+ touched, no externally-visible behavior change. Per the
219
+ `/instar-dev` rubric in `skills/instar-dev/SKILL.md` § Phase 5, a
220
+ second-pass review is reserved for changes that alter conversational
221
+ gates or recovery infrastructure.
222
+
223
+ ---
224
+
225
+ ## Evidence pointers
226
+
227
+ - `tests/unit/InitiativeTracker.project.test.ts` — 26 new tests, all
228
+ passing.
229
+ - Full InitiativeTracker test sweep:
230
+ `node node_modules/vitest/vitest.mjs run tests/unit/InitiativeTracker tests/unit/initiative-tracker-taskflow tests/unit/routes-initiatives` → 87 tests, all passing.
231
+ - `node_modules/typescript/bin/tsc --noEmit` → clean.
232
+ - Spec: `docs/specs/PROJECT-SCOPE-SPEC.md` Phase 1.1.
@@ -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"