@astroanywhere/agent 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +14 -0
  2. package/dist/lib/git-pr.d.ts +83 -11
  3. package/dist/lib/git-pr.d.ts.map +1 -1
  4. package/dist/lib/git-pr.js +249 -33
  5. package/dist/lib/git-pr.js.map +1 -1
  6. package/dist/lib/local-merge.d.ts +3 -1
  7. package/dist/lib/local-merge.d.ts.map +1 -1
  8. package/dist/lib/local-merge.js +29 -7
  9. package/dist/lib/local-merge.js.map +1 -1
  10. package/dist/lib/task-executor.d.ts.map +1 -1
  11. package/dist/lib/task-executor.js +101 -53
  12. package/dist/lib/task-executor.js.map +1 -1
  13. package/dist/lib/websocket-client.d.ts +6 -2
  14. package/dist/lib/websocket-client.d.ts.map +1 -1
  15. package/dist/lib/websocket-client.js +28 -5
  16. package/dist/lib/websocket-client.js.map +1 -1
  17. package/dist/lib/worktree.d.ts +3 -3
  18. package/dist/lib/worktree.d.ts.map +1 -1
  19. package/dist/lib/worktree.js +92 -48
  20. package/dist/lib/worktree.js.map +1 -1
  21. package/dist/providers/base-adapter.d.ts +29 -2
  22. package/dist/providers/base-adapter.d.ts.map +1 -1
  23. package/dist/providers/base-adapter.js +39 -1
  24. package/dist/providers/base-adapter.js.map +1 -1
  25. package/dist/providers/claude-sdk-adapter.d.ts +2 -4
  26. package/dist/providers/claude-sdk-adapter.d.ts.map +1 -1
  27. package/dist/providers/claude-sdk-adapter.js +46 -55
  28. package/dist/providers/claude-sdk-adapter.js.map +1 -1
  29. package/dist/providers/codex-adapter.d.ts.map +1 -1
  30. package/dist/providers/codex-adapter.js +6 -3
  31. package/dist/providers/codex-adapter.js.map +1 -1
  32. package/dist/providers/openclaw-adapter.d.ts.map +1 -1
  33. package/dist/providers/openclaw-adapter.js +4 -2
  34. package/dist/providers/openclaw-adapter.js.map +1 -1
  35. package/dist/providers/opencode-adapter.d.ts.map +1 -1
  36. package/dist/providers/opencode-adapter.js +6 -4
  37. package/dist/providers/opencode-adapter.js.map +1 -1
  38. package/dist/providers/pi-adapter.d.ts +7 -0
  39. package/dist/providers/pi-adapter.d.ts.map +1 -1
  40. package/dist/providers/pi-adapter.js +246 -70
  41. package/dist/providers/pi-adapter.js.map +1 -1
  42. package/dist/types.d.ts +16 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +2 -2
package/README.md CHANGED
@@ -20,6 +20,17 @@
20
20
 
21
21
  ---
22
22
 
23
+ <details open>
24
+ <summary><h2>📰 News</h2></summary>
25
+
26
+ | Date | Update |
27
+ |:-----|:-------|
28
+ | **2026-03-19** | **🤖 Pi Coding Agent support** &mdash; Astro now natively supports [Pi](https://github.com/badlogic/pi-mono), the coding agent powering [OpenClaw](https://github.com/openclaw-ai/openclaw). Auto-detected at launch alongside Claude Code, Codex, and OpenCode. Full streaming, tool result rendering, session preservation, and multi-turn resume. |
29
+
30
+ </details>
31
+
32
+ ---
33
+
23
34
  ## What is Astro?
24
35
 
25
36
  [**Astro**](https://astroanywhere.com/landing/) is an orchestrator for AI coding agents. It takes a complex goal, decomposes it into a dependency graph of tasks, and executes them **in parallel** across your machines &mdash; your laptop, GPU servers, HPC clusters, cloud VMs.
@@ -572,9 +583,12 @@ Astro works with the AI coding agents you already use. Install any supported age
572
583
  |---|---|---|
573
584
  | **Claude Code** | `npm i -g @anthropic-ai/claude-code` | [anthropic.com/claude-code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) |
574
585
  | **Codex** | `npm i -g @openai/codex` | [github.com/openai/codex](https://github.com/openai/codex) |
586
+ | **Pi** | `npm i -g @mariozechner/pi-coding-agent` | [github.com/badlogic/pi-mono](https://github.com/badlogic/pi-mono) |
575
587
  | **OpenClaw** | `npm i -g openclaw` | [github.com/openclaw-ai/openclaw](https://github.com/openclaw-ai/openclaw) |
576
588
  | **OpenCode** | `bun i -g opencode` | [github.com/opencode-ai/opencode](https://github.com/opencode-ai/opencode) |
577
589
 
590
+ Pi is the coding agent that powers OpenClaw. If you have OpenClaw installed, Pi is already available &mdash; Astro detects it automatically. You can also install Pi standalone for direct access to its multi-provider model support (Anthropic, OpenAI, Google, Bedrock, custom providers via `models.json`).
591
+
578
592
  All agents get full project context injection, real-time output streaming, and session preservation for multi-turn resume. Your API keys stay on your machine &mdash; Astro never sees them.
579
593
 
580
594
  ### 3. GitHub-Native Workflow
@@ -1,5 +1,13 @@
1
1
  /**
2
- * Git PR utilities for creating pull requests after task execution
2
+ * Git PR utilities for creating pull requests after task execution.
3
+ *
4
+ * GitHub workflow: task branch → PR → project branch (auto-merge).
5
+ * Task branches NEVER create PRs directly to the base branch (main).
6
+ * The only PR to main comes from the "Push to GitHub" task node.
7
+ *
8
+ * When the worktree is gone (agent cleaned it up during execution),
9
+ * git operations fall back to gitRoot (branch refs live in shared store),
10
+ * and gh commands use explicit --repo OWNER/REPO (no local context needed).
3
11
  */
4
12
  export interface PRResult {
5
13
  branchName: string;
@@ -20,35 +28,62 @@ export interface PRResult {
20
28
  */
21
29
  export declare function getDefaultBranch(repoDir: string): Promise<string>;
22
30
  /**
23
- * Check if the current branch has commits ahead of the base branch
31
+ * Check if a branch has commits ahead of the base branch.
32
+ *
33
+ * When called from a worktree, HEAD resolves to the task branch.
34
+ * When called from the git root (worktree cleaned up), pass branchName
35
+ * explicitly so we compare the right ref instead of HEAD.
24
36
  */
25
- export declare function hasBranchCommits(worktreePath: string, baseBranch: string): Promise<boolean>;
37
+ export declare function hasBranchCommits(repoDir: string, baseBranch: string, branchName?: string): Promise<boolean>;
26
38
  /**
27
39
  * Push the branch to origin
28
40
  */
29
- export declare function pushBranch(worktreePath: string, branchName: string): Promise<{
41
+ export declare function pushBranch(repoDir: string, branchName: string): Promise<{
30
42
  ok: boolean;
31
43
  error?: string;
32
44
  }>;
33
45
  /**
34
- * Create a pull request using the `gh` CLI
46
+ * Extract the GitHub repo slug (OWNER/REPO) from a git remote URL.
47
+ * Supports both SSH and HTTPS formats:
48
+ * git@github.com:owner/repo.git → owner/repo
49
+ * https://github.com/owner/repo.git → owner/repo
35
50
  */
36
- export declare function createPullRequest(worktreePath: string, options: {
51
+ export declare function parseRepoSlug(remoteUrl: string): string | null;
52
+ /**
53
+ * Get the GitHub repo slug (OWNER/REPO) from a git directory's origin remote.
54
+ */
55
+ export declare function getRepoSlug(repoDir: string): Promise<string | null>;
56
+ /**
57
+ * Create a pull request using the `gh` CLI.
58
+ *
59
+ * Uses --repo OWNER/REPO when provided so the command doesn't depend on
60
+ * the local git directory being valid (worktree may have been cleaned up).
61
+ */
62
+ export declare function createPullRequest(cwd: string, options: {
37
63
  branchName: string;
38
64
  baseBranch: string;
39
65
  title: string;
40
66
  body: string;
67
+ /** Explicit GitHub repo slug (OWNER/REPO) — avoids local git context resolution */
68
+ repoSlug?: string;
41
69
  }): Promise<{
42
70
  prUrl: string;
43
71
  prNumber: number;
44
- } | null>;
72
+ } | {
73
+ error: string;
74
+ }>;
45
75
  /**
46
76
  * Merge a pull request using the `gh` CLI.
47
77
  * Used to auto-merge per-task PRs into the project branch.
78
+ *
79
+ * Uses --repo OWNER/REPO when provided so the command doesn't depend on
80
+ * the local git directory being valid.
48
81
  */
49
- export declare function mergePullRequest(worktreePath: string, prNumber: number, options?: {
82
+ export declare function mergePullRequest(cwd: string, prNumber: number, options?: {
50
83
  method?: 'squash' | 'merge' | 'rebase';
51
84
  deleteBranch?: boolean;
85
+ /** Explicit GitHub repo slug (OWNER/REPO) */
86
+ repoSlug?: string;
52
87
  }): Promise<{
53
88
  ok: boolean;
54
89
  error?: string;
@@ -58,6 +93,27 @@ export declare function mergePullRequest(worktreePath: string, prNumber: number,
58
93
  * Fetches first to ensure we have the latest.
59
94
  */
60
95
  export declare function getRemoteBranchSha(repoDir: string, branchName: string): Promise<string | undefined>;
96
+ /**
97
+ * Push a branch to origin with retry, delay, and post-push verification.
98
+ *
99
+ * Shared by ensureProjectBranch() and the safety net in pushAndCreatePR().
100
+ * Returns { ok: true } on success, { ok: false, error } on failure.
101
+ *
102
+ * - 2-attempt retry with 2s delay between attempts
103
+ * - "Everything up-to-date" treated as success
104
+ * - Post-push verification via fetch + rev-parse
105
+ */
106
+ export declare function pushBranchToRemote(gitRoot: string, branchName: string, options?: {
107
+ /** Structured operational status line */
108
+ operational?: (message: string, source: 'astro' | 'git' | 'delivery') => void;
109
+ /** Label for log messages (default: 'push') */
110
+ label?: string;
111
+ }): Promise<{
112
+ ok: true;
113
+ } | {
114
+ ok: false;
115
+ error: string;
116
+ }>;
61
117
  /**
62
118
  * Check if `gh` CLI is available and authenticated
63
119
  */
@@ -76,7 +132,21 @@ export declare function getGitRoot(dir: string): Promise<string | null>;
76
132
  */
77
133
  export declare function autoCommitChanges(worktreePath: string, taskTitle: string): Promise<boolean>;
78
134
  /**
79
- * Full PR creation flow: auto-commit + push + create PR, with graceful fallbacks
135
+ * Full PR delivery following the accumulative project branch workflow:
136
+ *
137
+ * task branch → push → PR → auto-merge → project branch
138
+ *
139
+ * When the worktree has been cleaned up (agent removed it during execution):
140
+ * - git operations (push, rev-list) use gitRoot — the task branch lives in
141
+ * the shared git object store, not the worktree directory
142
+ * - gh operations (pr create, pr merge) use --repo OWNER/REPO — explicitly
143
+ * targets the GitHub repo without depending on local filesystem state
144
+ * - auto-commit is skipped (nothing to stage if the worktree is gone)
145
+ *
146
+ * baseBranch MUST be provided by the caller. It is the project branch
147
+ * (e.g., astro/7b19a9), never auto-detected. Task branches must never
148
+ * create PRs directly to the base branch (main) — that's exclusively
149
+ * the "Push to GitHub" node's responsibility.
80
150
  */
81
151
  export declare function pushAndCreatePR(worktreePath: string, options: {
82
152
  branchName: string;
@@ -88,13 +158,15 @@ export declare function pushAndCreatePR(worktreePath: string, options: {
88
158
  autoCommit?: boolean;
89
159
  /** Override the default PR body */
90
160
  body?: string;
91
- /** Target branch for PR base — avoids re-detecting (should match what worktree was created from) */
161
+ /** Target branch for PR base (project branch). Required for PR creation. */
92
162
  baseBranch?: string;
93
- /** If true, auto-merge the PR after creation (squash merge into base branch) */
163
+ /** If true, auto-merge the PR after creation (squash merge into project branch) */
94
164
  autoMerge?: boolean;
95
165
  /** Merge method for auto-merge (default: 'squash') */
96
166
  mergeMethod?: 'squash' | 'merge' | 'rebase';
97
167
  /** Git SHA of the base branch before this task — passed through to PRResult */
98
168
  commitBeforeSha?: string;
169
+ /** Git root directory — used when the worktree has been cleaned up */
170
+ gitRoot?: string;
99
171
  }): Promise<PRResult>;
100
172
  //# sourceMappingURL=git-pr.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-pr.d.ts","sourceRoot":"","sources":["../../src/lib/git-pr.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"git-pr.d.ts","sourceRoot":"","sources":["../../src/lib/git-pr.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,13 @@
1
1
  /**
2
- * Git PR utilities for creating pull requests after task execution
2
+ * Git PR utilities for creating pull requests after task execution.
3
+ *
4
+ * GitHub workflow: task branch → PR → project branch (auto-merge).
5
+ * Task branches NEVER create PRs directly to the base branch (main).
6
+ * The only PR to main comes from the "Push to GitHub" task node.
7
+ *
8
+ * When the worktree is gone (agent cleaned it up during execution),
9
+ * git operations fall back to gitRoot (branch refs live in shared store),
10
+ * and gh commands use explicit --repo OWNER/REPO (no local context needed).
3
11
  */
4
12
  import { execFile } from 'node:child_process';
5
13
  import { promisify } from 'node:util';
@@ -41,11 +49,16 @@ export async function getDefaultBranch(repoDir) {
41
49
  return 'main';
42
50
  }
43
51
  /**
44
- * Check if the current branch has commits ahead of the base branch
52
+ * Check if a branch has commits ahead of the base branch.
53
+ *
54
+ * When called from a worktree, HEAD resolves to the task branch.
55
+ * When called from the git root (worktree cleaned up), pass branchName
56
+ * explicitly so we compare the right ref instead of HEAD.
45
57
  */
46
- export async function hasBranchCommits(worktreePath, baseBranch) {
58
+ export async function hasBranchCommits(repoDir, baseBranch, branchName) {
47
59
  try {
48
- const { stdout } = await execFileAsync('git', ['-C', worktreePath, 'rev-list', '--count', `origin/${baseBranch}..HEAD`], { env: withGitEnv(), timeout: 10_000 });
60
+ const headRef = branchName ?? 'HEAD';
61
+ const { stdout } = await execFileAsync('git', ['-C', repoDir, 'rev-list', '--count', `origin/${baseBranch}..${headRef}`], { env: withGitEnv(), timeout: 10_000 });
49
62
  return parseInt(stdout.trim(), 10) > 0;
50
63
  }
51
64
  catch {
@@ -55,17 +68,17 @@ export async function hasBranchCommits(worktreePath, baseBranch) {
55
68
  /**
56
69
  * Push the branch to origin
57
70
  */
58
- export async function pushBranch(worktreePath, branchName) {
71
+ export async function pushBranch(repoDir, branchName) {
59
72
  // Log remote URL for debugging push target
60
73
  try {
61
- const { stdout: remoteUrl } = await execFileAsync('git', ['-C', worktreePath, 'remote', 'get-url', 'origin'], { env: withGitEnv(), timeout: 5_000 });
74
+ const { stdout: remoteUrl } = await execFileAsync('git', ['-C', repoDir, 'remote', 'get-url', 'origin'], { env: withGitEnv(), timeout: 5_000 });
62
75
  console.log(`[git-pr] Pushing branch ${branchName} to origin (${remoteUrl.trim()})`);
63
76
  }
64
77
  catch {
65
- console.warn(`[git-pr] Could not resolve origin URL for ${worktreePath}`);
78
+ console.warn(`[git-pr] Could not resolve origin URL for ${repoDir}`);
66
79
  }
67
80
  try {
68
- const { stdout, stderr } = await execFileAsync('git', ['-C', worktreePath, 'push', '-u', 'origin', branchName], { env: withGitEnv(), timeout: 60_000 });
81
+ const { stdout, stderr } = await execFileAsync('git', ['-C', repoDir, 'push', '-u', 'origin', branchName], { env: withGitEnv(), timeout: 60_000 });
69
82
  if (stderr)
70
83
  console.log(`[git-pr] Push stderr: ${stderr.trim()}`);
71
84
  if (stdout)
@@ -80,17 +93,52 @@ export async function pushBranch(worktreePath, branchName) {
80
93
  }
81
94
  }
82
95
  /**
83
- * Create a pull request using the `gh` CLI
96
+ * Extract the GitHub repo slug (OWNER/REPO) from a git remote URL.
97
+ * Supports both SSH and HTTPS formats:
98
+ * git@github.com:owner/repo.git → owner/repo
99
+ * https://github.com/owner/repo.git → owner/repo
84
100
  */
85
- export async function createPullRequest(worktreePath, options) {
101
+ export function parseRepoSlug(remoteUrl) {
102
+ // Handles both SSH (git@github.com:owner/repo.git)
103
+ // and HTTPS (https://github.com/owner/repo.git) formats.
104
+ // Note: assumes OWNER/REPO (two segments). GitLab subgroups
105
+ // (group/subgroup/repo) will extract only the last two segments.
106
+ const match = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
107
+ if (match)
108
+ return match[1];
109
+ return null;
110
+ }
111
+ /**
112
+ * Get the GitHub repo slug (OWNER/REPO) from a git directory's origin remote.
113
+ */
114
+ export async function getRepoSlug(repoDir) {
86
115
  try {
87
- const { stdout } = await execFileAsync('gh', [
116
+ const { stdout } = await execFileAsync('git', ['-C', repoDir, 'remote', 'get-url', 'origin'], { env: withGitEnv(), timeout: 5_000 });
117
+ return parseRepoSlug(stdout.trim());
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Create a pull request using the `gh` CLI.
125
+ *
126
+ * Uses --repo OWNER/REPO when provided so the command doesn't depend on
127
+ * the local git directory being valid (worktree may have been cleaned up).
128
+ */
129
+ export async function createPullRequest(cwd, options) {
130
+ try {
131
+ const args = [
88
132
  'pr', 'create',
89
133
  '--base', options.baseBranch,
90
134
  '--head', options.branchName,
91
135
  '--title', options.title,
92
136
  '--body', options.body,
93
- ], { cwd: worktreePath, env: withGitEnv(), timeout: 30_000 });
137
+ ];
138
+ if (options.repoSlug) {
139
+ args.push('--repo', options.repoSlug);
140
+ }
141
+ const { stdout } = await execFileAsync('gh', args, { cwd, env: withGitEnv(), timeout: 30_000 });
94
142
  const prUrl = stdout.trim();
95
143
  // Extract PR number from URL: https://github.com/user/repo/pull/123
96
144
  const match = prUrl.match(/\/pull\/(\d+)/);
@@ -100,14 +148,50 @@ export async function createPullRequest(worktreePath, options) {
100
148
  catch (err) {
101
149
  const msg = err instanceof Error ? err.message : String(err);
102
150
  console.error(`[git-pr] Failed to create PR: ${msg}`);
103
- return null;
151
+ // If a PR already exists for this branch, look it up instead of failing
152
+ if (/already exists|already a pull request/i.test(msg)) {
153
+ console.log(`[git-pr] PR already exists for ${options.branchName}, looking up existing PR`);
154
+ const existing = await findExistingPR(cwd, options.branchName, options.repoSlug);
155
+ if (existing)
156
+ return existing;
157
+ }
158
+ // Try to extract PR URL from the error message itself (gh sometimes includes it)
159
+ const urlMatch = msg.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
160
+ if (urlMatch) {
161
+ return { prUrl: urlMatch[0], prNumber: parseInt(urlMatch[1], 10) };
162
+ }
163
+ return { error: msg };
104
164
  }
105
165
  }
166
+ /**
167
+ * Look up an existing PR for a given branch using `gh pr view`.
168
+ */
169
+ async function findExistingPR(cwd, branchName, repoSlug) {
170
+ try {
171
+ const args = ['pr', 'view', branchName, '--json', 'url,number'];
172
+ if (repoSlug) {
173
+ args.push('--repo', repoSlug);
174
+ }
175
+ const { stdout } = await execFileAsync('gh', args, { cwd, env: withGitEnv(), timeout: 15_000 });
176
+ const parsed = JSON.parse(stdout.trim());
177
+ if (parsed.url && parsed.number) {
178
+ console.log(`[git-pr] Found existing PR #${parsed.number}: ${parsed.url}`);
179
+ return { prUrl: parsed.url, prNumber: parsed.number };
180
+ }
181
+ }
182
+ catch (lookupErr) {
183
+ console.warn(`[git-pr] Failed to look up existing PR for ${branchName}:`, lookupErr);
184
+ }
185
+ return null;
186
+ }
106
187
  /**
107
188
  * Merge a pull request using the `gh` CLI.
108
189
  * Used to auto-merge per-task PRs into the project branch.
190
+ *
191
+ * Uses --repo OWNER/REPO when provided so the command doesn't depend on
192
+ * the local git directory being valid.
109
193
  */
110
- export async function mergePullRequest(worktreePath, prNumber, options) {
194
+ export async function mergePullRequest(cwd, prNumber, options) {
111
195
  const method = options?.method ?? 'squash';
112
196
  const args = [
113
197
  'pr', 'merge', String(prNumber),
@@ -116,9 +200,12 @@ export async function mergePullRequest(worktreePath, prNumber, options) {
116
200
  if (options?.deleteBranch !== false) {
117
201
  args.push('--delete-branch');
118
202
  }
203
+ if (options?.repoSlug) {
204
+ args.push('--repo', options.repoSlug);
205
+ }
119
206
  try {
120
207
  await execFileAsync('gh', args, {
121
- cwd: worktreePath,
208
+ cwd,
122
209
  env: withGitEnv(),
123
210
  timeout: 60_000,
124
211
  });
@@ -145,6 +232,62 @@ export async function getRemoteBranchSha(repoDir, branchName) {
145
232
  return undefined;
146
233
  }
147
234
  }
235
+ /**
236
+ * Push a branch to origin with retry, delay, and post-push verification.
237
+ *
238
+ * Shared by ensureProjectBranch() and the safety net in pushAndCreatePR().
239
+ * Returns { ok: true } on success, { ok: false, error } on failure.
240
+ *
241
+ * - 2-attempt retry with 2s delay between attempts
242
+ * - "Everything up-to-date" treated as success
243
+ * - Post-push verification via fetch + rev-parse
244
+ */
245
+ export async function pushBranchToRemote(gitRoot, branchName, options) {
246
+ const operational = options?.operational;
247
+ const label = options?.label ?? 'push';
248
+ const MAX_ATTEMPTS = 2;
249
+ let lastError;
250
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
251
+ try {
252
+ operational?.(`Pushing ${branchName} to origin (${label} attempt ${attempt}/${MAX_ATTEMPTS})...`, 'git');
253
+ await execFileAsync('git', ['-C', gitRoot, 'push', '-u', 'origin', branchName], { env: withGitEnv(), timeout: 30_000 });
254
+ lastError = undefined;
255
+ operational?.(`${branchName} pushed to origin`, 'git');
256
+ console.log(`[git-pr] Pushed ${branchName} to origin (${label} attempt ${attempt})`);
257
+ break;
258
+ }
259
+ catch (err) {
260
+ lastError = err instanceof Error ? err.message : String(err);
261
+ // "everything up-to-date" is not an error
262
+ if (lastError.includes('Everything up-to-date') || lastError.includes('up to date')) {
263
+ operational?.(`${branchName} already up-to-date on origin`, 'git');
264
+ console.log(`[git-pr] ${branchName} already up-to-date on origin`);
265
+ lastError = undefined;
266
+ break;
267
+ }
268
+ console.error(`[git-pr] ${label} attempt ${attempt} failed: ${lastError}`);
269
+ if (attempt < MAX_ATTEMPTS) {
270
+ operational?.(`Push failed (attempt ${attempt}): ${lastError} — retrying in 2s...`, 'git');
271
+ await new Promise(resolve => setTimeout(resolve, 2_000));
272
+ }
273
+ }
274
+ }
275
+ if (lastError) {
276
+ operational?.(`ERROR: Failed to push ${branchName} to origin: ${lastError}`, 'git');
277
+ return { ok: false, error: `Failed to push ${branchName} to origin after ${MAX_ATTEMPTS} attempts: ${lastError}` };
278
+ }
279
+ // Verify the branch is actually on origin after push.
280
+ const remoteSha = await getRemoteBranchSha(gitRoot, branchName);
281
+ if (!remoteSha) {
282
+ const msg = `Push reported success but ${branchName} not visible on origin (fetch + rev-parse failed)`;
283
+ operational?.(`ERROR: ${msg}`, 'git');
284
+ console.error(`[git-pr] ${msg}`);
285
+ return { ok: false, error: msg };
286
+ }
287
+ operational?.(`Verified: ${branchName} on origin (${remoteSha.slice(0, 8)})`, 'git');
288
+ console.log(`[git-pr] Verified ${branchName} on origin (sha: ${remoteSha.slice(0, 8)})`);
289
+ return { ok: true };
290
+ }
148
291
  /**
149
292
  * Check if `gh` CLI is available and authenticated
150
293
  */
@@ -224,45 +367,88 @@ async function readBaseBranchFromConfig(gitRoot) {
224
367
  return null;
225
368
  }
226
369
  /**
227
- * Full PR creation flow: auto-commit + push + create PR, with graceful fallbacks
370
+ * Full PR delivery following the accumulative project branch workflow:
371
+ *
372
+ * task branch → push → PR → auto-merge → project branch
373
+ *
374
+ * When the worktree has been cleaned up (agent removed it during execution):
375
+ * - git operations (push, rev-list) use gitRoot — the task branch lives in
376
+ * the shared git object store, not the worktree directory
377
+ * - gh operations (pr create, pr merge) use --repo OWNER/REPO — explicitly
378
+ * targets the GitHub repo without depending on local filesystem state
379
+ * - auto-commit is skipped (nothing to stage if the worktree is gone)
380
+ *
381
+ * baseBranch MUST be provided by the caller. It is the project branch
382
+ * (e.g., astro/7b19a9), never auto-detected. Task branches must never
383
+ * create PRs directly to the base branch (main) — that's exclusively
384
+ * the "Push to GitHub" node's responsibility.
228
385
  */
229
386
  export async function pushAndCreatePR(worktreePath, options) {
230
387
  const result = { branchName: options.branchName };
231
- // Get git root from worktree to find default branch
232
- const gitRoot = await getGitRoot(worktreePath);
388
+ // Resolve git context: worktree if it still exists, gitRoot otherwise.
389
+ // The task branch lives in the shared git object store (all worktrees
390
+ // share one .git), so push/rev-list work from gitRoot even when the
391
+ // worktree directory is gone.
392
+ const worktreeGitRoot = await getGitRoot(worktreePath);
393
+ const gitRoot = worktreeGitRoot ?? options.gitRoot ?? null;
233
394
  if (!gitRoot) {
234
- console.warn(`[git-pr] No git root found for ${worktreePath}, skipping PR`);
395
+ console.warn(`[git-pr] No git root found for ${worktreePath} and no gitRoot provided`);
235
396
  result.error = 'Not a git repository';
236
397
  return result;
237
398
  }
399
+ const worktreeAlive = !!worktreeGitRoot;
400
+ // For git commands: use worktree when available (HEAD = task branch),
401
+ // fall back to gitRoot (need explicit branch name for rev-list).
402
+ const gitDir = worktreeAlive ? worktreePath : gitRoot;
403
+ if (!worktreeAlive) {
404
+ console.log(`[git-pr] Worktree at ${worktreePath} is gone, using gitRoot: ${gitRoot}`);
405
+ }
238
406
  // Check if repo has a remote
239
407
  if (!(await hasRemoteOrigin(gitRoot))) {
240
408
  console.warn(`[git-pr] No remote origin for ${gitRoot}, skipping PR`);
241
409
  result.error = 'No remote origin configured — cannot push';
242
410
  return result;
243
411
  }
244
- // Priority: caller-provided baseBranch > config file > auto-detection
412
+ // Resolve the repo slug for gh commands (OWNER/REPO from remote URL).
413
+ // This makes gh pr create/merge independent of the local filesystem.
414
+ // Only required when we'll actually run gh commands (not skipPR mode).
415
+ const repoSlug = await getRepoSlug(gitRoot);
416
+ if (!repoSlug && !worktreeAlive && !options.skipPR) {
417
+ console.warn(`[git-pr] Cannot resolve repo slug from ${gitRoot} and worktree is gone`);
418
+ result.error = 'Cannot resolve GitHub repo — worktree gone and no repo slug';
419
+ return result;
420
+ }
421
+ // baseBranch: caller-provided (project branch) > config > auto-detect.
422
+ // For the accumulative workflow, the caller should always provide the
423
+ // project branch. Auto-detection is only a fallback for edge cases
424
+ // (e.g., "Push to GitHub" node targeting the default branch).
245
425
  const baseBranch = options.baseBranch ?? await readBaseBranchFromConfig(gitRoot) ?? await getDefaultBranch(gitRoot);
246
- // Auto-commit any uncommitted changes the agent left behind (opt-in, default true)
247
- if (options.autoCommit !== false) {
426
+ // Auto-commit uncommitted changes only when the worktree still exists.
427
+ // When the worktree is gone, there's nothing to stage (agent cleaned it up).
428
+ if (options.autoCommit !== false && worktreeAlive) {
248
429
  await autoCommitChanges(worktreePath, options.taskTitle);
249
430
  }
250
- // Check if there are commits to push
251
- const hasCommits = await hasBranchCommits(worktreePath, baseBranch);
431
+ else if (options.autoCommit !== false && !worktreeAlive) {
432
+ console.log(`[git-pr] Skipping auto-commit worktree at ${worktreePath} no longer exists`);
433
+ }
434
+ // Check if there are commits to push.
435
+ // From gitRoot, HEAD is the main checkout (not the task branch), so we
436
+ // must compare by explicit branch name.
437
+ const hasCommits = await hasBranchCommits(gitDir, baseBranch, worktreeAlive ? undefined : options.branchName);
252
438
  if (!hasCommits) {
253
- console.log(`[git-pr] No commits ahead of ${baseBranch} in ${worktreePath}, skipping PR`);
254
- // Not an error — agent made no changes
439
+ console.log(`[git-pr] No commits ahead of ${baseBranch} for ${options.branchName}`);
255
440
  return result;
256
441
  }
257
442
  console.log(`[git-pr] Branch ${options.branchName} has commits ahead of ${baseBranch}`);
258
- // Push the branch
259
- const pushResult = await pushBranch(worktreePath, options.branchName);
443
+ // Push the task branch to origin.
444
+ // Works from gitRoot because the branch ref lives in the shared store.
445
+ const pushResult = await pushBranch(gitDir, options.branchName);
260
446
  if (!pushResult.ok) {
261
447
  result.error = pushResult.error || 'Failed to push branch';
262
448
  return result;
263
449
  }
264
450
  result.pushed = true;
265
- // Skip PR creation if requested (push-only mode)
451
+ // Skip PR creation if requested (push-only mode, e.g., "Push to GitHub" node)
266
452
  if (options.skipPR) {
267
453
  console.log(`[git-pr] skipPR=true, branch pushed (${options.branchName})`);
268
454
  return result;
@@ -277,21 +463,51 @@ export async function pushAndCreatePR(worktreePath, options) {
277
463
  ?? (options.taskDescription
278
464
  ? `## Task\n\n${options.taskDescription}\n\n---\n*Created by Astro task automation*`
279
465
  : '*Created by Astro task automation*');
280
- const pr = await createPullRequest(worktreePath, {
466
+ // Safety net: verify the project branch (base for PR) exists on the remote.
467
+ // ensureProjectBranch() handles this during workspace prep, but if the branch
468
+ // was deleted between prep and delivery, recover here as a last resort.
469
+ // Only runs for project branches (autoMerge=true), not for PRs to main/master.
470
+ if (options.autoMerge) {
471
+ const remoteSha = await getRemoteBranchSha(gitRoot, baseBranch);
472
+ if (!remoteSha) {
473
+ console.warn(`[git-pr] Base branch ${baseBranch} not found on remote — safety-net recovery`);
474
+ const pushRecovery = await pushBranchToRemote(gitRoot, baseBranch, {
475
+ label: 'safety-net',
476
+ });
477
+ if (!pushRecovery.ok) {
478
+ result.error = `Base branch ${baseBranch} not on remote: ${pushRecovery.error}. PR delivery cannot proceed.`;
479
+ return result;
480
+ }
481
+ }
482
+ }
483
+ // Create PR: task branch → project branch.
484
+ // Uses --repo when available so gh doesn't depend on local git context.
485
+ // Guard: if repoSlug is null (non-standard remote URL) and the worktree
486
+ // was deleted between the initial check and now, gh will fail with a
487
+ // misleading "not a git repository" error. Re-check and fail clearly.
488
+ if (!repoSlug && !(await getGitRoot(worktreePath))) {
489
+ console.warn(`[git-pr] Worktree gone after push and no repo slug — cannot create PR`);
490
+ result.error = 'Cannot resolve GitHub repo — worktree gone and no repo slug';
491
+ return result;
492
+ }
493
+ console.log(`[git-pr] Creating PR: ${options.branchName} → ${baseBranch}${repoSlug ? ` (repo: ${repoSlug})` : ''}`);
494
+ const pr = await createPullRequest(gitDir, {
281
495
  branchName: options.branchName,
282
496
  baseBranch,
283
497
  title: options.taskTitle,
284
498
  body,
499
+ repoSlug: repoSlug ?? undefined,
285
500
  });
286
- if (pr) {
501
+ if ('prUrl' in pr) {
287
502
  result.prUrl = pr.prUrl;
288
503
  result.prNumber = pr.prNumber;
289
504
  result.commitBeforeSha = options.commitBeforeSha;
290
505
  // Auto-merge: squash-merge the per-task PR into the project branch
291
506
  if (options.autoMerge && pr.prNumber) {
292
- const mergeResult = await mergePullRequest(worktreePath, pr.prNumber, {
507
+ const mergeResult = await mergePullRequest(gitDir, pr.prNumber, {
293
508
  method: options.mergeMethod ?? 'squash',
294
509
  deleteBranch: true,
510
+ repoSlug: repoSlug ?? undefined,
295
511
  });
296
512
  if (mergeResult.ok) {
297
513
  // Capture the project branch SHA after merge
@@ -308,7 +524,7 @@ export async function pushAndCreatePR(worktreePath, options) {
308
524
  }
309
525
  }
310
526
  else {
311
- result.error = 'PR creation failed (gh pr create returned an error)';
527
+ result.error = `PR creation failed: ${pr.error}`;
312
528
  }
313
529
  return result;
314
530
  }