@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
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** — 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 — 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 — 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 — Astro never sees them.
|
|
579
593
|
|
|
580
594
|
### 3. GitHub-Native Workflow
|
package/dist/lib/git-pr.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
41
|
+
export declare function pushBranch(repoDir: string, branchName: string): Promise<{
|
|
30
42
|
ok: boolean;
|
|
31
43
|
error?: string;
|
|
32
44
|
}>;
|
|
33
45
|
/**
|
|
34
|
-
*
|
|
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
|
|
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
|
-
} |
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/lib/git-pr.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-pr.d.ts","sourceRoot":"","sources":["../../src/lib/git-pr.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"git-pr.d.ts","sourceRoot":"","sources":["../../src/lib/git-pr.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/git-pr.js
CHANGED
|
@@ -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
|
|
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(
|
|
58
|
+
export async function hasBranchCommits(repoDir, baseBranch, branchName) {
|
|
47
59
|
try {
|
|
48
|
-
const
|
|
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(
|
|
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',
|
|
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 ${
|
|
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',
|
|
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
|
-
*
|
|
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
|
|
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('
|
|
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
|
-
]
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
232
|
-
|
|
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}
|
|
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
|
-
//
|
|
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
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
527
|
+
result.error = `PR creation failed: ${pr.error}`;
|
|
312
528
|
}
|
|
313
529
|
return result;
|
|
314
530
|
}
|