@coralai/sps-cli 0.23.18 → 0.23.19

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.
@@ -3,7 +3,6 @@ import type { PMClient } from './pm-client.js';
3
3
  import type { ProcessSupervisor, SpawnOpts } from './supervisor.js';
4
4
  import type { ResourceLimiter } from './resource-limiter.js';
5
5
  import type { Notifier } from '../interfaces/Notifier.js';
6
- import type { MergeMutex } from './merge-mutex.js';
7
6
  import type { AgentRuntime } from '../interfaces/AgentRuntime.js';
8
7
  export interface PostActionContext {
9
8
  project: string;
@@ -38,9 +37,8 @@ export declare class PostActions {
38
37
  private readonly supervisor;
39
38
  private readonly resourceLimiter;
40
39
  private readonly notifier;
41
- private readonly mergeMutex?;
42
40
  private readonly agentRuntime;
43
- constructor(pmClient: PMClient, supervisor: ProcessSupervisor, resourceLimiter: ResourceLimiter, notifier: Notifier | null, mergeMutex?: MergeMutex | undefined, agentRuntime?: AgentRuntime | null);
41
+ constructor(pmClient: PMClient, supervisor: ProcessSupervisor, resourceLimiter: ResourceLimiter, notifier: Notifier | null, agentRuntime?: AgentRuntime | null);
44
42
  private stateStore;
45
43
  /**
46
44
  * Handle worker completion.
@@ -61,40 +59,6 @@ export declare class PostActions {
61
59
  executeFailure(ctx: PostActionContext, completion: CompletionResult, exitCode: number, sessionId: string | null, retryCount: number,
62
60
  /** Original spawn options for retry */
63
61
  respawnOpts?: Partial<SpawnOpts>): Promise<StepResult[]>;
64
- /**
65
- * Attempt to merge the feature branch into the target branch.
66
- * Called while holding the MergeMutex — only one merge at a time per project.
67
- *
68
- * L0: fetch + rebase + merge (pure git, no AI)
69
- * L1: rebase failed — abort and retry rebase (in case of transient issue)
70
- * L2: rebase has real conflicts — spawn --resume Worker to resolve
71
- * L3: all retries exhausted — mark CONFLICT
72
- *
73
- * Returns true if merge succeeded, false if all attempts failed.
74
- */
75
- private serialMerge;
76
- /**
77
- * Try rebase + merge (L0). Returns { ok, error }.
78
- */
79
- private tryRebaseMerge;
80
- /**
81
- * Abort any in-progress rebase.
82
- */
83
- private abortRebase;
84
- /**
85
- * Spawn a --resume worker to resolve merge conflicts.
86
- * Waits for the worker to exit before returning.
87
- */
88
- private spawnConflictResolver;
89
- private spawnAcpConflictResolver;
90
- /**
91
- * Mark a card with CONFLICT label and comment.
92
- */
93
- private markConflict;
94
- /**
95
- * Update slot status in state.json.
96
- */
97
- private setSlotStatus;
98
62
  private createMR;
99
63
  private pmMoveQa;
100
64
  private pmMoveDone;
@@ -110,13 +74,8 @@ export declare class PostActions {
110
74
  private pmAddLabel;
111
75
  private notify;
112
76
  private log;
113
- private resolveRepoDir;
114
- private createMergeWorktree;
115
- private cleanupMergeWorktree;
77
+ private loadPhaseResumePrompt;
116
78
  private syncAcpRuntimeState;
117
- private waitForAcpRun;
118
- private isTerminalAcpStatus;
119
- private sleep;
120
79
  }
121
80
  export {};
122
81
  //# sourceMappingURL=post-actions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"post-actions.d.ts","sourceRoot":"","sources":["../../src/manager/post-actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"post-actions.d.ts","sourceRoot":"","sources":["../../src/manager/post-actions.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAWlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CACzF;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAJZ,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,iBAAiB,EAC7B,eAAe,EAAE,eAAe,EAChC,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,YAAY,GAAE,YAAY,GAAG,IAAW;IAG3D,OAAO,CAAC,UAAU;IAOlB;;;;;;;;;OASG;IACG,iBAAiB,CACrB,GAAG,EAAE,iBAAiB,EACtB,UAAU,EAAE,gBAAgB,EAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAAC,UAAU,EAAE,CAAC;YAOV,4BAA4B;YAsB5B,2BAA2B;IAwBzC;;OAEG;IACG,cAAc,CAClB,GAAG,EAAE,iBAAiB,EACtB,UAAU,EAAE,gBAAgB,EAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,UAAU,EAAE,MAAM;IAClB,uCAAuC;IACvC,WAAW,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,GAC/B,OAAO,CAAC,UAAU,EAAE,CAAC;YAyDV,QAAQ;YA6BR,QAAQ;YASR,UAAU;YASV,eAAe;YAgBf,WAAW;YAWX,cAAc;YASd,mBAAmB;YAmBnB,gBAAgB;YA4BhB,OAAO;YAyDP,UAAU;YAiCV,qBAAqB;YA4BrB,SAAS;YAST,UAAU;YASV,MAAM;IAiBpB,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,mBAAmB;CA2E5B"}
@@ -3,35 +3,27 @@
3
3
  *
4
4
  * Called immediately from Supervisor exit callback → CompletionJudge → here.
5
5
  *
6
- * v0.19: Merges are serialized via MergeMutex (per-project). Workers code
7
- * in parallel but merge one at a time. If merge conflicts, L2 spawns a
8
- * --resume worker to resolve conflicts before retrying.
6
+ * v0.23.19+: post-actions no longer owns merge/conflict resolution.
7
+ * Development completion hands the task off to QA. Integration work is
8
+ * owned by the QA worker and CloseoutEngine only finalizes outcomes.
9
9
  */
10
10
  import { execFileSync } from 'node:child_process';
11
- import { tmpdir } from 'node:os';
12
- import { appendFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
13
- import { dirname, resolve } from 'node:path';
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
14
13
  import { RuntimeStore } from '../core/runtimeStore.js';
15
- /** Max merge conflict resolution retries (L2 cycles) */
16
- const MAX_MERGE_RETRIES = 2;
17
- /** How often to check if resume worker has exited (ms) */
18
- const PID_POLL_INTERVAL = 3000;
19
- /** Max time to wait for a resume worker to exit (ms) */
20
- const RESOLVE_TIMEOUT = 300_000; // 5 minutes
14
+ import { buildResumePrompt, LEGACY_TASK_PROMPT_FILE, promptFileForPhase, selectWorkerPhase, } from '../core/taskPrompts.js';
21
15
  // ─── PostActions ────────────────────────────────────────────────
22
16
  export class PostActions {
23
17
  pmClient;
24
18
  supervisor;
25
19
  resourceLimiter;
26
20
  notifier;
27
- mergeMutex;
28
21
  agentRuntime;
29
- constructor(pmClient, supervisor, resourceLimiter, notifier, mergeMutex, agentRuntime = null) {
22
+ constructor(pmClient, supervisor, resourceLimiter, notifier, agentRuntime = null) {
30
23
  this.pmClient = pmClient;
31
24
  this.supervisor = supervisor;
32
25
  this.resourceLimiter = resourceLimiter;
33
26
  this.notifier = notifier;
34
- this.mergeMutex = mergeMutex;
35
27
  this.agentRuntime = agentRuntime;
36
28
  }
37
29
  stateStore(ctx) {
@@ -121,292 +113,7 @@ export class PostActions {
121
113
  this.supervisor.remove(workerId);
122
114
  return results;
123
115
  }
124
- // ─── Serial Merge with L0/L1/L2/L3 ────────────────────────────
125
- /**
126
- * Attempt to merge the feature branch into the target branch.
127
- * Called while holding the MergeMutex — only one merge at a time per project.
128
- *
129
- * L0: fetch + rebase + merge (pure git, no AI)
130
- * L1: rebase failed — abort and retry rebase (in case of transient issue)
131
- * L2: rebase has real conflicts — spawn --resume Worker to resolve
132
- * L3: all retries exhausted — mark CONFLICT
133
- *
134
- * Returns true if merge succeeded, false if all attempts failed.
135
- */
136
- async serialMerge(ctx, sessionId, results) {
137
- // L0: Try direct merge (rebase + merge)
138
- const l0Result = this.tryRebaseMerge(ctx);
139
- if (l0Result.ok) {
140
- this.log(`L0: Merged ${ctx.branch} → ${ctx.baseBranch}`);
141
- results.push({ step: 'merge-l0', ok: true });
142
- return true;
143
- }
144
- this.log(`L0: Merge failed for ${ctx.branch}: ${l0Result.error}`);
145
- // Abort any in-progress rebase before L2
146
- this.abortRebase(ctx.worktree);
147
- // L2: Spawn --resume Worker to resolve conflicts
148
- if (ctx.transport === 'proc' && !sessionId) {
149
- this.log(`L2: No sessionId for ${ctx.branch}, cannot spawn resume worker`);
150
- results.push({ step: 'merge-l0', ok: false, error: l0Result.error });
151
- await this.markConflict(ctx, results, `Merge conflict, no session to resume`);
152
- return false;
153
- }
154
- for (let attempt = 0; attempt < MAX_MERGE_RETRIES; attempt++) {
155
- this.log(`L2: Attempt ${attempt + 1}/${MAX_MERGE_RETRIES} — spawning resume worker for ${ctx.branch}`);
156
- // Update slot status to "resolving"
157
- this.setSlotStatus(ctx, 'resolving');
158
- // Release mutex while AI works (so other completed workers aren't blocked indefinitely)
159
- if (this.mergeMutex)
160
- this.mergeMutex.release();
161
- // Spawn resume worker and wait for exit
162
- const resolveResult = ctx.transport === 'proc'
163
- ? await this.spawnConflictResolver(ctx, sessionId, attempt)
164
- : await this.spawnAcpConflictResolver(ctx, attempt);
165
- // Re-acquire mutex for merge retry
166
- if (this.mergeMutex)
167
- await this.mergeMutex.acquire();
168
- // Update slot back to "merging"
169
- this.setSlotStatus(ctx, 'merging');
170
- if (!resolveResult.ok) {
171
- this.log(`L2: Resume worker failed: ${resolveResult.error}`);
172
- results.push({ step: `merge-l2-attempt-${attempt + 1}`, ok: false, error: resolveResult.error });
173
- continue;
174
- }
175
- // Retry L0 after conflict resolution
176
- const retryResult = this.tryRebaseMerge(ctx);
177
- if (retryResult.ok) {
178
- this.log(`L2: Merge succeeded after conflict resolution (attempt ${attempt + 1})`);
179
- results.push({ step: `merge-l2-attempt-${attempt + 1}`, ok: true });
180
- return true;
181
- }
182
- this.log(`L2: Merge still fails after resolution attempt ${attempt + 1}: ${retryResult.error}`);
183
- this.abortRebase(ctx.worktree);
184
- results.push({ step: `merge-l2-attempt-${attempt + 1}`, ok: false, error: retryResult.error });
185
- }
186
- // L3: All retries exhausted
187
- await this.markConflict(ctx, results, `Merge conflict after ${MAX_MERGE_RETRIES} resolution attempts`);
188
- return false;
189
- }
190
- /**
191
- * Try rebase + merge (L0). Returns { ok, error }.
192
- */
193
- tryRebaseMerge(ctx) {
194
- let mergeWorktree = null;
195
- try {
196
- const { worktree, branch, baseBranch } = ctx;
197
- // Fetch latest target
198
- try {
199
- execFileSync('git', ['-C', worktree, 'fetch', 'origin', baseBranch, '--quiet'], {
200
- timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
201
- });
202
- }
203
- catch { /* offline ok */ }
204
- // Checkout feature branch (may be on baseBranch from previous merge attempt)
205
- try {
206
- execFileSync('git', ['-C', worktree, 'checkout', branch], {
207
- timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
208
- });
209
- }
210
- catch { /* already on branch */ }
211
- // Rebase feature onto latest target
212
- execFileSync('git', ['-C', worktree, 'rebase', `origin/${baseBranch}`], {
213
- timeout: 30_000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'],
214
- });
215
- // Force push rebased feature
216
- execFileSync('git', ['-C', worktree, 'push', '--force-with-lease', 'origin', branch], {
217
- timeout: 30_000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'],
218
- });
219
- // Final integration happens in a temporary detached merge worktree so we do
220
- // not touch the user's main working copy or hit "branch already in use".
221
- const repoDir = this.resolveRepoDir(worktree);
222
- mergeWorktree = this.createMergeWorktree(repoDir, ctx);
223
- execFileSync('git', ['-C', mergeWorktree, 'fetch', 'origin', '--quiet'], {
224
- timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
225
- });
226
- execFileSync('git', ['-C', mergeWorktree, 'reset', '--hard', `origin/${baseBranch}`], {
227
- timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
228
- });
229
- execFileSync('git', ['-C', mergeWorktree, 'merge', '--no-ff', `origin/${branch}`, '-m',
230
- `Merge ${branch} into ${baseBranch}`], {
231
- timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'],
232
- });
233
- execFileSync('git', ['-C', mergeWorktree, 'push', 'origin', `HEAD:${baseBranch}`], {
234
- timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'],
235
- });
236
- return { ok: true };
237
- }
238
- catch (err) {
239
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
240
- }
241
- finally {
242
- this.cleanupMergeWorktree(mergeWorktree);
243
- }
244
- }
245
- /**
246
- * Abort any in-progress rebase.
247
- */
248
- abortRebase(worktree) {
249
- try {
250
- execFileSync('git', ['-C', worktree, 'rebase', '--abort'], {
251
- timeout: 5_000, stdio: ['ignore', 'pipe', 'pipe'],
252
- });
253
- }
254
- catch { /* no rebase in progress */ }
255
- }
256
- /**
257
- * Spawn a --resume worker to resolve merge conflicts.
258
- * Waits for the worker to exit before returning.
259
- */
260
- async spawnConflictResolver(ctx, sessionId, attempt) {
261
- const workerId = `${ctx.project}:${ctx.slot}:${ctx.seq}`;
262
- const outputFile = resolve(ctx.logsDir, `${ctx.project}-${ctx.slot}-conflict-${attempt + 1}-${Date.now()}.jsonl`);
263
- const instruction = [
264
- `There is a merge conflict on branch ${ctx.branch} when rebasing onto origin/${ctx.baseBranch}.`,
265
- `Working directory: ${ctx.worktree}`,
266
- '',
267
- 'Please resolve the conflict:',
268
- `1. Run: cd ${ctx.worktree}`,
269
- `2. Run: git fetch origin && git checkout ${ctx.branch}`,
270
- `3. Run: git rebase origin/${ctx.baseBranch}`,
271
- '4. For each conflicting file: open it, understand both sides, resolve the conflict',
272
- '5. Run: git add <resolved-files> && git rebase --continue',
273
- '6. Run: git push --force-with-lease origin ' + ctx.branch,
274
- '7. Say "done"',
275
- '',
276
- 'You have full context from the previous coding session. Use it to make correct conflict resolution decisions.',
277
- ].join('\n');
278
- // Remove old supervisor tracking before respawning
279
- this.supervisor.remove(workerId);
280
- // Release + re-acquire global resource for the resume worker
281
- this.resourceLimiter.release();
282
- const acquire = this.resourceLimiter.tryAcquireDetailed();
283
- if (!acquire.acquired) {
284
- return { ok: false, error: 'Global resource limit reached for conflict resolver' };
285
- }
286
- try {
287
- // Spawn and capture PID via Promise-based exit wait
288
- let spawnedPid = 0;
289
- const exitPromise = new Promise((resolveExit) => {
290
- const handle = this.supervisor.spawn({
291
- id: workerId,
292
- project: ctx.project,
293
- seq: ctx.seq,
294
- slot: ctx.slot,
295
- worktree: ctx.worktree,
296
- branch: ctx.branch,
297
- prompt: instruction,
298
- outputFile,
299
- tool: ctx.tool,
300
- resumeSessionId: sessionId,
301
- onExit: async (exitCode) => {
302
- resolveExit(exitCode);
303
- },
304
- });
305
- spawnedPid = handle.pid ?? 0;
306
- });
307
- // Update state with new PID
308
- this.stateStore(ctx).updateState('post-actions-conflict-resolve', (state) => {
309
- if (state.workers[ctx.slot]) {
310
- state.workers[ctx.slot].pid = spawnedPid;
311
- state.workers[ctx.slot].outputFile = outputFile;
312
- state.workers[ctx.slot].exitCode = null;
313
- state.workers[ctx.slot].mergeRetries = (state.workers[ctx.slot].mergeRetries ?? 0) + 1;
314
- }
315
- });
316
- this.log(`Spawned conflict resolver for ${workerId} (pid=${spawnedPid})`);
317
- // Wait for the resume worker to exit (with timeout)
318
- const exitCode = await Promise.race([
319
- exitPromise,
320
- new Promise((_, reject) => setTimeout(() => reject(new Error('Conflict resolver timed out')), RESOLVE_TIMEOUT)),
321
- ]);
322
- this.supervisor.remove(workerId);
323
- if (exitCode === 0) {
324
- this.log(`Conflict resolver exited successfully (pid=${spawnedPid})`);
325
- return { ok: true };
326
- }
327
- else {
328
- return { ok: false, error: `Conflict resolver exited with code ${exitCode}` };
329
- }
330
- }
331
- catch (err) {
332
- this.resourceLimiter.release();
333
- this.supervisor.remove(workerId);
334
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
335
- }
336
- }
337
- async spawnAcpConflictResolver(ctx, attempt) {
338
- if (!this.agentRuntime) {
339
- return { ok: false, error: 'ACP runtime is not configured' };
340
- }
341
- const instruction = [
342
- `There is a merge conflict on branch ${ctx.branch} when rebasing onto origin/${ctx.baseBranch}.`,
343
- `Working directory: ${ctx.worktree}`,
344
- '',
345
- 'Please resolve the conflict:',
346
- `1. Run: cd ${ctx.worktree}`,
347
- `2. Run: git fetch origin && git checkout ${ctx.branch}`,
348
- `3. Run: git rebase origin/${ctx.baseBranch}`,
349
- '4. For each conflicting file: open it, understand both sides, resolve the conflict',
350
- '5. Run: git add <resolved-files> && git rebase --continue',
351
- '6. Run: git push --force-with-lease origin ' + ctx.branch,
352
- '7. Say "done"',
353
- '',
354
- 'You are resuming the same task session. Use the existing context to make correct conflict resolution decisions.',
355
- ].join('\n');
356
- try {
357
- const session = await this.resumeOrStartAgentRun(ctx, instruction, 'resolving', 'post-actions-conflict-resume', { mergeRetryIncrement: true });
358
- this.log(`Spawned ${ctx.transport.toUpperCase()} conflict resolver for ${ctx.project}:${ctx.slot}:${ctx.seq} ` +
359
- `(run=${session.currentRun?.runId || 'unknown'})`);
360
- const completed = await this.waitForAcpRun(ctx, session.currentRun?.runId || null, RESOLVE_TIMEOUT, 'resolving');
361
- if (!completed.currentRun) {
362
- return { ok: false, error: 'ACP conflict resolver finished without a run record' };
363
- }
364
- if (completed.currentRun.status === 'completed') {
365
- return { ok: true };
366
- }
367
- return { ok: false, error: `${ctx.transport.toUpperCase()} conflict resolver ended with ${completed.currentRun.status}` };
368
- }
369
- catch (err) {
370
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
371
- }
372
- }
373
- /**
374
- * Mark a card with CONFLICT label and comment.
375
- */
376
- async markConflict(ctx, results, reason) {
377
- this.log(`L3: Giving up on ${ctx.branch}: ${reason}`);
378
- try {
379
- await this.pmClient.addLabel(ctx.seq, 'CONFLICT');
380
- }
381
- catch { /* best effort */ }
382
- try {
383
- await this.pmClient.comment(ctx.seq, `Auto-merge failed: ${reason}`);
384
- }
385
- catch { /* best effort */ }
386
- results.push(await this.notify(ctx, `seq:${ctx.seq} CONFLICT — ${reason}`, 'error'));
387
- results.push({ step: 'merge-conflict', ok: false, error: reason });
388
- }
389
- /**
390
- * Update slot status in state.json.
391
- */
392
- setSlotStatus(ctx, status) {
393
- try {
394
- this.stateStore(ctx).updateState(`post-actions-${status}`, (state) => {
395
- if (state.workers[ctx.slot]) {
396
- state.workers[ctx.slot].status = status;
397
- if (status === 'merging' && !state.workers[ctx.slot].completedAt) {
398
- state.workers[ctx.slot].completedAt = new Date().toISOString();
399
- }
400
- }
401
- if (state.leases[ctx.seq]) {
402
- state.leases[ctx.seq].phase = status === 'merging' ? 'merging' : 'resolving_conflict';
403
- state.leases[ctx.seq].lastTransitionAt = new Date().toISOString();
404
- }
405
- });
406
- }
407
- catch { /* best effort */ }
408
- }
409
- // ─── Individual Steps (unchanged) ──────────────────────────────
116
+ // ─── Individual Steps ──────────────────────────────────────────
410
117
  async createMR(ctx) {
411
118
  try {
412
119
  const { branch, baseBranch, seq, gitlabProjectId, gitlabUrl, gitlabToken } = ctx;
@@ -535,11 +242,7 @@ export class PostActions {
535
242
  async respawn(ctx, sessionId, retryCount, respawnOpts) {
536
243
  try {
537
244
  const workerId = `${ctx.project}:${ctx.slot}:${ctx.seq}`;
538
- const resumePrompt = [
539
- 'The previous attempt did not fully complete. Please continue where you left off.',
540
- 'Check the current state of the code, and complete any remaining steps.',
541
- 'Remember to push your changes when done.',
542
- ].join('\n');
245
+ const resumePrompt = this.loadPhaseResumePrompt(ctx);
543
246
  const outputFile = resolve(ctx.logsDir, `${ctx.project}-${ctx.slot}-retry${retryCount + 1}-${Date.now()}.jsonl`);
544
247
  const acquire = this.resourceLimiter.tryAcquireDetailed();
545
248
  if (!acquire.acquired) {
@@ -585,11 +288,7 @@ export class PostActions {
585
288
  return { step: 'respawn-acp', ok: false, error: 'ACP runtime is not configured' };
586
289
  }
587
290
  try {
588
- const resumePrompt = [
589
- 'The previous attempt did not fully complete. Please continue where you left off.',
590
- 'Check the current state of the code, and complete any remaining steps.',
591
- 'Remember to push your changes when done.',
592
- ].join('\n');
291
+ const resumePrompt = this.loadPhaseResumePrompt(ctx);
593
292
  const session = await this.resumeOrStartAgentRun(ctx, resumePrompt, 'active', 'post-actions-retry-acp', {
594
293
  retryCount: retryCount + 1,
595
294
  });
@@ -660,69 +359,14 @@ export class PostActions {
660
359
  log(msg) {
661
360
  process.stderr.write(`[post-actions] ${msg}\n`);
662
361
  }
663
- resolveRepoDir(worktree) {
664
- const commonDir = execFileSync('git', ['-C', worktree, 'rev-parse', '--git-common-dir'], {
665
- timeout: 10_000,
666
- encoding: 'utf-8',
667
- stdio: ['ignore', 'pipe', 'pipe'],
668
- }).trim();
669
- const absCommonDir = commonDir.startsWith('/') ? commonDir : resolve(worktree, commonDir);
670
- return dirname(absCommonDir);
671
- }
672
- createMergeWorktree(repoDir, ctx) {
673
- const mergeRoot = mkdtempSync(resolve(tmpdir(), `sps-merge-${ctx.project}-${ctx.seq}-`));
674
- execFileSync('git', ['-C', repoDir, 'worktree', 'add', '--detach', mergeRoot, `origin/${ctx.baseBranch}`], {
675
- timeout: 30_000,
676
- stdio: ['ignore', 'pipe', 'pipe'],
677
- });
678
- return mergeRoot;
679
- }
680
- cleanupMergeWorktree(mergeWorktree) {
681
- if (!mergeWorktree)
682
- return;
683
- let repoDir = null;
684
- try {
685
- repoDir = this.resolveRepoDir(mergeWorktree);
686
- }
687
- catch {
688
- repoDir = null;
689
- }
690
- try {
691
- execFileSync('git', ['-C', repoDir || mergeWorktree, 'worktree', 'remove', '--force', mergeWorktree], {
692
- timeout: 10_000,
693
- stdio: ['ignore', 'pipe', 'pipe'],
694
- });
695
- }
696
- catch {
697
- try {
698
- rmSync(mergeWorktree, { recursive: true, force: true });
699
- }
700
- catch {
701
- // best effort
702
- }
703
- if (repoDir) {
704
- try {
705
- execFileSync('git', ['-C', repoDir, 'worktree', 'prune'], {
706
- timeout: 10_000,
707
- stdio: ['ignore', 'pipe', 'pipe'],
708
- });
709
- }
710
- catch {
711
- // best effort
712
- }
713
- }
714
- else {
715
- try {
716
- execFileSync('git', ['-C', mergeWorktree, 'worktree', 'prune'], {
717
- timeout: 10_000,
718
- stdio: ['ignore', 'pipe', 'pipe'],
719
- });
720
- }
721
- catch {
722
- // best effort
723
- }
724
- }
725
- }
362
+ loadPhaseResumePrompt(ctx) {
363
+ const phase = selectWorkerPhase(ctx.pmStateObserved ?? null, null);
364
+ const phasePromptPath = resolve(ctx.worktree, '.sps', promptFileForPhase(phase));
365
+ const legacyPromptPath = resolve(ctx.worktree, '.sps', LEGACY_TASK_PROMPT_FILE);
366
+ const originalPrompt = existsSync(phasePromptPath)
367
+ ? readFileSync(phasePromptPath, 'utf-8')
368
+ : (existsSync(legacyPromptPath) ? readFileSync(legacyPromptPath, 'utf-8') : null);
369
+ return buildResumePrompt(phase, ctx.worktree, ctx.branch, originalPrompt);
726
370
  }
727
371
  syncAcpRuntimeState(ctx, session, slotStatus, updatedBy, options) {
728
372
  const workerId = `${ctx.project}:${ctx.slot}:${ctx.seq}`;
@@ -746,9 +390,6 @@ export class PostActions {
746
390
  slot.pid = null;
747
391
  slot.outputFile = null;
748
392
  slot.exitCode = null;
749
- if (options?.mergeRetryIncrement) {
750
- slot.mergeRetries = (slot.mergeRetries ?? 0) + 1;
751
- }
752
393
  claimedAt = slot.claimedAt || nowIso;
753
394
  }
754
395
  if (state.leases[ctx.seq]) {
@@ -793,49 +434,6 @@ export class PostActions {
793
434
  exitedAt: null,
794
435
  });
795
436
  }
796
- async waitForAcpRun(ctx, runId, timeoutMs, slotStatus) {
797
- if (!this.agentRuntime) {
798
- throw new Error('ACP runtime is not configured');
799
- }
800
- const deadline = Date.now() + timeoutMs;
801
- while (Date.now() < deadline) {
802
- const inspected = await this.agentRuntime.inspect(ctx.slot);
803
- const session = inspected.sessions[ctx.slot];
804
- if (!session) {
805
- throw new Error(`ACP session for ${ctx.slot} was lost`);
806
- }
807
- this.syncAcpRuntimeState(ctx, session, slotStatus, 'post-actions-acp-poll');
808
- const currentRun = session.currentRun;
809
- if (runId && currentRun && currentRun.runId !== runId) {
810
- await this.sleep(PID_POLL_INTERVAL);
811
- continue;
812
- }
813
- if (currentRun && currentRun.status === 'waiting_input') {
814
- this.log(`${ctx.transport.toUpperCase()} run ${currentRun.runId} is waiting for input`);
815
- await this.sleep(PID_POLL_INTERVAL);
816
- continue;
817
- }
818
- if (currentRun && this.isTerminalAcpStatus(currentRun.status)) {
819
- const exitCode = currentRun.status === 'completed' ? 0 : 1;
820
- this.supervisor.updateAcpHandle(`${ctx.project}:${ctx.slot}:${ctx.seq}`, {
821
- exitCode,
822
- exitedAt: new Date().toISOString(),
823
- sessionState: session.sessionState,
824
- remoteStatus: currentRun.status,
825
- lastEventAt: session.lastSeenAt,
826
- });
827
- return session;
828
- }
829
- await this.sleep(PID_POLL_INTERVAL);
830
- }
831
- throw new Error(`ACP run timed out after ${Math.round(timeoutMs / 1000)}s`);
832
- }
833
- isTerminalAcpStatus(status) {
834
- return ['completed', 'failed', 'cancelled', 'lost'].includes(status);
835
- }
836
- async sleep(ms) {
837
- await new Promise((resolve) => setTimeout(resolve, ms));
838
- }
839
437
  }
840
438
  // ─── Helpers ────────────────────────────────────────────────────
841
439
  function safeExec(cmd, args) {