@astroanywhere/agent 0.4.0 → 0.4.2
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 +14 -0
- package/dist/lib/git-pr.d.ts +83 -11
- package/dist/lib/git-pr.d.ts.map +1 -1
- package/dist/lib/git-pr.js +249 -33
- package/dist/lib/git-pr.js.map +1 -1
- package/dist/lib/local-merge.d.ts +3 -1
- package/dist/lib/local-merge.d.ts.map +1 -1
- package/dist/lib/local-merge.js +29 -7
- package/dist/lib/local-merge.js.map +1 -1
- package/dist/lib/task-executor.d.ts.map +1 -1
- package/dist/lib/task-executor.js +101 -53
- package/dist/lib/task-executor.js.map +1 -1
- package/dist/lib/websocket-client.d.ts +6 -2
- package/dist/lib/websocket-client.d.ts.map +1 -1
- package/dist/lib/websocket-client.js +28 -5
- package/dist/lib/websocket-client.js.map +1 -1
- package/dist/lib/worktree.d.ts +5 -5
- package/dist/lib/worktree.d.ts.map +1 -1
- package/dist/lib/worktree.js +92 -48
- package/dist/lib/worktree.js.map +1 -1
- package/dist/providers/base-adapter.d.ts +29 -2
- package/dist/providers/base-adapter.d.ts.map +1 -1
- package/dist/providers/base-adapter.js +39 -1
- package/dist/providers/base-adapter.js.map +1 -1
- package/dist/providers/claude-sdk-adapter.d.ts +2 -4
- package/dist/providers/claude-sdk-adapter.d.ts.map +1 -1
- package/dist/providers/claude-sdk-adapter.js +46 -55
- package/dist/providers/claude-sdk-adapter.js.map +1 -1
- package/dist/providers/codex-adapter.d.ts.map +1 -1
- package/dist/providers/codex-adapter.js +6 -3
- package/dist/providers/codex-adapter.js.map +1 -1
- package/dist/providers/openclaw-adapter.d.ts.map +1 -1
- package/dist/providers/openclaw-adapter.js +4 -2
- package/dist/providers/openclaw-adapter.js.map +1 -1
- package/dist/providers/opencode-adapter.d.ts.map +1 -1
- package/dist/providers/opencode-adapter.js +6 -4
- package/dist/providers/opencode-adapter.js.map +1 -1
- package/dist/providers/pi-adapter.d.ts +7 -0
- package/dist/providers/pi-adapter.d.ts.map +1 -1
- package/dist/providers/pi-adapter.js +246 -70
- package/dist/providers/pi-adapter.js.map +1 -1
- package/dist/types.d.ts +18 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@ import { SlurmJobMonitor } from './slurm-job-monitor.js';
|
|
|
16
16
|
import { createWorktree, syncProjectWorktree } from './worktree.js';
|
|
17
17
|
import { ensureProjectWorkspace } from './workspace-root.js';
|
|
18
18
|
import { BranchLockManager } from './branch-lock.js';
|
|
19
|
-
import { pushAndCreatePR, mergePullRequest, getRemoteBranchSha, isGhAvailable } from './git-pr.js';
|
|
19
|
+
import { pushAndCreatePR, mergePullRequest, getRemoteBranchSha, isGhAvailable, getRepoSlug } from './git-pr.js';
|
|
20
20
|
import { localMergeIntoProjectBranch } from './local-merge.js';
|
|
21
21
|
import { checkWorkdirSafety, isGitAvailable, isGitRepo, isUntrackedInParentRepo, createSandbox, WorkdirSafetyTier, } from './workdir-safety.js';
|
|
22
22
|
import { initializeGit } from './git-bootstrap.js';
|
|
@@ -94,22 +94,45 @@ After you force-push, I will automatically retry the GitHub merge.`;
|
|
|
94
94
|
* existing merge retry loop handles real conflicts.
|
|
95
95
|
*/
|
|
96
96
|
async function tryPreMergeRebase(workdir, targetBranch, isRemote) {
|
|
97
|
+
// `git rebase` always operates on the currently checked-out branch (HEAD).
|
|
98
|
+
// It cannot rebase a detached branch ref. When the worktree is gone, HEAD
|
|
99
|
+
// in gitRoot is the user's main checkout (e.g., main) — NOT the task branch.
|
|
100
|
+
// Rebasing from gitRoot would corrupt the user's working directory.
|
|
101
|
+
// Skip the rebase entirely; the merge/PR flow handles conflicts anyway.
|
|
102
|
+
//
|
|
103
|
+
// Best-effort check — worktree could still be deleted after this (TOCTOU),
|
|
104
|
+
// but the git commands will fail safely and be caught by the outer catch.
|
|
105
|
+
try {
|
|
106
|
+
statSync(workdir);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
console.log(`[git] Worktree at ${workdir} gone — skipping rebase (cannot rebase non-checked-out branch)`);
|
|
110
|
+
return { rebased: false, skipped: true };
|
|
111
|
+
}
|
|
97
112
|
try {
|
|
98
113
|
const rebaseTarget = isRemote ? `origin/${targetBranch}` : targetBranch;
|
|
114
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
99
115
|
if (isRemote) {
|
|
100
|
-
|
|
116
|
+
console.log(`[git] fetch origin ${targetBranch} (cwd: ${workdir})`);
|
|
117
|
+
await execFileAsync('git', ['fetch', 'origin', targetBranch], { cwd: workdir, env: gitEnv, timeout: 30_000 });
|
|
101
118
|
}
|
|
102
119
|
// Check if rebase is needed (target branch moved since we branched)
|
|
103
|
-
|
|
104
|
-
const { stdout:
|
|
120
|
+
console.log(`[git] merge-base HEAD ${rebaseTarget} (cwd: ${workdir})`);
|
|
121
|
+
const { stdout: mergeBase } = await execFileAsync('git', ['merge-base', 'HEAD', rebaseTarget], { cwd: workdir, env: gitEnv });
|
|
122
|
+
const { stdout: targetTip } = await execFileAsync('git', ['rev-parse', rebaseTarget], { cwd: workdir, env: gitEnv });
|
|
105
123
|
if (mergeBase.trim() === targetTip.trim()) {
|
|
106
|
-
|
|
124
|
+
console.log(`[git] Already up to date (merge-base = target tip: ${targetTip.trim().slice(0, 7)})`);
|
|
125
|
+
return { rebased: false, skipped: true };
|
|
107
126
|
}
|
|
108
127
|
// Try automatic rebase — timeout after 60s (should be fast for non-conflicting changes)
|
|
109
|
-
|
|
128
|
+
console.log(`[git] rebase ${rebaseTarget} (cwd: ${workdir})`);
|
|
129
|
+
await execFileAsync('git', ['rebase', rebaseTarget], { cwd: workdir, env: gitEnv, timeout: 60_000 });
|
|
130
|
+
console.log(`[git] Rebase onto ${rebaseTarget} succeeded`);
|
|
110
131
|
return { rebased: true };
|
|
111
132
|
}
|
|
112
|
-
catch {
|
|
133
|
+
catch (err) {
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
console.warn(`[git] Rebase onto ${targetBranch} failed: ${msg}`);
|
|
113
136
|
// Abort on failure — existing retry loop will handle conflicts
|
|
114
137
|
await execFileAsync('git', ['rebase', '--abort'], { cwd: workdir }).catch(() => { });
|
|
115
138
|
return { rebased: false };
|
|
@@ -214,12 +237,15 @@ export class TaskExecutor {
|
|
|
214
237
|
}
|
|
215
238
|
this.claimedTasks.add(task.id);
|
|
216
239
|
try {
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
//
|
|
240
|
+
// Only execution tasks need workingDirectory resolution, safety checks,
|
|
241
|
+
// git diff, and summary generation. All other types (plan/chat/summarize/
|
|
242
|
+
// playground) skip this overhead but still get full tool access when they
|
|
243
|
+
// have a working directory.
|
|
244
|
+
const isExecutionTask = !task.type || task.type === 'execution';
|
|
245
|
+
// Non-execution tasks can run without a working directory.
|
|
246
|
+
// For execution tasks, resolve the directory or auto-provision one.
|
|
221
247
|
let resolvedWorkDir;
|
|
222
|
-
if (
|
|
248
|
+
if (!isExecutionTask && !task.workingDirectory) {
|
|
223
249
|
resolvedWorkDir = undefined;
|
|
224
250
|
}
|
|
225
251
|
else {
|
|
@@ -251,14 +277,14 @@ export class TaskExecutor {
|
|
|
251
277
|
// thinks worktree isolation is active and allows parallel execution, but
|
|
252
278
|
// prepareTaskWorkspace() later falls back to direct in-place execution —
|
|
253
279
|
// causing file conflicts when multiple tasks run on the same non-git directory.
|
|
254
|
-
const isGitDir =
|
|
280
|
+
const isGitDir = isExecutionTask && normalizedTask.workingDirectory && this.gitAvailable
|
|
255
281
|
? await isGitRepo(normalizedTask.workingDirectory)
|
|
256
282
|
: false;
|
|
257
283
|
const willUseWorktree = this.useWorktree
|
|
258
284
|
&& normalizedTask.useWorktree !== false
|
|
259
285
|
&& normalizedTask.deliveryMode !== 'direct'
|
|
260
286
|
&& (isGitDir || normalizedTask.deliveryMode === 'copy');
|
|
261
|
-
if (
|
|
287
|
+
if (isExecutionTask && task.skipSafetyCheck) {
|
|
262
288
|
// Server already approved safety for this directory — skip the prompt.
|
|
263
289
|
// Only init git when the original safety decision was 'init-git'.
|
|
264
290
|
// When safetyDecision is 'proceed' (user chose non-git direct execution)
|
|
@@ -279,7 +305,7 @@ export class TaskExecutor {
|
|
|
279
305
|
}
|
|
280
306
|
return;
|
|
281
307
|
}
|
|
282
|
-
if (
|
|
308
|
+
if (isExecutionTask) {
|
|
283
309
|
// Perform safety check (worktree flag affects tier assignment)
|
|
284
310
|
const safetyCheck = await this.performSafetyCheck(normalizedTask, willUseWorktree);
|
|
285
311
|
// Handle safety tiers
|
|
@@ -571,11 +597,14 @@ export class TaskExecutor {
|
|
|
571
597
|
text: (data) => {
|
|
572
598
|
this.wsClient.sendTaskText(taskId, data, textSequence++);
|
|
573
599
|
},
|
|
574
|
-
|
|
575
|
-
this.wsClient.
|
|
600
|
+
operational: (message, source) => {
|
|
601
|
+
this.wsClient.sendTaskOperational(taskId, message, source);
|
|
602
|
+
},
|
|
603
|
+
toolUse: (toolName, toolInput, toolUseId) => {
|
|
604
|
+
this.wsClient.sendTaskToolUse(taskId, toolName, toolInput, toolUseId);
|
|
576
605
|
},
|
|
577
|
-
toolResult: (toolName, result, success) => {
|
|
578
|
-
this.wsClient.sendTaskToolResult(taskId, toolName, result, success);
|
|
606
|
+
toolResult: (toolName, result, success, toolUseId) => {
|
|
607
|
+
this.wsClient.sendTaskToolResult(taskId, toolName, result, success, toolUseId);
|
|
579
608
|
},
|
|
580
609
|
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
581
610
|
this.wsClient.sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff);
|
|
@@ -607,6 +636,7 @@ export class TaskExecutor {
|
|
|
607
636
|
projectBranch: meta.projectBranch,
|
|
608
637
|
stdout: stream.stdout,
|
|
609
638
|
stderr: stream.stderr,
|
|
639
|
+
operational: stream.operational,
|
|
610
640
|
});
|
|
611
641
|
if (worktree) {
|
|
612
642
|
resumeDir = worktree.workingDirectory;
|
|
@@ -902,13 +932,17 @@ export class TaskExecutor {
|
|
|
902
932
|
resetIdleTimeout();
|
|
903
933
|
this.wsClient.sendTaskText(normalizedTask.id, data, textSequence++);
|
|
904
934
|
},
|
|
905
|
-
|
|
935
|
+
operational: (message, source) => {
|
|
936
|
+
resetIdleTimeout();
|
|
937
|
+
this.wsClient.sendTaskOperational(normalizedTask.id, message, source);
|
|
938
|
+
},
|
|
939
|
+
toolUse: (toolName, toolInput, toolUseId) => {
|
|
906
940
|
resetIdleTimeout();
|
|
907
|
-
this.wsClient.sendTaskToolUse(normalizedTask.id, toolName, toolInput);
|
|
941
|
+
this.wsClient.sendTaskToolUse(normalizedTask.id, toolName, toolInput, toolUseId);
|
|
908
942
|
},
|
|
909
|
-
toolResult: (toolName, result, success) => {
|
|
943
|
+
toolResult: (toolName, result, success, toolUseId) => {
|
|
910
944
|
resetIdleTimeout();
|
|
911
|
-
this.wsClient.sendTaskToolResult(normalizedTask.id, toolName, result, success);
|
|
945
|
+
this.wsClient.sendTaskToolResult(normalizedTask.id, toolName, result, success, toolUseId);
|
|
912
946
|
},
|
|
913
947
|
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
914
948
|
resetIdleTimeout();
|
|
@@ -947,20 +981,22 @@ export class TaskExecutor {
|
|
|
947
981
|
// 30s aligns with the server's heartbeat check interval and is well under
|
|
948
982
|
// the 3-minute startup timeout (STARTUP_TIMEOUT_MS in dispatch.ts).
|
|
949
983
|
const TASK_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
950
|
-
let taskHeartbeatPhase = 'preparing';
|
|
951
984
|
let heartbeatSeq = 0;
|
|
952
985
|
const taskHeartbeatTimer = setInterval(() => {
|
|
953
986
|
// Send directly via wsClient to keep the server's activity timer alive
|
|
954
987
|
// WITHOUT resetting the agent-side idle timeout. stream.text() calls
|
|
955
988
|
// resetIdleTimeout(), which would make a hung agent run until hard cap
|
|
956
989
|
// instead of idle-timing out after 15 minutes of no real activity.
|
|
957
|
-
|
|
990
|
+
// Heartbeat text is intentionally empty — it exists only to keep the
|
|
991
|
+
// server's activity timer alive, not to display anything to the user.
|
|
992
|
+
this.wsClient.sendTaskText(normalizedTask.id, '', -(++heartbeatSeq));
|
|
958
993
|
}, TASK_HEARTBEAT_INTERVAL_MS);
|
|
959
|
-
//
|
|
960
|
-
|
|
994
|
+
// Only execution tasks get git diff, delivery, and summary — all other types (chat, plan, playground, summarize) skip them.
|
|
995
|
+
// These non-execution types still get full tool access when they have a working directory.
|
|
996
|
+
const isExecutionTask = !normalizedTask.type || normalizedTask.type === 'execution';
|
|
961
997
|
let prepared;
|
|
962
998
|
try {
|
|
963
|
-
prepared =
|
|
999
|
+
prepared = !isExecutionTask && !normalizedTask.workingDirectory
|
|
964
1000
|
? { workingDirectory: '', cleanup: async () => { } }
|
|
965
1001
|
: await this.prepareTaskWorkspace(normalizedTask, stream, abortController.signal);
|
|
966
1002
|
}
|
|
@@ -975,7 +1011,6 @@ export class TaskExecutor {
|
|
|
975
1011
|
}
|
|
976
1012
|
const taskWithWorkspace = { ...normalizedTask, workingDirectory: prepared.workingDirectory };
|
|
977
1013
|
runningTask.task = taskWithWorkspace;
|
|
978
|
-
taskHeartbeatPhase = 'executing';
|
|
979
1014
|
console.log(`[executor] Task ${task.id}: workspace prepared, cwd=${prepared.workingDirectory}`);
|
|
980
1015
|
// Execute with idle timeout + hard cap.
|
|
981
1016
|
// Idle timeout resets on every stream activity (text, tool, file, etc.).
|
|
@@ -999,7 +1034,7 @@ export class TaskExecutor {
|
|
|
999
1034
|
// Resume existing session if resumeSessionId is provided
|
|
1000
1035
|
const canResume = taskWithWorkspace.resumeSessionId
|
|
1001
1036
|
&& this.isResumableAdapter(adapter)
|
|
1002
|
-
&&
|
|
1037
|
+
&& !isExecutionTask;
|
|
1003
1038
|
let result;
|
|
1004
1039
|
if (canResume) {
|
|
1005
1040
|
console.log(`[executor] Task ${task.id}: resuming session ${taskWithWorkspace.resumeSessionId} with ${adapter.name} (type=${adapter.type}, hasMessages=${!!taskWithWorkspace.messages?.length})...`);
|
|
@@ -1052,7 +1087,7 @@ export class TaskExecutor {
|
|
|
1052
1087
|
});
|
|
1053
1088
|
}
|
|
1054
1089
|
// Emit accurate file change events via git diff (covers all change methods)
|
|
1055
|
-
if (
|
|
1090
|
+
if (isExecutionTask && prepared.workingDirectory) {
|
|
1056
1091
|
await this.emitGitDiffFileChanges(task.id, prepared.workingDirectory, prepared.commitBeforeSha, stream);
|
|
1057
1092
|
}
|
|
1058
1093
|
// Delivery-mode-aware result handling
|
|
@@ -1070,7 +1105,7 @@ export class TaskExecutor {
|
|
|
1070
1105
|
if (summary) {
|
|
1071
1106
|
console.log(`[executor] Task ${task.id}: summary available — status=${summary.status}, workCompleted=${!!summary.workCompleted}, executiveSummary=${!!summary.executiveSummary}, keyFindings=${summary.keyFindings?.length ?? 0}, filesChanged=${summary.filesChanged?.length ?? 0}`);
|
|
1072
1107
|
}
|
|
1073
|
-
else if (
|
|
1108
|
+
else if (isExecutionTask) {
|
|
1074
1109
|
console.warn(`[executor] Task ${task.id}: no summary available for PR body`);
|
|
1075
1110
|
}
|
|
1076
1111
|
const rawTitle = summary?.workCompleted || task.title || task.prompt.slice(0, 100);
|
|
@@ -1078,9 +1113,12 @@ export class TaskExecutor {
|
|
|
1078
1113
|
? `[${task.shortProjectId}/${task.shortNodeId}] ${rawTitle}`
|
|
1079
1114
|
: rawTitle;
|
|
1080
1115
|
if (prepared.branchName && result.status === 'completed') {
|
|
1081
|
-
|
|
1082
|
-
stream.text?.(`\n[Astro] Delivering changes (mode: ${deliveryMode})...\n`);
|
|
1116
|
+
stream.operational?.(`Delivering changes (mode: ${deliveryMode})...`, 'astro');
|
|
1083
1117
|
this.wsClient.sendTaskStatus(task.id, 'running', 90, 'Delivering changes...');
|
|
1118
|
+
// Resolve GitHub repo slug (OWNER/REPO) for explicit gh --repo targeting.
|
|
1119
|
+
// This makes gh pr create/merge independent of local filesystem state
|
|
1120
|
+
// (worktree may be cleaned up by the agent during execution).
|
|
1121
|
+
const repoSlug = prepared.gitRoot ? await getRepoSlug(prepared.gitRoot) : undefined;
|
|
1084
1122
|
// Build PR body: enrich with summary data when available
|
|
1085
1123
|
const prBodyParts = [];
|
|
1086
1124
|
if (summary) {
|
|
@@ -1113,14 +1151,16 @@ export class TaskExecutor {
|
|
|
1113
1151
|
try {
|
|
1114
1152
|
if (deliveryMode === 'direct') {
|
|
1115
1153
|
// No git delivery — files modified in-place
|
|
1154
|
+
stream.operational?.('Changes applied in-place (direct mode)', 'delivery');
|
|
1116
1155
|
console.log(`[executor] Task ${task.id}: direct mode, skipping git delivery`);
|
|
1117
1156
|
}
|
|
1118
1157
|
else if (deliveryMode === 'copy') {
|
|
1119
1158
|
// Copy mode: worktree preserved, no git operations
|
|
1159
|
+
stream.operational?.('Worktree preserved (copy mode)', 'delivery');
|
|
1120
1160
|
console.log(`[executor] Task ${task.id}: copy mode, worktree preserved at ${prepared.workingDirectory}`);
|
|
1121
1161
|
}
|
|
1122
1162
|
else if (deliveryMode === 'branch') {
|
|
1123
|
-
stream.
|
|
1163
|
+
stream.operational?.(`Merging into project branch ${prepared.projectBranch ?? 'local'}...`, 'git');
|
|
1124
1164
|
// Branch mode: commit locally, merge into project branch if available.
|
|
1125
1165
|
// The merge lock is held only during the squash-merge (seconds, not minutes),
|
|
1126
1166
|
// allowing tasks to execute in parallel. The squash merge naturally handles
|
|
@@ -1138,9 +1178,11 @@ export class TaskExecutor {
|
|
|
1138
1178
|
// don't overlap, and saves one retry cycle when they do.
|
|
1139
1179
|
const preRebase = await tryPreMergeRebase(prepared.workingDirectory, prepared.projectBranch, false);
|
|
1140
1180
|
if (preRebase.rebased) {
|
|
1181
|
+
stream.operational?.(`Rebased onto ${prepared.projectBranch}`, 'git');
|
|
1141
1182
|
console.log(`[executor] Task ${task.id}: pre-merge rebase onto ${prepared.projectBranch} succeeded`);
|
|
1142
1183
|
}
|
|
1143
1184
|
else if (!preRebase.skipped) {
|
|
1185
|
+
stream.operational?.('Rebase skipped (conflicts), retrying via merge', 'git');
|
|
1144
1186
|
console.log(`[executor] Task ${task.id}: pre-merge rebase had conflicts, falling back to merge retry loop`);
|
|
1145
1187
|
}
|
|
1146
1188
|
const mergeLockKey = BranchLockManager.computeLockKey(prepared.gitRoot, task.shortProjectId, task.shortNodeId, task.id);
|
|
@@ -1153,7 +1195,7 @@ export class TaskExecutor {
|
|
|
1153
1195
|
let mergeResult;
|
|
1154
1196
|
try {
|
|
1155
1197
|
this.wsClient.sendTaskStatus(task.id, 'running', 96, 'Merging into project branch...');
|
|
1156
|
-
mergeResult = await localMergeIntoProjectBranch(prepared.gitRoot, prepared.branchName, prepared.projectBranch, commitMessage);
|
|
1198
|
+
mergeResult = await localMergeIntoProjectBranch(prepared.gitRoot, prepared.branchName, prepared.projectBranch, commitMessage, (msg) => stream.operational?.(msg, 'git'));
|
|
1157
1199
|
}
|
|
1158
1200
|
finally {
|
|
1159
1201
|
mergeLock.release();
|
|
@@ -1162,7 +1204,7 @@ export class TaskExecutor {
|
|
|
1162
1204
|
if (mergeResult.merged) {
|
|
1163
1205
|
result.deliveryStatus = 'success';
|
|
1164
1206
|
result.commitAfterSha = mergeResult.commitSha;
|
|
1165
|
-
stream.
|
|
1207
|
+
stream.operational?.(`Merged into ${prepared.projectBranch} (${mergeResult.commitSha?.slice(0, 7)})`, 'git');
|
|
1166
1208
|
console.log(`[executor] Task ${task.id}: merged into ${prepared.projectBranch} (${mergeResult.commitSha})`);
|
|
1167
1209
|
// Sync project worktree to reflect the merged changes on disk
|
|
1168
1210
|
if (prepared.projectWorktreePath && prepared.projectBranch && prepared.gitRoot) {
|
|
@@ -1178,7 +1220,7 @@ export class TaskExecutor {
|
|
|
1178
1220
|
: null;
|
|
1179
1221
|
if (taskContext?.sessionId && this.isResumableAdapter(adapter) && attempt < MAX_MERGE_ATTEMPTS) {
|
|
1180
1222
|
const conflictFiles = mergeResult.conflictFiles?.join(', ') ?? 'unknown files';
|
|
1181
|
-
stream.
|
|
1223
|
+
stream.operational?.(`Merge conflict: ${conflictFiles} — agent resolving (attempt ${attempt})...`, 'git');
|
|
1182
1224
|
console.log(`[executor] Task ${task.id}: merge conflict (attempt ${attempt}), resuming ${adapter.name} to resolve: ${conflictFiles}`);
|
|
1183
1225
|
this.wsClient.sendTaskStatus(task.id, 'running', 97, `Merge conflict — agent resolving (attempt ${attempt})...`);
|
|
1184
1226
|
// Resume agent session with conflict resolution instructions.
|
|
@@ -1227,7 +1269,7 @@ export class TaskExecutor {
|
|
|
1227
1269
|
}
|
|
1228
1270
|
else if (deliveryMode === 'push') {
|
|
1229
1271
|
// Push branch to remote, but don't create a PR — user creates PR manually
|
|
1230
|
-
stream.
|
|
1272
|
+
stream.operational?.(`Pushing ${prepared.branchName} to origin...`, 'git');
|
|
1231
1273
|
this.wsClient.sendTaskStatus(task.id, 'running', 95, 'Pushing branch...');
|
|
1232
1274
|
console.log(`[executor] Task ${task.id}: push mode, pushing branch ${prepared.branchName}`);
|
|
1233
1275
|
const prResult = await pushAndCreatePR(prepared.workingDirectory, {
|
|
@@ -1236,30 +1278,31 @@ export class TaskExecutor {
|
|
|
1236
1278
|
taskDescription: task.description || task.prompt.slice(0, 500),
|
|
1237
1279
|
skipPR: true,
|
|
1238
1280
|
baseBranch: prepared.baseBranch,
|
|
1281
|
+
gitRoot: prepared.gitRoot,
|
|
1239
1282
|
});
|
|
1240
1283
|
result.branchName = prResult.branchName;
|
|
1241
1284
|
if (prResult.error) {
|
|
1242
1285
|
// Delivery failure — don't override execution status
|
|
1243
1286
|
result.deliveryStatus = 'failed';
|
|
1244
1287
|
result.deliveryError = `Push delivery failed: ${prResult.error}`;
|
|
1245
|
-
stream.
|
|
1288
|
+
stream.operational?.(`Push failed: ${prResult.error}`, 'git');
|
|
1246
1289
|
console.error(`[executor] Task ${task.id}: push delivery failed: ${prResult.error}`);
|
|
1247
1290
|
}
|
|
1248
1291
|
else if (prResult.pushed) {
|
|
1249
1292
|
result.deliveryStatus = 'success';
|
|
1250
1293
|
keepBranch = true;
|
|
1251
|
-
stream.
|
|
1294
|
+
stream.operational?.(`Pushed to origin: ${prepared.branchName}`, 'git');
|
|
1252
1295
|
console.log(`[executor] Task ${task.id}: branch pushed (${prepared.branchName})`);
|
|
1253
1296
|
}
|
|
1254
1297
|
else {
|
|
1255
1298
|
result.deliveryStatus = 'skipped';
|
|
1256
|
-
stream.
|
|
1299
|
+
stream.operational?.('No changes to push', 'git');
|
|
1257
1300
|
console.log(`[executor] Task ${task.id}: no changes to push`);
|
|
1258
1301
|
}
|
|
1259
1302
|
}
|
|
1260
1303
|
else {
|
|
1261
1304
|
// 'pr' — push + create PR, auto-merge into project branch if applicable
|
|
1262
|
-
stream.
|
|
1305
|
+
stream.operational?.(`Creating PR: ${prepared.branchName} → ${prepared.baseBranch ?? 'main'}...`, 'git');
|
|
1263
1306
|
this.wsClient.sendTaskStatus(task.id, 'running', 94, 'Rebasing before push...');
|
|
1264
1307
|
// Pre-push rebase: if the target branch moved forward, rebase our task
|
|
1265
1308
|
// branch so the PR will be cleanly mergeable. The branch hasn't been
|
|
@@ -1267,9 +1310,11 @@ export class TaskExecutor {
|
|
|
1267
1310
|
if (prepared.baseBranch) {
|
|
1268
1311
|
const preRebase = await tryPreMergeRebase(prepared.workingDirectory, prepared.baseBranch, true);
|
|
1269
1312
|
if (preRebase.rebased) {
|
|
1313
|
+
stream.operational?.(`Rebased onto origin/${prepared.baseBranch}`, 'git');
|
|
1270
1314
|
console.log(`[executor] Task ${task.id}: pre-push rebase onto origin/${prepared.baseBranch} succeeded`);
|
|
1271
1315
|
}
|
|
1272
1316
|
else if (!preRebase.skipped) {
|
|
1317
|
+
stream.operational?.('Rebase skipped (conflicts), proceeding without rebase', 'git');
|
|
1273
1318
|
console.log(`[executor] Task ${task.id}: pre-push rebase had conflicts, proceeding without rebase`);
|
|
1274
1319
|
}
|
|
1275
1320
|
}
|
|
@@ -1284,6 +1329,7 @@ export class TaskExecutor {
|
|
|
1284
1329
|
baseBranch: prepared.baseBranch,
|
|
1285
1330
|
autoMerge: hasProjectBranch,
|
|
1286
1331
|
commitBeforeSha: prepared.commitBeforeSha,
|
|
1332
|
+
gitRoot: prepared.gitRoot,
|
|
1287
1333
|
});
|
|
1288
1334
|
result.branchName = prResult.branchName;
|
|
1289
1335
|
if (prResult.prUrl) {
|
|
@@ -1292,7 +1338,7 @@ export class TaskExecutor {
|
|
|
1292
1338
|
result.commitBeforeSha = prResult.commitBeforeSha;
|
|
1293
1339
|
result.commitAfterSha = prResult.commitAfterSha;
|
|
1294
1340
|
keepBranch = true;
|
|
1295
|
-
stream.
|
|
1341
|
+
stream.operational?.(`PR created: ${prResult.prUrl}`, 'git');
|
|
1296
1342
|
if (prResult.autoMergeFailed) {
|
|
1297
1343
|
// PR was created but auto-merge failed (likely conflict).
|
|
1298
1344
|
// If the adapter supports session resume, ask the agent to
|
|
@@ -1304,7 +1350,7 @@ export class TaskExecutor {
|
|
|
1304
1350
|
: null;
|
|
1305
1351
|
if (prTaskContext?.sessionId && this.isResumableAdapter(adapter) && hasProjectBranch && prepared.branchName && prepared.baseBranch && prResult.prNumber && prepared.gitRoot) {
|
|
1306
1352
|
for (let attempt = 1; attempt <= MAX_PR_MERGE_ATTEMPTS; attempt++) {
|
|
1307
|
-
stream.
|
|
1353
|
+
stream.operational?.(`Auto-merge failed — agent resolving conflict (attempt ${attempt})...`, 'git');
|
|
1308
1354
|
console.log(`[executor] Task ${task.id}: PR auto-merge failed (attempt ${attempt}), resuming ${adapter.name} to resolve`);
|
|
1309
1355
|
this.wsClient.sendTaskStatus(task.id, 'running', 97, `PR merge conflict — agent resolving (attempt ${attempt})...`);
|
|
1310
1356
|
// Resume agent session — agent rebases and force-pushes.
|
|
@@ -1318,11 +1364,14 @@ export class TaskExecutor {
|
|
|
1318
1364
|
result.deliveryError = `PR created but auto-merge failed. Agent resolution failed: ${resumeErr instanceof Error ? resumeErr.message : resumeErr}`;
|
|
1319
1365
|
break;
|
|
1320
1366
|
}
|
|
1321
|
-
// Retry the GitHub merge
|
|
1367
|
+
// Retry the GitHub merge.
|
|
1368
|
+
// Use gitRoot as cwd (always valid, guarded at L1518) instead of
|
|
1369
|
+
// worktree path which may have been deleted during resolution.
|
|
1322
1370
|
this.wsClient.sendTaskStatus(task.id, 'running', 98, `Retrying PR merge (attempt ${attempt})...`);
|
|
1323
|
-
const retryMerge = await mergePullRequest(prepared.
|
|
1371
|
+
const retryMerge = await mergePullRequest(prepared.gitRoot, prResult.prNumber, {
|
|
1324
1372
|
method: 'squash',
|
|
1325
1373
|
deleteBranch: true,
|
|
1374
|
+
repoSlug: repoSlug ?? undefined,
|
|
1326
1375
|
});
|
|
1327
1376
|
if (retryMerge.ok) {
|
|
1328
1377
|
result.commitAfterSha = await getRemoteBranchSha(prepared.gitRoot, prepared.baseBranch) ?? undefined;
|
|
@@ -1362,18 +1411,18 @@ export class TaskExecutor {
|
|
|
1362
1411
|
result.deliveryStatus = 'failed';
|
|
1363
1412
|
result.deliveryError = `PR delivery failed: ${prResult.error}`;
|
|
1364
1413
|
keepBranch = prResult.pushed ?? false; // Keep branch if it was pushed
|
|
1365
|
-
stream.
|
|
1414
|
+
stream.operational?.(`PR delivery failed: ${prResult.error}`, 'git');
|
|
1366
1415
|
console.error(`[executor] Task ${task.id}: PR delivery failed: ${prResult.error}`);
|
|
1367
1416
|
}
|
|
1368
1417
|
else {
|
|
1369
|
-
stream.
|
|
1418
|
+
stream.operational?.('No changes to deliver', 'git');
|
|
1370
1419
|
console.log(`[executor] Task ${task.id}: no changes to push`);
|
|
1371
1420
|
}
|
|
1372
1421
|
}
|
|
1373
1422
|
}
|
|
1374
1423
|
catch (prError) {
|
|
1375
1424
|
const prMsg = prError instanceof Error ? prError.message : String(prError);
|
|
1376
|
-
stream.
|
|
1425
|
+
stream.operational?.(`Failed: ${prMsg}`, 'delivery');
|
|
1377
1426
|
console.error(`[executor] Task ${task.id}: delivery (${deliveryMode}) failed: ${prMsg}`);
|
|
1378
1427
|
// Delivery failure — don't override execution status
|
|
1379
1428
|
result.deliveryStatus = 'failed';
|
|
@@ -1430,14 +1479,13 @@ export class TaskExecutor {
|
|
|
1430
1479
|
clearTimeout(hardCapTimeoutId);
|
|
1431
1480
|
if (idleTimerId !== undefined)
|
|
1432
1481
|
clearTimeout(idleTimerId);
|
|
1433
|
-
taskHeartbeatPhase = 'cleaning up';
|
|
1434
1482
|
// Always cleanup the local worktree directory to reclaim disk space
|
|
1435
1483
|
// (node_modules alone is ~680MB per worktree). When keepBranch is true
|
|
1436
1484
|
// (PR created or branch pushed), we preserve the git branch but still
|
|
1437
1485
|
// remove the working copy — the branch lives on remote/local refs,
|
|
1438
1486
|
// and re-execution will create a fresh worktree if needed.
|
|
1439
1487
|
if (prepared.branchName) {
|
|
1440
|
-
stream.
|
|
1488
|
+
stream.operational?.(`Cleaning up worktree (branch: ${prepared.branchName})...`, 'astro');
|
|
1441
1489
|
}
|
|
1442
1490
|
if (this.preserveWorktrees) {
|
|
1443
1491
|
console.log(`[executor] Task ${task.id}: worktree preserved (debug mode)`);
|
|
@@ -1596,7 +1644,7 @@ export class TaskExecutor {
|
|
|
1596
1644
|
projectBranch: task.projectBranch,
|
|
1597
1645
|
stdout: stream.stdout,
|
|
1598
1646
|
stderr: stream.stderr,
|
|
1599
|
-
|
|
1647
|
+
operational: stream.operational,
|
|
1600
1648
|
signal,
|
|
1601
1649
|
});
|
|
1602
1650
|
if (!worktree) {
|