instar 0.28.95 → 0.28.97

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 (51) hide show
  1. package/dashboard/index.html +249 -2
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +90 -1
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/core/MachineHeartbeat.d.ts +94 -0
  6. package/dist/core/MachineHeartbeat.d.ts.map +1 -0
  7. package/dist/core/MachineHeartbeat.js +155 -0
  8. package/dist/core/MachineHeartbeat.js.map +1 -0
  9. package/dist/core/ProjectAutoAdvancePoller.d.ts +107 -0
  10. package/dist/core/ProjectAutoAdvancePoller.d.ts.map +1 -0
  11. package/dist/core/ProjectAutoAdvancePoller.js +176 -0
  12. package/dist/core/ProjectAutoAdvancePoller.js.map +1 -0
  13. package/dist/core/ProjectRoundCompleteMessage.d.ts +158 -0
  14. package/dist/core/ProjectRoundCompleteMessage.d.ts.map +1 -0
  15. package/dist/core/ProjectRoundCompleteMessage.js +253 -0
  16. package/dist/core/ProjectRoundCompleteMessage.js.map +1 -0
  17. package/dist/core/ProjectRoundExecution.d.ts +111 -0
  18. package/dist/core/ProjectRoundExecution.d.ts.map +1 -0
  19. package/dist/core/ProjectRoundExecution.js +359 -0
  20. package/dist/core/ProjectRoundExecution.js.map +1 -0
  21. package/dist/core/ProjectRoundLock.d.ts +67 -0
  22. package/dist/core/ProjectRoundLock.d.ts.map +1 -0
  23. package/dist/core/ProjectRoundLock.js +161 -0
  24. package/dist/core/ProjectRoundLock.js.map +1 -0
  25. package/dist/core/ProjectRoundRunner.d.ts +146 -0
  26. package/dist/core/ProjectRoundRunner.d.ts.map +1 -0
  27. package/dist/core/ProjectRoundRunner.js +371 -0
  28. package/dist/core/ProjectRoundRunner.js.map +1 -0
  29. package/dist/core/ProjectRoundWorktrees.d.ts +57 -0
  30. package/dist/core/ProjectRoundWorktrees.d.ts.map +1 -0
  31. package/dist/core/ProjectRoundWorktrees.js +111 -0
  32. package/dist/core/ProjectRoundWorktrees.js.map +1 -0
  33. package/dist/server/AgentServer.d.ts +12 -0
  34. package/dist/server/AgentServer.d.ts.map +1 -1
  35. package/dist/server/AgentServer.js +3 -0
  36. package/dist/server/AgentServer.js.map +1 -1
  37. package/dist/server/routes.d.ts +19 -0
  38. package/dist/server/routes.d.ts.map +1 -1
  39. package/dist/server/routes.js +782 -7
  40. package/dist/server/routes.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/data/builtin-manifest.json +48 -48
  43. package/upgrades/0.28.96.md +347 -0
  44. package/upgrades/0.28.97.md +146 -0
  45. package/upgrades/side-effects/project-scope-phase1-7-skill-surface.md +175 -0
  46. package/upgrades/side-effects/project-scope-phase1b-connect-the-dots.md +184 -0
  47. package/upgrades/side-effects/project-scope-phase1b-pr3.md +228 -0
  48. package/upgrades/side-effects/project-scope-phase1b-pr4.md +198 -0
  49. package/upgrades/side-effects/project-scope-phase1b-pr5.md +150 -0
  50. package/upgrades/side-effects/project-scope-phase1b-pr6.md +136 -0
  51. package/upgrades/side-effects/project-scope-phase1b-pr7.md +175 -0
@@ -0,0 +1,347 @@
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 1b connect-the-dots — wire the primitives
12
+
13
+ Wires the four primitives shipped during the Phase 1b autonomous burst
14
+ to their consumers, closing the deferrals named in side-effects reviews
15
+ of PRs #164, #166, #167, and #168.
16
+
17
+ - **Auto-advance poller now actually launches rounds.** On a successful
18
+ preflight + bookkeeping move, the poller fires-and-forgets a
19
+ `ProjectRoundExecution.runRound()` call. A per-project `inFlight`
20
+ guard prevents a slow run from being relaunched on the next 60s
21
+ tick. Errors are captured in `result.executorErrors[]` — they never
22
+ throw out of `tick()`.
23
+ - **`GET /projects/:id/next` returns a structured action.** Replaces
24
+ the 501 placeholder with the spec § 1.5 line 268 contract:
25
+ `{ action, params, skillCommand }`. Verbs include
26
+ `'await-user-approval'`, `'ack-required'`, `'resolve-conflict'`,
27
+ `'accept-partial'`, `'run-spec-converge'`, `'run-drift-check'`,
28
+ `'start-round'`. Each maps to a `/project ...` invocation so the
29
+ dashboard + skill UI can act on a single GET. 204 when all rounds
30
+ complete, 404 on non-project initiative.
31
+ - **`GET /projects/:id` lazy reconciler.** Per spec § 1.4 lines 256-258.
32
+ For 'building' children with `mergeCommitOid` set: 6h debounce via
33
+ `ciCheckedAt`, selection order oldest-`ciCheckedAt`-first
34
+ (ties → `roundIndex` ASC → `itemId` ASC, no starvation), cap of 3
35
+ per GET. Verified ancestor of `origin/main` → `'merged'`. NOT an
36
+ ancestor → `'regressed'` AND clear future `autoAdvanceAt` so the
37
+ chain doesn't auto-fire while a child is broken. `ciCheckedAt` is
38
+ written on every revalidation attempt (success or git failure) so
39
+ the debounce always backs off. Opt-out: `?reconcile=false`.
40
+ - **`POST /projects/:id/drift-check` route.** New HTTP wrapper around
41
+ `ProjectDriftChecker.run()`. **Mutex-guarded per project (spec line
42
+ 279)** — concurrent calls against the same project return 409,
43
+ protecting the drift-spend ledger from double-spend. Body:
44
+ `{ roundIndex, specPath, referencedFiles[], timeoutMs?, modelId? }`.
45
+ Returns 503 when no `IntelligenceProvider` is configured.
46
+
47
+ Drift cache + spend-ledger wiring are intentionally deferred until the
48
+ dashboard surfaces live verdicts and cost telemetry needs the cap. The
49
+ mutex is in-process per-server; multi-process protection comes from
50
+ the existing `proper-lockfile` on the drift-spend ledger itself.
51
+
52
+ ### Project-scope Phase 1b PR 7 — autonomous run loop
53
+
54
+ Final PR of project-scope Phase 1b. Ships the autonomous run loop the
55
+ spec § Phase 1.5 names (steps 1-11), wired against the lock, preflight,
56
+ and runner primitives that landed in PRs 3-6:
57
+
58
+ - **`ProjectRoundWorktrees`** — lazy worktree allocator under
59
+ `<targetRepoPath>/.worktrees/<projectId>/<roundIndex>/<itemId>`.
60
+ Appends `.worktrees/` to `.git/info/exclude` on first allocation so
61
+ the namespace doesn't pollute `git status`. Helpers for prune +
62
+ remove + ensure-exclude-entry.
63
+ - **`runRound(input, deps)`** — the run loop itself. Acquires the
64
+ round-runner lock, lazily allocates the first worktree, spawns the
65
+ autonomous child in a detached process group, polls the project
66
+ record every 60 seconds. On mid-round mutation to `round.itemIds`,
67
+ SIGTERMs the child's process group (5s grace, then SIGKILL) and
68
+ relaunches with the new stop condition. On the child's natural
69
+ exit, verifies per-item artifacts via the caller-injected
70
+ `verifyMergedItems` and sets `round.status` to `complete` or
71
+ `partially-complete`. On `haltedAt` set mid-run, kills the child
72
+ and returns `halted`. Three-attempt resume cap on transient
73
+ non-zero exits before `failed`.
74
+ - **`verifyMergedItemsViaGit`** — exported helper for production
75
+ callers; runs `git merge-base --is-ancestor <child.mergeCommitOid>
76
+ origin/main` per item.
77
+
78
+ The run loop is a module function, not a method on `ProjectRoundRunner`,
79
+ because it has different lifecycle semantics (long-running, subprocess
80
+ manager) than the synchronous verbs in PR 3's runner. Both modules
81
+ work in concert: callers call `runner.preflight(...)` first, then
82
+ `runRound(...)` if preflight passes.
83
+
84
+ Phase 1b is now fully decomposed: 7 PRs across drift + cache + ledger
85
+ + runner + halt/advance/ack + auto-advance + claim-ownership +
86
+ dashboard + filter + round-complete-message + run-loop. The remaining
87
+ spec items (lazy merged-state reconciler on GET, `GET /next` real
88
+ implementation) are wiring tasks rather than new infrastructure.
89
+
90
+ ### Project-scope Phase 1b PR 6 — tone-gated round-complete message + delivery
91
+
92
+ Sixth PR of project-scope Phase 1b. Ships the two final primitives the
93
+ autonomous run loop will need to emit round-complete digests safely:
94
+
95
+ - **`formatRoundCompleteMessage(input)`** — pure template function.
96
+ Enforces required-field PRESENCE (not non-emptiness, per spec —
97
+ "the gate never silently rejects on a legitimate halt"). Returns
98
+ `{ok:true, message, idempotencyKey}` or `{ok:false, missingFields}`.
99
+ Halt-flavor events additionally require `whatHalted`.
100
+
101
+ - **`RoundCompleteDeliveryHelper`** — retry + idempotency wrapper.
102
+ 3-attempt exponential backoff (1s, 2s, 4s by default). Records
103
+ the `(projectId, roundIndex, eventKind, projectVersion)` key in
104
+ `.instar/local/round-complete-sent.json` after the first successful
105
+ send so tone-gate retries don't produce duplicates. On permanent
106
+ failure, fires the caller's `onPermanentFail` callback (the run
107
+ loop will wire it to attention-queue + audit-log + awaitingUser
108
+ population when it ships).
109
+
110
+ Nothing wires these up yet — the autonomous run loop is the consumer.
111
+ PR 6 ships the primitives so the run loop in a follow-up PR doesn't
112
+ fork either of them.
113
+
114
+ ### Project-scope Phase 1b PR 5 — dashboard Projects tab + Initiatives filter
115
+
116
+ Fifth PR of project-scope Phase 1b. Ships the dashboard view of the
117
+ project surface and the small server-side filter the dashboard depends
118
+ on:
119
+
120
+ - **Projects tab** in the dashboard. Read-only card per active project
121
+ with title + id + version + drift status badge + per-round progress
122
+ bar + pipelineStage histogram. Halt + Ack buttons route through the
123
+ PR 3 endpoints; 409 responses silently re-render (no error toast).
124
+ 15-second poll while the tab is active; cleared on deactivation.
125
+ XSS-safe — every rendered string goes through `textContent`, no
126
+ `innerHTML`.
127
+ - **Initiatives tab filter** — by default the tab now hides
128
+ project-kind records (shown in their own Projects tab) and child
129
+ initiatives of any project (shown inside the parent's card). A
130
+ "Show projects + children" checkbox bypasses the filter for users
131
+ who want the full list. The filter is server-side: the records
132
+ never cross the wire when excluded.
133
+ - **`GET /initiatives` filter params** — accepts `excludeKind=<kind>`
134
+ (drops records of that kind) and `excludeParented=true` (drops
135
+ records that are children of any project). Additive — existing
136
+ callers see no behavior change.
137
+
138
+ The compaction-recovery hook's active-projects digest was already in
139
+ the template (it shipped alongside session-start.sh in PR 1's Phase
140
+ 1.9 work), so no template change is needed here.
141
+
142
+ ### Project-scope Phase 1b PR 4 — auto-advance poller + multi-machine claim-ownership
143
+
144
+ Fourth PR of project-scope Phase 1b. Ships three pieces:
145
+
146
+ - **`MachineHeartbeat`** — per-machine liveness signal at
147
+ `.instar/machine-health/<machineId>.json` (git-synced). Written
148
+ every 30 minutes; consulted by the claim-ownership flow for the
149
+ 48-hour staleness check. Machine ids with weird characters are
150
+ URL-encoded into the file name so a stray slash cannot escape
151
+ `.instar/machine-health/`.
152
+
153
+ - **`ProjectAutoAdvancePoller`** — periodic scan (1-minute tick) for
154
+ project rounds whose `autoAdvanceAt` has elapsed. Server-side
155
+ filter: `kind:'project'`, `status:'active'`, owner machine matches
156
+ current, `unacknowledgedAdvanceCount < 2`. On fire, calls
157
+ `ProjectRoundRunner.preflight`; structural rejects (first-launch
158
+ ack missing, cap hit, project inactive, etc.) clear `autoAdvanceAt`
159
+ to prevent re-firing on every tick.
160
+
161
+ - **`POST /projects/:id/claim-ownership`** — OCC-protected
162
+ multi-machine ownership transfer. Refuses with 409 when the current
163
+ owner has a fresh heartbeat unless `{force:true}` is passed.
164
+ Idempotent on already-owns. Response carries `previousOwner` for
165
+ audit.
166
+
167
+ Plus a one-shot **post-restore reconciler** at server startup: any
168
+ project round flagged `in-progress` is downgraded to `pending`. The
169
+ previous owner may have crashed or migrated; no TaskFlow yet exists
170
+ to verify whether a child is actually live.
171
+
172
+ ### Project-scope Phase 1b PR 3 — round runner + halt/advance/ack endpoints
173
+
174
+ Third PR of project-scope Phase 1b. Ships the single-chokepoint
175
+ runner that the spec names in § Phase 1.5, plus the four mutating
176
+ HTTP routes that thread through it:
177
+
178
+ - `POST /projects/:id/advance` — single-item stage transition driven
179
+ by the existing `StageTransitionValidator`. Requires `If-Match`
180
+ (OCC). Body: `{itemId, targetStage, artifact: {specPath?, prNumber?,
181
+ taskFlowRecordId?, skippedReason?, skippedBy?, unskippedAt?}}`.
182
+ Returns 409 with `{error, code, reason}` on validator reject.
183
+ - `POST /projects/:id/halt` — emergency stop. Writes `haltedAt` +
184
+ `haltReason` to the active round, releases the round-runner lock
185
+ if the calling machine holds it. Idempotent.
186
+ - `POST /projects/:id/ack` — records user acknowledgment for a round.
187
+ Populates `firstLaunchAckAt` if absent, advances
188
+ `lastAckedRoundIndex`, resets `unacknowledgedAdvanceCount`.
189
+ Idempotent on `forRoundIndex`.
190
+ - `POST /projects/:id/accept-partial` — closes a partially-complete
191
+ round. Non-merged, non-skipped child items transition to
192
+ `skipped` (requires `reason` + `skippedBy` per the validator);
193
+ round status → `complete-with-skips`; counts as ack for the
194
+ current `roundIndex`.
195
+
196
+ Behind the routes: `ProjectRoundRunner` runs the 9 deterministic
197
+ preflight checks from § Phase 1.5 (lock free, round shape valid,
198
+ items resolve, first-launch ack on round 0, unacked-advances cap,
199
+ ack-gap cap, owner machine matches, target repo is a git repo, no
200
+ pending reconciliation conflicts). Drift check (step 10) is
201
+ intentionally deferred until the drift-check HTTP endpoint and its
202
+ cache + ledger wiring ship in a follow-up PR — the drift verdict
203
+ cache and cost ledger from v0.28.94 are on disk waiting for that
204
+ consumer.
205
+
206
+ The autonomous-delegating `run()` loop (lazy worktrees, dynamic
207
+ stop-condition revalidation, SIGTERM/SIGKILL of process groups,
208
+ partial-complete detection) is also deferred to the next PR; this
209
+ PR ships the state-management verbs without the orchestration loop
210
+ so the routes can be exercised end-to-end against real validators
211
+ today.
212
+
213
+ Lock primitive lives at `.instar/local/round-runner.lock` —
214
+ machine-local (not git-synced) per spec. `O_CREAT|O_EXCL` rename +
215
+ stale-PID sweep on every acquire, so a crashed runner doesn't
216
+ permanently block subsequent acquires.
217
+
218
+ ## What to Tell Your User
219
+
220
+ - **The autonomous round loop is wired up end-to-end**: when a round
221
+ starts, I now spawn the actual autonomous work in its own process
222
+ group, watch every minute for changes to which items the round is
223
+ working on, and clean up after myself (process group + worktrees)
224
+ when I am done or you halt me. If you manually skip an item or
225
+ re-order the round mid-run, I gracefully stop the work I am doing
226
+ and relaunch with the new plan — no orphaned compilers, no
227
+ orphaned worktrees.
228
+
229
+ - **Round-complete digests are duplicate-safe**: when I finish a
230
+ round, the message I send you is built from a template that refuses
231
+ to send if any required field is missing, and the delivery layer
232
+ remembers what it already sent so a tone-gate retry can't spam you
233
+ with the same digest twice. If delivery fails permanently, I will
234
+ surface it through your attention queue rather than retry forever
235
+ in silence.
236
+
237
+ - **Projects have a dashboard tab now**: open the dashboard and you'll
238
+ see every active project I'm tracking with its round-by-round
239
+ progress, which items have merged vs which are still in flight, and
240
+ any drift warnings I've recorded. Halt and ack buttons are right
241
+ there if you want to stop me or tell me you've seen the digest. The
242
+ Initiatives tab no longer mixes project plumbing in by default —
243
+ flip the "Show projects + children" toggle if you want to see
244
+ everything together.
245
+
246
+ - **You can now drive a project round through the HTTP layer**: I can
247
+ advance a single item one stage with a real artifact check, halt the
248
+ active round on demand, record acknowledgment when I've shown you a
249
+ digest, or close out a round that landed only some of its items —
250
+ all from the dashboard or directly through the API. The actual
251
+ autonomous round loop that walks through items one by one is the
252
+ next piece I'm building.
253
+
254
+ - **I can recover ownership when a peer machine goes offline**: if my
255
+ other machine has been silent for more than 48 hours, I can claim
256
+ back ownership of a project we'd been working on together. If my
257
+ peer is still online, I will refuse the claim unless you tell me to
258
+ force it.
259
+
260
+ - **Time-based auto-advance is wired up**: when a round completes
261
+ cleanly, I will schedule the next round automatically. If you have
262
+ asked me to pause and have not acked, I will not auto-advance again
263
+ until you do — the two-rounds-ahead-without-ack brake stops me from
264
+ running off on my own.
265
+
266
+ ## Summary of New Capabilities
267
+
268
+ - `ProjectRoundRunner` class — single chokepoint for round-start.
269
+ `preflight(projectId, roundIndex)` runs deterministic checks and
270
+ returns a structured `PreflightResult`. `halt`, `recordAck`,
271
+ `acceptPartial` are idempotent state-management verbs that mutate
272
+ through `InitiativeTracker.update()` with OCC. Static
273
+ `validateChildFrontmatter` for callers that need to assert
274
+ `review-convergence: true` AND `approved: true` outside the runner.
275
+ - `ProjectRoundLock` class — machine-local mutex at
276
+ `.instar/local/round-runner.lock`. Atomic acquire via rename,
277
+ stale-PID sweep on every call. Exposes `acquire`, `release`, `read`.
278
+ - New HTTP routes: `POST /projects/:id/advance`,
279
+ `POST /projects/:id/halt`, `POST /projects/:id/ack`,
280
+ `POST /projects/:id/accept-partial`. All require Bearer auth;
281
+ `/advance` requires `If-Match`.
282
+ - `RouteContext.projectRoundRunner` — wired from
283
+ `AgentServer({ projectRoundRunner })` so other future routes (drift
284
+ check, run-round, claim-ownership) can route through the same
285
+ runner instance.
286
+ - `MachineHeartbeat` class — `start()` / `stop()` / `writeOnce()` /
287
+ `read(machineId)` / `isStale(machineId)` / `listAll()`. File-backed,
288
+ git-syncable, defense-in-depth on malformed reads.
289
+ - `ProjectAutoAdvancePoller` class — `tick()` returns a structured
290
+ `{scanned, fired, rejected, cleared}` report. Caller-driven cadence;
291
+ server wires a 60-second interval.
292
+ - `POST /projects/:id/claim-ownership` — body `{force?: boolean}`;
293
+ header `If-Match` (OCC). Returns 200 with
294
+ `{ownerMachineId, previousOwner, version}` on success; 409 on fresh
295
+ peer heartbeat or version mismatch.
296
+ - `RouteContext.machineHeartbeat` — bundled `{api, config}` so the
297
+ claim-ownership route can compare against the local machine id
298
+ without an extra top-level ctx field.
299
+ - Server startup runs a one-shot reconciler that downgrades any
300
+ `in-progress` round to `pending`. Best-effort; OCC races are
301
+ silently retried on subsequent reconciler passes (or via the
302
+ auto-advance poller's filter).
303
+ - Dashboard `Projects` tab — read-only project cards with progress
304
+ bar + drift badge + halt/ack buttons. 15-second poll while active.
305
+ - Dashboard `Initiatives` tab now defaults to hiding project-kind +
306
+ child records. "Show projects + children" checkbox bypasses.
307
+ - `GET /initiatives` accepts new optional query params: `excludeKind`
308
+ (drops records where `kind ?? 'task'` matches) and
309
+ `excludeParented=true` (drops records with a `parentProjectId`).
310
+ Additive; existing clients see no behavior change.
311
+ - `formatRoundCompleteMessage(input)` — pure template function with
312
+ required-field PRESENCE gate (empty strings accepted, undefined
313
+ rejected). Halt-flavor events additionally require `whatHalted`.
314
+ Returns `{message, idempotencyKey}` on success.
315
+ - `ProjectRoundWorktrees` class — lazy worktree allocator under
316
+ `<targetRepoPath>/.worktrees/<projectId>/<roundIndex>/<itemId>`.
317
+ Static `pathFor`/`allocate`/`prune`/`remove`/`ensureExcludeEntry`.
318
+ - `runRound(input, deps)` function — the run loop. Acquires lock,
319
+ spawns autonomous child detached, polls every 60s, SIGTERMs on
320
+ dynamic-stop changes or halt, verifies per-item artifacts on
321
+ natural exit, sets `round.status`. Returns
322
+ `{outcome, mergedItemIds, unmergedItemIds, relaunchCount,
323
+ resumeAttempts, reason}`.
324
+ - `verifyMergedItemsViaGit(targetRepoPath, childIds, tracker)` —
325
+ exported production verifier; checks `git merge-base --is-ancestor
326
+ <child.mergeCommitOid> origin/main` per item.
327
+ - `RoundCompleteDeliveryHelper` class — retry + idempotency wrapper.
328
+ 3-attempt exponential backoff (configurable); records the
329
+ idempotency key in `.instar/local/round-complete-sent.json` so
330
+ duplicate sends are suppressed. `onPermanentFail` callback fires
331
+ once when all retries are exhausted.
332
+
333
+ ## Evidence
334
+
335
+ Not reproducible in dev — this release is a feature addition, not a regression fix.
336
+ The words "crashed", "broken", and "resolve" in What Changed describe design properties
337
+ of new primitives, not prior bugs that were observed and repaired:
338
+
339
+ - "crashed runner" (lock primitive): stale-PID sweep so an interrupted runner doesn't
340
+ permanently hold the lock — a designed invariant of the new `ProjectRoundLock`, not
341
+ a fix for an observed crash.
342
+ - "broken" (reconciler): `ciCheckedAt`-debounced ancestor check transitions a child to
343
+ 'regressed' when its commit is not reachable from origin/main — a designed state
344
+ machine transition, not a regression repair.
345
+ - "resolve-conflict" (GET /next action verbs): one of seven structured action strings
346
+ the endpoint returns; the word "resolve" is part of the verb name, not a claim that
347
+ a conflict was resolved as a bug fix.
@@ -0,0 +1,146 @@
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 1.7 — `/project` skill surface (full command set)
12
+
13
+ Final code piece of project-scope Phase 1: the user-invocable
14
+ `/project` skill, the conversational interface spec § Phase 1.7
15
+ specifies as twelve canonical commands. Phase 1a shipped read-only
16
+ commands (`create`, `status`, plus a placeholder `next`); this PR
17
+ fleshes out the full mutating surface and adds the three small HTTP
18
+ routes the spec named but hadn't materialized yet.
19
+
20
+ - **`.claude/skills/instar-project/SKILL.md` rewritten end-to-end.**
21
+ Documents every spec § 1.7 command with its backing endpoint, body
22
+ schema, expected response codes, and a one-line conversational
23
+ rendering rule. The Phase 1a "next is a 501 placeholder" language
24
+ is gone — the structured `{action, params, skillCommand}` payload
25
+ shipped in the connect-the-dots PR is now the documented contract.
26
+ - **`POST /projects/:id/run-round`** — manual round-start trigger.
27
+ Calls `ProjectRoundRunner.preflight()` for the deterministic gate
28
+ checks and, on accept, sets `autoAdvanceAt = now` so the existing
29
+ auto-advance poller fires the executor on its next tick (≤60s).
30
+ Does NOT spawn the autonomous child directly — keeps one fire path
31
+ through one lock. Body: `{roundIndex}`. Returns 200 with
32
+ `{id, roundIndex, scheduledAt, version}` on accept, 409 with the
33
+ preflight verdict's `code` + `reason` on reject (surface those
34
+ verbatim — they're written for users).
35
+ - **`POST /projects/:id/resume`** — resume a halted or failed round.
36
+ Clears `haltedAt`/`haltReason`, schedules the round for the poller,
37
+ re-actives `project.status` from `'halted'`/`'abandoned'` back to
38
+ `'active'`. For rounds at `status: 'failed'` with
39
+ `resumeAttempts >= 3` (the spec's three-attempt cap), `{force: true}`
40
+ is required and the counter resets to zero on accept. Returns 409
41
+ when the round is neither halted nor failed.
42
+ - **`POST /projects/:id/abandon`** — archive a halted project.
43
+ Sets `project.status = 'abandoned'` and clears any future
44
+ `autoAdvanceAt` on remaining rounds so the poller stops considering
45
+ them. Children's `pipelineStage` is left untouched (per spec).
46
+ Idempotent. Refuses (409) if a round is currently `in-progress` —
47
+ halt first.
48
+ - **`skillCommandForAction()` realigned to canonical command names.**
49
+ The connect-the-dots PR added a mapping that returned
50
+ `/project approve` and `/project drift-check` for the
51
+ `await-user-approval` and `run-drift-check` actions. Spec § 1.7
52
+ uses `/project ack` and `/project drift` as the canonical command
53
+ names; the mapping now matches the spec so dashboard hints and
54
+ skill invocations stay in sync.
55
+
56
+ Tests added:
57
+
58
+ - `tests/unit/instar-project-skill.test.ts` — structural coverage of
59
+ SKILL.md (28 assertions). Pins the frontmatter contract, asserts
60
+ every spec § 1.7 command has a section header, and asserts each
61
+ documented endpoint actually exists in `src/server/routes.ts`.
62
+ Catches skill drift — if a route is renamed or removed without
63
+ updating the skill, the test fails on the next push.
64
+ - `tests/integration/projects-api.test.ts` — 10 new cases (44 total)
65
+ for the three new endpoints: preflight reject (409), happy path
66
+ (200 + autoAdvanceAt landed), out-of-range round (404),
67
+ haltedAt-clear on resume (200), neither-halted-nor-failed reject
68
+ (409), failed-at-cap-without-force reject (409),
69
+ failed-at-cap-with-force happy path (counter zeroed), abandon
70
+ happy path (autoAdvanceAt cleared + status='abandoned'),
71
+ abandon-while-in-progress reject (409), abandon idempotent
72
+ (`alreadyAbandoned: true`).
73
+
74
+ ## What to Tell Your User
75
+
76
+ - **You can now drive any project end-to-end through /project.**
77
+ Twelve commands, one skill: register a project from a plan doc,
78
+ read its state, advance a child item to its next stage, run a
79
+ drift check, fire a round, halt the active round, record your
80
+ acknowledgment, resume from halted or failed, abandon a project
81
+ permanently, close out a partially-complete round, claim ownership
82
+ on another machine. The skill walks me through each one with the
83
+ right API call and the right error handling — I don't have to
84
+ improvise the curl invocations.
85
+
86
+ - **The "what should I do next" answer is now actionable.**
87
+ Asking /project next returns a structured action — "approval
88
+ pending," "drift check needed," "round ready to start" — plus the
89
+ exact follow-up command. I'll surface the suggested next step to
90
+ you in plain English rather than dumping the JSON.
91
+
92
+ - **Halted projects can come back.** If a round halted because of a
93
+ spec drift, a halted child, or a runtime failure, /project resume
94
+ re-schedules it for the poller and re-actives the project. Rounds
95
+ that hit the three-attempt failure cap need an explicit force flag
96
+ before resuming, and the counter resets so the runner gets a fresh
97
+ budget.
98
+
99
+ - **Permanently shelving a project is a single command.**
100
+ /project abandon clears all pending auto-advance scheduling and
101
+ marks the project as abandoned. Children stay where they were —
102
+ you don't lose any pipeline progress; you just stop the auto-loop.
103
+
104
+ ## Summary of New Capabilities
105
+
106
+ - `.claude/skills/instar-project/SKILL.md` — twelve-command skill body
107
+ with frontmatter `user_invocable: true`. Each command section
108
+ documents its backing endpoint, body schema, response shape, and
109
+ expected error codes. Final "Conversational rendering" section pins
110
+ the don't-paste-curl rule.
111
+ - `POST /projects/:id/run-round` — body `{roundIndex: number}`.
112
+ Returns 200 `{id, roundIndex, scheduledAt, version}` on accept,
113
+ 409 `{error, code, reason}` on preflight reject, 404 on out-of-range
114
+ round, 503 if `ProjectRoundRunner` is not wired. OCC-protected via
115
+ `tracker.update(... ifMatch: project.version)`.
116
+ - `POST /projects/:id/resume` — body `{roundIndex?: number,
117
+ force?: boolean}`. Returns 200 `{id, roundIndex, scheduledAt,
118
+ forced, version}` on accept, 409 when the round is neither halted
119
+ nor failed, 409 when failed-at-cap without `force`. Restores
120
+ `project.status` from `'halted'`/`'abandoned'` to `'active'` and
121
+ zeroes `resumeAttempts` on forced accept.
122
+ - `POST /projects/:id/abandon` — no body required. Returns 200
123
+ `{id, status: 'abandoned', version}` on accept, 200
124
+ `{... alreadyAbandoned: true}` on idempotent repeat, 409 with
125
+ `activeRound` set when a round is in-progress. Clears
126
+ `autoAdvanceAt` on every remaining round.
127
+ - `skillCommandForAction()` now emits `/project ack` for both
128
+ `await-user-approval` and `ack-required` action verbs, and
129
+ `/project drift` for `run-drift-check`. Aligns the dashboard hint
130
+ strings with the spec § 1.7 canonical command set.
131
+
132
+ ## Evidence
133
+
134
+ Not reproducible in dev — this release is a feature addition, not a
135
+ regression fix. The words "halted", "failed", "regression repair",
136
+ and "skill drift" in What Changed describe design properties of the
137
+ state machine and the structural test the PR introduces:
138
+
139
+ - "halted" / "failed" (resume route): these are valid round states
140
+ in the existing `RoundStatus` union; the new endpoint's job is to
141
+ transition out of them. Not a fix for an observed halt incident.
142
+ - "skill drift" (structural test): the new
143
+ `instar-project-skill.test.ts` asserts every endpoint documented
144
+ in SKILL.md exists in `routes.ts`. The "drift" is the test's
145
+ failure mode if a future PR renames a route without updating the
146
+ skill — a preventive guard, not the patching of a prior incident.
@@ -0,0 +1,175 @@
1
+ # Side-Effects Review — project-scope Phase 1.7 skill surface
2
+
3
+ **Scope.** Closes the spec § Phase 1.7 user-facing surface. Rewrites
4
+ `.claude/skills/instar-project/SKILL.md` to cover all twelve canonical
5
+ commands, adds the three small HTTP routes the spec named but hadn't
6
+ materialized (`run-round`, `resume`, `abandon`), and realigns
7
+ `skillCommandForAction()` to the canonical command names.
8
+
9
+ ## What's IN this PR
10
+
11
+ 1. **`.claude/skills/instar-project/SKILL.md`** — full rewrite. Phase 1a
12
+ placeholder language for `/project next` is removed (the connect-the-dots
13
+ PR shipped the structured payload; the skill now documents that contract).
14
+ Every command from spec § 1.7 has a section header with its backing
15
+ endpoint, body schema, response codes, and a one-line rendering rule.
16
+ Final section pins the conversational-tone rule ("never paste a curl
17
+ command in a user-facing reply") that Echo's CLAUDE.md enforces.
18
+ 2. **`POST /projects/:id/run-round`** — manual round trigger. On
19
+ `preflight.ok`, writes `autoAdvanceAt = now` to the named round and
20
+ returns 200 + `scheduledAt`. The auto-advance poller (wired in the
21
+ connect-the-dots PR) picks it up on its next tick (≤60s) and dispatches
22
+ `ProjectRoundExecution.runRound()`. No child-process spawn from the
23
+ request handler — keeps one fire path through one lock.
24
+ 3. **`POST /projects/:id/resume`** — clears `haltedAt`/`haltReason`,
25
+ schedules the round via `autoAdvanceAt = now`, transitions
26
+ `project.status` from `'halted'`/`'abandoned'` back to `'active'`.
27
+ For `status: 'failed'` rounds at the 3-attempt cap, `{force: true}`
28
+ is required AND the `resumeAttempts` counter resets to zero on
29
+ accept. Returns 409 when the round is neither halted nor failed.
30
+ 4. **`POST /projects/:id/abandon`** — sets `project.status = 'abandoned'`,
31
+ strips `autoAdvanceAt` from every remaining round so the poller
32
+ stops considering them, leaves each child's `pipelineStage`
33
+ untouched. Idempotent (`alreadyAbandoned: true` on repeat).
34
+ Refuses 409 when any round is `in-progress` — halt first.
35
+ 5. **`skillCommandForAction()` realignment.** The connect-the-dots PR
36
+ returned `/project approve` and `/project drift-check` for the
37
+ `await-user-approval` and `run-drift-check` action verbs. Spec § 1.7
38
+ uses `/project ack` and `/project drift`; the mapping is updated so
39
+ the dashboard hints + the `/projects/:id/next` payload + the skill
40
+ stay in sync.
41
+ 6. **Tests.** `tests/unit/instar-project-skill.test.ts` (28 assertions)
42
+ pins the SKILL.md contract — frontmatter, every spec § 1.7 command
43
+ has a header, every documented endpoint exists in `routes.ts`.
44
+ `tests/integration/projects-api.test.ts` adds 10 cases for the new
45
+ routes covering happy paths + every documented error case.
46
+
47
+ ## What's explicitly DEFERRED
48
+
49
+ - **`ProjectRoundRunner.resume` / `.abandon` methods.** The new
50
+ endpoints mutate state via `tracker.update()` directly rather than
51
+ routing through a runner method. The runner's value-add is the
52
+ `preflight()` gate; resume/abandon don't have preflight semantics
53
+ (they're transitions OUT of stuck states, not transitions INTO an
54
+ autonomous run). If a future need surfaces — e.g., resume needs to
55
+ hold the round-runner lock briefly, or abandon needs to release a
56
+ worktree — those can land as runner methods then. Today, the
57
+ shorter path is cleaner.
58
+ - **Resume worktree cleanup.** Spec § 1.5 step "Halt switch" says
59
+ "Worktrees retained for inspection (cleanup deferred to user
60
+ `/project resume` or `/project abandon`)." The new resume/abandon
61
+ routes do NOT prune worktrees — the autonomous child the poller
62
+ fires next will recreate them lazily. Worktree GC is a separate
63
+ follow-up (registered as a child of the spec's deferral list).
64
+ - **No telemetry on action verbs.** The dashboard hint
65
+ (`skillCommandForAction()`) is read-only suggestion; we don't yet
66
+ count which verbs fire most frequently. A simple counter on the
67
+ `/projects/:id/next` handler would surface "what's the most common
68
+ unblocking step?" — interesting telemetry, but not Phase 1 scope.
69
+ - **Skill body translation hooks.** The SKILL.md is a single file; we
70
+ don't (yet) split per-command bodies into separate files the agent
71
+ can lazy-load. At ~10KB the file is well within the context budget
72
+ for a `/project` invocation. If the surface keeps growing past
73
+ Phase 1, splitting per-command is the natural next move.
74
+
75
+ ## Side-effects review
76
+
77
+ **Over-block.** All three new routes are pure tracker.update() calls
78
+ with OCC. No child-process spawn, no git shell-out, no LLM call.
79
+ P99 latency in test runs ~20ms (single tracker write + JSON encode).
80
+ The skill body grew from ~3.5KB to ~10KB — adds one file read per
81
+ `/project` invocation; well below the context-window pressure
82
+ threshold.
83
+
84
+ **Under-block.** `resume` does not re-run drift before scheduling.
85
+ The poller's preflight (which fires after the route writes
86
+ `autoAdvanceAt`) re-runs drift if the cached verdict is stale (>24h)
87
+ per spec § Phase 1.4. So drift is enforced at fire time, not at
88
+ resume time — same level as the original `/project run-round`. This
89
+ is intentional: resume is a state mutation, not a fire trigger.
90
+
91
+ **Level-of-abstraction fit.** Each new route sits at the same layer
92
+ as the surrounding routes:
93
+ - `run-round` belongs alongside `halt` and `ack` (all three are
94
+ user-initiated state changes that thread through the runner).
95
+ - `resume` and `abandon` belong alongside `accept-partial` (all
96
+ three close out a specific round-end condition).
97
+
98
+ The skill is the highest layer — it's the conversational shell over
99
+ the HTTP API. SKILL.md is markdown the agent reads on `/project`
100
+ invocation, not executable code; the body's instructions ARE the
101
+ implementation.
102
+
103
+ **Signal vs authority.** The new routes are authority paths — they
104
+ mutate state. None of them consult an LLM signal. `run-round` does
105
+ delegate to `ProjectRoundRunner.preflight()` for the deterministic
106
+ gate checks; that's the spec's single-chokepoint pattern, not a
107
+ signal/authority mix.
108
+
109
+ **Interactions.**
110
+ - `run-round` + `auto-advance poller`: the route writes `autoAdvanceAt`,
111
+ the poller reads it. The poller's `inFlight: Set<projectId>` guard
112
+ (shipped in connect-the-dots) ensures a manual run-round followed
113
+ by a poller tick doesn't double-fire.
114
+ - `resume` + `halt`: idempotent in either direction. `halt` writes
115
+ `haltedAt`; `resume` strips it. Repeat halts on an already-halted
116
+ round are no-ops; repeat resumes on an already-resumed round
117
+ return 409 ("round is not halted or failed").
118
+ - `abandon` + `claim-ownership`: abandon sets `project.status =
119
+ 'abandoned'`; the auto-advance poller's owner filter already
120
+ excludes abandoned projects. A peer claiming ownership of an
121
+ abandoned project would just re-claim — the poller still skips
122
+ abandoned-status projects regardless of owner.
123
+ - `skillCommandForAction()` realignment: existing test
124
+ `expect(typeof res.body.skillCommand).toBe('string')` doesn't
125
+ assert exact strings, so the rename is backwards-compatible at
126
+ the test level. Dashboard consumers render the string verbatim —
127
+ they get `/project ack` instead of `/project approve`, which is
128
+ what spec § 1.7 documented.
129
+
130
+ **Rollback cost.** Three options:
131
+ 1. **Skill-only rollback** — restore the prior SKILL.md from
132
+ `git show HEAD~1:.claude/skills/instar-project/SKILL.md`. Agent
133
+ reverts to Phase 1a read-only behavior; new HTTP routes still
134
+ exist but are not surfaced. Zero state risk.
135
+ 2. **Routes-only rollback** — `git revert` the three new route
136
+ handlers. Skill still references them; agent surfaces 404 from
137
+ `/project run-round`, `/project resume`, `/project abandon`.
138
+ Surface-but-not-functional, but no data corruption — state
139
+ mutations all go through `tracker.update()` with OCC, which is
140
+ already proven safe.
141
+ 3. **Full revert** — drop the whole commit. No on-disk state from
142
+ this PR persists (the abandon/halt/resume state fields all
143
+ pre-exist in the `Initiative` schema from Phase 1a).
144
+
145
+ All three are clean. The PR introduces no new persisted state, no
146
+ new file paths, no new background work — it adds surface area over
147
+ the existing state machine.
148
+
149
+ ## Files touched
150
+
151
+ - `.claude/skills/instar-project/SKILL.md` (rewritten)
152
+ - `src/server/routes.ts` (three new route handlers + one mapping fix)
153
+ - `tests/integration/projects-api.test.ts` (10 new cases)
154
+ - `tests/unit/instar-project-skill.test.ts` (new file, 28 assertions)
155
+ - `upgrades/NEXT.md` (new release notes)
156
+
157
+ ## Spec references
158
+
159
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.7 — canonical
160
+ command list.
161
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.5 — runner preflight
162
+ contract; halt/resume worktree-retention rule.
163
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.3 — HTTP layer
164
+ doesn't enforce gates the runner already enforces. `run-round`
165
+ delegates to `preflight()` cleanly.
166
+
167
+ ## Coverage
168
+
169
+ - Unit: structural coverage of SKILL.md + endpoint existence —
170
+ `tests/unit/instar-project-skill.test.ts`.
171
+ - Integration: happy + error paths for all three new routes against
172
+ the real `AgentServer` + `InitiativeTracker` + `ProjectRoundRunner`
173
+ — `tests/integration/projects-api.test.ts`.
174
+ - Lint: clean.
175
+ - Type-check: clean (new `RoundStatus` import).