@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
package/README.md
CHANGED
|
@@ -908,10 +908,13 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
908
908
|
`BLOCKED:` or waiting, the diff stays in scope (no secrets, not only
|
|
909
909
|
lockfiles, nothing outside the repo, within the card) and its stack parent
|
|
910
910
|
has merged, and the PR body's Code review section names that commit. `land` judges each item again at its own head right before
|
|
911
|
-
merging it.
|
|
912
|
-
as an ask, and the orchestrator stops merging that task.
|
|
913
|
-
|
|
914
|
-
|
|
911
|
+
merging it. A gate failing again on a different commit, or a repo with no
|
|
912
|
+
CI, goes to you as an ask, and the orchestrator stops merging that task.
|
|
913
|
+
A failure counts once per task, gate and head commit, however many times
|
|
914
|
+
and from wherever that commit is judged (the armed watcher, CI finishing,
|
|
915
|
+
`sign_off`, `done`, `land`); a pass in between doesn't reset it. A review
|
|
916
|
+
that blocks counts when its verdict arrives, whether or not `sign_off`
|
|
917
|
+
ever reads it.
|
|
915
918
|
- **Arm to merge:** `arm` (or **Arm merge** on a task's row) makes AgentOS
|
|
916
919
|
merge the task's PR itself the moment every `sign_off` gate passes at its
|
|
917
920
|
head, starting the review once CI is green, so nothing polls `sign_off`.
|
|
@@ -1258,6 +1261,13 @@ One Node process serves every terminal, chat stream and request, so a route
|
|
|
1258
1261
|
handler in `app/api/` may not call a `*Sync` child_process or fs function:
|
|
1259
1262
|
ESLint refuses it. Use the async form with a timeout.
|
|
1260
1263
|
|
|
1264
|
+
Every git process starts in `lib/git-exec.ts`: `git()`, `gitResult()`,
|
|
1265
|
+
`gitOk()` and named operations (`revParse`, `worktreeAdd`, `gitPush`, …). It
|
|
1266
|
+
sets one timeout, kills a hung git, never lets git prompt, keeps the server's
|
|
1267
|
+
tmux out of hooks and rejects with a `GitError` carrying the exit code and
|
|
1268
|
+
stderr. ESLint refuses git spawned anywhere else in `lib/`, `app/` or
|
|
1269
|
+
`server.ts`.
|
|
1270
|
+
|
|
1261
1271
|
`scripts/redeploy` (and `scripts/autodeploy`, which calls it) builds into
|
|
1262
1272
|
`.next-build` while the live server keeps serving `.next`. The build is swapped
|
|
1263
1273
|
in by rename only once it finished with a `BUILD_ID`, right before the restart.
|
|
@@ -45,7 +45,8 @@ export async function POST(request: NextRequest) {
|
|
|
45
45
|
|
|
46
46
|
if (
|
|
47
47
|
branchName &&
|
|
48
|
-
(typeof branchName !== "string" ||
|
|
48
|
+
(typeof branchName !== "string" ||
|
|
49
|
+
!(await isValidBranchName(path, branchName)))
|
|
49
50
|
) {
|
|
50
51
|
return NextResponse.json(
|
|
51
52
|
{ error: "Invalid branch name" },
|
|
@@ -64,8 +65,8 @@ export async function POST(request: NextRequest) {
|
|
|
64
65
|
|
|
65
66
|
// Create new branch if on main/master and branch name provided
|
|
66
67
|
let newBranch = false;
|
|
67
|
-
if (branchName && isMainBranch(path)) {
|
|
68
|
-
createBranch(path, branchName);
|
|
68
|
+
if (branchName && (await isMainBranch(path))) {
|
|
69
|
+
await createBranch(path, branchName);
|
|
69
70
|
newBranch = true;
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from "util";
|
|
2
|
+
import { git } from "@/lib/git-exec";
|
|
4
3
|
import { expandPath } from "@/lib/git-status";
|
|
5
4
|
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
5
|
/**
|
|
9
6
|
* GET /api/git/file-content?path=...&file=...
|
|
10
7
|
* Get file content from git HEAD (original version before changes)
|
|
@@ -33,17 +30,9 @@ export async function GET(request: NextRequest) {
|
|
|
33
30
|
|
|
34
31
|
try {
|
|
35
32
|
// Get file content from HEAD
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
cwd: expandedPath,
|
|
41
|
-
encoding: "utf-8",
|
|
42
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
43
|
-
timeout: 30_000,
|
|
44
|
-
killSignal: "SIGKILL",
|
|
45
|
-
}
|
|
46
|
-
);
|
|
33
|
+
const content = await git(expandedPath, ["show", `HEAD:${file}`], {
|
|
34
|
+
timeout: 30_000,
|
|
35
|
+
});
|
|
47
36
|
|
|
48
37
|
return NextResponse.json({ content });
|
|
49
38
|
} catch (gitError: unknown) {
|
|
@@ -31,7 +31,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const diff = getCommitFileDiff(path, hash, file);
|
|
34
|
+
const diff = await getCommitFileDiff(path, hash, file);
|
|
35
35
|
return NextResponse.json({ diff });
|
|
36
36
|
} catch (error) {
|
|
37
37
|
console.error("Error getting commit file diff:", error);
|
|
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const commit = getCommitDetail(path, hash);
|
|
26
|
+
const commit = await getCommitDetail(path, hash);
|
|
27
27
|
if (!commit) {
|
|
28
28
|
return NextResponse.json({ error: "Commit not found" }, { status: 404 });
|
|
29
29
|
}
|
|
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const commits = getCommitHistory(path, limit);
|
|
18
|
+
const commits = await getCommitHistory(path, limit);
|
|
19
19
|
return NextResponse.json({ commits });
|
|
20
20
|
} catch (error) {
|
|
21
21
|
console.error("Error getting commit history:", error);
|
|
@@ -28,7 +28,7 @@ export async function POST(request: NextRequest) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Check if remote exists
|
|
31
|
-
const remoteUrl = getRemoteUrl(path);
|
|
31
|
+
const remoteUrl = await getRemoteUrl(path);
|
|
32
32
|
if (!remoteUrl) {
|
|
33
33
|
return NextResponse.json(
|
|
34
34
|
{ error: "No remote origin configured" },
|
|
@@ -48,7 +48,7 @@ export async function POST(request: NextRequest) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Push (set upstream if needed)
|
|
51
|
-
const needsUpstream = !hasUpstream(path);
|
|
51
|
+
const needsUpstream = !(await hasUpstream(path));
|
|
52
52
|
const output = await push(path, needsUpstream);
|
|
53
53
|
|
|
54
54
|
forgetGitStatus(path);
|
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { execFile } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
4
|
import { getDb, queries, type Session } from "@/lib/db";
|
|
5
|
+
import { gitPush } from "@/lib/git-exec";
|
|
5
6
|
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
|
|
@@ -69,8 +70,7 @@ async function createPR(
|
|
|
69
70
|
): Promise<PRInfo> {
|
|
70
71
|
// First push the branch if not already pushed
|
|
71
72
|
try {
|
|
72
|
-
await
|
|
73
|
-
cwd: projectPath,
|
|
73
|
+
await gitPush(projectPath, ["-u", "origin", "--", branchName], {
|
|
74
74
|
timeout: 30000,
|
|
75
75
|
});
|
|
76
76
|
} catch {
|
package/lib/away/deploy.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import type { ProjectConfig } from "../project-config";
|
|
12
|
-
import {
|
|
12
|
+
import { gitFetch, isAncestor } from "../git-exec";
|
|
13
13
|
import { expandHome } from "../tasks/session";
|
|
14
14
|
import { selfUrl } from "../agents/self-env";
|
|
15
15
|
import { commitTime } from "../orchestrator/repo";
|
|
@@ -61,15 +61,11 @@ async function contains(
|
|
|
61
61
|
sha: string
|
|
62
62
|
): Promise<boolean> {
|
|
63
63
|
if (tip === sha) return true;
|
|
64
|
-
const ancestor = () =>
|
|
65
|
-
run("git", ["merge-base", "--is-ancestor", sha, tip], repo).then(
|
|
66
|
-
() => true,
|
|
67
|
-
() => false
|
|
68
|
-
);
|
|
64
|
+
const ancestor = () => isAncestor(repo, sha, tip);
|
|
69
65
|
if (await ancestor()) return true;
|
|
70
66
|
// The deployed commit may be newer than this checkout knows. A fetch that
|
|
71
67
|
// fails throws, so stale refs never read as "not deployed".
|
|
72
|
-
await
|
|
68
|
+
await gitFetch(repo, ["--quiet", "origin"]);
|
|
73
69
|
return ancestor();
|
|
74
70
|
}
|
|
75
71
|
|
package/lib/away/run.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { notifyPhone } from "../notify";
|
|
|
23
23
|
import { getProject } from "../projects";
|
|
24
24
|
import { loadProjectConfig } from "../project-config";
|
|
25
25
|
import { run } from "../tasks/gh";
|
|
26
|
+
import { git } from "../git-exec";
|
|
26
27
|
import { expandHome } from "../tasks/session";
|
|
27
28
|
import { auditId, auditsRoot, writeAtomic } from "../session-audit/store";
|
|
28
29
|
import {
|
|
@@ -199,11 +200,11 @@ async function dirtySessions(ids: string[]): Promise<Set<string>> {
|
|
|
199
200
|
// A linked machine's worktree isn't on this disk.
|
|
200
201
|
if (!s || (s.host_id && s.host_id !== "local")) return;
|
|
201
202
|
const dir = expandHome(s.worktree_path || s.working_directory);
|
|
202
|
-
const out = await
|
|
203
|
-
"
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
).catch(() => "");
|
|
203
|
+
const out = await git(dir, [
|
|
204
|
+
"status",
|
|
205
|
+
"--porcelain",
|
|
206
|
+
"--untracked-files=normal",
|
|
207
|
+
]).catch(() => "");
|
|
207
208
|
if (out.trim()) dirty.add(id);
|
|
208
209
|
})
|
|
209
210
|
);
|
package/lib/build.ts
CHANGED
|
@@ -3,20 +3,16 @@
|
|
|
3
3
|
// A worker whose build isn't the server's is running code from before a
|
|
4
4
|
// redeploy.
|
|
5
5
|
|
|
6
|
-
import { execFileSync } from "child_process";
|
|
7
6
|
import fs from "fs";
|
|
8
7
|
import path from "path";
|
|
9
8
|
import type { ChatState } from "./chat/events";
|
|
9
|
+
import { gitSync } from "./git-exec";
|
|
10
10
|
|
|
11
11
|
export function buildId(): string {
|
|
12
12
|
if (process.env.AGENTOS_BUILD) return process.env.AGENTOS_BUILD;
|
|
13
13
|
let id = "";
|
|
14
14
|
try {
|
|
15
|
-
id =
|
|
16
|
-
cwd: process.cwd(),
|
|
17
|
-
encoding: "utf8",
|
|
18
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
19
|
-
}).trim();
|
|
15
|
+
id = gitSync(process.cwd(), ["rev-parse", "HEAD"]).trim();
|
|
20
16
|
} catch {}
|
|
21
17
|
if (!id) {
|
|
22
18
|
try {
|
package/lib/chat/files.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 type { FileSuggestion } from "./events";
|
|
4
5
|
import {
|
|
5
6
|
indexPaths,
|
|
@@ -33,11 +34,36 @@ export function sweepFileIndexes(now = Date.now()): void {
|
|
|
33
34
|
|
|
34
35
|
export const fileIndexCount = () => cache.size;
|
|
35
36
|
|
|
37
|
+
const LIST_TIMEOUT_MS = 5000;
|
|
38
|
+
|
|
39
|
+
export interface Lister {
|
|
40
|
+
// The program it needs.
|
|
41
|
+
cmd: string;
|
|
42
|
+
list: (cwd: string) => Promise<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// Ways to list a folder's files with ignored ones left out: ripgrep, or
|
|
37
46
|
// git where ripgrep isn't installed.
|
|
38
|
-
export const LISTERS: [
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
export const LISTERS: Lister[] = [
|
|
48
|
+
{
|
|
49
|
+
cmd: "rg",
|
|
50
|
+
list: async (cwd) =>
|
|
51
|
+
(
|
|
52
|
+
await execFileAsync("rg", ["--files", "--hidden", "-g", "!.git"], {
|
|
53
|
+
cwd,
|
|
54
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
55
|
+
timeout: LIST_TIMEOUT_MS,
|
|
56
|
+
})
|
|
57
|
+
).stdout,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
cmd: "git",
|
|
61
|
+
list: (cwd) =>
|
|
62
|
+
git(cwd, ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
63
|
+
timeout: LIST_TIMEOUT_MS,
|
|
64
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
41
67
|
];
|
|
42
68
|
|
|
43
69
|
// The folder's files, for @mentions when no agent is running to match them
|
|
@@ -46,13 +72,9 @@ export async function listFiles(
|
|
|
46
72
|
cwd: string,
|
|
47
73
|
listers = LISTERS
|
|
48
74
|
): Promise<IndexedPath[]> {
|
|
49
|
-
for (const
|
|
75
|
+
for (const { list } of listers) {
|
|
50
76
|
try {
|
|
51
|
-
const
|
|
52
|
-
cwd,
|
|
53
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
54
|
-
timeout: 5000,
|
|
55
|
-
});
|
|
77
|
+
const stdout = await list(cwd);
|
|
56
78
|
return indexPaths(
|
|
57
79
|
withFolders(stdout.split("\n").filter(Boolean).slice(0, MAX_FILES))
|
|
58
80
|
);
|
package/lib/db/migrations.ts
CHANGED
|
@@ -1327,6 +1327,37 @@ const migrations: Migration[] = [
|
|
|
1327
1327
|
installChangeTriggers(db, TABLES_WATCHED_58);
|
|
1328
1328
|
},
|
|
1329
1329
|
},
|
|
1330
|
+
{
|
|
1331
|
+
id: 59,
|
|
1332
|
+
name: "gate_failure_commits",
|
|
1333
|
+
up: (db) => {
|
|
1334
|
+
// Each head a gate failed on, once: a gate's failure counts once per
|
|
1335
|
+
// commit however many times that commit is judged (the armed watcher,
|
|
1336
|
+
// CI finishing, sign_off, done, land), and the second commit it fails
|
|
1337
|
+
// on goes to Saad. The key is what makes it once, across restarts and
|
|
1338
|
+
// concurrent evaluations.
|
|
1339
|
+
db.exec(`
|
|
1340
|
+
CREATE TABLE IF NOT EXISTS orchestrator_gate_failure_commits (
|
|
1341
|
+
session_id TEXT NOT NULL,
|
|
1342
|
+
gate TEXT NOT NULL,
|
|
1343
|
+
sha TEXT NOT NULL,
|
|
1344
|
+
workspace_id TEXT NOT NULL,
|
|
1345
|
+
reason TEXT,
|
|
1346
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1347
|
+
PRIMARY KEY (session_id, gate, sha)
|
|
1348
|
+
);
|
|
1349
|
+
INSERT OR IGNORE INTO orchestrator_gate_failure_commits (session_id, gate, sha, workspace_id, reason)
|
|
1350
|
+
SELECT session_id, gate, last_sha, workspace_id, last_reason
|
|
1351
|
+
FROM orchestrator_gate_failures WHERE last_sha IS NOT NULL;
|
|
1352
|
+
CREATE TRIGGER IF NOT EXISTS gate_failures_clear_commits
|
|
1353
|
+
AFTER DELETE ON orchestrator_gate_failures
|
|
1354
|
+
BEGIN
|
|
1355
|
+
DELETE FROM orchestrator_gate_failure_commits
|
|
1356
|
+
WHERE session_id = OLD.session_id AND gate = OLD.gate;
|
|
1357
|
+
END;
|
|
1358
|
+
`);
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1330
1361
|
];
|
|
1331
1362
|
|
|
1332
1363
|
// `upTo`: stop after this id (tests that start from an older database).
|
package/lib/done/remote.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Session } from "../db";
|
|
6
6
|
import type { TaskPR } from "../tasks";
|
|
7
|
-
import {
|
|
7
|
+
import { git, gitPush } from "../git-exec";
|
|
8
8
|
import { projectPathFor } from "../tasks/session";
|
|
9
9
|
import { PROTECTED_BRANCHES } from "../tasks/finish";
|
|
10
10
|
import { liveChildren } from "../stacks/guard";
|
|
@@ -23,15 +23,14 @@ export async function deleteMergedRemote(
|
|
|
23
23
|
liveChildren(s.id).length
|
|
24
24
|
)
|
|
25
25
|
return false;
|
|
26
|
-
const line = await
|
|
27
|
-
"git",
|
|
28
|
-
["ls-remote", "--heads", "origin", `refs/heads/${branch}`],
|
|
26
|
+
const line = await git(
|
|
29
27
|
repo,
|
|
30
|
-
|
|
28
|
+
["ls-remote", "--heads", "origin", `refs/heads/${branch}`],
|
|
29
|
+
{ timeout: 30000 }
|
|
31
30
|
).catch(() => "");
|
|
32
31
|
const tip = line.trim().split(/\s+/)[0];
|
|
33
32
|
if (!tip || tip !== pr.head) return false;
|
|
34
|
-
return
|
|
33
|
+
return gitPush(repo, ["origin", "--delete", branch], { timeout: 60000 })
|
|
35
34
|
.then(() => true)
|
|
36
35
|
.catch(() => false);
|
|
37
36
|
}
|
package/lib/done/testing.ts
CHANGED
|
@@ -4,18 +4,13 @@
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import os from "os";
|
|
6
6
|
import path from "path";
|
|
7
|
-
import {
|
|
7
|
+
import { gitSync } from "../git-exec";
|
|
8
8
|
|
|
9
9
|
// Old enough that CI counts as settled on every commit.
|
|
10
10
|
const env = { ...process.env, GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z" };
|
|
11
11
|
|
|
12
12
|
export const git = (cwd: string, ...args: string[]) =>
|
|
13
|
-
|
|
14
|
-
cwd,
|
|
15
|
-
encoding: "utf8",
|
|
16
|
-
stdio: "pipe",
|
|
17
|
-
env,
|
|
18
|
-
}).trim();
|
|
13
|
+
gitSync(cwd, args, { env }).trim();
|
|
19
14
|
|
|
20
15
|
export function commitFile(cwd: string, file: string, text: string): string {
|
|
21
16
|
fs.mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
package/lib/done/worktree.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import type { Session } from "../db";
|
|
9
9
|
import { getDefaultBranch } from "../git";
|
|
10
|
-
import {
|
|
10
|
+
import { git as runGit } from "../git-exec";
|
|
11
11
|
import { mergePolicy } from "../tasks/merge-policy";
|
|
12
12
|
import { getProject } from "../projects";
|
|
13
13
|
import {
|
|
@@ -24,7 +24,7 @@ export type WorktreeFate =
|
|
|
24
24
|
| { action: "kept"; why: string; path: string };
|
|
25
25
|
|
|
26
26
|
const git = (cwd: string, ...args: string[]) =>
|
|
27
|
-
|
|
27
|
+
runGit(cwd, args, { timeout: 30000 }).then((out) => out.trim());
|
|
28
28
|
|
|
29
29
|
// The main checkout a worktree belongs to.
|
|
30
30
|
async function repoOf(worktree: string): Promise<string> {
|