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,359 @@
1
+ /**
2
+ * ProjectRoundExecution — the autonomous run loop for a project round.
3
+ *
4
+ * Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5 ("Run loop", steps 1-11).
5
+ *
6
+ * What this module is responsible for:
7
+ * 1. Acquiring the round-runner lock (delegated to ProjectRoundLock).
8
+ * 2. Spawning the autonomous child process in a detached process
9
+ * group, so SIGTERM/SIGKILL of the group never reaps the runner.
10
+ * 3. Polling the project record every `pollIntervalMs` to detect
11
+ * mid-round mutations to the round's itemIds (e.g., a user
12
+ * manually skips an item) — when detected, SIGTERM the group
13
+ * (5s grace, then SIGKILL) and relaunch with a recomputed stop
14
+ * condition.
15
+ * 4. On the autonomous child's NATURAL exit, verify per-item
16
+ * artifacts and set round.status = complete | partially-complete.
17
+ * 5. Cleanup: `git worktree prune` for the round namespace.
18
+ * 6. Release the lock.
19
+ *
20
+ * What this module is NOT responsible for:
21
+ * - The preflight gate. That's `ProjectRoundRunner.preflight`. The
22
+ * caller (the auto-advance poller, the /project run-round skill,
23
+ * a future HTTP endpoint) is expected to preflight before calling
24
+ * `runRound(input)`.
25
+ * - Drift checking. Same — preflight handles it.
26
+ * - Choosing the autonomous command. The `spawnCommand` is injected,
27
+ * defaulting to `claude` (which invokes the local /autonomous skill).
28
+ * Tests pass a custom command so they don't depend on the skill.
29
+ *
30
+ * Process group safety:
31
+ * The child is spawned with `detached: true` so it gets its own
32
+ * process group. `kill(-pgid, signal)` (Node passes negative PIDs
33
+ * for group signals) targets only the child's group — never reaps
34
+ * the runner.
35
+ */
36
+ import { spawn } from 'node:child_process';
37
+ import { ProjectRoundLock } from './ProjectRoundLock.js';
38
+ import { ProjectRoundWorktrees } from './ProjectRoundWorktrees.js';
39
+ import { SafeGitExecutor } from './SafeGitExecutor.js';
40
+ /** Per-spec defaults. Tests dial both down to keep the suite fast. */
41
+ export const DEFAULT_POLL_INTERVAL_MS = 60_000;
42
+ export const DEFAULT_SIGTERM_GRACE_MS = 5_000;
43
+ /** Resume cap from spec § Phase 1.5 step 11. */
44
+ export const DEFAULT_MAX_RESUME_ATTEMPTS = 3;
45
+ /**
46
+ * Run one round to completion. Caller is expected to preflight first.
47
+ */
48
+ export async function runRound(input, deps) {
49
+ const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
50
+ const sigtermGraceMs = input.sigtermGraceMs ?? DEFAULT_SIGTERM_GRACE_MS;
51
+ const maxResumeAttempts = input.maxResumeAttempts ?? DEFAULT_MAX_RESUME_ATTEMPTS;
52
+ const verifyMergedItems = input.verifyMergedItems ?? defaultVerifyMergedItems(input.targetRepoPath);
53
+ const lock = new ProjectRoundLock({ stateDir: deps.stateDir });
54
+ // Step 1: acquire lock.
55
+ const acquired = lock.acquire(input.projectId, input.roundIndex);
56
+ if (!acquired.ok) {
57
+ return {
58
+ outcome: 'failed',
59
+ mergedItemIds: [],
60
+ unmergedItemIds: [],
61
+ relaunchCount: 0,
62
+ resumeAttempts: 0,
63
+ reason: `lock held by pid ${acquired.currentHolder.pid}`,
64
+ };
65
+ }
66
+ let relaunchCount = 0;
67
+ let resumeAttempts = 0;
68
+ try {
69
+ let snapshot = await readRound(input.tracker, input.projectId, input.roundIndex);
70
+ if (!snapshot) {
71
+ return {
72
+ outcome: 'failed',
73
+ mergedItemIds: [],
74
+ unmergedItemIds: [],
75
+ relaunchCount,
76
+ resumeAttempts,
77
+ reason: 'project or round disappeared',
78
+ };
79
+ }
80
+ let lastItemIds = [...snapshot.itemIds];
81
+ // Step 3: lazy worktree allocation for round items. The autonomous
82
+ // child opens more as it reaches them; we kick off only the first
83
+ // here to give the child a starting cwd.
84
+ if (lastItemIds.length > 0) {
85
+ try {
86
+ ProjectRoundWorktrees.allocate({
87
+ targetRepoPath: input.targetRepoPath,
88
+ projectId: input.projectId,
89
+ roundIndex: input.roundIndex,
90
+ itemId: lastItemIds[0],
91
+ }, { refuseExisting: false });
92
+ }
93
+ catch {
94
+ // First-item worktree may pre-exist if a previous attempt
95
+ // crashed mid-way. Continue.
96
+ }
97
+ }
98
+ // Inner relaunch loop: steps 4-5.
99
+ // We loop until either the child exits naturally or we detect a
100
+ // halt / stop-condition-met.
101
+ let outcome = 'failed';
102
+ let mergedItemIds = [];
103
+ let unmergedItemIds = [];
104
+ let reason;
105
+ for (;;) {
106
+ // Per-step halt checkpoint.
107
+ const halted = await readHaltedAt(input.tracker, input.projectId, input.roundIndex);
108
+ if (halted) {
109
+ outcome = 'halted';
110
+ reason = `round halted at ${halted}`;
111
+ unmergedItemIds = [...lastItemIds];
112
+ break;
113
+ }
114
+ // Compute current stop condition: itemIds verified-merged.
115
+ const verified = await verifyMergedItems(lastItemIds);
116
+ if (lastItemIds.every((id) => verified.has(id))) {
117
+ outcome = 'complete';
118
+ mergedItemIds = [...lastItemIds];
119
+ break;
120
+ }
121
+ // Spawn the autonomous child with stop condition + ids in env.
122
+ const child = spawnAutonomousChild(input, lastItemIds);
123
+ const exit = await waitForExitOrPollChange(child, input.tracker, input.projectId, input.roundIndex, lastItemIds, pollIntervalMs, sigtermGraceMs);
124
+ if (exit.kind === 'set-changed') {
125
+ relaunchCount++;
126
+ const fresh = await readRound(input.tracker, input.projectId, input.roundIndex);
127
+ if (!fresh) {
128
+ outcome = 'failed';
129
+ reason = 'project disappeared during round';
130
+ break;
131
+ }
132
+ lastItemIds = [...fresh.itemIds];
133
+ // Loop continues; next iteration re-checks stop condition then
134
+ // either spawns again or proceeds to step 6.
135
+ continue;
136
+ }
137
+ if (exit.kind === 'halted') {
138
+ outcome = 'halted';
139
+ reason = 'round halted via API during run';
140
+ unmergedItemIds = [...lastItemIds];
141
+ break;
142
+ }
143
+ // Natural exit.
144
+ if (exit.kind === 'exited' && exit.code === 0) {
145
+ // Step 6: verify per-item artifacts.
146
+ const finalVerified = await verifyMergedItems(lastItemIds);
147
+ mergedItemIds = lastItemIds.filter((id) => finalVerified.has(id));
148
+ unmergedItemIds = lastItemIds.filter((id) => !finalVerified.has(id));
149
+ if (unmergedItemIds.length === 0)
150
+ outcome = 'complete';
151
+ else
152
+ outcome = 'partially-complete';
153
+ break;
154
+ }
155
+ // Non-zero exit → resume attempt.
156
+ resumeAttempts++;
157
+ if (resumeAttempts >= maxResumeAttempts) {
158
+ outcome = 'failed';
159
+ reason = `${resumeAttempts} resume attempts exhausted (last exit code: ${exit.kind === 'exited' ? exit.code : exit.kind})`;
160
+ unmergedItemIds = [...lastItemIds];
161
+ break;
162
+ }
163
+ // Backoff before relaunch.
164
+ await sleep(1000 * resumeAttempts);
165
+ }
166
+ // Step 7: cleanup worktrees.
167
+ try {
168
+ ProjectRoundWorktrees.prune(input.targetRepoPath);
169
+ }
170
+ catch { /* best-effort */ }
171
+ // Step 9-11: record round status. We DO NOT touch
172
+ // unacknowledgedAdvanceCount here — that's the auto-advance poller's
173
+ // job. We just record the round status.
174
+ if (outcome !== 'halted') {
175
+ await recordOutcome(input.tracker, input.projectId, input.roundIndex, outcome);
176
+ }
177
+ return { outcome, mergedItemIds, unmergedItemIds, relaunchCount, resumeAttempts, reason };
178
+ }
179
+ finally {
180
+ // Step 8: release lock.
181
+ lock.release();
182
+ }
183
+ }
184
+ async function readRound(tracker, projectId, roundIndex) {
185
+ const proj = tracker.get(projectId);
186
+ if (!proj)
187
+ return null;
188
+ const round = (proj.rounds ?? [])[roundIndex];
189
+ if (!round)
190
+ return null;
191
+ return { itemIds: round.itemIds ?? [], status: (round.status ?? 'pending') };
192
+ }
193
+ async function readHaltedAt(tracker, projectId, roundIndex) {
194
+ const proj = tracker.get(projectId);
195
+ if (!proj)
196
+ return null;
197
+ const round = (proj.rounds ?? [])[roundIndex];
198
+ return round?.haltedAt ?? null;
199
+ }
200
+ async function recordOutcome(tracker, projectId, roundIndex, outcome) {
201
+ const proj = tracker.get(projectId);
202
+ if (!proj)
203
+ return;
204
+ const map = {
205
+ complete: 'complete',
206
+ 'partially-complete': 'partially-complete',
207
+ failed: 'failed',
208
+ halted: 'failed',
209
+ };
210
+ const newStatus = map[outcome];
211
+ const completedAt = new Date().toISOString();
212
+ const rounds = (proj.rounds ?? []).map((r, i) => i === roundIndex ? { ...r, status: newStatus, completedAt } : r);
213
+ try {
214
+ await tracker.update(projectId, { rounds, ifMatch: proj.version });
215
+ }
216
+ catch {
217
+ // OCC race — caller (or next reconcile) will retry.
218
+ }
219
+ }
220
+ function spawnAutonomousChild(input, itemIds) {
221
+ const cmd = input.spawnCommand ?? 'claude';
222
+ const args = input.spawnArgs ?? ['--skill', 'autonomous'];
223
+ const workdir = input.initialWorkdir ??
224
+ (itemIds.length > 0
225
+ ? ProjectRoundWorktrees.pathFor({
226
+ targetRepoPath: input.targetRepoPath,
227
+ projectId: input.projectId,
228
+ roundIndex: input.roundIndex,
229
+ itemId: itemIds[0],
230
+ })
231
+ : input.targetRepoPath);
232
+ const child = spawn(cmd, args, {
233
+ cwd: workdir,
234
+ detached: true, // critical — makes the child its own process group leader
235
+ stdio: 'ignore',
236
+ env: {
237
+ ...process.env,
238
+ INSTAR_PROJECT_ID: input.projectId,
239
+ INSTAR_ROUND_INDEX: String(input.roundIndex),
240
+ INSTAR_ROUND_ITEM_IDS: JSON.stringify(itemIds),
241
+ INSTAR_STOP_CONDITION: 'all-items-merged-on-main',
242
+ },
243
+ });
244
+ return child;
245
+ }
246
+ async function waitForExitOrPollChange(child, tracker, projectId, roundIndex, initialItemIds, pollIntervalMs, sigtermGraceMs) {
247
+ let exited = false;
248
+ let exitCode = null;
249
+ let exitSignal = null;
250
+ const exitPromise = new Promise((resolve) => {
251
+ child.once('exit', (code, signal) => {
252
+ exited = true;
253
+ exitCode = code;
254
+ exitSignal = signal;
255
+ resolve();
256
+ });
257
+ });
258
+ while (!exited) {
259
+ // Wait either pollIntervalMs OR exit, whichever comes first.
260
+ await Promise.race([exitPromise, sleep(pollIntervalMs)]);
261
+ if (exited)
262
+ break;
263
+ // Halt check.
264
+ const halted = await readHaltedAt(tracker, projectId, roundIndex);
265
+ if (halted) {
266
+ await killProcessGroup(child, sigtermGraceMs);
267
+ return { kind: 'halted' };
268
+ }
269
+ // Stop-condition revalidation: did itemIds change?
270
+ const snap = await readRound(tracker, projectId, roundIndex);
271
+ if (!snap) {
272
+ await killProcessGroup(child, sigtermGraceMs);
273
+ return { kind: 'halted' };
274
+ }
275
+ if (!arraysEqual(snap.itemIds, initialItemIds)) {
276
+ await killProcessGroup(child, sigtermGraceMs);
277
+ return { kind: 'set-changed' };
278
+ }
279
+ }
280
+ return { kind: 'exited', code: exitCode, signal: exitSignal };
281
+ }
282
+ async function killProcessGroup(child, graceMs) {
283
+ if (!child.pid || child.exitCode !== null)
284
+ return;
285
+ try {
286
+ // Negative PID targets the process group. child is its own group
287
+ // leader because we spawned with detached:true.
288
+ process.kill(-child.pid, 'SIGTERM');
289
+ }
290
+ catch {
291
+ return; // Already dead.
292
+ }
293
+ const start = Date.now();
294
+ while (child.exitCode === null && Date.now() - start < graceMs) {
295
+ await sleep(100);
296
+ }
297
+ if (child.exitCode === null) {
298
+ try {
299
+ process.kill(-child.pid, 'SIGKILL');
300
+ }
301
+ catch { /* already dead */ }
302
+ // Brief wait for the OS to register the kill.
303
+ await sleep(100);
304
+ }
305
+ }
306
+ function arraysEqual(a, b) {
307
+ if (a.length !== b.length)
308
+ return false;
309
+ for (let i = 0; i < a.length; i++)
310
+ if (a[i] !== b[i])
311
+ return false;
312
+ return true;
313
+ }
314
+ function sleep(ms) {
315
+ return new Promise((resolve) => setTimeout(resolve, ms));
316
+ }
317
+ /**
318
+ * Real `gh pr view`-backed merged-state verifier. For each child id,
319
+ * looks up the child's `prNumber` + `mergeCommitOid` from the tracker
320
+ * and runs `git merge-base --is-ancestor <oid> origin/main` to confirm
321
+ * the merge is reachable. Tests inject a stub instead.
322
+ *
323
+ * For PR 7's scope, this default is intentionally simple: it only
324
+ * confirms reachability of `mergeCommitOid` if the child has one.
325
+ * Real CI-green checks via `gh pr view` ship in a follow-up (the
326
+ * StageTransitionValidator already performs them for /advance — when
327
+ * the merged-state reconciler wires up in the next PR, it'll call
328
+ * the validator here too).
329
+ */
330
+ function defaultVerifyMergedItems(targetRepoPath) {
331
+ return async (_childIds) => {
332
+ const verified = new Set();
333
+ // Best-effort no-op default — production callers should pass a
334
+ // real verifyMergedItems that calls StageTransitionValidator.
335
+ // The point of this default is to avoid a spurious shell-out
336
+ // during tests that didn't override it (rare; tests should
337
+ // override).
338
+ void targetRepoPath;
339
+ return verified;
340
+ };
341
+ }
342
+ /** Helper exported so callers can wire a SafeGit-backed verifier. */
343
+ export async function verifyMergedItemsViaGit(targetRepoPath, childIds, tracker) {
344
+ const verified = new Set();
345
+ for (const id of childIds) {
346
+ const child = tracker.get(id);
347
+ if (!child || !child.mergeCommitOid)
348
+ continue;
349
+ try {
350
+ SafeGitExecutor.run(['merge-base', '--is-ancestor', child.mergeCommitOid, 'origin/main'], { cwd: targetRepoPath, operation: 'ProjectRoundExecution.verifyMergedItemsViaGit' });
351
+ verified.add(id);
352
+ }
353
+ catch {
354
+ // Not an ancestor of origin/main — not verified.
355
+ }
356
+ }
357
+ return verified;
358
+ }
359
+ //# sourceMappingURL=ProjectRoundExecution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectRoundExecution.js","sourceRoot":"","sources":["../../src/core/ProjectRoundExecution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAG9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,sEAAsE;AACtE,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAC/C,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,CAAC;AAC9C,gDAAgD;AAChD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAyE7C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAoB,EAAE,IAAkB;IACrE,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,wBAAwB,CAAC;IACxE,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,wBAAwB,CAAC;IACxE,MAAM,iBAAiB,GAAG,KAAK,CAAC,iBAAiB,IAAI,2BAA2B,CAAC;IACjF,MAAM,iBAAiB,GAAG,KAAK,CAAC,iBAAiB,IAAI,wBAAwB,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAEpG,MAAM,IAAI,GAAG,IAAI,gBAAgB,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE/D,wBAAwB;IACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACjE,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO;YACL,OAAO,EAAE,QAAQ;YACjB,aAAa,EAAE,EAAE;YACjB,eAAe,EAAE,EAAE;YACnB,aAAa,EAAE,CAAC;YAChB,cAAc,EAAE,CAAC;YACjB,MAAM,EAAE,oBAAoB,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE;SACzD,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,IAAI,CAAC;QACH,IAAI,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;QACjF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO;gBACL,OAAO,EAAE,QAAQ;gBACjB,aAAa,EAAE,EAAE;gBACjB,eAAe,EAAE,EAAE;gBACnB,aAAa;gBACb,cAAc;gBACd,MAAM,EAAE,8BAA8B;aACvC,CAAC;QACJ,CAAC;QACD,IAAI,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAExC,mEAAmE;QACnE,kEAAkE;QAClE,yCAAyC;QACzC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,qBAAqB,CAAC,QAAQ,CAC5B;oBACE,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;iBACvB,EACD,EAAE,cAAc,EAAE,KAAK,EAAE,CAC1B,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,0DAA0D;gBAC1D,6BAA6B;YAC/B,CAAC;QACH,CAAC;QAED,kCAAkC;QAClC,gEAAgE;QAChE,6BAA6B;QAC7B,IAAI,OAAO,GAAiB,QAAQ,CAAC;QACrC,IAAI,aAAa,GAAa,EAAE,CAAC;QACjC,IAAI,eAAe,GAAa,EAAE,CAAC;QACnC,IAAI,MAA0B,CAAC;QAE/B,SAAS,CAAC;YACR,4BAA4B;YAC5B,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;YACpF,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,GAAG,QAAQ,CAAC;gBACnB,MAAM,GAAG,mBAAmB,MAAM,EAAE,CAAC;gBACrC,eAAe,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC;gBACnC,MAAM;YACR,CAAC;YAED,2DAA2D;YAC3D,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,WAAW,CAAC,CAAC;YACtD,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAChD,OAAO,GAAG,UAAU,CAAC;gBACrB,aAAa,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC;gBACjC,MAAM;YACR,CAAC;YAED,+DAA+D;YAC/D,MAAM,KAAK,GAAG,oBAAoB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,MAAM,uBAAuB,CACxC,KAAK,EACL,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,SAAS,EACf,KAAK,CAAC,UAAU,EAChB,WAAW,EACX,cAAc,EACd,cAAc,CACf,CAAC;YAEF,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBAChC,aAAa,EAAE,CAAC;gBAChB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;gBAChF,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,OAAO,GAAG,QAAQ,CAAC;oBACnB,MAAM,GAAG,kCAAkC,CAAC;oBAC5C,MAAM;gBACR,CAAC;gBACD,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;gBACjC,+DAA+D;gBAC/D,6CAA6C;gBAC7C,SAAS;YACX,CAAC;YAED,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC3B,OAAO,GAAG,QAAQ,CAAC;gBACnB,MAAM,GAAG,iCAAiC,CAAC;gBAC3C,eAAe,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC;gBACnC,MAAM;YACR,CAAC;YAED,gBAAgB;YAChB,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC9C,qCAAqC;gBACrC,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAAC,WAAW,CAAC,CAAC;gBAC3D,aAAa,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;gBAClE,eAAe,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;gBACrE,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO,GAAG,UAAU,CAAC;;oBAClD,OAAO,GAAG,oBAAoB,CAAC;gBACpC,MAAM;YACR,CAAC;YAED,kCAAkC;YAClC,cAAc,EAAE,CAAC;YACjB,IAAI,cAAc,IAAI,iBAAiB,EAAE,CAAC;gBACxC,OAAO,GAAG,QAAQ,CAAC;gBACnB,MAAM,GAAG,GAAG,cAAc,+CAA+C,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;gBAC3H,eAAe,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC;gBACnC,MAAM;YACR,CAAC;YACD,2BAA2B;YAC3B,MAAM,KAAK,CAAC,IAAI,GAAG,cAAc,CAAC,CAAC;QACrC,CAAC;QAED,6BAA6B;QAC7B,IAAI,CAAC;YAAC,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;QAEtF,kDAAkD;QAClD,qEAAqE;QACrE,wCAAwC;QACxC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,MAAM,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACjF,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAC5F,CAAC;YAAS,CAAC;QACT,wBAAwB;QACxB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;AACH,CAAC;AASD,KAAK,UAAU,SAAS,CACtB,OAA0B,EAC1B,SAAiB,EACjB,UAAkB;IAElB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,OAA0B,EAC1B,SAAiB,EACjB,UAAkB;IAElB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IAC9C,OAAO,KAAK,EAAE,QAAQ,IAAI,IAAI,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,OAA0B,EAC1B,SAAiB,EACjB,UAAkB,EAClB,OAAqB;IAErB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,MAAM,GAAG,GAAsC;QAC7C,QAAQ,EAAE,UAAU;QACpB,oBAAoB,EAAE,oBAAoB;QAC1C,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,QAAQ;KACjB,CAAC;IACF,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC9C,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAChE,CAAC;IACF,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,oDAAoD;IACtD,CAAC;AACH,CAAC;AAQD,SAAS,oBAAoB,CAAC,KAAoB,EAAE,OAAiB;IACnE,MAAM,GAAG,GAAG,KAAK,CAAC,YAAY,IAAI,QAAQ,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC1D,MAAM,OAAO,GACX,KAAK,CAAC,cAAc;QACpB,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YACjB,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC;gBAC5B,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;aACnB,CAAC;YACJ,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;QAC7B,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,IAAI,EAAE,0DAA0D;QAC1E,KAAK,EAAE,QAAQ;QACf,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,iBAAiB,EAAE,KAAK,CAAC,SAAS;YAClC,kBAAkB,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;YAC5C,qBAAqB,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YAC9C,qBAAqB,EAAE,0BAA0B;SAClD;KACF,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,uBAAuB,CACpC,KAAmB,EACnB,OAA0B,EAC1B,SAAiB,EACjB,UAAkB,EAClB,cAAwB,EACxB,cAAsB,EACtB,cAAsB;IAEtB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,UAAU,GAA0B,IAAI,CAAC;IAC7C,MAAM,WAAW,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAChD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YAClC,MAAM,GAAG,IAAI,CAAC;YACd,QAAQ,GAAG,IAAI,CAAC;YAChB,UAAU,GAAG,MAAM,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,MAAM,EAAE,CAAC;QACf,6DAA6D;QAC7D,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QACzD,IAAI,MAAM;YAAE,MAAM;QAElB,cAAc;QACd,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAClE,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,gBAAgB,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAC9C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC5B,CAAC;QAED,mDAAmD;QACnD,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,gBAAgB,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAC9C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC;YAC/C,MAAM,gBAAgB,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAC9C,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,KAAmB,EAAE,OAAe;IAClE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO;IAClD,IAAI,CAAC;QACH,iEAAiE;QACjE,gDAAgD;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,gBAAgB;IAC1B,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,OAAO,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,OAAO,EAAE,CAAC;QAC/D,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;QAC9B,8CAA8C;QAC9C,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,CAAoB,EAAE,CAAoB;IAC7D,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACnE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,wBAAwB,CAAC,cAAsB;IACtD,OAAO,KAAK,EAAE,SAAmB,EAAE,EAAE;QACnC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,+DAA+D;QAC/D,8DAA8D;QAC9D,6DAA6D;QAC7D,2DAA2D;QAC3D,aAAa;QACb,KAAK,cAAc,CAAC;QACpB,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,cAAsB,EACtB,QAAkB,EAClB,OAA0B;IAE1B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,cAAc;YAAE,SAAS;QAC9C,IAAI,CAAC;YACH,eAAe,CAAC,GAAG,CACjB,CAAC,YAAY,EAAE,eAAe,EAAE,KAAK,CAAC,cAAc,EAAE,aAAa,CAAC,EACpE,EAAE,GAAG,EAAE,cAAc,EAAE,SAAS,EAAE,+CAA+C,EAAE,CACpF,CAAC;YACF,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,iDAAiD;QACnD,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ProjectRoundLock — machine-local lock for the round runner.
3
+ *
4
+ * Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5 ("Lock file
5
+ * .instar/local/round-runner.lock is free."). The path is MACHINE-LOCAL
6
+ * (under .instar/local/, NOT git-synced) so two machines never fight
7
+ * over a 0-byte lockfile in the sync layer; cross-machine ownership is
8
+ * separately controlled by `ownerMachineId` on the project record and
9
+ * the claim-ownership flow (later PR).
10
+ *
11
+ * Contract:
12
+ * - At most one round-runner active per machine. The lock encodes the
13
+ * PID, projectId, and roundIndex of the holder.
14
+ * - Stale lock detection: if the recorded PID is no longer alive, the
15
+ * lock is considered free (handles crash without restart). This is
16
+ * intentional and matches the spec's pre-flight step 2 ("PID in lock
17
+ * is alive; if not, remove").
18
+ * - Acquisition is best-effort atomic: `O_CREAT | O_EXCL` rename of a
19
+ * tmp file holding the JSON payload. Concurrent acquirers race on
20
+ * the rename; the loser fails fast. This is the standard non-fcntl
21
+ * local-process mutex pattern; proper-lockfile is overkill for a
22
+ * single-machine single-runner gate.
23
+ */
24
+ export interface ProjectRoundLockPayload {
25
+ pid: number;
26
+ projectId: string;
27
+ roundIndex: number;
28
+ acquiredAt: string;
29
+ }
30
+ export interface ProjectRoundLockConfig {
31
+ /** Absolute path to the agent's `.instar/` directory. */
32
+ stateDir: string;
33
+ }
34
+ export type AcquireResult = {
35
+ ok: true;
36
+ payload: ProjectRoundLockPayload;
37
+ } | {
38
+ ok: false;
39
+ reason: 'held';
40
+ currentHolder: ProjectRoundLockPayload;
41
+ };
42
+ export declare class ProjectRoundLock {
43
+ private stateDir;
44
+ constructor(config: ProjectRoundLockConfig);
45
+ /**
46
+ * Try to acquire the lock for the given (projectId, roundIndex). Returns
47
+ * a structured result rather than throwing — callers decide whether to
48
+ * wait, abort, or report.
49
+ */
50
+ acquire(projectId: string, roundIndex: number, now?: Date): AcquireResult;
51
+ /**
52
+ * Release the lock unconditionally. Returns true if a lock was removed,
53
+ * false if it was already gone. Does NOT verify the caller owns it —
54
+ * callers higher in the stack (the runner itself) own that semantics.
55
+ */
56
+ release(): boolean;
57
+ /** Read the current holder, if any. */
58
+ read(): ProjectRoundLockPayload | null;
59
+ private ensureDir;
60
+ private lockPath;
61
+ }
62
+ /**
63
+ * Cross-platform "is this PID still running" check.
64
+ * `kill(pid, 0)` doesn't send a signal but throws if the PID isn't valid.
65
+ */
66
+ export declare function isAlive(pid: number): boolean;
67
+ //# sourceMappingURL=ProjectRoundLock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectRoundLock.d.ts","sourceRoot":"","sources":["../../src/core/ProjectRoundLock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,uBAAuB,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,uBAAuB,CAAC;CACxC,CAAC;AAEN,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,sBAAsB;IAI1C;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,aAAa;IAmDrF;;;;OAIG;IACH,OAAO,IAAI,OAAO;IAWlB,uCAAuC;IACvC,IAAI,IAAI,uBAAuB,GAAG,IAAI;IAgBtC,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,QAAQ;CAGjB;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAW5C"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * ProjectRoundLock — machine-local lock for the round runner.
3
+ *
4
+ * Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5 ("Lock file
5
+ * .instar/local/round-runner.lock is free."). The path is MACHINE-LOCAL
6
+ * (under .instar/local/, NOT git-synced) so two machines never fight
7
+ * over a 0-byte lockfile in the sync layer; cross-machine ownership is
8
+ * separately controlled by `ownerMachineId` on the project record and
9
+ * the claim-ownership flow (later PR).
10
+ *
11
+ * Contract:
12
+ * - At most one round-runner active per machine. The lock encodes the
13
+ * PID, projectId, and roundIndex of the holder.
14
+ * - Stale lock detection: if the recorded PID is no longer alive, the
15
+ * lock is considered free (handles crash without restart). This is
16
+ * intentional and matches the spec's pre-flight step 2 ("PID in lock
17
+ * is alive; if not, remove").
18
+ * - Acquisition is best-effort atomic: `O_CREAT | O_EXCL` rename of a
19
+ * tmp file holding the JSON payload. Concurrent acquirers race on
20
+ * the rename; the loser fails fast. This is the standard non-fcntl
21
+ * local-process mutex pattern; proper-lockfile is overkill for a
22
+ * single-machine single-runner gate.
23
+ */
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { SafeFsExecutor } from './SafeFsExecutor.js';
27
+ export class ProjectRoundLock {
28
+ stateDir;
29
+ constructor(config) {
30
+ this.stateDir = config.stateDir;
31
+ }
32
+ /**
33
+ * Try to acquire the lock for the given (projectId, roundIndex). Returns
34
+ * a structured result rather than throwing — callers decide whether to
35
+ * wait, abort, or report.
36
+ */
37
+ acquire(projectId, roundIndex, now = new Date()) {
38
+ this.ensureDir();
39
+ // Stale-PID sweep first.
40
+ const existing = this.read();
41
+ if (existing && !isAlive(existing.pid)) {
42
+ try {
43
+ SafeFsExecutor.safeUnlinkSync(this.lockPath(), { operation: 'ProjectRoundLock.acquire:sweep-stale' });
44
+ }
45
+ catch {
46
+ // Race with another acquirer doing the same — fine.
47
+ }
48
+ }
49
+ else if (existing) {
50
+ return { ok: false, reason: 'held', currentHolder: existing };
51
+ }
52
+ const payload = {
53
+ pid: process.pid,
54
+ projectId,
55
+ roundIndex,
56
+ acquiredAt: now.toISOString(),
57
+ };
58
+ const body = JSON.stringify(payload);
59
+ const tmp = this.lockPath() + '.tmp.' + process.pid;
60
+ try {
61
+ // O_CREAT | O_EXCL: fails if tmp already exists (in case of a
62
+ // previous crash with the same pid — unlikely but harmless).
63
+ const fd = fs.openSync(tmp, 'wx', 0o600);
64
+ fs.writeSync(fd, body);
65
+ fs.closeSync(fd);
66
+ }
67
+ catch (err) {
68
+ try {
69
+ SafeFsExecutor.safeUnlinkSync(tmp, { operation: 'ProjectRoundLock.acquire:cleanup-tmp' });
70
+ }
71
+ catch { /* ignore */ }
72
+ throw err;
73
+ }
74
+ try {
75
+ // POSIX rename is atomic on the same filesystem. If another process
76
+ // sneaked in between our stale-sweep and the rename, our rename
77
+ // overwrites theirs — which is wrong. So we re-check after.
78
+ // BUT: the spec accepts "at most one runner per machine" which is
79
+ // a single-process guarantee for serial invocation; concurrent
80
+ // acquires from the same process don't happen because the routes
81
+ // layer hold the call inside one promise. Cross-process concurrent
82
+ // acquires are a theoretical edge — round runners only spawn from
83
+ // the supervised AgentServer. We accept the rename racing on the
84
+ // narrow window between read() and rename() as acceptable.
85
+ fs.renameSync(tmp, this.lockPath());
86
+ }
87
+ catch (err) {
88
+ try {
89
+ SafeFsExecutor.safeUnlinkSync(tmp, { operation: 'ProjectRoundLock.acquire:cleanup-tmp' });
90
+ }
91
+ catch { /* ignore */ }
92
+ throw err;
93
+ }
94
+ return { ok: true, payload };
95
+ }
96
+ /**
97
+ * Release the lock unconditionally. Returns true if a lock was removed,
98
+ * false if it was already gone. Does NOT verify the caller owns it —
99
+ * callers higher in the stack (the runner itself) own that semantics.
100
+ */
101
+ release() {
102
+ try {
103
+ SafeFsExecutor.safeUnlinkSync(this.lockPath(), { operation: 'ProjectRoundLock.release' });
104
+ return true;
105
+ }
106
+ catch (err) {
107
+ const e = err;
108
+ if (e.code === 'ENOENT')
109
+ return false;
110
+ throw err;
111
+ }
112
+ }
113
+ /** Read the current holder, if any. */
114
+ read() {
115
+ try {
116
+ const raw = fs.readFileSync(this.lockPath(), 'utf-8');
117
+ const obj = JSON.parse(raw);
118
+ if (typeof obj.pid !== 'number')
119
+ return null;
120
+ if (typeof obj.projectId !== 'string')
121
+ return null;
122
+ if (typeof obj.roundIndex !== 'number')
123
+ return null;
124
+ if (typeof obj.acquiredAt !== 'string')
125
+ return null;
126
+ return obj;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ // ── Internals ─────────────────────────────────────────────────────
133
+ ensureDir() {
134
+ const localDir = path.join(this.stateDir, 'local');
135
+ if (!fs.existsSync(localDir))
136
+ fs.mkdirSync(localDir, { recursive: true });
137
+ }
138
+ lockPath() {
139
+ return path.join(this.stateDir, 'local', 'round-runner.lock');
140
+ }
141
+ }
142
+ /**
143
+ * Cross-platform "is this PID still running" check.
144
+ * `kill(pid, 0)` doesn't send a signal but throws if the PID isn't valid.
145
+ */
146
+ export function isAlive(pid) {
147
+ if (!Number.isInteger(pid) || pid <= 0)
148
+ return false;
149
+ try {
150
+ process.kill(pid, 0);
151
+ return true;
152
+ }
153
+ catch (err) {
154
+ const e = err;
155
+ // EPERM means the PID exists but we don't have permission; still alive.
156
+ if (e.code === 'EPERM')
157
+ return true;
158
+ return false;
159
+ }
160
+ }
161
+ //# sourceMappingURL=ProjectRoundLock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectRoundLock.js","sourceRoot":"","sources":["../../src/core/ProjectRoundLock.ts"],"names":[],"mappings":"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"}