instar 0.28.95 → 0.28.96

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 (49) 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 +590 -7
  40. package/dist/server/routes.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/data/builtin-manifest.json +47 -47
  43. package/upgrades/0.28.96.md +347 -0
  44. package/upgrades/side-effects/project-scope-phase1b-connect-the-dots.md +184 -0
  45. package/upgrades/side-effects/project-scope-phase1b-pr3.md +228 -0
  46. package/upgrades/side-effects/project-scope-phase1b-pr4.md +198 -0
  47. package/upgrades/side-effects/project-scope-phase1b-pr5.md +150 -0
  48. package/upgrades/side-effects/project-scope-phase1b-pr6.md +136 -0
  49. 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,184 @@
1
+ # Side-Effects Review — project-scope Phase 1b connect-the-dots wiring
2
+
3
+ **Scope.** Wire the four primitives shipped during the Phase 1b
4
+ autonomous burst to their consumers, closing the deferrals named in
5
+ the side-effects reviews of PRs #164 (round runner), #166 (dashboard),
6
+ #167 (round-complete message), and #168 (run loop). Each touch is
7
+ small; they ride together because they're all "primitive-without-consumer
8
+ gets its consumer."
9
+
10
+ ## What's IN this PR
11
+
12
+ 1. **`ProjectAutoAdvancePoller.executor`** — optional async hook on the
13
+ poller. When configured, a successful preflight + bookkeeping move
14
+ triggers a fire-and-forget `runRound()` call. The `inFlight: Set`
15
+ guard prevents a slow run from being relaunched on the next 60s tick.
16
+ Errors land in `result.executorErrors[]`; they never throw out of
17
+ `tick()`. Server startup wires the executor against
18
+ `ProjectRoundExecution.runRound`.
19
+ 2. **`GET /projects/:id/next`** — per spec § Phase 1.5 line 268.
20
+ Returns `{ action, params, skillCommand }` for the first
21
+ non-complete round. Action verbs (a non-exhaustive contract):
22
+ `'await-user-approval'` (no `firstLaunchAckAt`), `'ack-required'`
23
+ (unacked counter at cap), `'resolve-conflict'` (awaiting
24
+ reconciliation), `'accept-partial'` (round partially-complete),
25
+ `'run-spec-converge'` (item at spec-drafted), `'run-drift-check'`
26
+ (item approved, no fresh verdict), `'start-round'` (default). Each
27
+ verb maps to a suggested `/project ...` skill invocation via
28
+ `skillCommandForAction()`. Returns 204 when all rounds complete,
29
+ 404 on non-project initiative. Read-only, no OCC required.
30
+ 3. **`GET /projects/:id` lazy merged-state reconciler** — per spec
31
+ § Phase 1.4 lines 256-258. For 'building' children with
32
+ `mergeCommitOid` set:
33
+ - **6h debounce** via `ciCheckedAt`: skip any child checked in the
34
+ last 6 hours.
35
+ - **Selection order**: oldest `ciCheckedAt` first (treat missing as
36
+ epoch 0), ties broken by `roundIndex` ASC, then `itemId` ASC. No
37
+ child starves.
38
+ - **Cap**: at most 3 child-revalidations per GET.
39
+ - **Both directions**: verified ancestor of origin/main → bump to
40
+ `'merged'`. NOT an ancestor → transition to `'regressed'` AND
41
+ clear future `autoAdvanceAt` on subsequent rounds so the chain
42
+ doesn't auto-fire while a child is broken.
43
+ - **ciCheckedAt is always written** on a revalidation attempt
44
+ (even on git-shell-out failure) so the debounce backs off
45
+ instead of hot-looping.
46
+ - Skipped when `?reconcile=false`, or when the project has no
47
+ `targetRepoPath`.
48
+ 4. **`POST /projects/:id/drift-check`** — wraps
49
+ `ProjectDriftChecker.run()`. Body: `{ roundIndex, specPath,
50
+ referencedFiles[], timeoutMs?, modelId? }`. **Mutex-guarded per
51
+ project (spec § Phase 1.5 line 279)**: a concurrent call against
52
+ the same `projectId` returns 409 `{error: 'drift-check already in
53
+ flight for this project'}` — protects the drift-spend ledger from
54
+ double-spend and the LLM from double-billing. Returns 503 when no
55
+ `IntelligenceProvider` is configured (no checker wired), 400 on
56
+ validation, 200 with `{ verdict, projectId, roundIndex }` on
57
+ success. `AgentServer.routeContext` and the server-startup wiring
58
+ both gained a `projectDriftChecker` field, populated against the
59
+ shared `IntelligenceProvider`.
60
+
61
+ ## What's explicitly DEFERRED
62
+
63
+ - **Drift cache + spend ledger wiring.** `ProjectDriftChecker` accepts
64
+ optional `cache` (`ProjectDriftCheckerCache`) and `ledger`
65
+ (`DriftSpendLedger`). Today the production instantiation passes
66
+ neither — every call recomputes from scratch, no spend cap.
67
+ Follow-up: wire both in `src/commands/server.ts` once the dashboard
68
+ shows live drift verdicts and the cost telemetry actually needs the
69
+ cap. Tracked here, no new ticket.
70
+ - **Reconciler scope.** Only children at `pipelineStage = 'building'`
71
+ with `mergeCommitOid` set are revalidated. Future PRs may expand the
72
+ predicate (e.g., `'spec-converged'` items that have moved to
73
+ `'approved'` upstream). The 3-child cap is intentionally tight to
74
+ keep the GET fast (<200ms p99 in local benchmarks against a 20-child
75
+ fixture).
76
+ - **`/projects/:id/next` preflight integration.** This endpoint
77
+ deliberately does NOT run `ProjectRoundRunner.preflight`. Preflight
78
+ is a heavy check (lock, drift, owner, ack-gap) that belongs at fire
79
+ time, not on a dashboard peek. Callers needing to know "would this
80
+ round actually fire?" can call `POST /projects/:id/preflight` (a
81
+ future endpoint).
82
+ - **Concurrent executor launches.** A single `inFlight` Set is keyed by
83
+ `projectId`, so two ticks of the same project never produce two
84
+ spawn calls. The `ProjectRoundLock` inside `runRound()` is the
85
+ authoritative defense — `inFlight` is a fast path that avoids
86
+ spawning a child that would immediately bail on `LOCK_HELD`.
87
+
88
+ ## Side-effects review
89
+
90
+ **Over-block.** The lazy reconciler runs on every `GET /projects/:id`
91
+ unless `?reconcile=false`. Worst case: 3 git `merge-base` calls per
92
+ GET, each <50ms on a hot repo. No long-running git operations. Skips
93
+ entirely when `targetRepoPath` is missing or all children are in
94
+ non-`building` stages. The dashboard's projects-tab poll fires every
95
+ 15s and calls GET only when the tab is active.
96
+
97
+ **Under-block.** The drift-check route does NOT require OCC. Drift is
98
+ read-only; staleness of the project record doesn't compromise the
99
+ verdict (the verdict is keyed on file content hashes, not project
100
+ version). Adding OCC would force the dashboard to read-then-mutate
101
+ under If-Match for what is conceptually a query.
102
+
103
+ **Level-of-abstraction fit.** Each new wiring sits at the same layer
104
+ as its primitive:
105
+ - The executor hook is on the poller, matching the poller's
106
+ responsibility ("decide whether to fire").
107
+ - The reconciler lives in the GET handler because it's a presentation
108
+ concern — making sure the response reflects on-disk truth, not a
109
+ cached pipelineStage.
110
+ - `/projects/:id/next` is a peek, not a mutation — read-only by design.
111
+ - `/projects/:id/drift-check` posts because the checker spends LLM
112
+ tokens; POSTing matches the semantics of "this isn't safely
113
+ cacheable, retry-able, or idempotent at the HTTP layer."
114
+
115
+ **Signal vs authority.** The reconciler is signal-only — it bumps
116
+ `pipelineStage` but doesn't block, retry, or revalidate other state.
117
+ The drift-check route returns the verdict; the CALLER decides what
118
+ to do with it (the spec keeps drift verdicts as advisory for the
119
+ agent's planning, not blocking gates).
120
+
121
+ **Interactions.**
122
+ - Poller × runRound: `inFlight` Set + `ProjectRoundLock` produces a
123
+ belt-and-suspenders against duplicate launches across ticks.
124
+ - Reconciler × auto-update: if the reconciler bumps a child to
125
+ `'merged'`, that doesn't trigger the auto-advance poller directly
126
+ (the poller is `setInterval`-driven, not event-driven). The next
127
+ tick sees the new state.
128
+ - Drift-check × runRound: orthogonal — `runRound` doesn't call this
129
+ HTTP endpoint; the checker is invoked directly inside `runRound`
130
+ via dependency injection. The HTTP route is for the dashboard +
131
+ operator-driven probes.
132
+
133
+ **Rollback cost.** Each wiring is a thin addition:
134
+ - Remove the `executor` config from server.ts → poller goes back to
135
+ bookkeeping-only. No state loss.
136
+ - `GET /projects/:id/next` revert restores the 501 stub. No state.
137
+ - The reconciler is gated by `?reconcile=false`; clients can disable
138
+ it without a code change.
139
+ - The drift-check route is feature-flagged by `projectDriftChecker`
140
+ presence; removing the wiring in server.ts returns 503, no harm.
141
+
142
+ ## Files touched
143
+
144
+ - `src/core/ProjectAutoAdvancePoller.ts` — executor hook, `inFlight`
145
+ guard, new `executed[]` + `executorErrors[]` result fields.
146
+ - `src/server/routes.ts` — new `verifyMergedItemsViaGit` +
147
+ `ProjectDriftChecker` imports; `GET /projects/:id` made async with
148
+ reconciler logic; `GET /projects/:id/next` implemented;
149
+ `POST /projects/:id/drift-check` added; `projectDriftChecker` in
150
+ `RouteContext`.
151
+ - `src/server/AgentServer.ts` — `projectDriftChecker` in options +
152
+ RouteContext init.
153
+ - `src/commands/server.ts` — wire executor (calls `runRound`) and
154
+ instantiate `ProjectDriftChecker` with the shared
155
+ `IntelligenceProvider`.
156
+ - `tests/unit/ProjectAutoAdvancePoller.test.ts` — 3 new cases
157
+ exercising executor invocation, error capture, and the in-flight
158
+ guard.
159
+ - `tests/integration/projects-api.test.ts` — 3 new cases for
160
+ `/projects/:id/next` (200/204/404), 1 new case for
161
+ `/projects/:id/drift-check` 503, removed the old 501-placeholder
162
+ assertion.
163
+ - `upgrades/NEXT.md` — release notes entry stacked above PR #168.
164
+ - `upgrades/side-effects/project-scope-phase1b-connect-the-dots.md` —
165
+ this file.
166
+
167
+ ## Spec references
168
+
169
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.5 ("Run loop") — the
170
+ spec section the poller's executor wiring completes.
171
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.4 — drift checker
172
+ surface; HTTP route exposes it.
173
+
174
+ ## Coverage
175
+
176
+ 137 project-related tests pass locally:
177
+ - `tests/unit/ProjectAutoAdvancePoller.test.ts` (10 cases, +3 new)
178
+ - `tests/unit/ProjectRoundExecution.test.ts` (8 cases, no change)
179
+ - `tests/unit/ProjectRoundRunner.test.ts` (27 cases, no change)
180
+ - `tests/unit/ProjectRoundLock.test.ts` (9 cases, no change)
181
+ - `tests/unit/ProjectDriftChecker.test.ts` (existing coverage)
182
+ - `tests/integration/projects-api.test.ts` (33 cases, +3 new for
183
+ `/projects/:id/next`, +1 new for drift-check 503, -1 obsolete 501
184
+ placeholder).