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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +9 -0
- package/dist/commands/server.js.map +1 -1
- 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/ProjectDigestCache.d.ts +58 -0
- package/dist/core/ProjectDigestCache.d.ts.map +1 -0
- package/dist/core/ProjectDigestCache.js +146 -0
- package/dist/core/ProjectDigestCache.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 +58 -50
- package/src/templates/hooks/compaction-recovery.sh +50 -0
- package/src/templates/hooks/session-start.sh +50 -0
- package/upgrades/0.28.87.md +153 -0
- package/upgrades/0.28.88.md +91 -0
- package/upgrades/side-effects/0.28.86.md +13 -0
- package/upgrades/side-effects/0.28.87.md +13 -0
- package/upgrades/side-effects/project-scope-phase1a-pr2.md +377 -0
- package/upgrades/side-effects/project-scope-phase1a-pr3.md +477 -0
- 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"
|