instar 0.28.86 → 0.28.88

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 (45) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +9 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/InitiativeTracker.d.ts +1 -0
  5. package/dist/core/InitiativeTracker.d.ts.map +1 -1
  6. package/dist/core/InitiativeTracker.js +8 -1
  7. package/dist/core/InitiativeTracker.js.map +1 -1
  8. package/dist/core/PlanDocParser.d.ts +62 -0
  9. package/dist/core/PlanDocParser.d.ts.map +1 -0
  10. package/dist/core/PlanDocParser.js +328 -0
  11. package/dist/core/PlanDocParser.js.map +1 -0
  12. package/dist/core/ProjectDigestCache.d.ts +58 -0
  13. package/dist/core/ProjectDigestCache.d.ts.map +1 -0
  14. package/dist/core/ProjectDigestCache.js +146 -0
  15. package/dist/core/ProjectDigestCache.js.map +1 -0
  16. package/dist/core/SafeYaml.d.ts +45 -0
  17. package/dist/core/SafeYaml.d.ts.map +1 -0
  18. package/dist/core/SafeYaml.js +230 -0
  19. package/dist/core/SafeYaml.js.map +1 -0
  20. package/dist/core/SessionManager.d.ts +12 -0
  21. package/dist/core/SessionManager.d.ts.map +1 -1
  22. package/dist/core/SessionManager.js +60 -0
  23. package/dist/core/SessionManager.js.map +1 -1
  24. package/dist/core/StageTransitionValidator.d.ts +97 -0
  25. package/dist/core/StageTransitionValidator.d.ts.map +1 -0
  26. package/dist/core/StageTransitionValidator.js +338 -0
  27. package/dist/core/StageTransitionValidator.js.map +1 -0
  28. package/dist/monitoring/StallTriageNurse.d.ts +11 -0
  29. package/dist/monitoring/StallTriageNurse.d.ts.map +1 -1
  30. package/dist/monitoring/StallTriageNurse.js +66 -0
  31. package/dist/monitoring/StallTriageNurse.js.map +1 -1
  32. package/dist/server/routes.d.ts.map +1 -1
  33. package/dist/server/routes.js +312 -0
  34. package/dist/server/routes.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/data/builtin-manifest.json +58 -50
  37. package/src/templates/hooks/compaction-recovery.sh +50 -0
  38. package/src/templates/hooks/session-start.sh +50 -0
  39. package/upgrades/0.28.87.md +153 -0
  40. package/upgrades/0.28.88.md +91 -0
  41. package/upgrades/side-effects/0.28.86.md +13 -0
  42. package/upgrades/side-effects/0.28.87.md +13 -0
  43. package/upgrades/side-effects/project-scope-phase1a-pr2.md +377 -0
  44. package/upgrades/side-effects/project-scope-phase1a-pr3.md +477 -0
  45. package/upgrades/side-effects/verify-injection-stuck-input.md +107 -0
@@ -0,0 +1,153 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: minor -->
4
+ <!-- Valid values: patch, minor, major -->
5
+ <!-- patch = bug fixes, refactors, test additions, doc updates -->
6
+ <!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
7
+ <!-- major = breaking changes to existing APIs or behavior -->
8
+
9
+ ## What Changed
10
+
11
+ ### Stuck Telegram messages — auto-resend Enter after paste
12
+
13
+ On Claude Code v2.1.105 and newer, the Enter keypress after a bracketed-paste
14
+ sequence is occasionally eaten by a race with the paste-end handler. The
15
+ injected message — including Telegram and Slack relays — would land in the
16
+ prompt and sit there forever, never submitted, until someone pressed Enter
17
+ in the dashboard or tmux session manually.
18
+
19
+ Two-layer fix:
20
+
21
+ - **`SessionManager.verifyInjection`** (proactive). After every `rawInject`,
22
+ schedule a 1.5-second check that compares the first 40 chars of the message
23
+ against the pane at the `❯` prompt. If it's still there, send one extra
24
+ Enter. Single-shot per injection, no-op when the text submitted normally,
25
+ reports recovery via `DegradationReporter`.
26
+ - **`StallTriageNurse` fast-path** (recovery-time backstop). Before paying for
27
+ an LLM diagnosis call, the triage nurse checks for ≥20 chars of text at the
28
+ `❯` prompt without any processing glyphs (`⎿ ✶ ⏺` / "Coalescing" /
29
+ "thinking" / "esc to interrupt"). If detected, nudge with a single Enter and
30
+ skip the LLM round-trip. Conservative heuristic — false positives at worst
31
+ send a harmless Enter.
32
+
33
+ Tests: `tests/unit/session-injection-verify.test.ts` (7 tests),
34
+ `tests/unit/stall-triage-typed-not-submitted.test.ts` (5 tests).
35
+
36
+ Side-effects review:
37
+ `upgrades/side-effects/verify-injection-stuck-input.md`.
38
+
39
+ ### Project-scope Phase 1a PR 2 — Stage validator + `/projects` API
40
+
41
+ Second of three PRs scaffolding the project-scope feature. PR 1 added
42
+ the type-level fields; this PR adds the persistence-layer validator and
43
+ the HTTP surface your agent uses to register and inspect projects.
44
+
45
+ Spec source: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ Phase 1.2, 1.3, 1.6,
46
+ 1.10, 1.12.
47
+
48
+ **New files (persistence/validator layer):**
49
+ - `src/core/SafeYaml.ts` — minimal hand-rolled YAML-frontmatter
50
+ extractor with bounded depth and length, so user-supplied plan docs
51
+ can't smuggle in YAML-bomb input.
52
+ - `src/core/PlanDocParser.ts` — parses a plan-doc markdown file into
53
+ `{project, children, errors}`. Validates required frontmatter, slug
54
+ shape, source/effort/scope tags, child uniqueness, round membership.
55
+ - `src/core/StageTransitionValidator.ts` — deterministic validator for
56
+ child-initiative `pipelineStage` edges (`outline → approved →
57
+ building → merged`, plus skipped/regressed). Every check is an
58
+ artifact assertion: frontmatter literal equality, `gh pr view`
59
+ reports `MERGED`, file exists, realpath jailed under
60
+ `targetRepoPath`.
61
+
62
+ **New HTTP endpoints (all require auth):**
63
+
64
+ | Endpoint | Status | Notes |
65
+ |----------|--------|-------|
66
+ | `GET /projects` | functional | Lists project-kind initiatives. |
67
+ | `GET /projects/:id` | functional | Project + its children. Supports `?fields=<csv>` projection. |
68
+ | `GET /projects/:id/next` | placeholder (501) | Round-runner lands in Phase 1b. |
69
+ | `POST /projects` | functional | Creates project + children from a plan doc. Rate-limited 5/hour per auth token. |
70
+ | `POST /projects/validate` | functional | Dry-run plan-doc parse; no side effects. |
71
+ | `DELETE /projects/:id` | functional | Archives; requires `If-Match`; refuses while a round is in-progress. |
72
+
73
+ The full Phase 1.3 endpoint set (`advance`, `drift-check`, `halt`,
74
+ `ack`, `accept-partial`, `claim-ownership`, `resolve-conflict`) lands
75
+ in Phase 1b once the round-runner exists.
76
+
77
+ **Other changes:**
78
+ - `InitiativeTracker.list({ kind })` — new optional filter on `list()`.
79
+ Records without explicit `kind` are treated as `'task'` for back-compat.
80
+ - Rate-limit counter persisted at `.instar/local/projects-rate.json`
81
+ (gitignored; never syncs across machines — matches per-agent semantics).
82
+
83
+ ## What to Tell Your User
84
+
85
+ Two things landed in this release.
86
+
87
+ First — a fix for the stuck-Telegram-message bug. Sometimes your message would
88
+ land in my input box but never actually submit, and you'd have to press Enter
89
+ in the dashboard to unstick me. That race is now caught automatically: after
90
+ every injection I do a quick re-check, and if your message is still sitting
91
+ there waiting, I press Enter for it. There's also a backstop in the stall
92
+ recovery path. You shouldn't have to nudge me again.
93
+
94
+ Second — your agent now has a real HTTP surface for projects. You (or your
95
+ agent) can register a multi-spec project from a plan-doc markdown file and
96
+ have the agent track it as a top-level project-kind initiative with children
97
+ rolled up underneath.
98
+
99
+ Right now the projects piece is plumbing. It lets a caller do six things:
100
+ list projects, fetch one, dry-run a plan doc, create from a plan doc, ask
101
+ "what's next?" (currently returns a placeholder — the round-runner lands in
102
+ the next phase), and archive a project. The endpoints are auth-gated and
103
+ creation is rate-limited to five per hour so a buggy caller cannot
104
+ accidentally spawn a flood of projects. The advance, halt, and acknowledge
105
+ endpoints — the ones that actually drive a project forward — land in the
106
+ next phase along with the round-runner that powers them.
107
+
108
+ Nothing existing changes shape. The initiatives routes still return what
109
+ they always did. If you do not use the new endpoints, you will not notice
110
+ anything.
111
+
112
+ ## Summary of New Capabilities
113
+
114
+ | Capability | How to Use |
115
+ |-----------|-----------|
116
+ | Auto-recover stuck Telegram/Slack injections | automatic — `SessionManager.verifyInjection` resends Enter when needed |
117
+ | Register a project from a plan doc | `POST /projects` with `{"planDocPath": "<abs-path>"}` |
118
+ | Dry-run validate a plan doc | `POST /projects/validate` with `{"planDocPath": "<abs-path>"}` |
119
+ | List all projects | `GET /projects` |
120
+ | Fetch project + children | `GET /projects/:id` (optionally `?fields=id,title,pipelineStage`) |
121
+ | Archive a project | `DELETE /projects/:id` with `If-Match: <version>` |
122
+ | Stage-transition validator (library) | `import { validateStageTransition } from './core/StageTransitionValidator.js'` |
123
+
124
+ ## Evidence
125
+
126
+ **Stuck-input fix (PR #151).** Reproduction: Justin observed Telegram messages
127
+ landing in Claude Code's input buffer at the `❯` prompt without being submitted
128
+ on Claude Code v2.1.139, confirmed by a 2026-05-11 dashboard screenshot showing
129
+ the message text visible at the prompt with no processing glyphs. The race
130
+ matches the original failure mode that the verifyInjection design (April 13,
131
+ commit a81699d4) was written for. Observed-before: message sat in input
132
+ indefinitely until a manual Enter from the dashboard. Verified-after: with
133
+ verifyInjection in `rawInject`, a 1.5s post-injection check fires and
134
+ auto-resends Enter when the marker text is still at the prompt; recovery
135
+ reported via DegradationReporter. Coverage:
136
+ - `tests/unit/session-injection-verify.test.ts` — 7 tests covering marker
137
+ extraction, captureOutput, Enter resend, and the no-op when the text
138
+ submitted normally.
139
+ - `tests/unit/stall-triage-typed-not-submitted.test.ts` — 5 tests covering the
140
+ StallTriage fast-path: detection thresholds, glyph-presence refusal, the
141
+ LLM-bypass nudge.
142
+ - Side-effects review: `upgrades/side-effects/verify-injection-stuck-input.md`.
143
+
144
+ **Project-scope Phase 1a PR 2.** Spec:
145
+ `docs/specs/PROJECT-SCOPE-SPEC.md` §§ 1.2, 1.3, 1.6, 1.10, 1.12.
146
+
147
+ - `tests/unit/PlanDocParser.test.ts` — 9 new tests covering frontmatter
148
+ parsing, slug validation, source/effort/scope tags, child uniqueness.
149
+ - `tests/unit/StageTransitionValidator.test.ts` — 26 new tests covering
150
+ every named edge, reconciler bypass, jail check, PR state mapping.
151
+ - `tests/integration/projects-api.test.ts` — 14 new tests covering all
152
+ six endpoints end-to-end through the live express router.
153
+ - Side-effects review: `upgrades/side-effects/project-scope-phase1a-pr2.md`.
@@ -0,0 +1,91 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: minor -->
4
+ <!-- Valid values: patch, minor, major -->
5
+ <!-- patch = bug fixes, refactors, test additions, doc updates -->
6
+ <!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
7
+ <!-- major = breaking changes to existing APIs or behavior -->
8
+
9
+ ## What Changed
10
+
11
+ ### Project-scope Phase 1a PR 3 — `/project` skill + session-start digest
12
+
13
+ Final of three PRs scaffolding the project-scope feature. PR 1 added
14
+ the type-level fields; PR 2 added the stage-transition validator and
15
+ HTTP surface; PR 3 ships the user-invocable surface and the
16
+ session-start orientation block so active projects stay visible across
17
+ new sessions.
18
+
19
+ Spec source: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ Phase 1.7 (skill),
20
+ Phase 1.9 (session-start + compaction-recovery hooks).
21
+
22
+ **New files:**
23
+ - `src/core/ProjectDigestCache.ts` — synchronous writer that
24
+ snapshots active projects to `.instar/projects-digest.cache`. Top 5
25
+ by `lastTouchedAt`, sanitized strings (control-char strip + 80-char
26
+ per-field cap), atomic write.
27
+ - `.claude/skills/instar-project/SKILL.md` — slash-command skill. Phase
28
+ 1a ships `/project create`, `/project status`, and `/project next`
29
+ (read-only + create). The mutating commands ship in Phase 1b along
30
+ with the round-runner.
31
+
32
+ **Wired-in behavior:**
33
+ - Server boot now constructs a `ProjectDigestCache` and registers it
34
+ as the digest invalidator on `InitiativeTracker`. Every successful
35
+ project mutation (create / update / status change) re-renders the
36
+ cache file atomically. First boot writes the cache unconditionally
37
+ so the hook scripts always have a well-formed file to read.
38
+ - `.instar/hooks/instar/session-start.sh` and
39
+ `.instar/hooks/instar/compaction-recovery.sh` append an
40
+ `--- ACTIVE PROJECTS ---` block at the end of their orientation
41
+ output. They read the cache file (no HTTP, ≤50ms budget), re-sanitize
42
+ each line on read (defense in depth), and fall back to
43
+ `Active projects: state unavailable — run /project status when ready`
44
+ on miss / parse error.
45
+
46
+ **Cache file shape (`.instar/projects-digest.cache`):**
47
+ ```json
48
+ {
49
+ "generatedAt": "2026-05-11T...",
50
+ "digestLines": [
51
+ "Project [my-project]: 2 of 5 done. Next round: Round 3."
52
+ ],
53
+ "totalActiveProjects": 1,
54
+ "truncated": false
55
+ }
56
+ ```
57
+
58
+ ## What to Tell Your User
59
+
60
+ PR 3 closes the first slice of the project-tracking work. Your agent now has a built-in slash command for inspecting and registering projects, plus a session-start orientation line that keeps active projects visible at the top of every new conversation.
61
+
62
+ What you'll see at session start: a short Active Projects block with up to five projects, one line each — telling you the project name, how many rounds are done out of the total, and what the next round is. If you have more than five active projects, the block ends with a note saying how many more are on the dashboard. If your agent has no projects registered yet, the block is empty.
63
+
64
+ What this changes day-to-day: your agent won't lose track of multi-spec projects after a session restart or a context compaction. Before this, the first few features in a project would get attention and the rest would drift off the radar. Now the project roster is structurally visible at every new conversation. The same digest re-injects after context compression, so a long session that hits the compaction window still keeps the active-project orientation in context.
65
+
66
+ The project skill itself is this slice's read-only surface: you can register a project from a markdown plan doc, list all projects, fetch one with its children, or ask "what's next?" (which currently returns a placeholder — the next phase will wire the real answer). The mutating commands that actually drive a project forward — advancing rounds, halting, acknowledging, resuming, abandoning, running a round, drift-checking, accepting a partial round, and claiming ownership across machines — ship in the next phase once the round-runner lands.
67
+
68
+ Nothing existing changes shape. The initiatives routes still return what they always did. The new session-start block appears at the end of the orientation output and is plain text. If your agent doesn't register projects, you won't see any difference.
69
+
70
+ ## Summary of New Capabilities
71
+
72
+ | Capability | How to Use |
73
+ |-----------|-----------|
74
+ | `/project create <plan-doc>` | Register a project from a markdown plan via `POST /projects` |
75
+ | `/project status [id]` | List all projects, or fetch one with its children, via `GET /projects[/:id]` |
76
+ | `/project next [id]` | Phase 1a placeholder; returns the spec's 501 response |
77
+ | Session-start project digest | Top 5 active projects rendered at every new session start (no extra config) |
78
+ | Compaction-recovery digest | Same digest re-injects after context compression |
79
+ | Cache file (read-only consumer) | `.instar/projects-digest.cache` — JSON snapshot, atomic writes, sanitized |
80
+
81
+ ## Evidence
82
+
83
+ Spec: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ 1.7, 1.9.
84
+
85
+ - `tests/unit/ProjectDigestCache.test.ts` — 19 new tests covering
86
+ empty case, 3-project + 7-project cases, sanitization (control char
87
+ strip + 80-char cap), atomic write, invalidator wiring.
88
+ - `tests/unit/project-digest-hooks.test.ts` — 7 new tests exercising
89
+ the bash hook scripts against present / missing / malformed /
90
+ poisoned cache files.
91
+ - Side-effects review: `upgrades/side-effects/project-scope-phase1a-pr3.md`.
@@ -0,0 +1,13 @@
1
+ # Side-Effects Review — v0.28.86
2
+
3
+ This release shipped one user-facing change:
4
+
5
+ - **Project-scope Phase 1a PR 1 — Initiative type extension**
6
+
7
+ Side-effects review for that change lives at
8
+ [`project-scope-phase1a-pr1.md`](./project-scope-phase1a-pr1.md). It was
9
+ authored before the release-cut script started linking artifacts by version
10
+ filename; this file is the version-keyed pointer the pre-push gate looks for.
11
+
12
+ No other behavior changed in 0.28.86 beyond the type-level scaffolding
13
+ documented in that review.
@@ -0,0 +1,13 @@
1
+ # Side-Effects Review — v0.28.87
2
+
3
+ This release shipped two user-facing changes:
4
+
5
+ - **Stuck-Telegram-message auto-recovery (PR #151)** — side-effects review at
6
+ [`verify-injection-stuck-input.md`](./verify-injection-stuck-input.md).
7
+ - **Project-scope Phase 1a PR 2 — stage validator + `/projects` API** —
8
+ side-effects review at
9
+ [`project-scope-phase1a-pr2.md`](./project-scope-phase1a-pr2.md).
10
+
11
+ This file is the version-keyed pointer the pre-push gate looks for; the
12
+ per-change artifacts above contain the actual side-effects analysis. No
13
+ other behavior changed in 0.28.87 beyond what those two reviews cover.
@@ -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"