@saadnvd1/agent-os 0.3.465 → 0.3.467
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 -4
- package/app/api/git/commit/route.ts +4 -3
- package/app/api/git/file-content/route.ts +4 -15
- package/app/api/git/history/[hash]/diff/route.ts +1 -1
- package/app/api/git/history/[hash]/route.ts +1 -1
- package/app/api/git/history/route.ts +1 -1
- package/app/api/git/push/route.ts +2 -2
- package/app/api/sessions/[id]/pr/route.ts +2 -2
- package/lib/away/deploy.ts +3 -7
- package/lib/away/run.ts +6 -5
- package/lib/build.ts +2 -6
- package/lib/chat/files.ts +31 -9
- package/lib/db/migrations.ts +31 -0
- package/lib/done/remote.ts +5 -6
- package/lib/done/testing.ts +2 -7
- package/lib/done/worktree.ts +2 -2
- package/lib/git-exec.ts +334 -0
- package/lib/git-history.ts +29 -41
- package/lib/git-status.ts +45 -124
- package/lib/git.ts +44 -159
- package/lib/lumifyhub/publish.ts +2 -2
- package/lib/orchestrator/armed.ts +2 -2
- package/lib/orchestrator/brief.ts +1 -1
- package/lib/orchestrator/diff.ts +15 -13
- package/lib/orchestrator/escalate.ts +7 -7
- package/lib/orchestrator/gates.ts +36 -16
- package/lib/orchestrator/rebase.ts +20 -24
- package/lib/orchestrator/repo-slug.ts +4 -7
- package/lib/orchestrator/repo.ts +24 -35
- package/lib/orchestrator/signoff.ts +1 -1
- package/lib/pr-generation.ts +2 -1
- package/lib/pr.ts +16 -57
- package/lib/sessions/worktree-setup.ts +17 -13
- package/lib/stacks/start.ts +7 -11
- package/lib/tasks/branch-worktree.ts +3 -2
- package/lib/tasks/finish.ts +5 -6
- package/lib/tasks/gh.ts +5 -0
- package/lib/tasks/move-testing.ts +2 -2
- package/lib/tasks/move.ts +3 -2
- package/lib/tasks/project-ref.ts +12 -19
- package/lib/tasks/session.ts +5 -5
- package/lib/worktree-hooks.ts +3 -7
- package/lib/worktree-placed.ts +3 -10
- package/lib/worktrees.ts +4 -42
- package/package.json +1 -1
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* The merge gates, ported from dispatch orchestrate. Each one passes, waits
|
|
3
3
|
* (not ready yet: CI running or settling, no review of this commit yet),
|
|
4
4
|
* fails, or goes straight to Saad (no CI at all). Only a failure counts
|
|
5
|
-
* against the task, and
|
|
6
|
-
* instead of being retried.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* against the task, and a gate failing again on a different commit goes to
|
|
6
|
+
* Saad instead of being retried. A failure counts once per task, gate and
|
|
7
|
+
* head commit, whichever path judged it (the armed watcher, CI finishing,
|
|
8
|
+
* sign_off, done, land, a review's verdict arriving): judging the same head
|
|
9
|
+
* again is never another failure. A pass doesn't reset the count, so a gate
|
|
10
|
+
* that fails on A, passes on B and fails on C has failed twice.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { db } from "../db";
|
|
@@ -170,12 +172,10 @@ export interface FailureRow {
|
|
|
170
172
|
last_sha: string | null;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Counts one failure; returns the count now. With `sha`, a failure of the
|
|
178
|
-
// commit last counted for this gate isn't counted again.
|
|
175
|
+
// Counts one failure; returns the count now. With `sha`, a failure is
|
|
176
|
+
// counted once per commit: the (task, gate, sha) key in
|
|
177
|
+
// orchestrator_gate_failure_commits decides, so neither a restart nor two
|
|
178
|
+
// evaluations at once count the same head twice.
|
|
179
179
|
export function recordFailure(
|
|
180
180
|
workspaceId: string,
|
|
181
181
|
sessionId: string,
|
|
@@ -183,14 +183,34 @@ export function recordFailure(
|
|
|
183
183
|
reason: string,
|
|
184
184
|
sha: string | null = null
|
|
185
185
|
): number {
|
|
186
|
+
return db
|
|
187
|
+
.transaction(countOnce)
|
|
188
|
+
.immediate(workspaceId, sessionId, gate, reason, sha);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function countOnce(
|
|
192
|
+
workspaceId: string,
|
|
193
|
+
sessionId: string,
|
|
194
|
+
gate: string,
|
|
195
|
+
reason: string,
|
|
196
|
+
sha: string | null
|
|
197
|
+
): number {
|
|
198
|
+
const fresh =
|
|
199
|
+
sha === null ||
|
|
200
|
+
db
|
|
201
|
+
.prepare(
|
|
202
|
+
`INSERT OR IGNORE INTO orchestrator_gate_failure_commits (session_id, gate, sha, workspace_id, reason)
|
|
203
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
204
|
+
)
|
|
205
|
+
.run(sessionId, gate, sha, workspaceId, reason).changes > 0;
|
|
186
206
|
db.prepare(
|
|
187
207
|
`INSERT INTO orchestrator_gate_failures (session_id, gate, workspace_id, count, last_reason, last_sha)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
).run(sessionId, gate, workspaceId, reason, sha);
|
|
208
|
+
VALUES (?, ?, ?, 1, ?, ?)
|
|
209
|
+
ON CONFLICT(session_id, gate) DO UPDATE SET
|
|
210
|
+
count = count + ?,
|
|
211
|
+
last_reason = excluded.last_reason,
|
|
212
|
+
last_sha = excluded.last_sha`
|
|
213
|
+
).run(sessionId, gate, workspaceId, reason, sha, fresh ? 1 : 0);
|
|
194
214
|
return failureOf(sessionId, gate)!.count;
|
|
195
215
|
}
|
|
196
216
|
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* hears one event either way.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { execFile } from "child_process";
|
|
23
22
|
import fs from "fs";
|
|
24
23
|
import os from "os";
|
|
25
24
|
import path from "path";
|
|
@@ -28,6 +27,7 @@ import { sendMessage } from "../bus";
|
|
|
28
27
|
import { hostLink } from "../hosts/remote-api";
|
|
29
28
|
import type { TaskPR } from "../tasks/state";
|
|
30
29
|
import { run } from "../tasks/gh";
|
|
30
|
+
import { git as runGit, gitFetch, revParse } from "../git-exec";
|
|
31
31
|
import { withRestackedLine } from "../tasks/code-review";
|
|
32
32
|
import { outputOf, type Runner } from "../stacks/git";
|
|
33
33
|
import { restackedHeads, stackNotice } from "../stacks/restack";
|
|
@@ -145,7 +145,7 @@ export async function mergesCleanly(
|
|
|
145
145
|
head: string
|
|
146
146
|
): Promise<boolean> {
|
|
147
147
|
try {
|
|
148
|
-
await
|
|
148
|
+
await runGit(repo, ["merge-tree", "--write-tree", base, head]);
|
|
149
149
|
return true;
|
|
150
150
|
} catch (error) {
|
|
151
151
|
if ((error as { code?: unknown }).code === 1) return false;
|
|
@@ -163,14 +163,11 @@ async function rebaseOne(
|
|
|
163
163
|
const repo = repoOf(task);
|
|
164
164
|
const branch = task.base_branch || "main";
|
|
165
165
|
// A failed fetch throws: nothing is judged against a stale base.
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
repo,
|
|
170
|
-
60000
|
|
171
|
-
);
|
|
166
|
+
await gitFetch(repo, ["--quiet", "--", "origin", branch, task.branch_name!], {
|
|
167
|
+
timeout: 60000,
|
|
168
|
+
});
|
|
172
169
|
const onto = `origin/${branch}`;
|
|
173
|
-
const baseTip =
|
|
170
|
+
const baseTip = await revParse(repo, onto);
|
|
174
171
|
const attempt = `${pr.head}:${baseTip}`;
|
|
175
172
|
if (tried.get(task.id) === attempt) return;
|
|
176
173
|
|
|
@@ -344,22 +341,21 @@ async function rebaseAt(
|
|
|
344
341
|
|
|
345
342
|
// The PR's own patch against the base, as `git patch-id` sees it.
|
|
346
343
|
async function patchId(repo: string, range: string): Promise<string | null> {
|
|
347
|
-
const diff = await
|
|
348
|
-
"
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
const diff = await runGit(repo, [
|
|
345
|
+
"diff",
|
|
346
|
+
"--no-color",
|
|
347
|
+
"--no-ext-diff",
|
|
348
|
+
"--full-index",
|
|
349
|
+
range,
|
|
350
|
+
]).catch(() => null);
|
|
352
351
|
if (!diff) return null;
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
child.stdin?.on("error", () => {});
|
|
361
|
-
child.stdin?.end(diff);
|
|
362
|
-
});
|
|
352
|
+
return runGit(repo, ["patch-id", "--verbatim"], {
|
|
353
|
+
input: diff,
|
|
354
|
+
timeout: 30000,
|
|
355
|
+
}).then(
|
|
356
|
+
(out) => out.split(" ")[0] || null,
|
|
357
|
+
() => null
|
|
358
|
+
);
|
|
363
359
|
}
|
|
364
360
|
|
|
365
361
|
// The review of the head before the rebase, carried to the new head when
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// of its projects'.
|
|
5
5
|
|
|
6
6
|
import type { Project } from "../db";
|
|
7
|
-
import {
|
|
7
|
+
import { git } from "../git-exec";
|
|
8
8
|
import { expandHome } from "../tasks/session";
|
|
9
9
|
import { workspaceProjects } from "./brief";
|
|
10
10
|
|
|
@@ -24,12 +24,9 @@ const slugs = new Map<string, { at: number; slug: string | null }>();
|
|
|
24
24
|
export async function repoSlug(dir: string): Promise<string | null> {
|
|
25
25
|
const hit = slugs.get(dir);
|
|
26
26
|
if (hit && Date.now() - hit.at < SLUG_TTL_MS) return hit.slug;
|
|
27
|
-
const url = await
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
dir,
|
|
31
|
-
10000
|
|
32
|
-
).catch(() => "");
|
|
27
|
+
const url = await git(dir, ["config", "--get", "remote.origin.url"], {
|
|
28
|
+
timeout: 10000,
|
|
29
|
+
}).catch(() => "");
|
|
33
30
|
const slug = slugOfRemote(url);
|
|
34
31
|
slugs.set(dir, { at: Date.now(), slug });
|
|
35
32
|
return slug;
|
package/lib/orchestrator/repo.ts
CHANGED
|
@@ -8,7 +8,7 @@ import path from "path";
|
|
|
8
8
|
import type { Session } from "../db";
|
|
9
9
|
import { SKILL_DIRS } from "../agents/skill-dirs";
|
|
10
10
|
import { getProject } from "../projects";
|
|
11
|
-
import {
|
|
11
|
+
import { git, gitFetch, gitOk, worktreeAdd, worktreeRemove } from "../git-exec";
|
|
12
12
|
import { expandHome } from "../tasks/session";
|
|
13
13
|
|
|
14
14
|
export function repoOf(task: Session): string {
|
|
@@ -23,26 +23,21 @@ export async function fetchRefs(
|
|
|
23
23
|
task: Pick<Session, "base_branch" | "branch_name">
|
|
24
24
|
): Promise<string> {
|
|
25
25
|
const base = task.base_branch || "main";
|
|
26
|
-
await
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
await gitFetch(repo, [
|
|
27
|
+
"--quiet",
|
|
28
|
+
"--",
|
|
29
|
+
"origin",
|
|
30
|
+
base,
|
|
31
|
+
task.branch_name ?? base,
|
|
32
|
+
]).catch(() => {});
|
|
31
33
|
const remote = `origin/${base}`;
|
|
32
|
-
const has = await
|
|
33
|
-
"git",
|
|
34
|
-
["rev-parse", "--verify", "--quiet", remote],
|
|
35
|
-
repo
|
|
36
|
-
).then(
|
|
37
|
-
() => true,
|
|
38
|
-
() => false
|
|
39
|
-
);
|
|
34
|
+
const has = await gitOk(repo, ["rev-parse", "--verify", "--quiet", remote]);
|
|
40
35
|
return has ? remote : base;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
// When the commit was made, in seconds.
|
|
44
39
|
export async function commitTime(repo: string, sha: string): Promise<number> {
|
|
45
|
-
const out = await
|
|
40
|
+
const out = await git(repo, ["show", "-s", "--format=%ct", sha]);
|
|
46
41
|
return Number(out.trim()) || 0;
|
|
47
42
|
}
|
|
48
43
|
|
|
@@ -61,25 +56,21 @@ export async function baseReviewSkill(
|
|
|
61
56
|
repo: string,
|
|
62
57
|
base: string
|
|
63
58
|
): Promise<{ path: string; text: string } | null> {
|
|
64
|
-
const listed = await
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...SKILL_DIRS,
|
|
74
|
-
],
|
|
75
|
-
repo
|
|
76
|
-
).catch(() => "");
|
|
59
|
+
const listed = await git(repo, [
|
|
60
|
+
"ls-tree",
|
|
61
|
+
"-r",
|
|
62
|
+
"--name-only",
|
|
63
|
+
base,
|
|
64
|
+
"--",
|
|
65
|
+
".claude/agents",
|
|
66
|
+
...SKILL_DIRS,
|
|
67
|
+
]).catch(() => "");
|
|
77
68
|
const files = listed.split("\n").map((f) => f.trim());
|
|
78
69
|
const agents = files.filter((f) => AGENT_RULES.test(f));
|
|
79
70
|
if (agents.length) {
|
|
80
71
|
const parts = await Promise.all(
|
|
81
72
|
agents.map(async (f) => {
|
|
82
|
-
const text = await
|
|
73
|
+
const text = await git(repo, ["show", `${base}:${f}`]);
|
|
83
74
|
return `## ${f.slice(".claude/agents/".length, -3)}\n${rulesOf(text)}`;
|
|
84
75
|
})
|
|
85
76
|
);
|
|
@@ -94,7 +85,7 @@ export async function baseReviewSkill(
|
|
|
94
85
|
/(^|\/)[^/]*review[^/]*(\/SKILL\.md|\.md)$/i.test(f)
|
|
95
86
|
);
|
|
96
87
|
if (!file) return null;
|
|
97
|
-
const text = await
|
|
88
|
+
const text = await git(repo, ["show", `${base}:${file}`]);
|
|
98
89
|
return { path: file, text: text.slice(0, 20000) };
|
|
99
90
|
}
|
|
100
91
|
|
|
@@ -113,8 +104,8 @@ export async function checkout(repo: string, sha: string): Promise<string> {
|
|
|
113
104
|
const dir = fs.realpathSync(
|
|
114
105
|
fs.mkdtempSync(path.join(os.tmpdir(), "aos-review-"))
|
|
115
106
|
);
|
|
116
|
-
await
|
|
117
|
-
const staged = await
|
|
107
|
+
await worktreeAdd(repo, ["--detach", dir, sha]);
|
|
108
|
+
const staged = await git(dir, ["ls-files", "-s"]);
|
|
118
109
|
for (const line of staged.split("\n"))
|
|
119
110
|
if (line.startsWith("120000 "))
|
|
120
111
|
fs.rmSync(path.join(dir, line.split("\t")[1]), { force: true });
|
|
@@ -122,8 +113,6 @@ export async function checkout(repo: string, sha: string): Promise<string> {
|
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
export async function removeCheckout(repo: string, dir: string) {
|
|
125
|
-
await
|
|
126
|
-
() => {}
|
|
127
|
-
);
|
|
116
|
+
await worktreeRemove(repo, dir).catch(() => {});
|
|
128
117
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
129
118
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* commit that was judged. With merge approvals on (merge-approvals.ts),
|
|
4
4
|
* anything touching CI config, build and hook scripts, agent config, deploys
|
|
5
5
|
* or secrets handling goes to Saad whatever the gates say. A repository with
|
|
6
|
-
* no CI and a gate failing
|
|
6
|
+
* no CI and a gate failing on two different commits go to him either way. Once a task
|
|
7
7
|
* is with Saad the orchestrator doesn't merge it; he signs it off or drops it.
|
|
8
8
|
*/
|
|
9
9
|
|
package/lib/pr-generation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
|
+
import { git as runGit } from "./git-exec";
|
|
2
3
|
import { run } from "./tasks/gh";
|
|
3
4
|
import { UNTRUSTED_RULE, untrusted } from "./orchestrator/untrusted";
|
|
4
5
|
|
|
@@ -56,7 +57,7 @@ async function getGitContext(
|
|
|
56
57
|
|
|
57
58
|
// Names and text here come from the repository; every one is an
|
|
58
59
|
// argument to git, never shell text or an option.
|
|
59
|
-
const git = (args: string[]) =>
|
|
60
|
+
const git = (args: string[]) => runGit(workingDir, args, { timeout: 10_000 });
|
|
60
61
|
const lines = (out: string) =>
|
|
61
62
|
out
|
|
62
63
|
.split("\n")
|
package/lib/pr.ts
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDefaultBranch } from "./git";
|
|
2
|
+
import { currentBranch, git, gitPush, mergeBase } from "./git-exec";
|
|
2
3
|
import { run } from "./tasks/gh";
|
|
3
4
|
import { GhBackoffError, isRateLimit } from "./tasks/gh-limit";
|
|
4
5
|
|
|
5
|
-
/** The remote's default branch, or "main" when it has none. */
|
|
6
|
-
export function remoteDefaultBranch(workingDir: string): string {
|
|
7
|
-
try {
|
|
8
|
-
return execFileSync(
|
|
9
|
-
"git",
|
|
10
|
-
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
11
|
-
{
|
|
12
|
-
cwd: workingDir,
|
|
13
|
-
encoding: "utf-8",
|
|
14
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
15
|
-
}
|
|
16
|
-
)
|
|
17
|
-
.trim()
|
|
18
|
-
.replace(/^origin\//, "");
|
|
19
|
-
} catch {
|
|
20
|
-
return "main";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
6
|
export interface PRInfo {
|
|
25
7
|
number: number;
|
|
26
8
|
url: string;
|
|
@@ -62,26 +44,20 @@ export async function getCommitsSinceBase(
|
|
|
62
44
|
// Get the merge base
|
|
63
45
|
// Names come from the repository (its remote's default branch): they
|
|
64
46
|
// are arguments, never shell text or options.
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
["merge-base", "--end-of-options", baseBranch, "HEAD"],
|
|
69
|
-
workingDir,
|
|
70
|
-
10_000
|
|
71
|
-
)
|
|
72
|
-
).trim();
|
|
47
|
+
const base = await mergeBase(workingDir, baseBranch, "HEAD", {
|
|
48
|
+
timeout: 10_000,
|
|
49
|
+
});
|
|
73
50
|
|
|
74
51
|
// Get commits since merge base
|
|
75
|
-
const output = await
|
|
76
|
-
|
|
52
|
+
const output = await git(
|
|
53
|
+
workingDir,
|
|
77
54
|
[
|
|
78
55
|
"log",
|
|
79
|
-
`${
|
|
56
|
+
`${base}..HEAD`,
|
|
80
57
|
"--format=COMMIT_START%n%H%n%s%n%b%nCOMMIT_END",
|
|
81
58
|
"--",
|
|
82
59
|
],
|
|
83
|
-
|
|
84
|
-
10_000
|
|
60
|
+
{ timeout: 10_000 }
|
|
85
61
|
);
|
|
86
62
|
|
|
87
63
|
const commits: CommitInfo[] = [];
|
|
@@ -214,12 +190,9 @@ export async function createPR(
|
|
|
214
190
|
): Promise<PRInfo> {
|
|
215
191
|
// First ensure branch is pushed
|
|
216
192
|
try {
|
|
217
|
-
await
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
workingDir,
|
|
221
|
-
30_000
|
|
222
|
-
);
|
|
193
|
+
await gitPush(workingDir, ["-u", "origin", "--", branchName], {
|
|
194
|
+
timeout: 30_000,
|
|
195
|
+
});
|
|
223
196
|
} catch {
|
|
224
197
|
// Branch might already be pushed
|
|
225
198
|
}
|
|
@@ -251,27 +224,13 @@ export async function createPR(
|
|
|
251
224
|
}
|
|
252
225
|
|
|
253
226
|
/**
|
|
254
|
-
* Get current branch name
|
|
227
|
+
* Get current branch name, "" when HEAD is detached.
|
|
255
228
|
*/
|
|
256
229
|
export async function getCurrentBranch(workingDir: string): Promise<string> {
|
|
257
|
-
return (
|
|
258
|
-
await run("git", ["branch", "--show-current"], workingDir, 10_000)
|
|
259
|
-
).trim();
|
|
230
|
+
return currentBranch(workingDir, { timeout: 10_000 });
|
|
260
231
|
}
|
|
261
232
|
|
|
262
233
|
/**
|
|
263
|
-
*
|
|
234
|
+
* The PR's default base: the repository's default branch.
|
|
264
235
|
*/
|
|
265
|
-
export
|
|
266
|
-
try {
|
|
267
|
-
const ref = await run(
|
|
268
|
-
"git",
|
|
269
|
-
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
270
|
-
workingDir,
|
|
271
|
-
10_000
|
|
272
|
-
);
|
|
273
|
-
return ref.trim().replace(/^origin\//, "");
|
|
274
|
-
} catch {
|
|
275
|
-
return "main";
|
|
276
|
-
}
|
|
277
|
-
}
|
|
236
|
+
export const getBaseBranch = getDefaultBranch;
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* nothing.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { execFile } from "child_process";
|
|
10
|
-
import { promisify } from "util";
|
|
11
9
|
import { db } from "../db";
|
|
12
10
|
import { createWorktree } from "../worktrees";
|
|
11
|
+
import { git as runGit } from "../git-exec";
|
|
13
12
|
import { setupWorktree } from "../env-setup";
|
|
14
13
|
import {
|
|
15
14
|
generateBranchName,
|
|
@@ -35,8 +34,6 @@ import {
|
|
|
35
34
|
type SetupView,
|
|
36
35
|
} from "./setup-progress";
|
|
37
36
|
|
|
38
|
-
const execFileAsync = promisify(execFile);
|
|
39
|
-
|
|
40
37
|
export interface WorktreeSetupInput {
|
|
41
38
|
sessionId: string;
|
|
42
39
|
projectPath: string;
|
|
@@ -50,7 +47,18 @@ export interface WorktreeSetupInput {
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
const git = (cwd: string, args: string[], timeout = 60_000) =>
|
|
53
|
-
|
|
50
|
+
runGit(cwd, args, { timeout });
|
|
51
|
+
|
|
52
|
+
// git's own last line says it best ("not a git repository ..."), without
|
|
53
|
+
// the "git fetch failed: " a GitError puts before a one-line stderr (or
|
|
54
|
+
// createWorktree's "Failed to create worktree: " before that).
|
|
55
|
+
const lastLine = (message: string) =>
|
|
56
|
+
(message.trim().split("\n").pop() ?? "")
|
|
57
|
+
.replace(
|
|
58
|
+
/^(?:Failed to create worktree: )?git [\w-]+(?: [\w-]+)? failed: /,
|
|
59
|
+
""
|
|
60
|
+
)
|
|
61
|
+
.replace(/^fatal: /, "");
|
|
54
62
|
|
|
55
63
|
// No remote: nothing to fetch. A fetch that fails fails the setup: a
|
|
56
64
|
// worktree cut from an old base would start the agent on stale code.
|
|
@@ -64,9 +72,7 @@ async function fetchBase(view: SetupView, cwd: string, base: string) {
|
|
|
64
72
|
try {
|
|
65
73
|
await git(cwd, ["fetch", "origin", "--", base]);
|
|
66
74
|
} catch (error) {
|
|
67
|
-
const why = (
|
|
68
|
-
(error as Error).message.trim().split("\n").pop() ?? ""
|
|
69
|
-
).replace(/^fatal: /, "");
|
|
75
|
+
const why = lastLine((error as Error).message);
|
|
70
76
|
throw new Error(`Fetching ${base} failed: ${why}`);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -140,11 +146,9 @@ export async function setUpWorktree(input: WorktreeSetupInput): Promise<void> {
|
|
|
140
146
|
// No worktree: the agent doesn't start. Its message stays queued, and
|
|
141
147
|
// the session falls back to the project's folder if sent anyway.
|
|
142
148
|
// git's own last line says it best ("not a git repository ...").
|
|
143
|
-
const message = (
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
.pop()!
|
|
147
|
-
.replace(/^fatal: /, "");
|
|
149
|
+
const message = lastLine(
|
|
150
|
+
error instanceof Error ? error.message : String(error)
|
|
151
|
+
);
|
|
148
152
|
db.prepare(`UPDATE sessions SET working_directory = ? WHERE id = ?`).run(
|
|
149
153
|
projectPath,
|
|
150
154
|
sessionId
|
package/lib/stacks/start.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { getProject } from "../projects";
|
|
|
13
13
|
import { requireClient } from "../lumifyhub/connection";
|
|
14
14
|
import { promptFromCard } from "../lumifyhub/task-cards";
|
|
15
15
|
import { createTask } from "../tasks";
|
|
16
|
-
import { run } from "../tasks/gh";
|
|
17
16
|
import { expandHome } from "../tasks/session";
|
|
18
17
|
import { itemName } from "./guard";
|
|
19
18
|
import { outputOf } from "./git";
|
|
19
|
+
import { gitFetch, revParse } from "../git-exec";
|
|
20
20
|
|
|
21
21
|
async function stackedBase(
|
|
22
22
|
parent: StackItemRow,
|
|
@@ -31,16 +31,12 @@ async function stackedBase(
|
|
|
31
31
|
if (!branch) throw new Error(`${itemName(parent)} has no branch to stack on`);
|
|
32
32
|
// The branch must be on ORIGIN: the PR's base is the remote branch, and a
|
|
33
33
|
// child cut from unpushed commits opens a PR whose diff includes them.
|
|
34
|
-
await
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
const tip = (
|
|
42
|
-
await run("git", ["rev-parse", `origin/${branch}`], repo)
|
|
43
|
-
).trim();
|
|
34
|
+
await gitFetch(repo, ["origin", branch]).catch((error: unknown) => {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${branch} is not on origin: ${outputOf(error).slice(0, 200)}`
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
const tip = await revParse(repo, `origin/${branch}`);
|
|
44
40
|
const also = itemIds(item.also_item_ids)
|
|
45
41
|
.map((id) => items.find((i) => i.id === id))
|
|
46
42
|
.filter((i): i is StackItemRow => !!i && i.status !== "merged")
|
|
@@ -8,10 +8,11 @@ import fs from "fs";
|
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { getRepoName, slugify } from "../git";
|
|
10
10
|
import { WORKTREES_DIR } from "../worktrees";
|
|
11
|
-
import {
|
|
11
|
+
import { git as runGit } from "../git-exec";
|
|
12
12
|
import { pushedRef } from "./move";
|
|
13
13
|
|
|
14
|
-
const git = (cwd: string, ...args: string[]) =>
|
|
14
|
+
const git = (cwd: string, ...args: string[]) =>
|
|
15
|
+
runGit(cwd, args, { timeout: 120000 });
|
|
15
16
|
|
|
16
17
|
/** The worktree that has this branch checked out, if one does. */
|
|
17
18
|
export function worktreeOnBranch(
|
package/lib/tasks/finish.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { itemName, liveChildren, signOffRefusal } from "../stacks/guard";
|
|
|
12
12
|
import { resolveMergedTaskAsks } from "../orchestrator/ask-settle";
|
|
13
13
|
import { stopChat } from "../chat/runner";
|
|
14
14
|
import { run } from "./gh";
|
|
15
|
+
import { gitFetch, gitPush } from "../git-exec";
|
|
15
16
|
import {
|
|
16
17
|
mergePolicy,
|
|
17
18
|
type MergeMethod,
|
|
@@ -61,13 +62,11 @@ async function cleanup(
|
|
|
61
62
|
!keepRemoteBranch &&
|
|
62
63
|
!PROTECTED_BRANCHES.has(session.branch_name)
|
|
63
64
|
) {
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
repo
|
|
68
|
-
).catch(() => {});
|
|
65
|
+
await gitPush(repo, ["origin", "--delete", session.branch_name]).catch(
|
|
66
|
+
() => {}
|
|
67
|
+
);
|
|
69
68
|
}
|
|
70
|
-
await
|
|
69
|
+
await gitFetch(repo, ["--prune", "--quiet"]).catch(() => {});
|
|
71
70
|
forgetPR(session.id);
|
|
72
71
|
}
|
|
73
72
|
|
package/lib/tasks/gh.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
+
import { git } from "../git-exec";
|
|
3
4
|
import { parseCodeReview } from "./code-review";
|
|
4
5
|
import { parseScopeChange } from "./scope-change";
|
|
5
6
|
import { assertGhAllowed, noteGhFailure, noteGhSuccess } from "./gh-limit";
|
|
@@ -16,12 +17,16 @@ import {
|
|
|
16
17
|
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
|
|
20
|
+
// Runs gh (or another CLI) and resolves stdout. git goes through
|
|
21
|
+
// lib/git-exec, so an injected runner (stacks, the orchestrator's rebase)
|
|
22
|
+
// gets the same git as everything else.
|
|
19
23
|
export async function run(
|
|
20
24
|
cmd: string,
|
|
21
25
|
args: string[],
|
|
22
26
|
cwd: string,
|
|
23
27
|
timeout = 60000
|
|
24
28
|
): Promise<string> {
|
|
29
|
+
if (cmd === "git") return git(cwd, args, { timeout });
|
|
25
30
|
if (cmd === "gh") assertGhAllowed(args);
|
|
26
31
|
try {
|
|
27
32
|
const { stdout } = await execFileAsync(cmd, args, {
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import path from "path";
|
|
8
|
-
import { execFileSync } from "child_process";
|
|
9
8
|
import { randomUUID } from "crypto";
|
|
10
9
|
import { db, type Session } from "../db";
|
|
11
10
|
import { createProject } from "../projects";
|
|
12
11
|
import { writeTranscript } from "./transcript";
|
|
12
|
+
import { gitSync } from "../git-exec";
|
|
13
13
|
|
|
14
14
|
export const CLAUDE_ID = "11111111-2222-3333-4444-555555555555";
|
|
15
15
|
|
|
16
16
|
export const git = (cwd: string, ...args: string[]) =>
|
|
17
|
-
|
|
17
|
+
gitSync(cwd, args).trim();
|
|
18
18
|
|
|
19
19
|
export const row = (id: string) =>
|
|
20
20
|
db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as Session;
|
package/lib/tasks/move.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { db, type Session } from "../db";
|
|
|
13
13
|
import { getProject } from "../projects";
|
|
14
14
|
import { launchClaude } from "../agents/launch";
|
|
15
15
|
import { buildTaskBrief } from "./brief";
|
|
16
|
-
import {
|
|
16
|
+
import { git as runGit } from "../git-exec";
|
|
17
17
|
import { getTaskSession } from "./session";
|
|
18
18
|
import { projectRef } from "./project-ref";
|
|
19
19
|
import { moveRefusal } from "./move-guard";
|
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
export * from "./move-bundle";
|
|
33
33
|
|
|
34
34
|
const execFileAsync = promisify(execFile);
|
|
35
|
-
const git = (cwd: string, ...args: string[]) =>
|
|
35
|
+
const git = (cwd: string, ...args: string[]) =>
|
|
36
|
+
runGit(cwd, args, { timeout: 120000 });
|
|
36
37
|
|
|
37
38
|
// Moves in flight in this process; the row's 'moving' outlives a restart.
|
|
38
39
|
const g = globalThis as unknown as { __agentosExporting?: Set<string> };
|