@saadnvd1/agent-os 0.3.464 → 0.3.466
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 +20 -0
- package/app/api/fresh-start/route.ts +5 -3
- 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/components/Chat/ChatPanel.tsx +23 -1
- package/components/Chat/Messages.tsx +2 -3
- package/components/Chat/StaleChip.tsx +55 -0
- package/components/Workspaces/FreshStartForm.tsx +29 -6
- package/data/chat/useChat.ts +10 -2
- package/lib/away/deploy.ts +3 -7
- package/lib/away/run.ts +6 -5
- package/lib/build.ts +2 -6
- package/lib/bus/index.ts +3 -0
- package/lib/chat/client-send.ts +11 -3
- package/lib/chat/drivers/claude-mapper.ts +11 -1
- package/lib/chat/drivers/claude.ts +9 -1
- package/lib/chat/events.ts +14 -1
- package/lib/chat/files.ts +31 -9
- package/lib/chat/refresh.ts +14 -2
- package/lib/chat/runner.ts +8 -1
- package/lib/chat/stale.ts +95 -0
- package/lib/chat/worker/host.ts +133 -4
- package/lib/chat/worker/protocol.ts +3 -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/deliver.ts +9 -1
- package/lib/orchestrator/diff.ts +15 -13
- package/lib/orchestrator/rebase.ts +20 -24
- package/lib/orchestrator/repo-slug.ts +4 -7
- package/lib/orchestrator/repo.ts +24 -35
- package/lib/pr-generation.ts +2 -1
- package/lib/pr.ts +16 -57
- package/lib/schedules/start.ts +1 -0
- 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
|
@@ -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
|
}
|
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;
|
package/lib/schedules/start.ts
CHANGED
|
@@ -139,6 +139,7 @@ async function postToOrchestrator(
|
|
|
139
139
|
? `${fromLabel(schedule)}, set up by "${creator.name}"`
|
|
140
140
|
: fromLabel(schedule),
|
|
141
141
|
origin: scheduleOrigin(schedule, creator),
|
|
142
|
+
compact: "auto",
|
|
142
143
|
});
|
|
143
144
|
onSession(orchestrator.id);
|
|
144
145
|
return orchestrator.id;
|
|
@@ -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> };
|
package/lib/tasks/project-ref.ts
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import { execFile } from "child_process";
|
|
11
|
-
import { promisify } from "util";
|
|
12
10
|
import { createProject, getAllProjects } from "../projects";
|
|
13
11
|
import { isRemoteHost } from "../hosts";
|
|
14
12
|
import type { Project } from "../db";
|
|
15
|
-
import {
|
|
13
|
+
import { git, gitOk } from "../git-exec";
|
|
16
14
|
import { expandHome } from "./session";
|
|
17
15
|
|
|
18
16
|
export interface ProjectRef {
|
|
@@ -23,8 +21,6 @@ export interface ProjectRef {
|
|
|
23
21
|
remote: string | null;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
const execFileAsync = promisify(execFile);
|
|
27
|
-
|
|
28
24
|
/** A ref as another machine sent it: its shape, before anything reads it. */
|
|
29
25
|
export function isProjectRef(v: unknown): v is ProjectRef {
|
|
30
26
|
const r = v as ProjectRef | null;
|
|
@@ -65,9 +61,7 @@ export function repoIdentity(remote: string | null): string | null {
|
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
export async function originOf(dir: string): Promise<string | null> {
|
|
68
|
-
const url = await
|
|
69
|
-
() => ""
|
|
70
|
-
);
|
|
64
|
+
const url = await git(dir, ["remote", "get-url", "origin"]).catch(() => "");
|
|
71
65
|
return url.trim() || null;
|
|
72
66
|
}
|
|
73
67
|
|
|
@@ -122,7 +116,9 @@ export async function ensureProject(ref: ProjectRef): Promise<Project> {
|
|
|
122
116
|
if (!ref.remote || !REMOTE_URL.test(ref.remote))
|
|
123
117
|
throw new Error(`${ref.name} isn't here and has no remote to clone`);
|
|
124
118
|
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
125
|
-
await
|
|
119
|
+
await git(os.homedir(), ["clone", "--", ref.remote, dir], {
|
|
120
|
+
timeout: 600000,
|
|
121
|
+
});
|
|
126
122
|
}
|
|
127
123
|
return createProject({
|
|
128
124
|
name: ref.name || path.basename(dir),
|
|
@@ -148,18 +144,15 @@ export async function whyNotHere(ref: ProjectRef): Promise<string | null> {
|
|
|
148
144
|
const remote = withoutCredentials(ref.remote);
|
|
149
145
|
if (!remote || !REMOTE_URL.test(remote))
|
|
150
146
|
return "Not there, and no remote to clone";
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
const reachable = await gitOk(
|
|
148
|
+
os.homedir(),
|
|
149
|
+
["ls-remote", "--heads", "--", remote],
|
|
150
|
+
{
|
|
154
151
|
timeout: 15000,
|
|
155
152
|
env: {
|
|
156
|
-
...process.env,
|
|
157
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
158
153
|
GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
|
|
159
154
|
},
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return "Can't clone it there";
|
|
164
|
-
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
return reachable ? null : "Can't clone it there";
|
|
165
158
|
}
|
package/lib/tasks/session.ts
CHANGED
|
@@ -3,7 +3,8 @@ import os from "os";
|
|
|
3
3
|
import { db, queries, type Session } from "../db";
|
|
4
4
|
import { isBranchName } from "../git";
|
|
5
5
|
import { getProject } from "../projects";
|
|
6
|
-
import { findPRStrict
|
|
6
|
+
import { findPRStrict } from "./gh";
|
|
7
|
+
import { currentBranch, git } from "../git-exec";
|
|
7
8
|
import { lookupPR } from "./pr-poll";
|
|
8
9
|
import type { TaskPR } from "./state";
|
|
9
10
|
|
|
@@ -43,15 +44,14 @@ async function worktreeBranch(session: Session): Promise<string | null> {
|
|
|
43
44
|
const dir = localWorktree(session);
|
|
44
45
|
if (!dir) return null;
|
|
45
46
|
try {
|
|
46
|
-
|
|
47
|
-
return out.trim() || null;
|
|
47
|
+
return (await currentBranch(dir, { timeout: 5000 })) || null;
|
|
48
48
|
} catch {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const hasRef = (dir: string, ref: string) =>
|
|
54
|
-
|
|
54
|
+
git(dir, ["rev-parse", "--verify", "--quiet", ref], { timeout: 5000 }).then(
|
|
55
55
|
() => true,
|
|
56
56
|
// Exit 1 is "no such branch"; a timeout or a lock isn't an answer.
|
|
57
57
|
(e: { code?: unknown }) => e?.code !== 1
|
|
@@ -94,7 +94,7 @@ function moveBranch(session: Session, branch: string): void {
|
|
|
94
94
|
|
|
95
95
|
// git merge-base --is-ancestor: exit 0 yes, 1 no, anything else can't say.
|
|
96
96
|
const isAncestor = (dir: string, sha: string, ref: string) =>
|
|
97
|
-
|
|
97
|
+
git(dir, ["merge-base", "--is-ancestor", sha, ref], { timeout: 5000 }).then(
|
|
98
98
|
() => true as const,
|
|
99
99
|
(e: { code?: unknown }) => (e?.code === 1 ? (false as const) : null)
|
|
100
100
|
);
|