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.
- package/dashboard/index.html +249 -2
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +90 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/MachineHeartbeat.d.ts +94 -0
- package/dist/core/MachineHeartbeat.d.ts.map +1 -0
- package/dist/core/MachineHeartbeat.js +155 -0
- package/dist/core/MachineHeartbeat.js.map +1 -0
- package/dist/core/ProjectAutoAdvancePoller.d.ts +107 -0
- package/dist/core/ProjectAutoAdvancePoller.d.ts.map +1 -0
- package/dist/core/ProjectAutoAdvancePoller.js +176 -0
- package/dist/core/ProjectAutoAdvancePoller.js.map +1 -0
- package/dist/core/ProjectRoundCompleteMessage.d.ts +158 -0
- package/dist/core/ProjectRoundCompleteMessage.d.ts.map +1 -0
- package/dist/core/ProjectRoundCompleteMessage.js +253 -0
- package/dist/core/ProjectRoundCompleteMessage.js.map +1 -0
- package/dist/core/ProjectRoundExecution.d.ts +111 -0
- package/dist/core/ProjectRoundExecution.d.ts.map +1 -0
- package/dist/core/ProjectRoundExecution.js +359 -0
- package/dist/core/ProjectRoundExecution.js.map +1 -0
- package/dist/core/ProjectRoundLock.d.ts +67 -0
- package/dist/core/ProjectRoundLock.d.ts.map +1 -0
- package/dist/core/ProjectRoundLock.js +161 -0
- package/dist/core/ProjectRoundLock.js.map +1 -0
- package/dist/core/ProjectRoundRunner.d.ts +146 -0
- package/dist/core/ProjectRoundRunner.d.ts.map +1 -0
- package/dist/core/ProjectRoundRunner.js +371 -0
- package/dist/core/ProjectRoundRunner.js.map +1 -0
- package/dist/core/ProjectRoundWorktrees.d.ts +57 -0
- package/dist/core/ProjectRoundWorktrees.d.ts.map +1 -0
- package/dist/core/ProjectRoundWorktrees.js +111 -0
- package/dist/core/ProjectRoundWorktrees.js.map +1 -0
- package/dist/server/AgentServer.d.ts +12 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +19 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +590 -7
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/0.28.96.md +347 -0
- package/upgrades/side-effects/project-scope-phase1b-connect-the-dots.md +184 -0
- package/upgrades/side-effects/project-scope-phase1b-pr3.md +228 -0
- package/upgrades/side-effects/project-scope-phase1b-pr4.md +198 -0
- package/upgrades/side-effects/project-scope-phase1b-pr5.md +150 -0
- package/upgrades/side-effects/project-scope-phase1b-pr6.md +136 -0
- 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).
|