@astroanywhere/agent 0.3.2 → 0.3.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"task-executor.d.ts","sourceRoot":"","sources":["../../src/lib/task-executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-executor.d.ts","sourceRoot":"","sources":["../../src/lib/task-executor.ts"],"names":[],"mappings":"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"}
@@ -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
- await execFileAsync('git', ['fetch', 'origin', targetBranch], { cwd: workdir, timeout: 30_000 });
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
- const { stdout: mergeBase } = await execFileAsync('git', ['merge-base', 'HEAD', rebaseTarget], { cwd: workdir });
104
- const { stdout: targetTip } = await execFileAsync('git', ['rev-parse', rebaseTarget], { cwd: workdir });
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
- return { rebased: false, skipped: true }; // Already up to date
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
- await execFileAsync('git', ['rebase', rebaseTarget], { cwd: workdir, timeout: 60_000 });
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 };
@@ -571,11 +594,11 @@ export class TaskExecutor {
571
594
  text: (data) => {
572
595
  this.wsClient.sendTaskText(taskId, data, textSequence++);
573
596
  },
574
- toolUse: (toolName, toolInput) => {
575
- this.wsClient.sendTaskToolUse(taskId, toolName, toolInput);
597
+ toolUse: (toolName, toolInput, toolUseId) => {
598
+ this.wsClient.sendTaskToolUse(taskId, toolName, toolInput, toolUseId);
576
599
  },
577
- toolResult: (toolName, result, success) => {
578
- this.wsClient.sendTaskToolResult(taskId, toolName, result, success);
600
+ toolResult: (toolName, result, success, toolUseId) => {
601
+ this.wsClient.sendTaskToolResult(taskId, toolName, result, success, toolUseId);
579
602
  },
580
603
  fileChange: (path, action, linesAdded, linesRemoved, diff) => {
581
604
  this.wsClient.sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff);
@@ -902,13 +925,13 @@ export class TaskExecutor {
902
925
  resetIdleTimeout();
903
926
  this.wsClient.sendTaskText(normalizedTask.id, data, textSequence++);
904
927
  },
905
- toolUse: (toolName, toolInput) => {
928
+ toolUse: (toolName, toolInput, toolUseId) => {
906
929
  resetIdleTimeout();
907
- this.wsClient.sendTaskToolUse(normalizedTask.id, toolName, toolInput);
930
+ this.wsClient.sendTaskToolUse(normalizedTask.id, toolName, toolInput, toolUseId);
908
931
  },
909
- toolResult: (toolName, result, success) => {
932
+ toolResult: (toolName, result, success, toolUseId) => {
910
933
  resetIdleTimeout();
911
- this.wsClient.sendTaskToolResult(normalizedTask.id, toolName, result, success);
934
+ this.wsClient.sendTaskToolResult(normalizedTask.id, toolName, result, success, toolUseId);
912
935
  },
913
936
  fileChange: (path, action, linesAdded, linesRemoved, diff) => {
914
937
  resetIdleTimeout();
@@ -947,14 +970,15 @@ export class TaskExecutor {
947
970
  // 30s aligns with the server's heartbeat check interval and is well under
948
971
  // the 3-minute startup timeout (STARTUP_TIMEOUT_MS in dispatch.ts).
949
972
  const TASK_HEARTBEAT_INTERVAL_MS = 30_000;
950
- let taskHeartbeatPhase = 'preparing';
951
973
  let heartbeatSeq = 0;
952
974
  const taskHeartbeatTimer = setInterval(() => {
953
975
  // Send directly via wsClient to keep the server's activity timer alive
954
976
  // WITHOUT resetting the agent-side idle timeout. stream.text() calls
955
977
  // resetIdleTimeout(), which would make a hung agent run until hard cap
956
978
  // instead of idle-timing out after 15 minutes of no real activity.
957
- this.wsClient.sendTaskText(normalizedTask.id, `[Astro] Heartbeat — ${taskHeartbeatPhase}...\n`, -(++heartbeatSeq));
979
+ // Heartbeat text is intentionally empty it exists only to keep the
980
+ // server's activity timer alive, not to display anything to the user.
981
+ this.wsClient.sendTaskText(normalizedTask.id, '', -(++heartbeatSeq));
958
982
  }, TASK_HEARTBEAT_INTERVAL_MS);
959
983
  // Text-only tasks (plan/chat/summarize) without a working directory skip workspace prep
960
984
  const isTextOnly = normalizedTask.type === 'summarize' || normalizedTask.type === 'chat' || normalizedTask.type === 'plan';
@@ -975,7 +999,6 @@ export class TaskExecutor {
975
999
  }
976
1000
  const taskWithWorkspace = { ...normalizedTask, workingDirectory: prepared.workingDirectory };
977
1001
  runningTask.task = taskWithWorkspace;
978
- taskHeartbeatPhase = 'executing';
979
1002
  console.log(`[executor] Task ${task.id}: workspace prepared, cwd=${prepared.workingDirectory}`);
980
1003
  // Execute with idle timeout + hard cap.
981
1004
  // Idle timeout resets on every stream activity (text, tool, file, etc.).
@@ -1078,9 +1101,12 @@ export class TaskExecutor {
1078
1101
  ? `[${task.shortProjectId}/${task.shortNodeId}] ${rawTitle}`
1079
1102
  : rawTitle;
1080
1103
  if (prepared.branchName && result.status === 'completed') {
1081
- taskHeartbeatPhase = 'delivering';
1082
- stream.text?.(`\n[Astro] Delivering changes (mode: ${deliveryMode})...\n`);
1104
+ stream.text?.(`\n── [Astro] Delivering changes (mode: ${deliveryMode})...\n`);
1083
1105
  this.wsClient.sendTaskStatus(task.id, 'running', 90, 'Delivering changes...');
1106
+ // Resolve GitHub repo slug (OWNER/REPO) for explicit gh --repo targeting.
1107
+ // This makes gh pr create/merge independent of local filesystem state
1108
+ // (worktree may be cleaned up by the agent during execution).
1109
+ const repoSlug = prepared.gitRoot ? await getRepoSlug(prepared.gitRoot) : undefined;
1084
1110
  // Build PR body: enrich with summary data when available
1085
1111
  const prBodyParts = [];
1086
1112
  if (summary) {
@@ -1113,14 +1139,16 @@ export class TaskExecutor {
1113
1139
  try {
1114
1140
  if (deliveryMode === 'direct') {
1115
1141
  // No git delivery — files modified in-place
1142
+ stream.text?.(`── [Delivery] Changes applied in-place (direct mode)\n`);
1116
1143
  console.log(`[executor] Task ${task.id}: direct mode, skipping git delivery`);
1117
1144
  }
1118
1145
  else if (deliveryMode === 'copy') {
1119
1146
  // Copy mode: worktree preserved, no git operations
1147
+ stream.text?.(`── [Delivery] Worktree preserved (copy mode)\n`);
1120
1148
  console.log(`[executor] Task ${task.id}: copy mode, worktree preserved at ${prepared.workingDirectory}`);
1121
1149
  }
1122
1150
  else if (deliveryMode === 'branch') {
1123
- stream.text?.(`[Astro] Merging into project branch ${prepared.projectBranch ?? 'local'}...\n`);
1151
+ stream.text?.(`── [Git] Merging into project branch ${prepared.projectBranch ?? 'local'}...\n`);
1124
1152
  // Branch mode: commit locally, merge into project branch if available.
1125
1153
  // The merge lock is held only during the squash-merge (seconds, not minutes),
1126
1154
  // allowing tasks to execute in parallel. The squash merge naturally handles
@@ -1138,9 +1166,11 @@ export class TaskExecutor {
1138
1166
  // don't overlap, and saves one retry cycle when they do.
1139
1167
  const preRebase = await tryPreMergeRebase(prepared.workingDirectory, prepared.projectBranch, false);
1140
1168
  if (preRebase.rebased) {
1169
+ stream.text?.(`── [Git] Rebased onto ${prepared.projectBranch}\n`);
1141
1170
  console.log(`[executor] Task ${task.id}: pre-merge rebase onto ${prepared.projectBranch} succeeded`);
1142
1171
  }
1143
1172
  else if (!preRebase.skipped) {
1173
+ stream.text?.(`── [Git] Rebase skipped (conflicts), retrying via merge\n`);
1144
1174
  console.log(`[executor] Task ${task.id}: pre-merge rebase had conflicts, falling back to merge retry loop`);
1145
1175
  }
1146
1176
  const mergeLockKey = BranchLockManager.computeLockKey(prepared.gitRoot, task.shortProjectId, task.shortNodeId, task.id);
@@ -1153,7 +1183,7 @@ export class TaskExecutor {
1153
1183
  let mergeResult;
1154
1184
  try {
1155
1185
  this.wsClient.sendTaskStatus(task.id, 'running', 96, 'Merging into project branch...');
1156
- mergeResult = await localMergeIntoProjectBranch(prepared.gitRoot, prepared.branchName, prepared.projectBranch, commitMessage);
1186
+ mergeResult = await localMergeIntoProjectBranch(prepared.gitRoot, prepared.branchName, prepared.projectBranch, commitMessage, (msg) => stream.text?.(`── [Git] ${msg}\n`));
1157
1187
  }
1158
1188
  finally {
1159
1189
  mergeLock.release();
@@ -1162,7 +1192,7 @@ export class TaskExecutor {
1162
1192
  if (mergeResult.merged) {
1163
1193
  result.deliveryStatus = 'success';
1164
1194
  result.commitAfterSha = mergeResult.commitSha;
1165
- stream.text?.(`[Astro] Merged into ${prepared.projectBranch} (${mergeResult.commitSha?.slice(0, 7)})\n`);
1195
+ stream.text?.(`── [Git] Merged into ${prepared.projectBranch} (${mergeResult.commitSha?.slice(0, 7)})\n`);
1166
1196
  console.log(`[executor] Task ${task.id}: merged into ${prepared.projectBranch} (${mergeResult.commitSha})`);
1167
1197
  // Sync project worktree to reflect the merged changes on disk
1168
1198
  if (prepared.projectWorktreePath && prepared.projectBranch && prepared.gitRoot) {
@@ -1178,7 +1208,7 @@ export class TaskExecutor {
1178
1208
  : null;
1179
1209
  if (taskContext?.sessionId && this.isResumableAdapter(adapter) && attempt < MAX_MERGE_ATTEMPTS) {
1180
1210
  const conflictFiles = mergeResult.conflictFiles?.join(', ') ?? 'unknown files';
1181
- stream.text?.(`[Astro] Merge conflict in: ${conflictFiles} — agent resolving (attempt ${attempt})...\n`);
1211
+ stream.text?.(`── [Git] Merge conflict: ${conflictFiles} — agent resolving (attempt ${attempt})...\n`);
1182
1212
  console.log(`[executor] Task ${task.id}: merge conflict (attempt ${attempt}), resuming ${adapter.name} to resolve: ${conflictFiles}`);
1183
1213
  this.wsClient.sendTaskStatus(task.id, 'running', 97, `Merge conflict — agent resolving (attempt ${attempt})...`);
1184
1214
  // Resume agent session with conflict resolution instructions.
@@ -1227,7 +1257,7 @@ export class TaskExecutor {
1227
1257
  }
1228
1258
  else if (deliveryMode === 'push') {
1229
1259
  // Push branch to remote, but don't create a PR — user creates PR manually
1230
- stream.text?.(`[Astro] Pushing branch ${prepared.branchName} to origin...\n`);
1260
+ stream.text?.(`── [Git] Pushing ${prepared.branchName} to origin...\n`);
1231
1261
  this.wsClient.sendTaskStatus(task.id, 'running', 95, 'Pushing branch...');
1232
1262
  console.log(`[executor] Task ${task.id}: push mode, pushing branch ${prepared.branchName}`);
1233
1263
  const prResult = await pushAndCreatePR(prepared.workingDirectory, {
@@ -1236,30 +1266,31 @@ export class TaskExecutor {
1236
1266
  taskDescription: task.description || task.prompt.slice(0, 500),
1237
1267
  skipPR: true,
1238
1268
  baseBranch: prepared.baseBranch,
1269
+ gitRoot: prepared.gitRoot,
1239
1270
  });
1240
1271
  result.branchName = prResult.branchName;
1241
1272
  if (prResult.error) {
1242
1273
  // Delivery failure — don't override execution status
1243
1274
  result.deliveryStatus = 'failed';
1244
1275
  result.deliveryError = `Push delivery failed: ${prResult.error}`;
1245
- stream.text?.(`[Astro] Push failed: ${prResult.error}\n`);
1276
+ stream.text?.(`── [Git] Push failed: ${prResult.error}\n`);
1246
1277
  console.error(`[executor] Task ${task.id}: push delivery failed: ${prResult.error}`);
1247
1278
  }
1248
1279
  else if (prResult.pushed) {
1249
1280
  result.deliveryStatus = 'success';
1250
1281
  keepBranch = true;
1251
- stream.text?.(`[Astro] Branch pushed to origin: ${prepared.branchName}\n`);
1282
+ stream.text?.(`── [Git] Pushed to origin: ${prepared.branchName}\n`);
1252
1283
  console.log(`[executor] Task ${task.id}: branch pushed (${prepared.branchName})`);
1253
1284
  }
1254
1285
  else {
1255
1286
  result.deliveryStatus = 'skipped';
1256
- stream.text?.(`[Astro] No changes to push\n`);
1287
+ stream.text?.(`── [Git] No changes to push\n`);
1257
1288
  console.log(`[executor] Task ${task.id}: no changes to push`);
1258
1289
  }
1259
1290
  }
1260
1291
  else {
1261
1292
  // 'pr' — push + create PR, auto-merge into project branch if applicable
1262
- stream.text?.(`[Astro] Creating pull request for branch ${prepared.branchName}...\n`);
1293
+ stream.text?.(`── [Git] Creating PR: ${prepared.branchName} ${prepared.baseBranch ?? 'main'}...\n`);
1263
1294
  this.wsClient.sendTaskStatus(task.id, 'running', 94, 'Rebasing before push...');
1264
1295
  // Pre-push rebase: if the target branch moved forward, rebase our task
1265
1296
  // branch so the PR will be cleanly mergeable. The branch hasn't been
@@ -1267,9 +1298,11 @@ export class TaskExecutor {
1267
1298
  if (prepared.baseBranch) {
1268
1299
  const preRebase = await tryPreMergeRebase(prepared.workingDirectory, prepared.baseBranch, true);
1269
1300
  if (preRebase.rebased) {
1301
+ stream.text?.(`── [Git] Rebased onto origin/${prepared.baseBranch}\n`);
1270
1302
  console.log(`[executor] Task ${task.id}: pre-push rebase onto origin/${prepared.baseBranch} succeeded`);
1271
1303
  }
1272
1304
  else if (!preRebase.skipped) {
1305
+ stream.text?.(`── [Git] Rebase skipped (conflicts), proceeding without rebase\n`);
1273
1306
  console.log(`[executor] Task ${task.id}: pre-push rebase had conflicts, proceeding without rebase`);
1274
1307
  }
1275
1308
  }
@@ -1284,6 +1317,7 @@ export class TaskExecutor {
1284
1317
  baseBranch: prepared.baseBranch,
1285
1318
  autoMerge: hasProjectBranch,
1286
1319
  commitBeforeSha: prepared.commitBeforeSha,
1320
+ gitRoot: prepared.gitRoot,
1287
1321
  });
1288
1322
  result.branchName = prResult.branchName;
1289
1323
  if (prResult.prUrl) {
@@ -1292,7 +1326,7 @@ export class TaskExecutor {
1292
1326
  result.commitBeforeSha = prResult.commitBeforeSha;
1293
1327
  result.commitAfterSha = prResult.commitAfterSha;
1294
1328
  keepBranch = true;
1295
- stream.text?.(`[Astro] Pull request created: ${prResult.prUrl}\n`);
1329
+ stream.text?.(`── [Git] PR created: ${prResult.prUrl}\n`);
1296
1330
  if (prResult.autoMergeFailed) {
1297
1331
  // PR was created but auto-merge failed (likely conflict).
1298
1332
  // If the adapter supports session resume, ask the agent to
@@ -1304,7 +1338,7 @@ export class TaskExecutor {
1304
1338
  : null;
1305
1339
  if (prTaskContext?.sessionId && this.isResumableAdapter(adapter) && hasProjectBranch && prepared.branchName && prepared.baseBranch && prResult.prNumber && prepared.gitRoot) {
1306
1340
  for (let attempt = 1; attempt <= MAX_PR_MERGE_ATTEMPTS; attempt++) {
1307
- stream.text?.(`[Astro] PR auto-merge failed — agent resolving conflict (attempt ${attempt})...\n`);
1341
+ stream.text?.(`── [Git] Auto-merge failed — agent resolving conflict (attempt ${attempt})...\n`);
1308
1342
  console.log(`[executor] Task ${task.id}: PR auto-merge failed (attempt ${attempt}), resuming ${adapter.name} to resolve`);
1309
1343
  this.wsClient.sendTaskStatus(task.id, 'running', 97, `PR merge conflict — agent resolving (attempt ${attempt})...`);
1310
1344
  // Resume agent session — agent rebases and force-pushes.
@@ -1318,11 +1352,14 @@ export class TaskExecutor {
1318
1352
  result.deliveryError = `PR created but auto-merge failed. Agent resolution failed: ${resumeErr instanceof Error ? resumeErr.message : resumeErr}`;
1319
1353
  break;
1320
1354
  }
1321
- // Retry the GitHub merge
1355
+ // Retry the GitHub merge.
1356
+ // Use gitRoot as cwd (always valid, guarded at L1518) instead of
1357
+ // worktree path which may have been deleted during resolution.
1322
1358
  this.wsClient.sendTaskStatus(task.id, 'running', 98, `Retrying PR merge (attempt ${attempt})...`);
1323
- const retryMerge = await mergePullRequest(prepared.workingDirectory, prResult.prNumber, {
1359
+ const retryMerge = await mergePullRequest(prepared.gitRoot, prResult.prNumber, {
1324
1360
  method: 'squash',
1325
1361
  deleteBranch: true,
1362
+ repoSlug: repoSlug ?? undefined,
1326
1363
  });
1327
1364
  if (retryMerge.ok) {
1328
1365
  result.commitAfterSha = await getRemoteBranchSha(prepared.gitRoot, prepared.baseBranch) ?? undefined;
@@ -1362,18 +1399,18 @@ export class TaskExecutor {
1362
1399
  result.deliveryStatus = 'failed';
1363
1400
  result.deliveryError = `PR delivery failed: ${prResult.error}`;
1364
1401
  keepBranch = prResult.pushed ?? false; // Keep branch if it was pushed
1365
- stream.text?.(`[Astro] PR delivery failed: ${prResult.error}\n`);
1402
+ stream.text?.(`── [Git] PR delivery failed: ${prResult.error}\n`);
1366
1403
  console.error(`[executor] Task ${task.id}: PR delivery failed: ${prResult.error}`);
1367
1404
  }
1368
1405
  else {
1369
- stream.text?.(`[Astro] No changes to deliver\n`);
1406
+ stream.text?.(`── [Git] No changes to deliver\n`);
1370
1407
  console.log(`[executor] Task ${task.id}: no changes to push`);
1371
1408
  }
1372
1409
  }
1373
1410
  }
1374
1411
  catch (prError) {
1375
1412
  const prMsg = prError instanceof Error ? prError.message : String(prError);
1376
- stream.text?.(`[Astro] Delivery failed: ${prMsg}\n`);
1413
+ stream.text?.(`── [Delivery] Failed: ${prMsg}\n`);
1377
1414
  console.error(`[executor] Task ${task.id}: delivery (${deliveryMode}) failed: ${prMsg}`);
1378
1415
  // Delivery failure — don't override execution status
1379
1416
  result.deliveryStatus = 'failed';
@@ -1430,14 +1467,13 @@ export class TaskExecutor {
1430
1467
  clearTimeout(hardCapTimeoutId);
1431
1468
  if (idleTimerId !== undefined)
1432
1469
  clearTimeout(idleTimerId);
1433
- taskHeartbeatPhase = 'cleaning up';
1434
1470
  // Always cleanup the local worktree directory to reclaim disk space
1435
1471
  // (node_modules alone is ~680MB per worktree). When keepBranch is true
1436
1472
  // (PR created or branch pushed), we preserve the git branch but still
1437
1473
  // remove the working copy — the branch lives on remote/local refs,
1438
1474
  // and re-execution will create a fresh worktree if needed.
1439
1475
  if (prepared.branchName) {
1440
- stream.text?.(`\n[Astro] Cleaning up worktree (branch: ${prepared.branchName})...\n`);
1476
+ stream.text?.(`\n── [Astro] Cleaning up worktree (branch: ${prepared.branchName})...\n`);
1441
1477
  }
1442
1478
  if (this.preserveWorktrees) {
1443
1479
  console.log(`[executor] Task ${task.id}: worktree preserved (debug mode)`);