@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.
- package/README.md +5 -7
- package/dist/commands/pipelineTick.js +1 -1
- package/dist/commands/pipelineTick.js.map +1 -1
- package/dist/commands/tick.d.ts.map +1 -1
- package/dist/commands/tick.js +2 -6
- package/dist/commands/tick.js.map +1 -1
- package/dist/commands/workerLaunch.js +1 -1
- package/dist/commands/workerLaunch.js.map +1 -1
- package/dist/engines/CloseoutEngine.d.ts +0 -10
- package/dist/engines/CloseoutEngine.d.ts.map +1 -1
- package/dist/engines/CloseoutEngine.js +0 -358
- package/dist/engines/CloseoutEngine.js.map +1 -1
- package/dist/engines/ExecutionEngine.d.ts.map +1 -1
- package/dist/engines/ExecutionEngine.js +2 -1
- package/dist/engines/ExecutionEngine.js.map +1 -1
- package/dist/manager/completion-judge.d.ts +13 -5
- package/dist/manager/completion-judge.d.ts.map +1 -1
- package/dist/manager/completion-judge.js +43 -36
- package/dist/manager/completion-judge.js.map +1 -1
- package/dist/manager/post-actions.d.ts +2 -43
- package/dist/manager/post-actions.d.ts.map +1 -1
- package/dist/manager/post-actions.js +18 -420
- package/dist/manager/post-actions.js.map +1 -1
- package/dist/manager/recovery.d.ts.map +1 -1
- package/dist/manager/recovery.js +4 -0
- package/dist/manager/recovery.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
|
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":"
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
12
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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
|
-
// ───
|
|
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
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return
|
|
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) {
|