@glrs-dev/cli 0.3.1 → 1.0.0
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/CHANGELOG.md +10 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +18 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +19 -9
- package/dist/vendor/harness-opencode/dist/chunk-57EOY72Y.js +174 -0
- package/dist/vendor/harness-opencode/dist/chunk-5TAMY7P6.js +67 -0
- package/dist/vendor/harness-opencode/dist/chunk-BKTFWXLG.js +204 -0
- package/dist/vendor/harness-opencode/dist/chunk-KB7M7JXU.js +145 -0
- package/dist/vendor/harness-opencode/dist/chunk-RNRCXQ65.js +56 -0
- package/dist/vendor/harness-opencode/dist/cli.js +955 -1453
- package/dist/vendor/harness-opencode/dist/index.js +1 -1
- package/dist/vendor/harness-opencode/dist/paths-LT3QQKCF.js +18 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.d.ts +1 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.js +228 -0
- package/dist/vendor/harness-opencode/dist/pilot-config-7LJZ23YK.js +55 -0
- package/dist/vendor/harness-opencode/dist/runs-QWPL3TKV.js +18 -0
- package/dist/vendor/harness-opencode/dist/safety-gate-WM3EWOCY.js +10 -0
- package/dist/vendor/harness-opencode/dist/setup-hook-FHTXMAQL.js +88 -0
- package/dist/vendor/harness-opencode/dist/skills/adr/SKILL.md +328 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/SKILL.md +40 -13
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +27 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/self-review.md +1 -1
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/touches-scope.md +34 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/verify-design.md +78 -14
- package/dist/vendor/harness-opencode/dist/tasks-KJ3WN2KY.js +32 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +1 -1
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/setup-authoring.md +0 -68
|
@@ -3,23 +3,64 @@ import {
|
|
|
3
3
|
createAgents,
|
|
4
4
|
validateModelOverride
|
|
5
5
|
} from "./chunk-CZMAJISX.js";
|
|
6
|
+
import {
|
|
7
|
+
getSessionsPath,
|
|
8
|
+
registerSession,
|
|
9
|
+
unregisterSession
|
|
10
|
+
} from "./chunk-RNRCXQ65.js";
|
|
6
11
|
import {
|
|
7
12
|
install,
|
|
8
13
|
requirePlugin
|
|
9
14
|
} from "./chunk-WBBN7OVN.js";
|
|
10
15
|
import "./chunk-VJUETC6A.js";
|
|
16
|
+
import {
|
|
17
|
+
getPilotDir,
|
|
18
|
+
getPlanDir,
|
|
19
|
+
getPlansDir,
|
|
20
|
+
getRunDir,
|
|
21
|
+
getStateDbPath,
|
|
22
|
+
getTaskJsonlPath,
|
|
23
|
+
getWorkerJsonlPath,
|
|
24
|
+
migratePlans,
|
|
25
|
+
resolveBaseDir
|
|
26
|
+
} from "./chunk-BKTFWXLG.js";
|
|
27
|
+
import {
|
|
28
|
+
createRun,
|
|
29
|
+
getRun,
|
|
30
|
+
markRunFinished,
|
|
31
|
+
markRunResumed,
|
|
32
|
+
markRunRunning
|
|
33
|
+
} from "./chunk-5TAMY7P6.js";
|
|
34
|
+
import {
|
|
35
|
+
countByStatus,
|
|
36
|
+
getTask,
|
|
37
|
+
listTasks,
|
|
38
|
+
markAborted,
|
|
39
|
+
markBlocked,
|
|
40
|
+
markFailed,
|
|
41
|
+
markReady,
|
|
42
|
+
markRunning,
|
|
43
|
+
markSucceeded,
|
|
44
|
+
resetTasksForResume,
|
|
45
|
+
setCostUsd,
|
|
46
|
+
upsertFromPlan
|
|
47
|
+
} from "./chunk-57EOY72Y.js";
|
|
48
|
+
import {
|
|
49
|
+
checkCwdSafety,
|
|
50
|
+
headSha
|
|
51
|
+
} from "./chunk-KB7M7JXU.js";
|
|
11
52
|
|
|
12
53
|
// src/cli.ts
|
|
13
54
|
import {
|
|
14
55
|
binary,
|
|
15
|
-
command as
|
|
16
|
-
flag as
|
|
17
|
-
option as
|
|
18
|
-
optional as
|
|
19
|
-
positional as
|
|
56
|
+
command as command9,
|
|
57
|
+
flag as flag7,
|
|
58
|
+
option as option7,
|
|
59
|
+
optional as optional8,
|
|
60
|
+
positional as positional5,
|
|
20
61
|
restPositionals,
|
|
21
|
-
string as
|
|
22
|
-
subcommands as
|
|
62
|
+
string as string8,
|
|
63
|
+
subcommands as subcommands2,
|
|
23
64
|
run
|
|
24
65
|
} from "cmd-ts";
|
|
25
66
|
|
|
@@ -115,9 +156,9 @@ function getOpencodeConfigPath2() {
|
|
|
115
156
|
const configHome = process.env["XDG_CONFIG_HOME"] ?? path2.join(os2.homedir(), ".config");
|
|
116
157
|
return path2.join(configHome, "opencode", "opencode.json");
|
|
117
158
|
}
|
|
118
|
-
function cmd(
|
|
159
|
+
function cmd(command10) {
|
|
119
160
|
try {
|
|
120
|
-
return execSync(
|
|
161
|
+
return execSync(command10, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
121
162
|
} catch {
|
|
122
163
|
return null;
|
|
123
164
|
}
|
|
@@ -328,132 +369,17 @@ function planCheck(args) {
|
|
|
328
369
|
}
|
|
329
370
|
}
|
|
330
371
|
|
|
331
|
-
// src/plan-paths.ts
|
|
332
|
-
import { execFile } from "child_process";
|
|
333
|
-
import * as fs3 from "fs/promises";
|
|
334
|
-
import * as os3 from "os";
|
|
335
|
-
import * as path3 from "path";
|
|
336
|
-
function execFileP(file, args, opts = {}) {
|
|
337
|
-
const { cwd, timeoutMs = 5e3 } = opts;
|
|
338
|
-
return new Promise((resolve5, reject) => {
|
|
339
|
-
const controller = new AbortController();
|
|
340
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
341
|
-
execFile(
|
|
342
|
-
file,
|
|
343
|
-
args,
|
|
344
|
-
{ signal: controller.signal, cwd, encoding: "utf8" },
|
|
345
|
-
(err, stdout) => {
|
|
346
|
-
clearTimeout(timer);
|
|
347
|
-
if (err) {
|
|
348
|
-
reject(err);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
resolve5(stdout ?? "");
|
|
352
|
-
}
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
function expandTilde(p) {
|
|
357
|
-
if (p === "~") return os3.homedir();
|
|
358
|
-
if (p.startsWith("~/")) return path3.join(os3.homedir(), p.slice(2));
|
|
359
|
-
return p;
|
|
360
|
-
}
|
|
361
|
-
async function getRepoFolder(worktreeDir) {
|
|
362
|
-
let stdout;
|
|
363
|
-
try {
|
|
364
|
-
stdout = await execFileP(
|
|
365
|
-
"git",
|
|
366
|
-
["rev-parse", "--git-common-dir"],
|
|
367
|
-
{ cwd: worktreeDir }
|
|
368
|
-
);
|
|
369
|
-
} catch (err) {
|
|
370
|
-
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
371
|
-
throw new Error(
|
|
372
|
-
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
const gitCommonDir = stdout.trim();
|
|
376
|
-
if (!gitCommonDir) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
const absCommonDir = path3.isAbsolute(gitCommonDir) ? gitCommonDir : path3.resolve(worktreeDir, gitCommonDir);
|
|
382
|
-
const repoRoot = path3.dirname(absCommonDir);
|
|
383
|
-
return path3.basename(repoRoot);
|
|
384
|
-
}
|
|
385
|
-
async function getPlanDir(worktreeDir) {
|
|
386
|
-
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
387
|
-
const base = override ? expandTilde(override) : path3.join(os3.homedir(), ".glorious", "opencode");
|
|
388
|
-
const repoFolder = await getRepoFolder(worktreeDir);
|
|
389
|
-
const planDir = path3.join(base, repoFolder, "plans");
|
|
390
|
-
await fs3.mkdir(planDir, { recursive: true });
|
|
391
|
-
return planDir;
|
|
392
|
-
}
|
|
393
|
-
async function migratePlans(worktreeDir, planDir) {
|
|
394
|
-
const oldDir = path3.join(worktreeDir, ".agent", "plans");
|
|
395
|
-
const marker = path3.join(oldDir, ".migrated");
|
|
396
|
-
try {
|
|
397
|
-
await fs3.stat(oldDir);
|
|
398
|
-
} catch {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
await fs3.stat(marker);
|
|
403
|
-
return;
|
|
404
|
-
} catch {
|
|
405
|
-
}
|
|
406
|
-
let entries;
|
|
407
|
-
try {
|
|
408
|
-
entries = await fs3.readdir(oldDir);
|
|
409
|
-
} catch {
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const planFiles = entries.filter(
|
|
413
|
-
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
414
|
-
);
|
|
415
|
-
await fs3.mkdir(planDir, { recursive: true });
|
|
416
|
-
for (const name of planFiles) {
|
|
417
|
-
const src = path3.join(oldDir, name);
|
|
418
|
-
const dst = path3.join(planDir, name);
|
|
419
|
-
let dstExists = false;
|
|
420
|
-
try {
|
|
421
|
-
await fs3.stat(dst);
|
|
422
|
-
dstExists = true;
|
|
423
|
-
} catch {
|
|
424
|
-
dstExists = false;
|
|
425
|
-
}
|
|
426
|
-
if (!dstExists) {
|
|
427
|
-
await fs3.rename(src, dst);
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
const [srcBuf, dstBuf] = await Promise.all([
|
|
431
|
-
fs3.readFile(src),
|
|
432
|
-
fs3.readFile(dst)
|
|
433
|
-
]);
|
|
434
|
-
if (srcBuf.equals(dstBuf)) {
|
|
435
|
-
await fs3.unlink(src);
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
process.stderr.write(
|
|
439
|
-
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
440
|
-
`
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
await fs3.writeFile(marker, "");
|
|
444
|
-
}
|
|
445
|
-
|
|
446
372
|
// src/pilot/cli/index.ts
|
|
447
|
-
import { subcommands
|
|
373
|
+
import { subcommands } from "cmd-ts";
|
|
448
374
|
|
|
449
375
|
// src/pilot/cli/validate.ts
|
|
450
376
|
import { command, optional, positional, string, flag } from "cmd-ts";
|
|
451
|
-
import { promises as fs6 } from "fs";
|
|
452
|
-
import * as path6 from "path";
|
|
453
|
-
|
|
454
|
-
// src/pilot/plan/load.ts
|
|
455
377
|
import { promises as fs4 } from "fs";
|
|
456
378
|
import * as path4 from "path";
|
|
379
|
+
|
|
380
|
+
// src/pilot/plan/load.ts
|
|
381
|
+
import { promises as fs3 } from "fs";
|
|
382
|
+
import * as path3 from "path";
|
|
457
383
|
import { parse as parseYaml, YAMLParseError } from "yaml";
|
|
458
384
|
|
|
459
385
|
// src/pilot/plan/schema.ts
|
|
@@ -496,6 +422,7 @@ var TaskSchema = z.object({
|
|
|
496
422
|
prompt: z.string().min(1, "task prompt must be non-empty"),
|
|
497
423
|
context: z.string().optional(),
|
|
498
424
|
touches: TouchesSchema.default([]),
|
|
425
|
+
tolerate: TouchesSchema.default([]),
|
|
499
426
|
verify: z.array(VerifyCommandSchema).default([]),
|
|
500
427
|
depends_on: z.array(z.string().regex(TASK_ID_PATTERN, "depends_on entries must be valid task IDs")).default([]),
|
|
501
428
|
agent: z.string().min(1).optional(),
|
|
@@ -514,9 +441,34 @@ var PlanSchema = z.object({
|
|
|
514
441
|
branch_prefix: z.string().min(1).optional(),
|
|
515
442
|
defaults: DefaultsSchema,
|
|
516
443
|
milestones: z.array(MilestoneSchema).default([]),
|
|
517
|
-
setup: z.array(VerifyCommandSchema).default([]),
|
|
518
444
|
tasks: z.array(TaskSchema).min(1, "plan must declare at least one task")
|
|
519
|
-
}).
|
|
445
|
+
}).passthrough().superRefine((val, ctx) => {
|
|
446
|
+
const known = /* @__PURE__ */ new Set([
|
|
447
|
+
"name",
|
|
448
|
+
"branch_prefix",
|
|
449
|
+
"defaults",
|
|
450
|
+
"milestones",
|
|
451
|
+
"tasks"
|
|
452
|
+
]);
|
|
453
|
+
if (typeof val !== "object" || val === null) return;
|
|
454
|
+
const v = val;
|
|
455
|
+
for (const key of Object.keys(v)) {
|
|
456
|
+
if (known.has(key)) continue;
|
|
457
|
+
if (key === "setup") {
|
|
458
|
+
ctx.addIssue({
|
|
459
|
+
code: z.ZodIssueCode.custom,
|
|
460
|
+
path: ["setup"],
|
|
461
|
+
message: "The 'setup:' field was removed in the cwd-mode rollback. Run setup commands manually before 'pilot build' \u2014 see src/pilot/AGENTS.md for the new contract."
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
ctx.addIssue({
|
|
465
|
+
code: z.ZodIssueCode.custom,
|
|
466
|
+
path: [key],
|
|
467
|
+
message: `Unrecognized key: ${JSON.stringify(key)}`
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
});
|
|
520
472
|
function parsePlan(input) {
|
|
521
473
|
const result = PlanSchema.safeParse(input);
|
|
522
474
|
if (result.success) {
|
|
@@ -547,10 +499,10 @@ async function loadPlan(absPath) {
|
|
|
547
499
|
if (typeof absPath !== "string") {
|
|
548
500
|
throw new TypeError(`loadPlan: expected string path, got ${typeof absPath}`);
|
|
549
501
|
}
|
|
550
|
-
const resolved =
|
|
502
|
+
const resolved = path3.resolve(absPath);
|
|
551
503
|
let raw;
|
|
552
504
|
try {
|
|
553
|
-
raw = await
|
|
505
|
+
raw = await fs3.readFile(resolved, "utf8");
|
|
554
506
|
} catch (err) {
|
|
555
507
|
return {
|
|
556
508
|
ok: false,
|
|
@@ -804,98 +756,6 @@ function findTouchConflicts(tasks) {
|
|
|
804
756
|
return conflicts;
|
|
805
757
|
}
|
|
806
758
|
|
|
807
|
-
// src/pilot/paths.ts
|
|
808
|
-
import { promises as fs5 } from "fs";
|
|
809
|
-
import * as os4 from "os";
|
|
810
|
-
import * as path5 from "path";
|
|
811
|
-
function expandTilde2(p) {
|
|
812
|
-
if (p === "~") return os4.homedir();
|
|
813
|
-
if (p.startsWith("~/")) return path5.join(os4.homedir(), p.slice(2));
|
|
814
|
-
return p;
|
|
815
|
-
}
|
|
816
|
-
function resolveBaseDir() {
|
|
817
|
-
const pilotEnv = process.env.GLORIOUS_PILOT_DIR;
|
|
818
|
-
if (pilotEnv) return expandTilde2(pilotEnv);
|
|
819
|
-
const planEnv = process.env.GLORIOUS_PLAN_DIR;
|
|
820
|
-
if (planEnv) {
|
|
821
|
-
return path5.dirname(expandTilde2(planEnv));
|
|
822
|
-
}
|
|
823
|
-
return path5.join(os4.homedir(), ".glorious", "opencode");
|
|
824
|
-
}
|
|
825
|
-
function padWorker(n) {
|
|
826
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
827
|
-
throw new RangeError(`worker index must be a non-negative integer, got ${n}`);
|
|
828
|
-
}
|
|
829
|
-
return n.toString().padStart(2, "0");
|
|
830
|
-
}
|
|
831
|
-
async function getPilotDir(cwd) {
|
|
832
|
-
const base = resolveBaseDir();
|
|
833
|
-
const repoFolder = await getRepoFolder(cwd);
|
|
834
|
-
const dir = path5.join(base, repoFolder, "pilot");
|
|
835
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
836
|
-
return dir;
|
|
837
|
-
}
|
|
838
|
-
async function getPlansDir(cwd) {
|
|
839
|
-
const pilot = await getPilotDir(cwd);
|
|
840
|
-
const dir = path5.join(pilot, "plans");
|
|
841
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
842
|
-
return dir;
|
|
843
|
-
}
|
|
844
|
-
async function getRunDir(cwd, runId) {
|
|
845
|
-
if (!isSafeRunId(runId)) {
|
|
846
|
-
throw new Error(
|
|
847
|
-
`getRunDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
const pilot = await getPilotDir(cwd);
|
|
851
|
-
const dir = path5.join(pilot, "runs", runId);
|
|
852
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
853
|
-
return dir;
|
|
854
|
-
}
|
|
855
|
-
async function getWorktreeDir(cwd, runId, n) {
|
|
856
|
-
if (!isSafeRunId(runId)) {
|
|
857
|
-
throw new Error(
|
|
858
|
-
`getWorktreeDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
const pilot = await getPilotDir(cwd);
|
|
862
|
-
const parent = path5.join(pilot, "worktrees", runId);
|
|
863
|
-
await fs5.mkdir(parent, { recursive: true });
|
|
864
|
-
return path5.join(parent, padWorker(n));
|
|
865
|
-
}
|
|
866
|
-
async function getStateDbPath(cwd, runId) {
|
|
867
|
-
const runDir = await getRunDir(cwd, runId);
|
|
868
|
-
return path5.join(runDir, "state.db");
|
|
869
|
-
}
|
|
870
|
-
async function getWorkerJsonlPath(cwd, runId, n) {
|
|
871
|
-
const runDir = await getRunDir(cwd, runId);
|
|
872
|
-
const workersDir = path5.join(runDir, "workers");
|
|
873
|
-
await fs5.mkdir(workersDir, { recursive: true });
|
|
874
|
-
return path5.join(workersDir, `${padWorker(n)}.jsonl`);
|
|
875
|
-
}
|
|
876
|
-
async function getTaskJsonlPath(cwd, runId, taskId) {
|
|
877
|
-
if (!isSafeRunId(runId)) {
|
|
878
|
-
throw new Error(
|
|
879
|
-
`getTaskJsonlPath: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
if (!isSafeTaskId(taskId)) {
|
|
883
|
-
throw new Error(
|
|
884
|
-
`getTaskJsonlPath: taskId ${JSON.stringify(taskId)} is not a safe filesystem segment`
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
const runDir = await getRunDir(cwd, runId);
|
|
888
|
-
const taskDir = path5.join(runDir, "tasks", taskId);
|
|
889
|
-
await fs5.mkdir(taskDir, { recursive: true });
|
|
890
|
-
return path5.join(taskDir, "session.jsonl");
|
|
891
|
-
}
|
|
892
|
-
function isSafeRunId(runId) {
|
|
893
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(runId);
|
|
894
|
-
}
|
|
895
|
-
function isSafeTaskId(taskId) {
|
|
896
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
759
|
// src/pilot/cli/validate.ts
|
|
900
760
|
var validateCmd = command({
|
|
901
761
|
name: "validate",
|
|
@@ -982,17 +842,17 @@ async function runValidate(opts) {
|
|
|
982
842
|
}
|
|
983
843
|
async function resolvePlanPath(input) {
|
|
984
844
|
if (input !== void 0 && input.length > 0) {
|
|
985
|
-
const resolved =
|
|
986
|
-
let
|
|
845
|
+
const resolved = path4.resolve(input);
|
|
846
|
+
let stat;
|
|
987
847
|
try {
|
|
988
|
-
|
|
848
|
+
stat = await fs4.stat(resolved);
|
|
989
849
|
} catch (err) {
|
|
990
850
|
throw new Error(
|
|
991
851
|
`cannot stat ${JSON.stringify(resolved)}: ${err instanceof Error ? err.message : String(err)}`
|
|
992
852
|
);
|
|
993
853
|
}
|
|
994
|
-
if (
|
|
995
|
-
if (
|
|
854
|
+
if (stat.isFile()) return resolved;
|
|
855
|
+
if (stat.isDirectory()) return findNewestYaml(resolved);
|
|
996
856
|
throw new Error(
|
|
997
857
|
`${JSON.stringify(resolved)} is neither a file nor a directory`
|
|
998
858
|
);
|
|
@@ -1003,7 +863,7 @@ async function resolvePlanPath(input) {
|
|
|
1003
863
|
async function findNewestYaml(dir) {
|
|
1004
864
|
let entries;
|
|
1005
865
|
try {
|
|
1006
|
-
entries = await
|
|
866
|
+
entries = await fs4.readdir(dir);
|
|
1007
867
|
} catch (err) {
|
|
1008
868
|
throw new Error(
|
|
1009
869
|
`cannot read directory ${JSON.stringify(dir)}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1017,14 +877,14 @@ async function findNewestYaml(dir) {
|
|
|
1017
877
|
}
|
|
1018
878
|
let newest = null;
|
|
1019
879
|
for (const name of yamls) {
|
|
1020
|
-
const full =
|
|
1021
|
-
let
|
|
880
|
+
const full = path4.join(dir, name);
|
|
881
|
+
let stat;
|
|
1022
882
|
try {
|
|
1023
|
-
|
|
883
|
+
stat = await fs4.stat(full);
|
|
1024
884
|
} catch {
|
|
1025
885
|
continue;
|
|
1026
886
|
}
|
|
1027
|
-
const mtime =
|
|
887
|
+
const mtime = stat.mtimeMs;
|
|
1028
888
|
if (newest === null || mtime > newest.mtime) {
|
|
1029
889
|
newest = { name, mtime };
|
|
1030
890
|
}
|
|
@@ -1032,14 +892,14 @@ async function findNewestYaml(dir) {
|
|
|
1032
892
|
if (newest === null) {
|
|
1033
893
|
throw new Error(`no readable *.yaml files in ${JSON.stringify(dir)}`);
|
|
1034
894
|
}
|
|
1035
|
-
return
|
|
895
|
+
return path4.join(dir, newest.name);
|
|
1036
896
|
}
|
|
1037
897
|
|
|
1038
898
|
// src/pilot/cli/plan.ts
|
|
1039
899
|
import { command as command2, optional as optional2, positional as positional2, string as string2, option } from "cmd-ts";
|
|
1040
900
|
import { spawn } from "child_process";
|
|
1041
|
-
import { promises as
|
|
1042
|
-
import * as
|
|
901
|
+
import { promises as fs5 } from "fs";
|
|
902
|
+
import * as path5 from "path";
|
|
1043
903
|
var PLANNER_AGENT = "pilot-planner";
|
|
1044
904
|
var planCmd = command2({
|
|
1045
905
|
name: "plan",
|
|
@@ -1133,15 +993,15 @@ async function snapshotYamls(dir) {
|
|
|
1133
993
|
const out = /* @__PURE__ */ new Map();
|
|
1134
994
|
let entries;
|
|
1135
995
|
try {
|
|
1136
|
-
entries = await
|
|
996
|
+
entries = await fs5.readdir(dir);
|
|
1137
997
|
} catch {
|
|
1138
998
|
return out;
|
|
1139
999
|
}
|
|
1140
1000
|
for (const name of entries) {
|
|
1141
1001
|
if (!name.endsWith(".yaml") && !name.endsWith(".yml")) continue;
|
|
1142
|
-
const full =
|
|
1002
|
+
const full = path5.join(dir, name);
|
|
1143
1003
|
try {
|
|
1144
|
-
const st = await
|
|
1004
|
+
const st = await fs5.stat(full);
|
|
1145
1005
|
out.set(full, st.mtimeMs);
|
|
1146
1006
|
} catch {
|
|
1147
1007
|
}
|
|
@@ -1165,18 +1025,18 @@ function pickNewestNew(before, after) {
|
|
|
1165
1025
|
return { path: pool[0].path, mtimeMs: pool[0].mtimeMs };
|
|
1166
1026
|
}
|
|
1167
1027
|
function spawnTui(args) {
|
|
1168
|
-
return new Promise((
|
|
1028
|
+
return new Promise((resolve6) => {
|
|
1169
1029
|
const child = spawn(args.bin, args.args, {
|
|
1170
1030
|
cwd: args.cwd,
|
|
1171
1031
|
stdio: "inherit"
|
|
1172
1032
|
});
|
|
1173
|
-
child.on("exit", (code) =>
|
|
1033
|
+
child.on("exit", (code) => resolve6(code ?? 1));
|
|
1174
1034
|
child.on("error", (err) => {
|
|
1175
1035
|
process.stderr.write(
|
|
1176
1036
|
`pilot plan: failed to spawn ${args.bin}: ${err.message}
|
|
1177
1037
|
`
|
|
1178
1038
|
);
|
|
1179
|
-
|
|
1039
|
+
resolve6(1);
|
|
1180
1040
|
});
|
|
1181
1041
|
});
|
|
1182
1042
|
}
|
|
@@ -1191,7 +1051,7 @@ import {
|
|
|
1191
1051
|
string as string3,
|
|
1192
1052
|
number as cmdNumber
|
|
1193
1053
|
} from "cmd-ts";
|
|
1194
|
-
import * as
|
|
1054
|
+
import * as path7 from "path";
|
|
1195
1055
|
|
|
1196
1056
|
// src/pilot/plan/slug.ts
|
|
1197
1057
|
var MAX_SLUG_LENGTH = 50;
|
|
@@ -1378,18 +1238,18 @@ function splitStatements(sql) {
|
|
|
1378
1238
|
}
|
|
1379
1239
|
|
|
1380
1240
|
// src/pilot/state/db.ts
|
|
1381
|
-
function openStateDb(
|
|
1382
|
-
const db = new Database(
|
|
1241
|
+
function openStateDb(path11) {
|
|
1242
|
+
const db = new Database(path11, { create: true });
|
|
1383
1243
|
try {
|
|
1384
1244
|
db.run("PRAGMA foreign_keys = ON");
|
|
1385
|
-
if (
|
|
1245
|
+
if (path11 !== ":memory:") {
|
|
1386
1246
|
db.run("PRAGMA journal_mode = WAL");
|
|
1387
1247
|
db.run("PRAGMA synchronous = NORMAL");
|
|
1388
1248
|
}
|
|
1389
1249
|
} catch (err) {
|
|
1390
1250
|
db.close();
|
|
1391
1251
|
throw new Error(
|
|
1392
|
-
`openStateDb: failed to set PRAGMAs on ${JSON.stringify(
|
|
1252
|
+
`openStateDb: failed to set PRAGMAs on ${JSON.stringify(path11)}: ${err instanceof Error ? err.message : String(err)}`
|
|
1393
1253
|
);
|
|
1394
1254
|
}
|
|
1395
1255
|
let newlyApplied;
|
|
@@ -1406,180 +1266,6 @@ function openStateDb(path13) {
|
|
|
1406
1266
|
};
|
|
1407
1267
|
}
|
|
1408
1268
|
|
|
1409
|
-
// src/pilot/state/runs.ts
|
|
1410
|
-
import { ulid } from "ulid";
|
|
1411
|
-
function createRun(db, args) {
|
|
1412
|
-
const id = ulid();
|
|
1413
|
-
const now = args.now ?? Date.now();
|
|
1414
|
-
db.run(
|
|
1415
|
-
`INSERT INTO runs (id, plan_path, plan_slug, started_at, status)
|
|
1416
|
-
VALUES (?, ?, ?, ?, 'pending')`,
|
|
1417
|
-
[id, args.planPath, args.slug, now]
|
|
1418
|
-
);
|
|
1419
|
-
void args.plan;
|
|
1420
|
-
return id;
|
|
1421
|
-
}
|
|
1422
|
-
function markRunRunning(db, runId) {
|
|
1423
|
-
const cur = getRun(db, runId);
|
|
1424
|
-
if (!cur) throw new Error(`markRunRunning: run ${JSON.stringify(runId)} not found`);
|
|
1425
|
-
if (cur.status === "running") return;
|
|
1426
|
-
if (cur.status !== "pending") {
|
|
1427
|
-
throw new Error(
|
|
1428
|
-
`markRunRunning: cannot move run ${JSON.stringify(runId)} from ${cur.status} to running`
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
db.run("UPDATE runs SET status='running' WHERE id=?", [runId]);
|
|
1432
|
-
}
|
|
1433
|
-
function markRunFinished(db, runId, status, now = Date.now()) {
|
|
1434
|
-
if (status !== "completed" && status !== "aborted" && status !== "failed") {
|
|
1435
|
-
throw new Error(
|
|
1436
|
-
`markRunFinished: ${JSON.stringify(status)} is not a terminal status`
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
const cur = getRun(db, runId);
|
|
1440
|
-
if (!cur) {
|
|
1441
|
-
throw new Error(`markRunFinished: run ${JSON.stringify(runId)} not found`);
|
|
1442
|
-
}
|
|
1443
|
-
db.run("UPDATE runs SET status=?, finished_at=? WHERE id=?", [status, now, runId]);
|
|
1444
|
-
}
|
|
1445
|
-
function getRun(db, runId) {
|
|
1446
|
-
const row = db.query("SELECT * FROM runs WHERE id=?").get(runId);
|
|
1447
|
-
return row;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// src/pilot/state/tasks.ts
|
|
1451
|
-
function upsertFromPlan(db, runId, plan) {
|
|
1452
|
-
const stmt = db.prepare(
|
|
1453
|
-
`INSERT OR IGNORE INTO tasks (run_id, task_id, status) VALUES (?, ?, 'pending')`
|
|
1454
|
-
);
|
|
1455
|
-
const tx = db.transaction(() => {
|
|
1456
|
-
for (const t of plan.tasks) {
|
|
1457
|
-
stmt.run(runId, t.id);
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
tx();
|
|
1461
|
-
}
|
|
1462
|
-
function markReady(db, runId, taskId) {
|
|
1463
|
-
requireStatus(db, runId, taskId, ["pending"], "ready");
|
|
1464
|
-
db.run(
|
|
1465
|
-
"UPDATE tasks SET status='ready' WHERE run_id=? AND task_id=?",
|
|
1466
|
-
[runId, taskId]
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
function markRunning(db, args) {
|
|
1470
|
-
requireStatus(db, args.runId, args.taskId, ["ready"], "running");
|
|
1471
|
-
const now = args.now ?? Date.now();
|
|
1472
|
-
db.run(
|
|
1473
|
-
`UPDATE tasks
|
|
1474
|
-
SET status='running',
|
|
1475
|
-
attempts = attempts + 1,
|
|
1476
|
-
session_id = ?,
|
|
1477
|
-
branch = ?,
|
|
1478
|
-
worktree_path = ?,
|
|
1479
|
-
started_at = COALESCE(started_at, ?)
|
|
1480
|
-
WHERE run_id=? AND task_id=?`,
|
|
1481
|
-
[args.sessionId, args.branch, args.worktreePath, now, args.runId, args.taskId]
|
|
1482
|
-
);
|
|
1483
|
-
}
|
|
1484
|
-
function markSucceeded(db, runId, taskId, now = Date.now()) {
|
|
1485
|
-
requireStatus(db, runId, taskId, ["running"], "succeeded");
|
|
1486
|
-
db.run(
|
|
1487
|
-
`UPDATE tasks
|
|
1488
|
-
SET status='succeeded', finished_at=?, last_error=NULL
|
|
1489
|
-
WHERE run_id=? AND task_id=?`,
|
|
1490
|
-
[now, runId, taskId]
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
function markFailed(db, runId, taskId, reason, now = Date.now()) {
|
|
1494
|
-
requireStatus(db, runId, taskId, ["running", "ready"], "failed");
|
|
1495
|
-
db.run(
|
|
1496
|
-
`UPDATE tasks
|
|
1497
|
-
SET status='failed', finished_at=?, last_error=?
|
|
1498
|
-
WHERE run_id=? AND task_id=?`,
|
|
1499
|
-
[now, reason, runId, taskId]
|
|
1500
|
-
);
|
|
1501
|
-
}
|
|
1502
|
-
function markBlocked(db, runId, taskId, reason) {
|
|
1503
|
-
requireStatus(db, runId, taskId, ["pending", "ready"], "blocked");
|
|
1504
|
-
db.run(
|
|
1505
|
-
`UPDATE tasks
|
|
1506
|
-
SET status='blocked', last_error=?
|
|
1507
|
-
WHERE run_id=? AND task_id=?`,
|
|
1508
|
-
[reason, runId, taskId]
|
|
1509
|
-
);
|
|
1510
|
-
}
|
|
1511
|
-
function markAborted(db, runId, taskId, reason, now = Date.now()) {
|
|
1512
|
-
requireStatus(db, runId, taskId, ["running", "ready"], "aborted");
|
|
1513
|
-
db.run(
|
|
1514
|
-
`UPDATE tasks
|
|
1515
|
-
SET status='aborted', finished_at=?, last_error=?
|
|
1516
|
-
WHERE run_id=? AND task_id=?`,
|
|
1517
|
-
[now, reason, runId, taskId]
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
function markPending(db, runId, taskId) {
|
|
1521
|
-
const cur = getTask(db, runId, taskId);
|
|
1522
|
-
if (!cur) {
|
|
1523
|
-
throw new Error(
|
|
1524
|
-
`markPending: task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
db.run(
|
|
1528
|
-
`UPDATE tasks
|
|
1529
|
-
SET status='pending',
|
|
1530
|
-
session_id=NULL,
|
|
1531
|
-
branch=NULL,
|
|
1532
|
-
worktree_path=NULL,
|
|
1533
|
-
started_at=NULL,
|
|
1534
|
-
finished_at=NULL,
|
|
1535
|
-
last_error=NULL
|
|
1536
|
-
WHERE run_id=? AND task_id=?`,
|
|
1537
|
-
[runId, taskId]
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
function setCostUsd(db, runId, taskId, costUsd) {
|
|
1541
|
-
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1542
|
-
throw new RangeError(`setCostUsd: invalid cost ${costUsd}`);
|
|
1543
|
-
}
|
|
1544
|
-
db.run(
|
|
1545
|
-
"UPDATE tasks SET cost_usd=? WHERE run_id=? AND task_id=?",
|
|
1546
|
-
[costUsd, runId, taskId]
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1549
|
-
function getTask(db, runId, taskId) {
|
|
1550
|
-
return db.query("SELECT * FROM tasks WHERE run_id=? AND task_id=?").get(runId, taskId);
|
|
1551
|
-
}
|
|
1552
|
-
function listTasks(db, runId) {
|
|
1553
|
-
return db.query("SELECT * FROM tasks WHERE run_id=? ORDER BY task_id").all(runId);
|
|
1554
|
-
}
|
|
1555
|
-
function countByStatus(db, runId) {
|
|
1556
|
-
const rows = db.query("SELECT status, COUNT(*) as n FROM tasks WHERE run_id=? GROUP BY status").all(runId);
|
|
1557
|
-
const out = {
|
|
1558
|
-
pending: 0,
|
|
1559
|
-
ready: 0,
|
|
1560
|
-
running: 0,
|
|
1561
|
-
succeeded: 0,
|
|
1562
|
-
failed: 0,
|
|
1563
|
-
blocked: 0,
|
|
1564
|
-
aborted: 0
|
|
1565
|
-
};
|
|
1566
|
-
for (const r of rows) out[r.status] = r.n;
|
|
1567
|
-
return out;
|
|
1568
|
-
}
|
|
1569
|
-
function requireStatus(db, runId, taskId, expected, intended) {
|
|
1570
|
-
const row = getTask(db, runId, taskId);
|
|
1571
|
-
if (!row) {
|
|
1572
|
-
throw new Error(
|
|
1573
|
-
`task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
1574
|
-
);
|
|
1575
|
-
}
|
|
1576
|
-
if (!expected.includes(row.status)) {
|
|
1577
|
-
throw new Error(
|
|
1578
|
-
`cannot move task ${JSON.stringify(taskId)} from ${row.status} to ${intended} (expected one of: ${expected.join(", ")})`
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
1269
|
// src/pilot/state/events.ts
|
|
1584
1270
|
function appendEvent(db, args) {
|
|
1585
1271
|
const ts = args.now ?? Date.now();
|
|
@@ -1644,9 +1330,10 @@ function tryParseJson(s) {
|
|
|
1644
1330
|
}
|
|
1645
1331
|
|
|
1646
1332
|
// src/pilot/opencode/server.ts
|
|
1647
|
-
import { execFile
|
|
1648
|
-
import * as
|
|
1649
|
-
import * as
|
|
1333
|
+
import { execFile } from "child_process";
|
|
1334
|
+
import * as fs6 from "fs";
|
|
1335
|
+
import * as path6 from "path";
|
|
1336
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1650
1337
|
import {
|
|
1651
1338
|
createOpencodeServer,
|
|
1652
1339
|
createOpencodeClient
|
|
@@ -1658,7 +1345,7 @@ async function startOpencodeServer(options = {}) {
|
|
|
1658
1345
|
const port = options.port ?? DEFAULT_PORT;
|
|
1659
1346
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
1660
1347
|
await ensureOpencodeOnPath();
|
|
1661
|
-
const serverConfig = buildPilotServerConfig();
|
|
1348
|
+
const serverConfig = buildPilotServerConfig(options.runContext);
|
|
1662
1349
|
void options.cwd;
|
|
1663
1350
|
let server;
|
|
1664
1351
|
try {
|
|
@@ -1675,8 +1362,8 @@ async function startOpencodeServer(options = {}) {
|
|
|
1675
1362
|
}
|
|
1676
1363
|
if (options.serverLogPath) {
|
|
1677
1364
|
try {
|
|
1678
|
-
|
|
1679
|
-
|
|
1365
|
+
fs6.mkdirSync(path6.dirname(options.serverLogPath), { recursive: true });
|
|
1366
|
+
fs6.writeFileSync(
|
|
1680
1367
|
options.serverLogPath,
|
|
1681
1368
|
`# pilot opencode server spawn ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1682
1369
|
# url=${server.url} hostname=${hostname} port=${port} timeoutMs=${timeoutMs}
|
|
@@ -1702,15 +1389,38 @@ async function startOpencodeServer(options = {}) {
|
|
|
1702
1389
|
};
|
|
1703
1390
|
return { url: server.url, client, shutdown };
|
|
1704
1391
|
}
|
|
1705
|
-
function buildPilotServerConfig() {
|
|
1392
|
+
function buildPilotServerConfig(runContext) {
|
|
1706
1393
|
const agents = createAgents();
|
|
1707
1394
|
const pilotAgents = {};
|
|
1708
1395
|
for (const name of ["pilot-builder", "pilot-planner"]) {
|
|
1709
1396
|
if (name in agents) pilotAgents[name] = agents[name];
|
|
1710
1397
|
}
|
|
1711
|
-
|
|
1398
|
+
const config = {
|
|
1712
1399
|
agent: pilotAgents
|
|
1713
1400
|
};
|
|
1401
|
+
if (runContext) {
|
|
1402
|
+
const sessionsPath = getSessionsPath(runContext.runDir);
|
|
1403
|
+
const distDir = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1404
|
+
const statusServerPath = path6.resolve(
|
|
1405
|
+
distDir,
|
|
1406
|
+
"pilot",
|
|
1407
|
+
"mcp",
|
|
1408
|
+
"status-server.js"
|
|
1409
|
+
);
|
|
1410
|
+
config.mcp = {
|
|
1411
|
+
pilot_status: {
|
|
1412
|
+
type: "local",
|
|
1413
|
+
command: ["bun", "run", statusServerPath],
|
|
1414
|
+
env: {
|
|
1415
|
+
PILOT_SESSIONS_PATH: sessionsPath,
|
|
1416
|
+
PILOT_STATE_DB_PATH: runContext.dbPath,
|
|
1417
|
+
PILOT_RUN_ID: runContext.runId
|
|
1418
|
+
},
|
|
1419
|
+
enabled: true
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return config;
|
|
1714
1424
|
}
|
|
1715
1425
|
function resolveTimeoutMs(explicit) {
|
|
1716
1426
|
if (typeof explicit === "number" && explicit > 0) return explicit;
|
|
@@ -1726,10 +1436,10 @@ function resolveTimeoutMs(explicit) {
|
|
|
1726
1436
|
return DEFAULT_STARTUP_TIMEOUT_MS;
|
|
1727
1437
|
}
|
|
1728
1438
|
async function ensureOpencodeOnPath() {
|
|
1729
|
-
await new Promise((
|
|
1439
|
+
await new Promise((resolve6, reject) => {
|
|
1730
1440
|
const controller = new AbortController();
|
|
1731
1441
|
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1732
|
-
|
|
1442
|
+
execFile(
|
|
1733
1443
|
"opencode",
|
|
1734
1444
|
["--version"],
|
|
1735
1445
|
{ signal: controller.signal, encoding: "utf8" },
|
|
@@ -1743,7 +1453,7 @@ async function ensureOpencodeOnPath() {
|
|
|
1743
1453
|
);
|
|
1744
1454
|
return;
|
|
1745
1455
|
}
|
|
1746
|
-
|
|
1456
|
+
resolve6();
|
|
1747
1457
|
}
|
|
1748
1458
|
);
|
|
1749
1459
|
});
|
|
@@ -1823,7 +1533,7 @@ var EventBus = class {
|
|
|
1823
1533
|
waitForIdle(sessionId, options = {}) {
|
|
1824
1534
|
const stallMs = options.stallMs ?? 60 * 60 * 1e3;
|
|
1825
1535
|
const errorIsFatal = options.errorIsFatal ?? true;
|
|
1826
|
-
return new Promise((
|
|
1536
|
+
return new Promise((resolve6) => {
|
|
1827
1537
|
let settled = false;
|
|
1828
1538
|
let stallTimer = null;
|
|
1829
1539
|
let unsubscribe = () => {
|
|
@@ -1836,7 +1546,7 @@ var EventBus = class {
|
|
|
1836
1546
|
if (stallTimer) clearTimeout(stallTimer);
|
|
1837
1547
|
unsubscribe();
|
|
1838
1548
|
removeAbortListener();
|
|
1839
|
-
|
|
1549
|
+
resolve6(result);
|
|
1840
1550
|
};
|
|
1841
1551
|
const armStallTimer = () => {
|
|
1842
1552
|
if (stallTimer) clearTimeout(stallTimer);
|
|
@@ -1962,416 +1672,17 @@ function isEventLike(v) {
|
|
|
1962
1672
|
return typeof o.type === "string" && typeof o.properties === "object" && o.properties !== null;
|
|
1963
1673
|
}
|
|
1964
1674
|
|
|
1965
|
-
// src/pilot/
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
signal: controller.signal,
|
|
1977
|
-
cwd,
|
|
1978
|
-
encoding: "utf8",
|
|
1979
|
-
env,
|
|
1980
|
-
// Increase maxBuffer — git diff/log output can exceed the
|
|
1981
|
-
// 1MB default on large repos.
|
|
1982
|
-
maxBuffer: 16 * 1024 * 1024
|
|
1983
|
-
},
|
|
1984
|
-
(err, stdout, stderr) => {
|
|
1985
|
-
clearTimeout(timer);
|
|
1986
|
-
if (err) {
|
|
1987
|
-
const msg = `${err.message}${stderr ? `
|
|
1988
|
-
stderr:
|
|
1989
|
-
${stderr}` : ""}`;
|
|
1990
|
-
reject(new Error(msg));
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
resolve5({ stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
1994
|
-
}
|
|
1995
|
-
);
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
|
-
function assertSafeArg(s, label) {
|
|
1999
|
-
if (typeof s !== "string" || s.length === 0) {
|
|
2000
|
-
throw new TypeError(`${label}: expected non-empty string, got ${JSON.stringify(s)}`);
|
|
2001
|
-
}
|
|
2002
|
-
if (s.includes("\0")) {
|
|
2003
|
-
throw new TypeError(`${label}: contains null byte: ${JSON.stringify(s)}`);
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
async function headSha(repoOrWorktree) {
|
|
2007
|
-
assertSafeArg(repoOrWorktree, "headSha repo");
|
|
2008
|
-
const { stdout } = await execFileP2("git", [
|
|
2009
|
-
"-C",
|
|
2010
|
-
repoOrWorktree,
|
|
2011
|
-
"rev-parse",
|
|
2012
|
-
"HEAD"
|
|
2013
|
-
]);
|
|
2014
|
-
return stdout.trim();
|
|
2015
|
-
}
|
|
2016
|
-
async function gitWorktreeAdd(args) {
|
|
2017
|
-
assertSafeArg(args.repoPath, "repoPath");
|
|
2018
|
-
assertSafeArg(args.worktreePath, "worktreePath");
|
|
2019
|
-
assertSafeArg(args.commitIsh, "commitIsh");
|
|
2020
|
-
const cmd2 = ["-C", args.repoPath, "worktree", "add"];
|
|
2021
|
-
if (args.branch !== void 0) {
|
|
2022
|
-
assertSafeArg(args.branch, "branch");
|
|
2023
|
-
cmd2.push("-B", args.branch);
|
|
2024
|
-
}
|
|
2025
|
-
cmd2.push(args.worktreePath, args.commitIsh);
|
|
2026
|
-
await execFileP2("git", cmd2);
|
|
2027
|
-
}
|
|
2028
|
-
async function gitWorktreeRemove(args) {
|
|
2029
|
-
assertSafeArg(args.repoPath, "repoPath");
|
|
2030
|
-
assertSafeArg(args.worktreePath, "worktreePath");
|
|
2031
|
-
try {
|
|
2032
|
-
await execFileP2("git", [
|
|
2033
|
-
"-C",
|
|
2034
|
-
args.repoPath,
|
|
2035
|
-
"worktree",
|
|
2036
|
-
"remove",
|
|
2037
|
-
"--force",
|
|
2038
|
-
args.worktreePath
|
|
2039
|
-
]);
|
|
2040
|
-
} catch (err) {
|
|
2041
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2042
|
-
if (/is not a working tree|worktree.*does not exist/i.test(msg)) {
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
throw err;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
async function gitWorktreeList(repoPath) {
|
|
2049
|
-
assertSafeArg(repoPath, "repoPath");
|
|
2050
|
-
const { stdout } = await execFileP2("git", [
|
|
2051
|
-
"-C",
|
|
2052
|
-
repoPath,
|
|
2053
|
-
"worktree",
|
|
2054
|
-
"list",
|
|
2055
|
-
"--porcelain"
|
|
2056
|
-
]);
|
|
2057
|
-
const records = [];
|
|
2058
|
-
let cur = null;
|
|
2059
|
-
for (const line of stdout.split("\n")) {
|
|
2060
|
-
if (line.length === 0) {
|
|
2061
|
-
if (cur && cur.path) records.push(finalizeWorktreeInfo(cur));
|
|
2062
|
-
cur = null;
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
if (cur === null) cur = {};
|
|
2066
|
-
const [keyRaw, ...rest] = line.split(" ");
|
|
2067
|
-
const value = rest.join(" ");
|
|
2068
|
-
switch (keyRaw) {
|
|
2069
|
-
case "worktree":
|
|
2070
|
-
cur.path = value;
|
|
2071
|
-
break;
|
|
2072
|
-
case "HEAD":
|
|
2073
|
-
cur.head = value;
|
|
2074
|
-
break;
|
|
2075
|
-
case "branch":
|
|
2076
|
-
cur.branch = value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
|
|
2077
|
-
break;
|
|
2078
|
-
case "detached":
|
|
2079
|
-
cur.branch = null;
|
|
2080
|
-
break;
|
|
2081
|
-
case "bare":
|
|
2082
|
-
cur.bare = true;
|
|
2083
|
-
break;
|
|
2084
|
-
default:
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
if (cur && cur.path) records.push(finalizeWorktreeInfo(cur));
|
|
2089
|
-
return records;
|
|
2090
|
-
}
|
|
2091
|
-
function finalizeWorktreeInfo(p) {
|
|
2092
|
-
return {
|
|
2093
|
-
path: p.path,
|
|
2094
|
-
head: p.head ?? "",
|
|
2095
|
-
branch: p.branch ?? null,
|
|
2096
|
-
bare: p.bare ?? false
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
2099
|
-
async function checkoutFreshBranch(args) {
|
|
2100
|
-
assertSafeArg(args.worktree, "worktree");
|
|
2101
|
-
assertSafeArg(args.branch, "branch");
|
|
2102
|
-
assertSafeArg(args.base, "base");
|
|
2103
|
-
await execFileP2("git", [
|
|
2104
|
-
"-C",
|
|
2105
|
-
args.worktree,
|
|
2106
|
-
"checkout",
|
|
2107
|
-
"-B",
|
|
2108
|
-
args.branch,
|
|
2109
|
-
args.base
|
|
2110
|
-
]);
|
|
2111
|
-
}
|
|
2112
|
-
async function cleanWorktree(worktree) {
|
|
2113
|
-
assertSafeArg(worktree, "worktree");
|
|
2114
|
-
await execFileP2("git", ["-C", worktree, "reset", "--hard"]);
|
|
2115
|
-
await execFileP2("git", ["-C", worktree, "clean", "-fdx"]);
|
|
2116
|
-
}
|
|
2117
|
-
async function commitAll(args) {
|
|
2118
|
-
assertSafeArg(args.worktree, "worktree");
|
|
2119
|
-
if (typeof args.message !== "string" || args.message.length === 0) {
|
|
2120
|
-
throw new TypeError("commitAll: message must be non-empty");
|
|
2121
|
-
}
|
|
2122
|
-
await execFileP2("git", ["-C", args.worktree, "add", "-A"]);
|
|
2123
|
-
const env = { ...process.env };
|
|
2124
|
-
if (args.authorName) env.GIT_AUTHOR_NAME = args.authorName;
|
|
2125
|
-
if (args.authorEmail) env.GIT_AUTHOR_EMAIL = args.authorEmail;
|
|
2126
|
-
if (args.authorName) env.GIT_COMMITTER_NAME = args.authorName;
|
|
2127
|
-
if (args.authorEmail) env.GIT_COMMITTER_EMAIL = args.authorEmail;
|
|
2128
|
-
await execFileP2("git", ["-C", args.worktree, "commit", "-m", args.message], {
|
|
2129
|
-
env
|
|
2130
|
-
});
|
|
2131
|
-
return headSha(args.worktree);
|
|
2132
|
-
}
|
|
2133
|
-
async function diffNamesSince(worktree, sinceSha) {
|
|
2134
|
-
assertSafeArg(worktree, "worktree");
|
|
2135
|
-
assertSafeArg(sinceSha, "sinceSha");
|
|
2136
|
-
const sets = await Promise.all([
|
|
2137
|
-
runDiffNames(worktree, ["diff", "--name-only", `${sinceSha}..HEAD`]),
|
|
2138
|
-
runDiffNames(worktree, ["diff", "--name-only", "--cached"]),
|
|
2139
|
-
runDiffNames(worktree, ["diff", "--name-only"]),
|
|
2140
|
-
runDiffNames(worktree, [
|
|
2141
|
-
"ls-files",
|
|
2142
|
-
"--others",
|
|
2143
|
-
"--exclude-standard"
|
|
2144
|
-
])
|
|
2145
|
-
]);
|
|
2146
|
-
const all = /* @__PURE__ */ new Set();
|
|
2147
|
-
for (const s of sets) for (const p of s) all.add(p);
|
|
2148
|
-
return [...all].sort();
|
|
2149
|
-
}
|
|
2150
|
-
async function runDiffNames(worktree, args) {
|
|
2151
|
-
const { stdout } = await execFileP2("git", ["-C", worktree, ...args]);
|
|
2152
|
-
return stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// src/pilot/worktree/pool.ts
|
|
2156
|
-
import { promises as fs9 } from "fs";
|
|
2157
|
-
var WorktreePool = class {
|
|
2158
|
-
repoPath;
|
|
2159
|
-
worktreeDirOf;
|
|
2160
|
-
slots = /* @__PURE__ */ new Map();
|
|
2161
|
-
/**
|
|
2162
|
-
* Slots that were preserved on failure. No longer reachable via
|
|
2163
|
-
* `acquire` — they stay here for `shutdown` (so `keepPreserved=false`
|
|
2164
|
-
* can still clean them up) and `inspect` (so debug tooling sees them).
|
|
2165
|
-
*
|
|
2166
|
-
* When a slot is preserved and a subsequent `acquire(n)` happens, the
|
|
2167
|
-
* current live slot at index `n` is MOVED here, and `slots` gets a
|
|
2168
|
-
* fresh stub at `n` with a bumped `retryCounter`. This is what
|
|
2169
|
-
* prevents a single failed task from poisoning every downstream task
|
|
2170
|
-
* (the pre-v0.2 bug: one preserve → all subsequent `prepare` calls
|
|
2171
|
-
* threw "slot N is preserved").
|
|
2172
|
-
*/
|
|
2173
|
-
retiredSlots = [];
|
|
2174
|
-
/**
|
|
2175
|
-
* Per-index retry counter. Bumps every time `acquire` retires a
|
|
2176
|
-
* preserved slot. Read by `prepare` to decide whether the worktree
|
|
2177
|
-
* path needs a `-<counter>` suffix (for retried slots) or the bare
|
|
2178
|
-
* `worktreeDirOf(n)` path (first-ever use — back-compat with the
|
|
2179
|
-
* existing on-disk layout).
|
|
2180
|
-
*/
|
|
2181
|
-
retryCounter = /* @__PURE__ */ new Map();
|
|
2182
|
-
workerCount;
|
|
2183
|
-
/**
|
|
2184
|
-
* Set of workers currently held by an `acquire`. v0.1 only ever holds
|
|
2185
|
-
* one at a time, but the structure scales to v0.3.
|
|
2186
|
-
*/
|
|
2187
|
-
busy = /* @__PURE__ */ new Set();
|
|
2188
|
-
constructor(opts) {
|
|
2189
|
-
this.repoPath = opts.repoPath;
|
|
2190
|
-
this.worktreeDirOf = opts.worktreeDir;
|
|
2191
|
-
const requested = opts.workerCount ?? 1;
|
|
2192
|
-
if (requested > 1) {
|
|
2193
|
-
process.stderr.write(
|
|
2194
|
-
`[pilot] WorktreePool: workerCount=${requested} requested, but v0.1 supports only 1 \u2014 clamping.
|
|
2195
|
-
`
|
|
2196
|
-
);
|
|
2197
|
-
}
|
|
2198
|
-
this.workerCount = 1;
|
|
2199
|
-
}
|
|
2200
|
-
/**
|
|
2201
|
-
* Acquire a worker slot. Returns the live slot for the given worker
|
|
2202
|
-
* index, or a fresh stub if the current live slot was preserved on
|
|
2203
|
-
* failure.
|
|
2204
|
-
*
|
|
2205
|
-
* v0.1 always uses slot 0. First call returns a fresh stub. If that
|
|
2206
|
-
* slot is later `preserveOnFailure`'d, the next `acquire()` retires
|
|
2207
|
-
* the preserved slot into `retiredSlots`, bumps the retry counter,
|
|
2208
|
-
* and mints a new stub at index 0. The old slot stays on disk (for
|
|
2209
|
-
* operator inspection) but is no longer the pool's live slot.
|
|
2210
|
-
*/
|
|
2211
|
-
acquire() {
|
|
2212
|
-
for (let n = 0; n < this.workerCount; n++) {
|
|
2213
|
-
if (this.busy.has(n)) continue;
|
|
2214
|
-
this.busy.add(n);
|
|
2215
|
-
const existing = this.slots.get(n);
|
|
2216
|
-
if (existing && existing.preserved) {
|
|
2217
|
-
this.retiredSlots.push(existing);
|
|
2218
|
-
this.slots.delete(n);
|
|
2219
|
-
this.retryCounter.set(n, (this.retryCounter.get(n) ?? 0) + 1);
|
|
2220
|
-
} else if (existing) {
|
|
2221
|
-
return existing;
|
|
2222
|
-
}
|
|
2223
|
-
const stub = {
|
|
2224
|
-
index: n,
|
|
2225
|
-
path: "",
|
|
2226
|
-
// filled by prepare
|
|
2227
|
-
prepared: false,
|
|
2228
|
-
preserved: false,
|
|
2229
|
-
setupCompleted: false
|
|
2230
|
-
};
|
|
2231
|
-
this.slots.set(n, stub);
|
|
2232
|
-
return stub;
|
|
2233
|
-
}
|
|
2234
|
-
throw new Error(
|
|
2235
|
-
`WorktreePool.acquire: no free worker slots (workerCount=${this.workerCount}, busy=${[...this.busy].join(",")})`
|
|
2236
|
-
);
|
|
2237
|
-
}
|
|
2238
|
-
/**
|
|
2239
|
-
* Prepare a worktree for the given task. Idempotent: on first call,
|
|
2240
|
-
* runs `git worktree add`; on subsequent calls, recycles the existing
|
|
2241
|
-
* worktree (clean + checkout fresh branch).
|
|
2242
|
-
*
|
|
2243
|
-
* Returns the SHA at HEAD post-prepare. The worker records this as
|
|
2244
|
-
* `sinceSha` for the post-task `enforceTouches` diff.
|
|
2245
|
-
*
|
|
2246
|
-
* `branchPrefix` typically = `pilot/<plan-slug>`; the actual branch
|
|
2247
|
-
* is `<branchPrefix>/<taskId>`. `base` is the commit-ish the branch
|
|
2248
|
-
* is created from — usually the main branch's HEAD or a specific
|
|
2249
|
-
* sha if reproducibility matters.
|
|
2250
|
-
*
|
|
2251
|
-
* For retried slots (i.e. `retryCounter[n] > 0`), the resolved path
|
|
2252
|
-
* gets a `-<counter>` suffix so retries don't collide with the
|
|
2253
|
-
* preserved predecessor on disk.
|
|
2254
|
-
*/
|
|
2255
|
-
async prepare(args) {
|
|
2256
|
-
if (args.slot.preserved) {
|
|
2257
|
-
throw new Error(
|
|
2258
|
-
`WorktreePool.prepare: slot ${args.slot.index} is preserved (failed task awaiting cleanup); cannot reuse`
|
|
2259
|
-
);
|
|
2260
|
-
}
|
|
2261
|
-
const branch = `${args.branchPrefix}/${args.taskId}`;
|
|
2262
|
-
if (!args.slot.prepared) {
|
|
2263
|
-
const basePath = await this.worktreeDirOf(args.slot.index);
|
|
2264
|
-
const counter = this.retryCounter.get(args.slot.index) ?? 0;
|
|
2265
|
-
const wtPath = counter > 0 ? `${basePath}-${counter}` : basePath;
|
|
2266
|
-
args.slot.path = wtPath;
|
|
2267
|
-
try {
|
|
2268
|
-
await fs9.stat(wtPath);
|
|
2269
|
-
await gitWorktreeRemove({
|
|
2270
|
-
repoPath: this.repoPath,
|
|
2271
|
-
worktreePath: wtPath
|
|
2272
|
-
});
|
|
2273
|
-
await fs9.rm(wtPath, { recursive: true, force: true });
|
|
2274
|
-
} catch {
|
|
2275
|
-
}
|
|
2276
|
-
await gitWorktreeAdd({
|
|
2277
|
-
repoPath: this.repoPath,
|
|
2278
|
-
worktreePath: wtPath,
|
|
2279
|
-
commitIsh: args.base,
|
|
2280
|
-
branch
|
|
2281
|
-
});
|
|
2282
|
-
args.slot.prepared = true;
|
|
2283
|
-
} else {
|
|
2284
|
-
await cleanWorktree(args.slot.path);
|
|
2285
|
-
await checkoutFreshBranch({
|
|
2286
|
-
worktree: args.slot.path,
|
|
2287
|
-
branch,
|
|
2288
|
-
base: args.base
|
|
2289
|
-
});
|
|
2290
|
-
}
|
|
2291
|
-
const sinceSha = await headSha(args.slot.path);
|
|
2292
|
-
return { sinceSha, branch, path: args.slot.path };
|
|
2293
|
-
}
|
|
2294
|
-
/**
|
|
2295
|
-
* Release a slot back to the pool — slot becomes available for
|
|
2296
|
-
* `acquire` again. Call after a clean task completion (commit
|
|
2297
|
-
* succeeded, no preserved state needed).
|
|
2298
|
-
*
|
|
2299
|
-
* Does NOT clean the worktree — the next `prepare` call will reset
|
|
2300
|
-
* it. If you want eager cleanup (e.g. before a long idle), call
|
|
2301
|
-
* `cleanWorktree(slot.path)` separately.
|
|
2302
|
-
*/
|
|
2303
|
-
release(slot) {
|
|
2304
|
-
if (!this.busy.has(slot.index)) {
|
|
2305
|
-
throw new Error(
|
|
2306
|
-
`WorktreePool.release: slot ${slot.index} is not held`
|
|
2307
|
-
);
|
|
2308
|
-
}
|
|
2309
|
-
this.busy.delete(slot.index);
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Preserve a slot's state on failure. The slot is marked preserved
|
|
2313
|
-
* and removed from the busy set. Unlike pre-v0.2 behaviour, the
|
|
2314
|
-
* next `acquire()` call retires this slot into `retiredSlots` and
|
|
2315
|
-
* mints a fresh stub — so a single failure doesn't cascade-block
|
|
2316
|
-
* the rest of the run.
|
|
2317
|
-
*
|
|
2318
|
-
* The CLI's `pilot worktrees prune` (Phase G6) remains the path to
|
|
2319
|
-
* permanently remove preserved slots from disk.
|
|
2320
|
-
*/
|
|
2321
|
-
preserveOnFailure(slot) {
|
|
2322
|
-
slot.preserved = true;
|
|
2323
|
-
this.busy.delete(slot.index);
|
|
2324
|
-
}
|
|
2325
|
-
/**
|
|
2326
|
-
* Tear down all worktrees managed by this pool — BOTH live and
|
|
2327
|
-
* retired. Called at end of `pilot build` (whether success or
|
|
2328
|
-
* failure). Preserved slots are skipped when `keepPreserved` is
|
|
2329
|
-
* true (the default) — those are the user's to inspect.
|
|
2330
|
-
*/
|
|
2331
|
-
async shutdown(args = {}) {
|
|
2332
|
-
const keepPreserved = args.keepPreserved ?? true;
|
|
2333
|
-
const errors = [];
|
|
2334
|
-
const all = [...this.slots.values(), ...this.retiredSlots];
|
|
2335
|
-
for (const slot of all) {
|
|
2336
|
-
if (slot.preserved && keepPreserved) continue;
|
|
2337
|
-
if (!slot.prepared || slot.path === "") continue;
|
|
2338
|
-
try {
|
|
2339
|
-
await gitWorktreeRemove({
|
|
2340
|
-
repoPath: this.repoPath,
|
|
2341
|
-
worktreePath: slot.path
|
|
2342
|
-
});
|
|
2343
|
-
} catch (err) {
|
|
2344
|
-
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
if (errors.length > 0) {
|
|
2348
|
-
throw new Error(
|
|
2349
|
-
`WorktreePool.shutdown: ${errors.length} worktree removal(s) failed:
|
|
2350
|
-
` + errors.map((e) => e.message).join("\n---\n")
|
|
2351
|
-
);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Inspect current slots (for tests / `pilot worktrees list`). Returns
|
|
2356
|
-
* live slots followed by retired slots, in insertion order within
|
|
2357
|
-
* each group.
|
|
2358
|
-
*/
|
|
2359
|
-
inspect() {
|
|
2360
|
-
return [...this.slots.values(), ...this.retiredSlots];
|
|
2361
|
-
}
|
|
2362
|
-
};
|
|
2363
|
-
|
|
2364
|
-
// src/pilot/scheduler/ready-set.ts
|
|
2365
|
-
function makeScheduler(args) {
|
|
2366
|
-
const { db, runId, plan } = args;
|
|
2367
|
-
const planById = /* @__PURE__ */ new Map();
|
|
2368
|
-
for (const t of plan.tasks) planById.set(t.id, t);
|
|
2369
|
-
const dependentsOf = /* @__PURE__ */ new Map();
|
|
2370
|
-
for (const t of plan.tasks) {
|
|
2371
|
-
for (const dep of t.depends_on) {
|
|
2372
|
-
const list = dependentsOf.get(dep);
|
|
2373
|
-
if (list) list.push(t.id);
|
|
2374
|
-
else dependentsOf.set(dep, [t.id]);
|
|
1675
|
+
// src/pilot/scheduler/ready-set.ts
|
|
1676
|
+
function makeScheduler(args) {
|
|
1677
|
+
const { db, runId, plan } = args;
|
|
1678
|
+
const planById = /* @__PURE__ */ new Map();
|
|
1679
|
+
for (const t of plan.tasks) planById.set(t.id, t);
|
|
1680
|
+
const dependentsOf = /* @__PURE__ */ new Map();
|
|
1681
|
+
for (const t of plan.tasks) {
|
|
1682
|
+
for (const dep of t.depends_on) {
|
|
1683
|
+
const list = dependentsOf.get(dep);
|
|
1684
|
+
if (list) list.push(t.id);
|
|
1685
|
+
else dependentsOf.set(dep, [t.id]);
|
|
2375
1686
|
}
|
|
2376
1687
|
}
|
|
2377
1688
|
return {
|
|
@@ -2437,6 +1748,8 @@ function depsSatisfied(db, runId, task) {
|
|
|
2437
1748
|
|
|
2438
1749
|
// src/pilot/worker/worker.ts
|
|
2439
1750
|
import * as fsSync from "fs";
|
|
1751
|
+
import { execFile as execFileCb } from "child_process";
|
|
1752
|
+
import { promisify as promisifyUtil } from "util";
|
|
2440
1753
|
|
|
2441
1754
|
// src/pilot/opencode/prompts.ts
|
|
2442
1755
|
function kickoffPrompt(task, ctx) {
|
|
@@ -2491,6 +1804,18 @@ function kickoffPrompt(task, ctx) {
|
|
|
2491
1804
|
);
|
|
2492
1805
|
}
|
|
2493
1806
|
}
|
|
1807
|
+
sections.push(``, `## Progress updates`, ``);
|
|
1808
|
+
sections.push(
|
|
1809
|
+
`A \`provide_status_update\` tool is available. Use it to emit one-sentence progress updates during long-running work.`,
|
|
1810
|
+
``,
|
|
1811
|
+
`Guidelines:`,
|
|
1812
|
+
`- Keep messages under 200 characters`,
|
|
1813
|
+
`- One sentence only \u2014 describe what you're currently working on`,
|
|
1814
|
+
`- Rate limited to once per 60 seconds per task`,
|
|
1815
|
+
`- Call it sparingly; the user sees these interleaved with the task log`,
|
|
1816
|
+
``,
|
|
1817
|
+
`Example: "Writing the route handler for /api/users" or "Running typecheck after edits"`
|
|
1818
|
+
);
|
|
2494
1819
|
if (task.context !== void 0 && task.context.trim().length > 0) {
|
|
2495
1820
|
sections.push(``, `## Context`, ``, task.context.trim());
|
|
2496
1821
|
}
|
|
@@ -2547,8 +1872,8 @@ var DEFAULT_OUTPUT_CAP_BYTES = 256 * 1024;
|
|
|
2547
1872
|
var TRUNCATION_NOTICE = "\n[pilot] verify output truncated\n";
|
|
2548
1873
|
async function runVerify(commands, options) {
|
|
2549
1874
|
const results = [];
|
|
2550
|
-
for (const
|
|
2551
|
-
const result = await runOne(
|
|
1875
|
+
for (const command10 of commands) {
|
|
1876
|
+
const result = await runOne(command10, options);
|
|
2552
1877
|
results.push(result);
|
|
2553
1878
|
if (!result.ok) {
|
|
2554
1879
|
return { ok: false, results, failure: result };
|
|
@@ -2559,8 +1884,8 @@ async function runVerify(commands, options) {
|
|
|
2559
1884
|
results
|
|
2560
1885
|
};
|
|
2561
1886
|
}
|
|
2562
|
-
async function runOne(
|
|
2563
|
-
if (typeof
|
|
1887
|
+
async function runOne(command10, options) {
|
|
1888
|
+
if (typeof command10 !== "string" || command10.length === 0) {
|
|
2564
1889
|
throw new TypeError(`runOne: command must be a non-empty string`);
|
|
2565
1890
|
}
|
|
2566
1891
|
if (typeof options.cwd !== "string" || options.cwd.length === 0) {
|
|
@@ -2576,7 +1901,7 @@ async function runOne(command12, options) {
|
|
|
2576
1901
|
stdout: { partial: "" },
|
|
2577
1902
|
stderr: { partial: "" }
|
|
2578
1903
|
};
|
|
2579
|
-
const child = spawn2("bash", ["-c",
|
|
1904
|
+
const child = spawn2("bash", ["-c", command10], {
|
|
2580
1905
|
cwd: options.cwd,
|
|
2581
1906
|
env: options.env ?? process.env,
|
|
2582
1907
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2621,18 +1946,18 @@ async function runOne(command12, options) {
|
|
|
2621
1946
|
const lines = combined.split("\n");
|
|
2622
1947
|
state.partial = lines.pop();
|
|
2623
1948
|
for (const line of lines) {
|
|
2624
|
-
options.onLine({ stream, line, command:
|
|
1949
|
+
options.onLine({ stream, line, command: command10 });
|
|
2625
1950
|
}
|
|
2626
1951
|
}
|
|
2627
1952
|
};
|
|
2628
1953
|
child.stdout?.on("data", (c2) => handleChunk("stdout", c2));
|
|
2629
1954
|
child.stderr?.on("data", (c2) => handleChunk("stderr", c2));
|
|
2630
|
-
const { code, signal } = await new Promise((
|
|
1955
|
+
const { code, signal } = await new Promise((resolve6) => {
|
|
2631
1956
|
let resolved = false;
|
|
2632
1957
|
const finalize = (code2, signal2) => {
|
|
2633
1958
|
if (resolved) return;
|
|
2634
1959
|
resolved = true;
|
|
2635
|
-
|
|
1960
|
+
resolve6({ code: code2, signal: signal2 });
|
|
2636
1961
|
};
|
|
2637
1962
|
child.on("error", (err) => {
|
|
2638
1963
|
if (!truncated) {
|
|
@@ -2652,7 +1977,7 @@ async function runOne(command12, options) {
|
|
|
2652
1977
|
for (const stream of ["stdout", "stderr"]) {
|
|
2653
1978
|
const partial = streamState[stream].partial;
|
|
2654
1979
|
if (partial.length > 0) {
|
|
2655
|
-
options.onLine({ stream, line: partial, command:
|
|
1980
|
+
options.onLine({ stream, line: partial, command: command10 });
|
|
2656
1981
|
}
|
|
2657
1982
|
}
|
|
2658
1983
|
}
|
|
@@ -2661,7 +1986,7 @@ async function runOne(command12, options) {
|
|
|
2661
1986
|
if (code === 0 && !timedOut && !aborted) {
|
|
2662
1987
|
return {
|
|
2663
1988
|
ok: true,
|
|
2664
|
-
command:
|
|
1989
|
+
command: command10,
|
|
2665
1990
|
exitCode: 0,
|
|
2666
1991
|
output,
|
|
2667
1992
|
durationMs
|
|
@@ -2669,7 +1994,7 @@ async function runOne(command12, options) {
|
|
|
2669
1994
|
}
|
|
2670
1995
|
return {
|
|
2671
1996
|
ok: false,
|
|
2672
|
-
command:
|
|
1997
|
+
command: command10,
|
|
2673
1998
|
exitCode: code ?? -1,
|
|
2674
1999
|
signal,
|
|
2675
2000
|
timedOut,
|
|
@@ -2697,15 +2022,66 @@ function killTree(child) {
|
|
|
2697
2022
|
|
|
2698
2023
|
// src/pilot/verify/touches.ts
|
|
2699
2024
|
import picomatch2 from "picomatch";
|
|
2025
|
+
import { execFile as execFile2 } from "child_process";
|
|
2026
|
+
import { promisify } from "util";
|
|
2027
|
+
var execFileP = promisify(execFile2);
|
|
2028
|
+
var DEFAULT_TOLERATE = [
|
|
2029
|
+
// Next.js — `next build` regenerates this every run.
|
|
2030
|
+
"**/next-env.d.ts",
|
|
2031
|
+
// Next.js app-router generated types (routes.d.ts, etc.)
|
|
2032
|
+
"**/.next/types/**",
|
|
2033
|
+
"**/.next/dev/types/**",
|
|
2034
|
+
// TypeScript project-reference build info — tsc writes these.
|
|
2035
|
+
"**/*.tsbuildinfo",
|
|
2036
|
+
// Snapshot test updates — `vitest -u` / `jest -u` rewrites these
|
|
2037
|
+
// when assertions match; allowing them lets snapshot-driven tasks
|
|
2038
|
+
// pass without the agent authoring every snapshot path.
|
|
2039
|
+
"**/__snapshots__/**",
|
|
2040
|
+
"**/*.snap"
|
|
2041
|
+
];
|
|
2042
|
+
async function diffNamesSince(cwd, sinceSha) {
|
|
2043
|
+
const run2 = async (args) => {
|
|
2044
|
+
for (const a of args) {
|
|
2045
|
+
if (a.includes("\0")) {
|
|
2046
|
+
throw new Error(`git arg contains null byte: ${JSON.stringify(a)}`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const { stdout } = await execFileP("git", ["-C", cwd, ...args], {
|
|
2050
|
+
timeout: 3e4,
|
|
2051
|
+
maxBuffer: 16 * 1024 * 1024
|
|
2052
|
+
});
|
|
2053
|
+
return stdout.toString().split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2054
|
+
};
|
|
2055
|
+
const sets = await Promise.all([
|
|
2056
|
+
run2(["diff", "--name-only", `${sinceSha}..HEAD`]),
|
|
2057
|
+
run2(["diff", "--cached", "--name-only"]),
|
|
2058
|
+
run2(["diff", "--name-only"]),
|
|
2059
|
+
run2(["ls-files", "--others", "--exclude-standard"])
|
|
2060
|
+
]);
|
|
2061
|
+
const all = /* @__PURE__ */ new Set();
|
|
2062
|
+
for (const s of sets) for (const p of s) all.add(p);
|
|
2063
|
+
return [...all].sort();
|
|
2064
|
+
}
|
|
2700
2065
|
async function enforceTouches(args) {
|
|
2701
|
-
const changed = await diffNamesSince(args.
|
|
2066
|
+
const changed = await diffNamesSince(args.cwd, args.sinceSha);
|
|
2702
2067
|
if (changed.length === 0) {
|
|
2703
2068
|
return { ok: true, changed: [] };
|
|
2704
2069
|
}
|
|
2070
|
+
const combined = [
|
|
2071
|
+
...args.allowed,
|
|
2072
|
+
...args.tolerate ?? [],
|
|
2073
|
+
...DEFAULT_TOLERATE
|
|
2074
|
+
];
|
|
2705
2075
|
if (args.allowed.length === 0) {
|
|
2706
|
-
|
|
2076
|
+
const matchPassthrough = picomatch2(
|
|
2077
|
+
[...args.tolerate ?? [], ...DEFAULT_TOLERATE],
|
|
2078
|
+
{ dot: true }
|
|
2079
|
+
);
|
|
2080
|
+
const violators2 = changed.filter((p) => !matchPassthrough(p));
|
|
2081
|
+
if (violators2.length === 0) return { ok: true, changed };
|
|
2082
|
+
return { ok: false, changed, violators: violators2 };
|
|
2707
2083
|
}
|
|
2708
|
-
const matchAllowed = picomatch2(
|
|
2084
|
+
const matchAllowed = picomatch2(combined, { dot: true });
|
|
2709
2085
|
const violators = changed.filter((p) => !matchAllowed(p));
|
|
2710
2086
|
if (violators.length === 0) return { ok: true, changed };
|
|
2711
2087
|
return { ok: false, changed, violators };
|
|
@@ -2860,12 +2236,71 @@ function isTextPart(v) {
|
|
|
2860
2236
|
}
|
|
2861
2237
|
|
|
2862
2238
|
// src/pilot/worker/worker.ts
|
|
2239
|
+
var execFileWorker = promisifyUtil(execFileCb);
|
|
2240
|
+
async function commitAll(cwd, subject, authorName, authorEmail) {
|
|
2241
|
+
const env = { ...process.env };
|
|
2242
|
+
if (authorName) env.GIT_AUTHOR_NAME = authorName;
|
|
2243
|
+
if (authorEmail) env.GIT_AUTHOR_EMAIL = authorEmail;
|
|
2244
|
+
if (authorName) env.GIT_COMMITTER_NAME = authorName;
|
|
2245
|
+
if (authorEmail) env.GIT_COMMITTER_EMAIL = authorEmail;
|
|
2246
|
+
await execFileWorker("git", ["add", "-A"], { cwd, timeout: 1e4 });
|
|
2247
|
+
await execFileWorker("git", ["commit", "-m", subject], {
|
|
2248
|
+
cwd,
|
|
2249
|
+
timeout: 3e4,
|
|
2250
|
+
env
|
|
2251
|
+
});
|
|
2252
|
+
const { stdout } = await execFileWorker("git", ["rev-parse", "HEAD"], {
|
|
2253
|
+
cwd,
|
|
2254
|
+
timeout: 1e4
|
|
2255
|
+
});
|
|
2256
|
+
return stdout.toString().trim();
|
|
2257
|
+
}
|
|
2258
|
+
async function resetTree(cwd) {
|
|
2259
|
+
try {
|
|
2260
|
+
await execFileWorker("git", ["reset", "--hard", "HEAD"], {
|
|
2261
|
+
cwd,
|
|
2262
|
+
timeout: 3e4
|
|
2263
|
+
});
|
|
2264
|
+
await execFileWorker("git", ["clean", "-fd"], {
|
|
2265
|
+
cwd,
|
|
2266
|
+
timeout: 3e4
|
|
2267
|
+
});
|
|
2268
|
+
return true;
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
const e = err;
|
|
2271
|
+
process.stderr.write(
|
|
2272
|
+
`[pilot] tree cleanup failed: ${(e.stderr ?? e.message ?? "").toString()}
|
|
2273
|
+
`
|
|
2274
|
+
);
|
|
2275
|
+
return false;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2863
2278
|
async function runWorker(deps) {
|
|
2864
2279
|
const attempted = [];
|
|
2865
|
-
const maxAttempts = deps.maxAttempts ??
|
|
2280
|
+
const maxAttempts = deps.maxAttempts ?? 5;
|
|
2866
2281
|
const stallMs = deps.stallMs ?? 60 * 60 * 1e3;
|
|
2867
|
-
|
|
2868
|
-
const
|
|
2282
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
2283
|
+
const gate = await checkCwdSafety(cwd);
|
|
2284
|
+
if (!gate.ok) {
|
|
2285
|
+
process.stderr.write(`[pilot] ${gate.reason}
|
|
2286
|
+
`);
|
|
2287
|
+
return { aborted: true, attempted: [] };
|
|
2288
|
+
}
|
|
2289
|
+
for (const w of gate.warnings) {
|
|
2290
|
+
process.stderr.write(`[pilot] ${w}
|
|
2291
|
+
`);
|
|
2292
|
+
}
|
|
2293
|
+
const { loadPilotConfig } = await import("./pilot-config-7LJZ23YK.js");
|
|
2294
|
+
let pilotConfig;
|
|
2295
|
+
try {
|
|
2296
|
+
pilotConfig = await loadPilotConfig(cwd);
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
process.stderr.write(
|
|
2299
|
+
`[pilot] ${err instanceof Error ? err.message : String(err)}
|
|
2300
|
+
`
|
|
2301
|
+
);
|
|
2302
|
+
return { aborted: true, attempted: [] };
|
|
2303
|
+
}
|
|
2869
2304
|
while (true) {
|
|
2870
2305
|
if (deps.abortSignal?.aborted) {
|
|
2871
2306
|
return { aborted: true, attempted };
|
|
@@ -2875,9 +2310,13 @@ async function runWorker(deps) {
|
|
|
2875
2310
|
return { aborted: false, attempted };
|
|
2876
2311
|
}
|
|
2877
2312
|
attempted.push(pick.task.id);
|
|
2878
|
-
await runOneTask(
|
|
2879
|
-
if (
|
|
2880
|
-
|
|
2313
|
+
await runOneTask(deps, pick.task, { maxAttempts, stallMs, cwd, pilotConfig });
|
|
2314
|
+
if (deps.treeCleanupFailed) {
|
|
2315
|
+
process.stderr.write(
|
|
2316
|
+
`[pilot] halting run: tree cleanup failed after task ${pick.task.id}; subsequent tasks cannot safely run on a dirty tree
|
|
2317
|
+
`
|
|
2318
|
+
);
|
|
2319
|
+
return { aborted: true, attempted };
|
|
2881
2320
|
}
|
|
2882
2321
|
const row = getTask(deps.db, deps.runId, pick.task.id);
|
|
2883
2322
|
if (row && (row.status === "failed" || row.status === "aborted")) {
|
|
@@ -2929,118 +2368,50 @@ function openForensics(args) {
|
|
|
2929
2368
|
};
|
|
2930
2369
|
}
|
|
2931
2370
|
async function runOneTask(deps, task, opts) {
|
|
2932
|
-
|
|
2371
|
+
try {
|
|
2372
|
+
await runOneTaskImpl(deps, task, opts);
|
|
2373
|
+
} finally {
|
|
2374
|
+
const ok = await resetTree(opts.cwd);
|
|
2375
|
+
if (!ok) {
|
|
2376
|
+
deps.treeCleanupFailed = true;
|
|
2377
|
+
appendEvent(deps.db, {
|
|
2378
|
+
runId: deps.runId,
|
|
2379
|
+
taskId: task.id,
|
|
2380
|
+
kind: "run.cleanup.failed",
|
|
2381
|
+
payload: {
|
|
2382
|
+
reason: "git reset --hard HEAD && git clean -fd failed after task; subsequent tasks aborted"
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
async function runOneTaskImpl(deps, task, opts) {
|
|
2389
|
+
const cwd = opts.cwd;
|
|
2933
2390
|
appendEvent(deps.db, {
|
|
2934
2391
|
runId: deps.runId,
|
|
2935
2392
|
taskId: task.id,
|
|
2936
2393
|
kind: "task.started",
|
|
2937
2394
|
payload: {}
|
|
2938
2395
|
});
|
|
2939
|
-
let
|
|
2940
|
-
let prepared;
|
|
2396
|
+
let sinceSha;
|
|
2941
2397
|
try {
|
|
2942
|
-
|
|
2943
|
-
prepared = await deps.pool.prepare({
|
|
2944
|
-
slot,
|
|
2945
|
-
taskId: task.id,
|
|
2946
|
-
branchPrefix: deps.branchPrefix,
|
|
2947
|
-
base: deps.base
|
|
2948
|
-
});
|
|
2398
|
+
sinceSha = await headSha(cwd);
|
|
2949
2399
|
} catch (err) {
|
|
2950
|
-
const reason2 = `
|
|
2951
|
-
|
|
2952
|
-
const row = getTask(deps.db, deps.runId, task.id);
|
|
2953
|
-
if (row?.status === "pending") {
|
|
2954
|
-
deps.scheduler.next();
|
|
2955
|
-
}
|
|
2956
|
-
markFailed(deps.db, deps.runId, task.id, reason2);
|
|
2957
|
-
} catch {
|
|
2958
|
-
}
|
|
2400
|
+
const reason2 = `headSha failed: ${errorMessage2(err)}`;
|
|
2401
|
+
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
2959
2402
|
appendEvent(deps.db, {
|
|
2960
2403
|
runId: deps.runId,
|
|
2961
2404
|
taskId: task.id,
|
|
2962
2405
|
kind: "task.failed",
|
|
2963
|
-
payload: { phase: "
|
|
2406
|
+
payload: { phase: "headSha", reason: reason2 }
|
|
2964
2407
|
});
|
|
2965
2408
|
return;
|
|
2966
2409
|
}
|
|
2967
|
-
const setupCommands = deps.plan.setup ?? [];
|
|
2968
|
-
if (setupCommands.length > 0 && !slot.setupCompleted) {
|
|
2969
|
-
const setupStart = Date.now();
|
|
2970
|
-
appendEvent(deps.db, {
|
|
2971
|
-
runId: deps.runId,
|
|
2972
|
-
taskId: task.id,
|
|
2973
|
-
kind: "slot.setup.started",
|
|
2974
|
-
payload: {
|
|
2975
|
-
slotIndex: slot.index,
|
|
2976
|
-
commands: deps.plan.setup,
|
|
2977
|
-
taskId: task.id
|
|
2978
|
-
}
|
|
2979
|
-
});
|
|
2980
|
-
const setupResult = await runVerify(setupCommands, {
|
|
2981
|
-
cwd: prepared.path,
|
|
2982
|
-
abortSignal: deps.abortSignal,
|
|
2983
|
-
onLine: deps.onVerifyLine
|
|
2984
|
-
});
|
|
2985
|
-
if (!setupResult.ok) {
|
|
2986
|
-
const durationMs = Date.now() - setupStart;
|
|
2987
|
-
const failure = setupResult.failure;
|
|
2988
|
-
const reason2 = `setup failed: ${failure.command} \u2192 exit ${failure.exitCode}`;
|
|
2989
|
-
appendEvent(deps.db, {
|
|
2990
|
-
runId: deps.runId,
|
|
2991
|
-
taskId: task.id,
|
|
2992
|
-
kind: "slot.setup.failed",
|
|
2993
|
-
payload: {
|
|
2994
|
-
slotIndex: slot.index,
|
|
2995
|
-
command: failure.command,
|
|
2996
|
-
exitCode: failure.exitCode,
|
|
2997
|
-
output: failure.output.slice(0, 4096),
|
|
2998
|
-
// truncate
|
|
2999
|
-
durationMs
|
|
3000
|
-
}
|
|
3001
|
-
});
|
|
3002
|
-
deps.pool.preserveOnFailure(slot);
|
|
3003
|
-
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3004
|
-
const blocked = new Set(
|
|
3005
|
-
deps.scheduler.cascadeFail(task.id, reason2)
|
|
3006
|
-
);
|
|
3007
|
-
for (const row of listTasks(deps.db, deps.runId)) {
|
|
3008
|
-
if (row.task_id === task.id) continue;
|
|
3009
|
-
if (blocked.has(row.task_id)) continue;
|
|
3010
|
-
if (row.status !== "pending" && row.status !== "ready") continue;
|
|
3011
|
-
try {
|
|
3012
|
-
markBlocked(deps.db, deps.runId, row.task_id, reason2);
|
|
3013
|
-
blocked.add(row.task_id);
|
|
3014
|
-
} catch {
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
for (const blockedId of blocked) {
|
|
3018
|
-
appendEvent(deps.db, {
|
|
3019
|
-
runId: deps.runId,
|
|
3020
|
-
taskId: blockedId,
|
|
3021
|
-
kind: "task.blocked",
|
|
3022
|
-
payload: { reason: reason2, failedDep: task.id }
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
deps.setupAborted = true;
|
|
3026
|
-
return;
|
|
3027
|
-
}
|
|
3028
|
-
slot.setupCompleted = true;
|
|
3029
|
-
appendEvent(deps.db, {
|
|
3030
|
-
runId: deps.runId,
|
|
3031
|
-
taskId: task.id,
|
|
3032
|
-
kind: "slot.setup.completed",
|
|
3033
|
-
payload: {
|
|
3034
|
-
slotIndex: slot.index,
|
|
3035
|
-
durationMs: Date.now() - setupStart
|
|
3036
|
-
}
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
2410
|
let sessionId;
|
|
3040
2411
|
try {
|
|
3041
2412
|
const created = await deps.client.session.create({
|
|
3042
2413
|
body: { title: `pilot/${deps.runId}/${task.id}` },
|
|
3043
|
-
query: { directory:
|
|
2414
|
+
query: { directory: cwd }
|
|
3044
2415
|
});
|
|
3045
2416
|
if (!created.data?.id) {
|
|
3046
2417
|
throw new Error(`session.create returned no id`);
|
|
@@ -3048,7 +2419,6 @@ async function runOneTask(deps, task, opts) {
|
|
|
3048
2419
|
sessionId = created.data.id;
|
|
3049
2420
|
} catch (err) {
|
|
3050
2421
|
const reason2 = `session.create failed: ${errorMessage2(err)}`;
|
|
3051
|
-
deps.pool.preserveOnFailure(slot);
|
|
3052
2422
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3053
2423
|
appendEvent(deps.db, {
|
|
3054
2424
|
runId: deps.runId,
|
|
@@ -3058,7 +2428,14 @@ async function runOneTask(deps, task, opts) {
|
|
|
3058
2428
|
});
|
|
3059
2429
|
return;
|
|
3060
2430
|
}
|
|
3061
|
-
const
|
|
2431
|
+
const runDir = await getRunDir(process.cwd(), deps.runId);
|
|
2432
|
+
await registerSession({
|
|
2433
|
+
runDir,
|
|
2434
|
+
sessionId,
|
|
2435
|
+
runId: deps.runId,
|
|
2436
|
+
taskId: task.id
|
|
2437
|
+
});
|
|
2438
|
+
const bus = deps.busFactory(cwd);
|
|
3062
2439
|
await new Promise((r) => setTimeout(r, 200));
|
|
3063
2440
|
const disposeBus = async () => {
|
|
3064
2441
|
try {
|
|
@@ -3077,18 +2454,23 @@ async function runOneTask(deps, task, opts) {
|
|
|
3077
2454
|
if (forensics) forensics.dispose();
|
|
3078
2455
|
void disposeBus();
|
|
3079
2456
|
};
|
|
2457
|
+
const unregisterSessionSafe = async () => {
|
|
2458
|
+
try {
|
|
2459
|
+
await unregisterSession({ runDir, sessionId });
|
|
2460
|
+
} catch {
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
3080
2463
|
const forensicsCounters = () => forensics ? forensics.counters() : { lastEventTs: null, eventCount: 0 };
|
|
3081
2464
|
try {
|
|
3082
2465
|
markRunning(deps.db, {
|
|
3083
2466
|
runId: deps.runId,
|
|
3084
2467
|
taskId: task.id,
|
|
3085
2468
|
sessionId,
|
|
3086
|
-
branch:
|
|
3087
|
-
worktreePath:
|
|
2469
|
+
branch: "",
|
|
2470
|
+
worktreePath: cwd
|
|
3088
2471
|
});
|
|
3089
2472
|
} catch (err) {
|
|
3090
2473
|
disposeForensics();
|
|
3091
|
-
deps.pool.preserveOnFailure(slot);
|
|
3092
2474
|
const reason2 = `markRunning failed: ${errorMessage2(err)}`;
|
|
3093
2475
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3094
2476
|
appendEvent(deps.db, {
|
|
@@ -3103,12 +2485,12 @@ async function runOneTask(deps, task, opts) {
|
|
|
3103
2485
|
runId: deps.runId,
|
|
3104
2486
|
taskId: task.id,
|
|
3105
2487
|
kind: "task.session.created",
|
|
3106
|
-
payload: { sessionId, branch:
|
|
2488
|
+
payload: { sessionId, branch: "", worktreePath: cwd }
|
|
3107
2489
|
});
|
|
3108
2490
|
const ctx = {
|
|
3109
2491
|
planName: deps.plan.name,
|
|
3110
|
-
branch:
|
|
3111
|
-
worktreePath:
|
|
2492
|
+
branch: "",
|
|
2493
|
+
worktreePath: cwd,
|
|
3112
2494
|
milestone: task.milestone,
|
|
3113
2495
|
verifyAfterEach: deps.plan.defaults.verify_after_each,
|
|
3114
2496
|
verifyMilestone: task.milestone !== void 0 ? deps.plan.milestones.find((m) => m.name === task.milestone)?.verify ?? [] : []
|
|
@@ -3116,8 +2498,50 @@ async function runOneTask(deps, task, opts) {
|
|
|
3116
2498
|
const allVerify = [
|
|
3117
2499
|
...task.verify,
|
|
3118
2500
|
...deps.plan.defaults.verify_after_each,
|
|
3119
|
-
...ctx.verifyMilestone
|
|
2501
|
+
...ctx.verifyMilestone,
|
|
2502
|
+
...opts.pilotConfig.after_each
|
|
2503
|
+
];
|
|
2504
|
+
const baselineVerify = [
|
|
2505
|
+
...deps.plan.defaults.verify_after_each,
|
|
2506
|
+
...ctx.verifyMilestone,
|
|
2507
|
+
...opts.pilotConfig.after_each,
|
|
2508
|
+
...opts.pilotConfig.baseline.filter(
|
|
2509
|
+
(c2) => !deps.plan.defaults.verify_after_each.includes(c2) && !ctx.verifyMilestone.includes(c2) && !opts.pilotConfig.after_each.includes(c2)
|
|
2510
|
+
)
|
|
3120
2511
|
];
|
|
2512
|
+
if (baselineVerify.length > 0) {
|
|
2513
|
+
const baselineResult = await runVerify(baselineVerify, {
|
|
2514
|
+
cwd,
|
|
2515
|
+
abortSignal: deps.abortSignal,
|
|
2516
|
+
onLine: deps.onVerifyLine,
|
|
2517
|
+
env: process.env
|
|
2518
|
+
});
|
|
2519
|
+
if (!baselineResult.ok) {
|
|
2520
|
+
const f = baselineResult.failure;
|
|
2521
|
+
const reason2 = `baseline verify failed: ${f.command} \u2192 exit ${f.exitCode}. This command fails on the clean tree BEFORE the agent starts \u2014 fix your environment or narrow the verify scope.`;
|
|
2522
|
+
disposeForensics();
|
|
2523
|
+
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
2524
|
+
appendEvent(deps.db, {
|
|
2525
|
+
runId: deps.runId,
|
|
2526
|
+
taskId: task.id,
|
|
2527
|
+
kind: "task.baseline.failed",
|
|
2528
|
+
payload: {
|
|
2529
|
+
phase: "baseline",
|
|
2530
|
+
command: f.command,
|
|
2531
|
+
exitCode: f.exitCode,
|
|
2532
|
+
output: f.output.slice(0, 4096),
|
|
2533
|
+
reason: reason2
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
appendEvent(deps.db, {
|
|
2539
|
+
runId: deps.runId,
|
|
2540
|
+
taskId: task.id,
|
|
2541
|
+
kind: "task.baseline.passed",
|
|
2542
|
+
payload: { commands: allVerify.length }
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
3121
2545
|
let lastFailure = null;
|
|
3122
2546
|
let stopReason = null;
|
|
3123
2547
|
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
@@ -3125,7 +2549,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3125
2549
|
await abortSession(deps, sessionId);
|
|
3126
2550
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal");
|
|
3127
2551
|
disposeForensics();
|
|
3128
|
-
|
|
2552
|
+
await unregisterSessionSafe();
|
|
3129
2553
|
appendEvent(deps.db, {
|
|
3130
2554
|
runId: deps.runId,
|
|
3131
2555
|
taskId: task.id,
|
|
@@ -3155,7 +2579,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3155
2579
|
try {
|
|
3156
2580
|
await deps.client.session.promptAsync({
|
|
3157
2581
|
path: { id: sessionId },
|
|
3158
|
-
query: { directory:
|
|
2582
|
+
query: { directory: cwd },
|
|
3159
2583
|
body: {
|
|
3160
2584
|
agent: task.agent ?? deps.plan.defaults.agent,
|
|
3161
2585
|
parts: [{ type: "text", text: promptText }]
|
|
@@ -3165,7 +2589,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3165
2589
|
unsubStop();
|
|
3166
2590
|
const reason2 = `promptAsync failed: ${errorMessage2(err)}`;
|
|
3167
2591
|
disposeForensics();
|
|
3168
|
-
|
|
2592
|
+
await unregisterSessionSafe();
|
|
3169
2593
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3170
2594
|
appendEvent(deps.db, {
|
|
3171
2595
|
runId: deps.runId,
|
|
@@ -3185,7 +2609,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3185
2609
|
await abortSession(deps, sessionId);
|
|
3186
2610
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal");
|
|
3187
2611
|
disposeForensics();
|
|
3188
|
-
|
|
2612
|
+
await unregisterSessionSafe();
|
|
3189
2613
|
appendEvent(deps.db, {
|
|
3190
2614
|
runId: deps.runId,
|
|
3191
2615
|
taskId: task.id,
|
|
@@ -3203,8 +2627,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3203
2627
|
} catch {
|
|
3204
2628
|
}
|
|
3205
2629
|
disposeForensics();
|
|
2630
|
+
await unregisterSessionSafe();
|
|
3206
2631
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3207
|
-
deps.pool.preserveOnFailure(slot);
|
|
3208
2632
|
appendEvent(deps.db, {
|
|
3209
2633
|
runId: deps.runId,
|
|
3210
2634
|
taskId: task.id,
|
|
@@ -3222,8 +2646,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3222
2646
|
if (idleResult.kind === "session-error") {
|
|
3223
2647
|
const reason2 = `session error: ${JSON.stringify(idleResult.properties)}`;
|
|
3224
2648
|
disposeForensics();
|
|
2649
|
+
await unregisterSessionSafe();
|
|
3225
2650
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3226
|
-
deps.pool.preserveOnFailure(slot);
|
|
3227
2651
|
appendEvent(deps.db, {
|
|
3228
2652
|
runId: deps.runId,
|
|
3229
2653
|
taskId: task.id,
|
|
@@ -3238,8 +2662,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3238
2662
|
}
|
|
3239
2663
|
if (stopReason !== null) {
|
|
3240
2664
|
disposeForensics();
|
|
2665
|
+
await unregisterSessionSafe();
|
|
3241
2666
|
markFailedSafe(deps.db, deps.runId, task.id, stopReason);
|
|
3242
|
-
deps.pool.preserveOnFailure(slot);
|
|
3243
2667
|
appendEvent(deps.db, {
|
|
3244
2668
|
runId: deps.runId,
|
|
3245
2669
|
taskId: task.id,
|
|
@@ -3249,9 +2673,10 @@ async function runOneTask(deps, task, opts) {
|
|
|
3249
2673
|
return;
|
|
3250
2674
|
}
|
|
3251
2675
|
const verifyResult = await runVerify(allVerify, {
|
|
3252
|
-
cwd
|
|
2676
|
+
cwd,
|
|
3253
2677
|
abortSignal: deps.abortSignal,
|
|
3254
|
-
onLine: deps.onVerifyLine
|
|
2678
|
+
onLine: deps.onVerifyLine,
|
|
2679
|
+
env: process.env
|
|
3255
2680
|
});
|
|
3256
2681
|
if (!verifyResult.ok) {
|
|
3257
2682
|
lastFailure = {
|
|
@@ -3269,20 +2694,21 @@ async function runOneTask(deps, task, opts) {
|
|
|
3269
2694
|
command: lastFailure.command,
|
|
3270
2695
|
exitCode: lastFailure.exitCode,
|
|
3271
2696
|
timedOut: verifyResult.failure.timedOut,
|
|
3272
|
-
aborted: verifyResult.failure.aborted
|
|
2697
|
+
aborted: verifyResult.failure.aborted,
|
|
2698
|
+
output: verifyResult.failure.output.slice(-2048)
|
|
3273
2699
|
}
|
|
3274
2700
|
});
|
|
3275
2701
|
if (verifyResult.failure.aborted) {
|
|
3276
2702
|
disposeForensics();
|
|
2703
|
+
await unregisterSessionSafe();
|
|
3277
2704
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal during verify");
|
|
3278
|
-
deps.pool.preserveOnFailure(slot);
|
|
3279
2705
|
return;
|
|
3280
2706
|
}
|
|
3281
2707
|
if (attempt < opts.maxAttempts) continue;
|
|
3282
2708
|
const reason2 = `verify failed after ${opts.maxAttempts} attempts: ${lastFailure.command} \u2192 exit ${lastFailure.exitCode}`;
|
|
3283
2709
|
disposeForensics();
|
|
2710
|
+
await unregisterSessionSafe();
|
|
3284
2711
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3285
|
-
deps.pool.preserveOnFailure(slot);
|
|
3286
2712
|
appendEvent(deps.db, {
|
|
3287
2713
|
runId: deps.runId,
|
|
3288
2714
|
taskId: task.id,
|
|
@@ -3298,9 +2724,10 @@ async function runOneTask(deps, task, opts) {
|
|
|
3298
2724
|
payload: { attempt }
|
|
3299
2725
|
});
|
|
3300
2726
|
const touches = await enforceTouches({
|
|
3301
|
-
|
|
3302
|
-
sinceSha
|
|
3303
|
-
allowed: task.touches
|
|
2727
|
+
cwd,
|
|
2728
|
+
sinceSha,
|
|
2729
|
+
allowed: task.touches,
|
|
2730
|
+
tolerate: task.tolerate
|
|
3304
2731
|
});
|
|
3305
2732
|
if (!touches.ok) {
|
|
3306
2733
|
lastFailure = {
|
|
@@ -3318,8 +2745,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3318
2745
|
if (attempt < opts.maxAttempts) continue;
|
|
3319
2746
|
const reason2 = `touches violation after ${opts.maxAttempts} attempts: ${touches.violators.join(", ")}`;
|
|
3320
2747
|
disposeForensics();
|
|
2748
|
+
await unregisterSessionSafe();
|
|
3321
2749
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3322
|
-
deps.pool.preserveOnFailure(slot);
|
|
3323
2750
|
appendEvent(deps.db, {
|
|
3324
2751
|
runId: deps.runId,
|
|
3325
2752
|
taskId: task.id,
|
|
@@ -3330,8 +2757,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3330
2757
|
}
|
|
3331
2758
|
if (touches.changed.length === 0) {
|
|
3332
2759
|
disposeForensics();
|
|
2760
|
+
await unregisterSessionSafe();
|
|
3333
2761
|
markSucceeded(deps.db, deps.runId, task.id);
|
|
3334
|
-
deps.pool.release(slot);
|
|
3335
2762
|
appendEvent(deps.db, {
|
|
3336
2763
|
runId: deps.runId,
|
|
3337
2764
|
taskId: task.id,
|
|
@@ -3342,15 +2769,15 @@ async function runOneTask(deps, task, opts) {
|
|
|
3342
2769
|
}
|
|
3343
2770
|
try {
|
|
3344
2771
|
const commitMessage = `${task.id}: ${task.title}`;
|
|
3345
|
-
const sha = await commitAll(
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
2772
|
+
const sha = await commitAll(
|
|
2773
|
+
cwd,
|
|
2774
|
+
commitMessage,
|
|
2775
|
+
deps.authorName,
|
|
2776
|
+
deps.authorEmail
|
|
2777
|
+
);
|
|
3351
2778
|
disposeForensics();
|
|
2779
|
+
await unregisterSessionSafe();
|
|
3352
2780
|
markSucceeded(deps.db, deps.runId, task.id);
|
|
3353
|
-
deps.pool.release(slot);
|
|
3354
2781
|
appendEvent(deps.db, {
|
|
3355
2782
|
runId: deps.runId,
|
|
3356
2783
|
taskId: task.id,
|
|
@@ -3359,23 +2786,36 @@ async function runOneTask(deps, task, opts) {
|
|
|
3359
2786
|
});
|
|
3360
2787
|
return;
|
|
3361
2788
|
} catch (err) {
|
|
3362
|
-
const
|
|
2789
|
+
const errMsg = errorMessage2(err);
|
|
2790
|
+
lastFailure = {
|
|
2791
|
+
command: "git commit (pre-commit hook)",
|
|
2792
|
+
exitCode: 1,
|
|
2793
|
+
output: errMsg.slice(0, 8192)
|
|
2794
|
+
};
|
|
2795
|
+
appendEvent(deps.db, {
|
|
2796
|
+
runId: deps.runId,
|
|
2797
|
+
taskId: task.id,
|
|
2798
|
+
kind: "task.commit.failed",
|
|
2799
|
+
payload: { attempt, error: errMsg.slice(0, 4096) }
|
|
2800
|
+
});
|
|
2801
|
+
if (attempt < opts.maxAttempts) continue;
|
|
2802
|
+
const reason2 = `commit failed after ${opts.maxAttempts} attempts: ${errMsg.slice(0, 500)}`;
|
|
3363
2803
|
disposeForensics();
|
|
2804
|
+
await unregisterSessionSafe();
|
|
3364
2805
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3365
|
-
deps.pool.preserveOnFailure(slot);
|
|
3366
2806
|
appendEvent(deps.db, {
|
|
3367
2807
|
runId: deps.runId,
|
|
3368
2808
|
taskId: task.id,
|
|
3369
2809
|
kind: "task.failed",
|
|
3370
|
-
payload: { phase: "commit", reason: reason2 }
|
|
2810
|
+
payload: { phase: "commit", reason: reason2, attempts: opts.maxAttempts }
|
|
3371
2811
|
});
|
|
3372
2812
|
return;
|
|
3373
2813
|
}
|
|
3374
2814
|
}
|
|
3375
2815
|
const reason = "worker loop exited unexpectedly";
|
|
3376
2816
|
disposeForensics();
|
|
2817
|
+
await unregisterSessionSafe();
|
|
3377
2818
|
markFailedSafe(deps.db, deps.runId, task.id, reason);
|
|
3378
|
-
deps.pool.preserveOnFailure(slot);
|
|
3379
2819
|
appendEvent(deps.db, {
|
|
3380
2820
|
runId: deps.runId,
|
|
3381
2821
|
taskId: task.id,
|
|
@@ -3447,7 +2887,7 @@ function errorMessage2(err) {
|
|
|
3447
2887
|
}
|
|
3448
2888
|
|
|
3449
2889
|
// src/pilot/cli/build.ts
|
|
3450
|
-
import { promises as
|
|
2890
|
+
import { promises as fs7 } from "fs";
|
|
3451
2891
|
var buildCmd = command3({
|
|
3452
2892
|
name: "build",
|
|
3453
2893
|
description: "Execute a pilot.yaml plan via the worker loop.",
|
|
@@ -3552,8 +2992,8 @@ async function runBuild(opts) {
|
|
|
3552
2992
|
const opened = openStateDb(":memory:");
|
|
3553
2993
|
opened.close();
|
|
3554
2994
|
const cleanup = [];
|
|
3555
|
-
const { ulid
|
|
3556
|
-
const runId =
|
|
2995
|
+
const { ulid } = await import("ulid");
|
|
2996
|
+
const runId = ulid();
|
|
3557
2997
|
const dbPath = await getStateDbPath(cwd, runId);
|
|
3558
2998
|
const runDir = await getRunDir(cwd, runId);
|
|
3559
2999
|
const branchPrefix = deriveBranchPrefix(plan.branch_prefix, slug, runId);
|
|
@@ -3572,6 +3012,49 @@ async function runBuild(opts) {
|
|
|
3572
3012
|
kind: "run.started",
|
|
3573
3013
|
payload: { planPath, slug, runDir, branchPrefix }
|
|
3574
3014
|
});
|
|
3015
|
+
const { runSetupHook, SETUP_HOOK_RELATIVE_PATH } = await import("./setup-hook-FHTXMAQL.js");
|
|
3016
|
+
const hookResult = await runSetupHook({
|
|
3017
|
+
cwd,
|
|
3018
|
+
onLine: (c2) => stderrWriter(c2)
|
|
3019
|
+
});
|
|
3020
|
+
switch (hookResult.kind) {
|
|
3021
|
+
case "skipped":
|
|
3022
|
+
break;
|
|
3023
|
+
case "ok":
|
|
3024
|
+
stderrWriter(
|
|
3025
|
+
`[pilot] setup hook ${SETUP_HOOK_RELATIVE_PATH} passed (${Math.round(hookResult.durationMs / 1e3)}s)
|
|
3026
|
+
`
|
|
3027
|
+
);
|
|
3028
|
+
break;
|
|
3029
|
+
case "not-executable":
|
|
3030
|
+
stderrWriter(
|
|
3031
|
+
`[pilot] setup hook ${hookResult.hookPath} is not executable. Run \`chmod +x ${SETUP_HOOK_RELATIVE_PATH}\` and re-run pilot.
|
|
3032
|
+
`
|
|
3033
|
+
);
|
|
3034
|
+
await runCleanup(cleanup);
|
|
3035
|
+
return 1;
|
|
3036
|
+
case "timed-out":
|
|
3037
|
+
stderrWriter(
|
|
3038
|
+
`[pilot] setup hook ${hookResult.hookPath} timed out after ${Math.round(hookResult.timeoutMs / 1e3)}s
|
|
3039
|
+
`
|
|
3040
|
+
);
|
|
3041
|
+
await runCleanup(cleanup);
|
|
3042
|
+
return 1;
|
|
3043
|
+
case "failed":
|
|
3044
|
+
stderrWriter(
|
|
3045
|
+
`[pilot] setup hook ${hookResult.hookPath} exited ${hookResult.exitCode} (after ${Math.round(hookResult.durationMs / 1e3)}s). Fix the environment and re-run pilot.
|
|
3046
|
+
`
|
|
3047
|
+
);
|
|
3048
|
+
await runCleanup(cleanup);
|
|
3049
|
+
return 1;
|
|
3050
|
+
case "spawn-error":
|
|
3051
|
+
stderrWriter(
|
|
3052
|
+
`[pilot] setup hook ${hookResult.hookPath} failed to spawn: ${hookResult.error}
|
|
3053
|
+
`
|
|
3054
|
+
);
|
|
3055
|
+
await runCleanup(cleanup);
|
|
3056
|
+
return 1;
|
|
3057
|
+
}
|
|
3575
3058
|
return executeRun({
|
|
3576
3059
|
db: real,
|
|
3577
3060
|
runId,
|
|
@@ -3589,10 +3072,17 @@ async function executeRun(args) {
|
|
|
3589
3072
|
const { db, runId, plan, planPath, runDir, branchPrefix, cleanup } = args;
|
|
3590
3073
|
const cwd = process.cwd();
|
|
3591
3074
|
const stderrWriter = args.stderrWriter ?? ((s) => void process.stderr.write(s));
|
|
3075
|
+
const runDirForMcp = await getRunDir(cwd, runId);
|
|
3076
|
+
const dbPathForMcp = await getStateDbPath(cwd, runId);
|
|
3592
3077
|
let server;
|
|
3593
3078
|
try {
|
|
3594
3079
|
server = await startOpencodeServer({
|
|
3595
|
-
port: args.opencodePort ?? 0
|
|
3080
|
+
port: args.opencodePort ?? 0,
|
|
3081
|
+
runContext: {
|
|
3082
|
+
runDir: runDirForMcp,
|
|
3083
|
+
dbPath: dbPathForMcp,
|
|
3084
|
+
runId
|
|
3085
|
+
}
|
|
3596
3086
|
});
|
|
3597
3087
|
} catch (err) {
|
|
3598
3088
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -3609,28 +3099,7 @@ async function executeRun(args) {
|
|
|
3609
3099
|
}
|
|
3610
3100
|
cleanup.push(() => server.shutdown());
|
|
3611
3101
|
const busFactory = (directory) => new EventBus(server.client, directory);
|
|
3612
|
-
const pool = new WorktreePool({
|
|
3613
|
-
repoPath: cwd,
|
|
3614
|
-
worktreeDir: async (n) => getWorktreeDir(cwd, runId, n)
|
|
3615
|
-
});
|
|
3616
|
-
cleanup.push(() => pool.shutdown({ keepPreserved: true }));
|
|
3617
3102
|
const scheduler = makeScheduler({ db: db.db, runId, plan });
|
|
3618
|
-
let base;
|
|
3619
|
-
try {
|
|
3620
|
-
base = await headSha(cwd);
|
|
3621
|
-
} catch (err) {
|
|
3622
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
3623
|
-
process.stderr.write(`pilot: cannot resolve HEAD sha: ${reason}
|
|
3624
|
-
`);
|
|
3625
|
-
appendEvent(db.db, {
|
|
3626
|
-
runId,
|
|
3627
|
-
kind: "run.error",
|
|
3628
|
-
payload: { phase: "head-sha", reason }
|
|
3629
|
-
});
|
|
3630
|
-
markRunFinished(db.db, runId, "failed");
|
|
3631
|
-
await runCleanup(cleanup);
|
|
3632
|
-
return 1;
|
|
3633
|
-
}
|
|
3634
3103
|
const aborter = new AbortController();
|
|
3635
3104
|
const sigintHandler = () => aborter.abort("SIGINT");
|
|
3636
3105
|
process.once("SIGINT", sigintHandler);
|
|
@@ -3638,28 +3107,26 @@ async function executeRun(args) {
|
|
|
3638
3107
|
process.off("SIGINT", sigintHandler);
|
|
3639
3108
|
});
|
|
3640
3109
|
if (args.quiet !== true) {
|
|
3110
|
+
stderrWriter(
|
|
3111
|
+
`pilot build: run ${runId} started (${plan.tasks.length} tasks)
|
|
3112
|
+
`
|
|
3113
|
+
);
|
|
3641
3114
|
const unsubLogger = startStreamingLogger({
|
|
3642
3115
|
stderrWriter,
|
|
3643
3116
|
runId,
|
|
3644
3117
|
totalTasks: plan.tasks.length,
|
|
3645
|
-
subscribe: subscribeToEvents
|
|
3118
|
+
subscribe: subscribeToEvents,
|
|
3119
|
+
db: db.db
|
|
3646
3120
|
});
|
|
3647
3121
|
cleanup.push(() => unsubLogger());
|
|
3648
|
-
stderrWriter(
|
|
3649
|
-
`pilot build: run ${runId} started (${plan.tasks.length} tasks)
|
|
3650
|
-
`
|
|
3651
|
-
);
|
|
3652
3122
|
}
|
|
3653
3123
|
const result = await runWorker({
|
|
3654
3124
|
db: db.db,
|
|
3655
3125
|
runId,
|
|
3656
3126
|
plan,
|
|
3657
3127
|
scheduler,
|
|
3658
|
-
pool,
|
|
3659
3128
|
client: server.client,
|
|
3660
3129
|
busFactory,
|
|
3661
|
-
branchPrefix,
|
|
3662
|
-
base,
|
|
3663
3130
|
abortSignal: aborter.signal
|
|
3664
3131
|
});
|
|
3665
3132
|
const counts = countByStatus(db.db, runId);
|
|
@@ -3678,7 +3145,7 @@ async function executeRun(args) {
|
|
|
3678
3145
|
}
|
|
3679
3146
|
async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
3680
3147
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
3681
|
-
const resolved =
|
|
3148
|
+
const resolved = path7.isAbsolute(input.flag) ? input.flag : path7.resolve(cwd, input.flag);
|
|
3682
3149
|
if (await isFile(resolved)) {
|
|
3683
3150
|
return { kind: "ok", path: resolved };
|
|
3684
3151
|
}
|
|
@@ -3690,14 +3157,14 @@ async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
|
3690
3157
|
if (input.positional !== void 0 && input.positional.length > 0) {
|
|
3691
3158
|
const plansDir2 = await getPlansDir(cwd);
|
|
3692
3159
|
const candidates = [];
|
|
3693
|
-
if (
|
|
3160
|
+
if (path7.isAbsolute(input.positional)) {
|
|
3694
3161
|
candidates.push(input.positional);
|
|
3695
3162
|
} else {
|
|
3696
|
-
candidates.push(
|
|
3697
|
-
candidates.push(
|
|
3163
|
+
candidates.push(path7.resolve(cwd, input.positional));
|
|
3164
|
+
candidates.push(path7.join(plansDir2, input.positional));
|
|
3698
3165
|
if (!/\.(ya?ml)$/i.test(input.positional)) {
|
|
3699
|
-
candidates.push(
|
|
3700
|
-
candidates.push(
|
|
3166
|
+
candidates.push(path7.join(plansDir2, `${input.positional}.yaml`));
|
|
3167
|
+
candidates.push(path7.join(plansDir2, `${input.positional}.yml`));
|
|
3701
3168
|
}
|
|
3702
3169
|
}
|
|
3703
3170
|
for (const c2 of candidates) {
|
|
@@ -3731,7 +3198,7 @@ async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
|
3731
3198
|
}
|
|
3732
3199
|
async function isFile(p) {
|
|
3733
3200
|
try {
|
|
3734
|
-
const st = await
|
|
3201
|
+
const st = await fs7.stat(p);
|
|
3735
3202
|
return st.isFile();
|
|
3736
3203
|
} catch {
|
|
3737
3204
|
return false;
|
|
@@ -3740,7 +3207,7 @@ async function isFile(p) {
|
|
|
3740
3207
|
async function findNewestYaml2(dir) {
|
|
3741
3208
|
let entries;
|
|
3742
3209
|
try {
|
|
3743
|
-
entries = await
|
|
3210
|
+
entries = await fs7.readdir(dir);
|
|
3744
3211
|
} catch {
|
|
3745
3212
|
return null;
|
|
3746
3213
|
}
|
|
@@ -3751,7 +3218,7 @@ async function findNewestYaml2(dir) {
|
|
|
3751
3218
|
let newest = null;
|
|
3752
3219
|
for (const name of yamls) {
|
|
3753
3220
|
try {
|
|
3754
|
-
const st = await
|
|
3221
|
+
const st = await fs7.stat(path7.join(dir, name));
|
|
3755
3222
|
if (newest === null || st.mtimeMs > newest.mtime) {
|
|
3756
3223
|
newest = { name, mtime: st.mtimeMs };
|
|
3757
3224
|
}
|
|
@@ -3759,13 +3226,13 @@ async function findNewestYaml2(dir) {
|
|
|
3759
3226
|
continue;
|
|
3760
3227
|
}
|
|
3761
3228
|
}
|
|
3762
|
-
return newest ?
|
|
3229
|
+
return newest ? path7.join(dir, newest.name) : null;
|
|
3763
3230
|
}
|
|
3764
3231
|
async function defaultReadPlanSelection(cwd) {
|
|
3765
3232
|
const plansDir = await getPlansDir(cwd);
|
|
3766
3233
|
let entries;
|
|
3767
3234
|
try {
|
|
3768
|
-
entries = await
|
|
3235
|
+
entries = await fs7.readdir(plansDir);
|
|
3769
3236
|
} catch {
|
|
3770
3237
|
return void 0;
|
|
3771
3238
|
}
|
|
@@ -3775,9 +3242,9 @@ async function defaultReadPlanSelection(cwd) {
|
|
|
3775
3242
|
if (yamls.length === 0) return void 0;
|
|
3776
3243
|
const stats = await Promise.all(
|
|
3777
3244
|
yamls.map(async (name) => {
|
|
3778
|
-
const full =
|
|
3245
|
+
const full = path7.join(plansDir, name);
|
|
3779
3246
|
try {
|
|
3780
|
-
const st = await
|
|
3247
|
+
const st = await fs7.stat(full);
|
|
3781
3248
|
return { name, full, mtime: st.mtimeMs };
|
|
3782
3249
|
} catch {
|
|
3783
3250
|
return null;
|
|
@@ -3828,6 +3295,13 @@ function relativeTimeFromNow(thenMs) {
|
|
|
3828
3295
|
const d = Math.round(h / 24);
|
|
3829
3296
|
return `${d}d ago`;
|
|
3830
3297
|
}
|
|
3298
|
+
function formatDuration(ms) {
|
|
3299
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1e3));
|
|
3300
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
3301
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3302
|
+
const seconds = totalSeconds % 60;
|
|
3303
|
+
return `${minutes}m ${seconds}s`;
|
|
3304
|
+
}
|
|
3831
3305
|
function isExitPromptError2(err) {
|
|
3832
3306
|
return err !== null && typeof err === "object" && "name" in err && err.name === "ExitPromptError";
|
|
3833
3307
|
}
|
|
@@ -3851,8 +3325,9 @@ function startStreamingLogger(args) {
|
|
|
3851
3325
|
return `${hh}:${mm}:${ss}`;
|
|
3852
3326
|
};
|
|
3853
3327
|
const write = (line) => {
|
|
3854
|
-
|
|
3855
|
-
|
|
3328
|
+
const msg = `[${formatTs3(clock())}] ${line}
|
|
3329
|
+
`;
|
|
3330
|
+
stderrWriter(msg);
|
|
3856
3331
|
};
|
|
3857
3332
|
const writeRaw = (line) => {
|
|
3858
3333
|
stderrWriter(`${line}
|
|
@@ -3885,6 +3360,11 @@ function startStreamingLogger(args) {
|
|
|
3885
3360
|
write(
|
|
3886
3361
|
`task.verify.failed ${id ?? "?"} attempt ${p.attempt}/${p.of} (${p.command} \u2192 exit ${p.exitCode}${timedOutSuffix})`
|
|
3887
3362
|
);
|
|
3363
|
+
const output = typeof p.output === "string" ? p.output : null;
|
|
3364
|
+
if (output !== null && output.length > 0) {
|
|
3365
|
+
const tail = output.trim().split("\n").slice(-3).map((l) => ` ${l}`).join("\n");
|
|
3366
|
+
writeRaw(tail);
|
|
3367
|
+
}
|
|
3888
3368
|
} else {
|
|
3889
3369
|
write(`task.verify.failed ${id ?? "?"}`);
|
|
3890
3370
|
}
|
|
@@ -3893,14 +3373,14 @@ function startStreamingLogger(args) {
|
|
|
3893
3373
|
case "task.succeeded": {
|
|
3894
3374
|
succeeded += 1;
|
|
3895
3375
|
const ms = id !== null ? event.ts - (taskStart.get(id) ?? event.ts) : 0;
|
|
3896
|
-
write(`task.succeeded ${id ?? "?"} in ${
|
|
3376
|
+
write(`task.succeeded ${id ?? "?"} in ${formatDuration(ms)}`);
|
|
3897
3377
|
write(`run.progress ${succeeded}/${totalTasks} succeeded`);
|
|
3898
3378
|
break;
|
|
3899
3379
|
}
|
|
3900
3380
|
case "task.failed": {
|
|
3901
3381
|
failed += 1;
|
|
3902
3382
|
const ms = id !== null ? event.ts - (taskStart.get(id) ?? event.ts) : 0;
|
|
3903
|
-
write(`task.failed ${id ?? "?"} in ${
|
|
3383
|
+
write(`task.failed ${id ?? "?"} in ${formatDuration(ms)}`);
|
|
3904
3384
|
const detail = extractPhaseReason(event.payload);
|
|
3905
3385
|
if (detail !== null) {
|
|
3906
3386
|
writeRaw(` \u2192 ${detail.phase}: ${truncate(detail.reason, 200)}`);
|
|
@@ -3965,6 +3445,12 @@ function startStreamingLogger(args) {
|
|
|
3965
3445
|
case "task.touches.violation":
|
|
3966
3446
|
write(`task.touches.violation ${id ?? "?"}`);
|
|
3967
3447
|
break;
|
|
3448
|
+
case "task.progress": {
|
|
3449
|
+
const p = event.payload;
|
|
3450
|
+
const message = p?.message ?? "(no message)";
|
|
3451
|
+
write(`${id ?? "?"} > ${message}`);
|
|
3452
|
+
break;
|
|
3453
|
+
}
|
|
3968
3454
|
// Other kinds (task.session.created, run.*) are intentionally
|
|
3969
3455
|
// suppressed — too chatty for stdout. `pilot logs` carries the
|
|
3970
3456
|
// full trace.
|
|
@@ -3972,8 +3458,39 @@ function startStreamingLogger(args) {
|
|
|
3972
3458
|
break;
|
|
3973
3459
|
}
|
|
3974
3460
|
});
|
|
3461
|
+
let progressPollTimer = null;
|
|
3462
|
+
let lastProgressId = 0;
|
|
3463
|
+
if (args.db) {
|
|
3464
|
+
const pollDb = args.db;
|
|
3465
|
+
const pollMs = args.progressPollMs ?? 2e3;
|
|
3466
|
+
try {
|
|
3467
|
+
const row = pollDb.query(
|
|
3468
|
+
`SELECT MAX(id) as maxId FROM events WHERE run_id=? AND kind='task.progress'`
|
|
3469
|
+
).get(runId);
|
|
3470
|
+
lastProgressId = row?.maxId ?? 0;
|
|
3471
|
+
} catch {
|
|
3472
|
+
}
|
|
3473
|
+
progressPollTimer = setInterval(() => {
|
|
3474
|
+
try {
|
|
3475
|
+
const rows = pollDb.query(
|
|
3476
|
+
`SELECT id, task_id, ts, payload FROM events WHERE run_id=? AND kind='task.progress' AND id > ? ORDER BY id`
|
|
3477
|
+
).all(runId, lastProgressId);
|
|
3478
|
+
for (const row of rows) {
|
|
3479
|
+
lastProgressId = row.id;
|
|
3480
|
+
try {
|
|
3481
|
+
const p = JSON.parse(row.payload);
|
|
3482
|
+
const message = p?.message ?? "(no message)";
|
|
3483
|
+
write(`${row.task_id ?? "?"} > ${message}`);
|
|
3484
|
+
} catch {
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
}, pollMs);
|
|
3490
|
+
}
|
|
3975
3491
|
return () => {
|
|
3976
3492
|
flushBlockedSummary();
|
|
3493
|
+
if (progressPollTimer) clearInterval(progressPollTimer);
|
|
3977
3494
|
unsub();
|
|
3978
3495
|
};
|
|
3979
3496
|
}
|
|
@@ -3994,11 +3511,11 @@ function deriveBranchPrefix(planBranchPrefix, slug, runId) {
|
|
|
3994
3511
|
return `${base}/${runId}`;
|
|
3995
3512
|
}
|
|
3996
3513
|
async function deriveUniqueSlug(plan, planPath, cwd) {
|
|
3997
|
-
const base =
|
|
3514
|
+
const base = path7.basename(planPath, path7.extname(planPath)) || deriveSlug(plan.name);
|
|
3998
3515
|
const dir = await getPlansDir(cwd);
|
|
3999
|
-
const entries = await
|
|
3516
|
+
const entries = await fs7.readdir(dir).catch(() => []);
|
|
4000
3517
|
const existingSlugs = new Set(
|
|
4001
|
-
entries.filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).map((n) =>
|
|
3518
|
+
entries.filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).map((n) => path7.basename(n, path7.extname(n)))
|
|
4002
3519
|
);
|
|
4003
3520
|
existingSlugs.delete(base);
|
|
4004
3521
|
return resolveUniqueSlug(base, existingSlugs);
|
|
@@ -4036,14 +3553,14 @@ Failed tasks (${failed.length}):
|
|
|
4036
3553
|
const { phase, reason } = resolveFailureDetail(db, runId, t);
|
|
4037
3554
|
const session = t.session_id ?? "(none \u2014 failed before session.create)";
|
|
4038
3555
|
const worktree = t.worktree_path ?? "(none)";
|
|
4039
|
-
const elapsed = t.started_at !== null && t.finished_at !== null ?
|
|
3556
|
+
const elapsed = t.started_at !== null && t.finished_at !== null ? formatDuration(t.finished_at - t.started_at) : "0s";
|
|
4040
3557
|
process.stdout.write(
|
|
4041
3558
|
` ${t.task_id}
|
|
4042
3559
|
phase: ${phase}
|
|
4043
3560
|
reason: ${truncateSummary(reason, 300)}
|
|
4044
3561
|
session: ${session}
|
|
4045
3562
|
worktree: ${worktree}
|
|
4046
|
-
elapsed: ${elapsed}
|
|
3563
|
+
elapsed: ${elapsed} attempts: ${t.attempts}
|
|
4047
3564
|
|
|
4048
3565
|
`
|
|
4049
3566
|
);
|
|
@@ -4089,12 +3606,16 @@ async function runCleanup(cleanup) {
|
|
|
4089
3606
|
}
|
|
4090
3607
|
}
|
|
4091
3608
|
|
|
4092
|
-
// src/pilot/cli/
|
|
4093
|
-
import { command as command4, flag as flag3, option as option3, optional as optional4, string as string4 } from "cmd-ts";
|
|
3609
|
+
// src/pilot/cli/build-resume.ts
|
|
3610
|
+
import { command as command4, flag as flag3, option as option3, optional as optional4, string as string4, number as cmdNumber2 } from "cmd-ts";
|
|
3611
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
3612
|
+
import { promises as fs9 } from "fs";
|
|
3613
|
+
import * as path9 from "path";
|
|
3614
|
+
import { promisify as promisify2 } from "util";
|
|
4094
3615
|
|
|
4095
3616
|
// src/pilot/cli/discover.ts
|
|
4096
|
-
import { promises as
|
|
4097
|
-
import * as
|
|
3617
|
+
import { promises as fs8 } from "fs";
|
|
3618
|
+
import * as path8 from "path";
|
|
4098
3619
|
async function discoverRun(args) {
|
|
4099
3620
|
const cwd = args.cwd;
|
|
4100
3621
|
if (args.runId !== void 0 && args.runId.length > 0) {
|
|
@@ -4102,7 +3623,7 @@ async function discoverRun(args) {
|
|
|
4102
3623
|
const cwdDbPath = await getStateDbPath(cwd, args.runId);
|
|
4103
3624
|
tried.push(cwdDbPath);
|
|
4104
3625
|
try {
|
|
4105
|
-
await
|
|
3626
|
+
await fs8.stat(cwdDbPath);
|
|
4106
3627
|
const runDir = await getRunDir(cwd, args.runId);
|
|
4107
3628
|
return { runId: args.runId, dbPath: cwdDbPath, runDir };
|
|
4108
3629
|
} catch {
|
|
@@ -4110,14 +3631,14 @@ async function discoverRun(args) {
|
|
|
4110
3631
|
const base = resolveBaseDir();
|
|
4111
3632
|
let repoFolders;
|
|
4112
3633
|
try {
|
|
4113
|
-
repoFolders = await
|
|
3634
|
+
repoFolders = await fs8.readdir(base);
|
|
4114
3635
|
} catch {
|
|
4115
3636
|
throw new Error(
|
|
4116
3637
|
`pilot: no state.db for run ${JSON.stringify(args.runId)} (looked at ${tried.join(", ")}; base ${base} does not exist)`
|
|
4117
3638
|
);
|
|
4118
3639
|
}
|
|
4119
3640
|
for (const folder of repoFolders) {
|
|
4120
|
-
const candidateDbPath =
|
|
3641
|
+
const candidateDbPath = path8.join(
|
|
4121
3642
|
base,
|
|
4122
3643
|
folder,
|
|
4123
3644
|
"pilot",
|
|
@@ -4127,15 +3648,15 @@ async function discoverRun(args) {
|
|
|
4127
3648
|
);
|
|
4128
3649
|
if (tried.includes(candidateDbPath)) continue;
|
|
4129
3650
|
try {
|
|
4130
|
-
const
|
|
4131
|
-
if (!
|
|
3651
|
+
const stat = await fs8.stat(path8.join(base, folder));
|
|
3652
|
+
if (!stat.isDirectory()) continue;
|
|
4132
3653
|
} catch {
|
|
4133
3654
|
continue;
|
|
4134
3655
|
}
|
|
4135
3656
|
tried.push(candidateDbPath);
|
|
4136
3657
|
try {
|
|
4137
|
-
await
|
|
4138
|
-
const candidateRunDir =
|
|
3658
|
+
await fs8.stat(candidateDbPath);
|
|
3659
|
+
const candidateRunDir = path8.join(
|
|
4139
3660
|
base,
|
|
4140
3661
|
folder,
|
|
4141
3662
|
"pilot",
|
|
@@ -4155,10 +3676,10 @@ async function discoverRun(args) {
|
|
|
4155
3676
|
);
|
|
4156
3677
|
}
|
|
4157
3678
|
const pilot = await getPilotDir(cwd);
|
|
4158
|
-
const runsDir =
|
|
3679
|
+
const runsDir = path8.join(pilot, "runs");
|
|
4159
3680
|
let entries;
|
|
4160
3681
|
try {
|
|
4161
|
-
entries = await
|
|
3682
|
+
entries = await fs8.readdir(runsDir);
|
|
4162
3683
|
} catch {
|
|
4163
3684
|
throw new Error(
|
|
4164
3685
|
`pilot: no runs found at ${runsDir} (run \`pilot build\` first)`
|
|
@@ -4166,10 +3687,10 @@ async function discoverRun(args) {
|
|
|
4166
3687
|
}
|
|
4167
3688
|
let newest = null;
|
|
4168
3689
|
for (const id of entries) {
|
|
4169
|
-
const dbPath =
|
|
3690
|
+
const dbPath = path8.join(runsDir, id, "state.db");
|
|
4170
3691
|
let st;
|
|
4171
3692
|
try {
|
|
4172
|
-
st = await
|
|
3693
|
+
st = await fs8.stat(dbPath);
|
|
4173
3694
|
} catch {
|
|
4174
3695
|
continue;
|
|
4175
3696
|
}
|
|
@@ -4185,21 +3706,338 @@ async function discoverRun(args) {
|
|
|
4185
3706
|
return {
|
|
4186
3707
|
runId: newest.id,
|
|
4187
3708
|
dbPath: newest.dbPath,
|
|
4188
|
-
runDir:
|
|
3709
|
+
runDir: path8.join(runsDir, newest.id)
|
|
4189
3710
|
};
|
|
4190
3711
|
}
|
|
4191
3712
|
|
|
3713
|
+
// src/pilot/cli/build-resume.ts
|
|
3714
|
+
var execFileP2 = promisify2(execFileCb2);
|
|
3715
|
+
var buildResumeCmd = command4({
|
|
3716
|
+
name: "build-resume",
|
|
3717
|
+
description: "Resume a partially-completed pilot run from where it left off.",
|
|
3718
|
+
args: {
|
|
3719
|
+
run: option3({
|
|
3720
|
+
long: "run",
|
|
3721
|
+
type: optional4(string4),
|
|
3722
|
+
description: "Run ID to resume. Defaults to the newest resumable run matching --plan (or interactive picker if multiple exist)."
|
|
3723
|
+
}),
|
|
3724
|
+
plan: option3({
|
|
3725
|
+
long: "plan",
|
|
3726
|
+
type: optional4(string4),
|
|
3727
|
+
description: "Filter to runs that used this plan path (absolute, cwd-relative, or bare filename resolved against the plans dir). Disambiguates when multiple worktrees share a state dir."
|
|
3728
|
+
}),
|
|
3729
|
+
opencodePort: option3({
|
|
3730
|
+
long: "opencode-port",
|
|
3731
|
+
type: optional4(cmdNumber2),
|
|
3732
|
+
description: "Port for the spawned opencode server (default: 0 = random)."
|
|
3733
|
+
}),
|
|
3734
|
+
quiet: flag3({
|
|
3735
|
+
long: "quiet",
|
|
3736
|
+
description: "Suppress per-task progress lines on stderr. Summary still prints."
|
|
3737
|
+
})
|
|
3738
|
+
},
|
|
3739
|
+
handler: async (args) => {
|
|
3740
|
+
await requirePlugin();
|
|
3741
|
+
const code = await runBuildResume(args);
|
|
3742
|
+
process.exit(code);
|
|
3743
|
+
}
|
|
3744
|
+
});
|
|
3745
|
+
async function runBuildResume(opts) {
|
|
3746
|
+
const cwd = process.cwd();
|
|
3747
|
+
const stderrWriter = opts.stderrWriter ?? ((s) => void process.stderr.write(s));
|
|
3748
|
+
let runId;
|
|
3749
|
+
let dbPath;
|
|
3750
|
+
let runDir;
|
|
3751
|
+
try {
|
|
3752
|
+
if (opts.run !== void 0 && opts.run.length > 0) {
|
|
3753
|
+
const d = await discoverRun({ cwd, runId: opts.run });
|
|
3754
|
+
runId = d.runId;
|
|
3755
|
+
dbPath = d.dbPath;
|
|
3756
|
+
runDir = d.runDir;
|
|
3757
|
+
} else {
|
|
3758
|
+
const planFilter = opts.plan !== void 0 && opts.plan.length > 0 ? await resolvePlanFilter(cwd, opts.plan) : void 0;
|
|
3759
|
+
const resumable = await findLatestResumableRun(cwd, planFilter);
|
|
3760
|
+
if (resumable === null) {
|
|
3761
|
+
const suffix = planFilter ? ` matching plan "${path9.basename(planFilter)}"` : "";
|
|
3762
|
+
process.stderr.write(
|
|
3763
|
+
`pilot build-resume: no resumable runs found in this repo${suffix} (no run has non-succeeded tasks)
|
|
3764
|
+
`
|
|
3765
|
+
);
|
|
3766
|
+
return 2;
|
|
3767
|
+
}
|
|
3768
|
+
runId = resumable.runId;
|
|
3769
|
+
dbPath = resumable.dbPath;
|
|
3770
|
+
runDir = resumable.runDir;
|
|
3771
|
+
}
|
|
3772
|
+
} catch (err) {
|
|
3773
|
+
process.stderr.write(
|
|
3774
|
+
`pilot build-resume: ${err instanceof Error ? err.message : String(err)}
|
|
3775
|
+
`
|
|
3776
|
+
);
|
|
3777
|
+
return 1;
|
|
3778
|
+
}
|
|
3779
|
+
const opened = openStateDb(dbPath);
|
|
3780
|
+
const cleanup = [];
|
|
3781
|
+
cleanup.push(() => opened.close());
|
|
3782
|
+
const run2 = getRun(opened.db, runId);
|
|
3783
|
+
if (run2 === null) {
|
|
3784
|
+
process.stderr.write(
|
|
3785
|
+
`pilot build-resume: state.db exists at ${dbPath} but has no row for ${runId}
|
|
3786
|
+
`
|
|
3787
|
+
);
|
|
3788
|
+
await runCleanup2(cleanup);
|
|
3789
|
+
return 1;
|
|
3790
|
+
}
|
|
3791
|
+
const counts = countByStatus(opened.db, runId);
|
|
3792
|
+
const remaining = counts.pending + counts.ready + counts.running + counts.failed + counts.blocked + counts.aborted;
|
|
3793
|
+
if (remaining === 0) {
|
|
3794
|
+
process.stderr.write(
|
|
3795
|
+
`pilot build-resume: run ${runId} has no tasks to resume (all ${counts.succeeded} succeeded)
|
|
3796
|
+
`
|
|
3797
|
+
);
|
|
3798
|
+
await runCleanup2(cleanup);
|
|
3799
|
+
return 2;
|
|
3800
|
+
}
|
|
3801
|
+
const planPath = run2.plan_path;
|
|
3802
|
+
const loaded = await loadPlan(planPath);
|
|
3803
|
+
if (!loaded.ok) {
|
|
3804
|
+
process.stderr.write(
|
|
3805
|
+
`pilot build-resume: plan at ${planPath} failed to load:
|
|
3806
|
+
` + loaded.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n") + `
|
|
3807
|
+
`
|
|
3808
|
+
);
|
|
3809
|
+
await runCleanup2(cleanup);
|
|
3810
|
+
return 1;
|
|
3811
|
+
}
|
|
3812
|
+
const plan = loaded.plan;
|
|
3813
|
+
const preflight = await runResumePreflight({ cwd, opened, runId });
|
|
3814
|
+
if (!preflight.ok) {
|
|
3815
|
+
process.stderr.write(`pilot build-resume: ${preflight.reason}
|
|
3816
|
+
`);
|
|
3817
|
+
await runCleanup2(cleanup);
|
|
3818
|
+
return 1;
|
|
3819
|
+
}
|
|
3820
|
+
for (const w of preflight.warnings) {
|
|
3821
|
+
stderrWriter(`[pilot] ${w}
|
|
3822
|
+
`);
|
|
3823
|
+
}
|
|
3824
|
+
const { runSetupHook, SETUP_HOOK_RELATIVE_PATH } = await import("./setup-hook-FHTXMAQL.js");
|
|
3825
|
+
const hookResult = await runSetupHook({
|
|
3826
|
+
cwd,
|
|
3827
|
+
onLine: (c2) => stderrWriter(c2)
|
|
3828
|
+
});
|
|
3829
|
+
switch (hookResult.kind) {
|
|
3830
|
+
case "skipped":
|
|
3831
|
+
break;
|
|
3832
|
+
case "ok":
|
|
3833
|
+
stderrWriter(
|
|
3834
|
+
`[pilot] setup hook ${SETUP_HOOK_RELATIVE_PATH} passed (${Math.round(hookResult.durationMs / 1e3)}s)
|
|
3835
|
+
`
|
|
3836
|
+
);
|
|
3837
|
+
break;
|
|
3838
|
+
case "not-executable":
|
|
3839
|
+
stderrWriter(
|
|
3840
|
+
`[pilot] setup hook ${hookResult.hookPath} is not executable. Run \`chmod +x ${SETUP_HOOK_RELATIVE_PATH}\` and re-run pilot.
|
|
3841
|
+
`
|
|
3842
|
+
);
|
|
3843
|
+
await runCleanup2(cleanup);
|
|
3844
|
+
return 1;
|
|
3845
|
+
case "timed-out":
|
|
3846
|
+
stderrWriter(
|
|
3847
|
+
`[pilot] setup hook ${hookResult.hookPath} timed out after ${Math.round(hookResult.timeoutMs / 1e3)}s
|
|
3848
|
+
`
|
|
3849
|
+
);
|
|
3850
|
+
await runCleanup2(cleanup);
|
|
3851
|
+
return 1;
|
|
3852
|
+
case "failed":
|
|
3853
|
+
stderrWriter(
|
|
3854
|
+
`[pilot] setup hook ${hookResult.hookPath} exited ${hookResult.exitCode} (after ${Math.round(hookResult.durationMs / 1e3)}s). Fix the environment and re-run pilot.
|
|
3855
|
+
`
|
|
3856
|
+
);
|
|
3857
|
+
await runCleanup2(cleanup);
|
|
3858
|
+
return 1;
|
|
3859
|
+
case "spawn-error":
|
|
3860
|
+
stderrWriter(
|
|
3861
|
+
`[pilot] setup hook ${hookResult.hookPath} failed to spawn: ${hookResult.error}
|
|
3862
|
+
`
|
|
3863
|
+
);
|
|
3864
|
+
await runCleanup2(cleanup);
|
|
3865
|
+
return 1;
|
|
3866
|
+
}
|
|
3867
|
+
const resetIds = resetTasksForResume(opened.db, runId);
|
|
3868
|
+
try {
|
|
3869
|
+
markRunResumed(opened.db, runId);
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
process.stderr.write(
|
|
3872
|
+
`pilot build-resume: cannot mark run as running: ${err instanceof Error ? err.message : String(err)}
|
|
3873
|
+
`
|
|
3874
|
+
);
|
|
3875
|
+
await runCleanup2(cleanup);
|
|
3876
|
+
return 1;
|
|
3877
|
+
}
|
|
3878
|
+
appendEvent(opened.db, {
|
|
3879
|
+
runId,
|
|
3880
|
+
kind: "run.resumed",
|
|
3881
|
+
payload: {
|
|
3882
|
+
resetTaskIds: resetIds,
|
|
3883
|
+
skippedSucceeded: counts.succeeded
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
if (opts.quiet !== true) {
|
|
3887
|
+
stderrWriter(
|
|
3888
|
+
`pilot build-resume: resuming run ${runId} \u2014 ${resetIds.length} task(s) reset (skipping ${counts.succeeded} succeeded)
|
|
3889
|
+
`
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
const branchPrefix = deriveBranchPrefix(plan.branch_prefix, run2.plan_slug, runId);
|
|
3893
|
+
return executeRun({
|
|
3894
|
+
db: opened,
|
|
3895
|
+
runId,
|
|
3896
|
+
plan,
|
|
3897
|
+
planPath,
|
|
3898
|
+
runDir,
|
|
3899
|
+
branchPrefix,
|
|
3900
|
+
cleanup,
|
|
3901
|
+
opencodePort: opts.opencodePort,
|
|
3902
|
+
quiet: opts.quiet,
|
|
3903
|
+
stderrWriter
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
async function runResumePreflight(args) {
|
|
3907
|
+
const { cwd, opened, runId } = args;
|
|
3908
|
+
const { checkCwdSafety: checkCwdSafety2 } = await import("./safety-gate-WM3EWOCY.js");
|
|
3909
|
+
const gate = await checkCwdSafety2(cwd);
|
|
3910
|
+
if (!gate.ok) {
|
|
3911
|
+
return { ok: false, reason: gate.reason };
|
|
3912
|
+
}
|
|
3913
|
+
const { listTasks: listTasks2 } = await import("./tasks-KJ3WN2KY.js");
|
|
3914
|
+
const tasks = listTasks2(opened.db, runId);
|
|
3915
|
+
const withBranch = tasks.filter(
|
|
3916
|
+
(t) => t.branch !== null && t.branch.length > 0
|
|
3917
|
+
);
|
|
3918
|
+
if (withBranch.length > 0) {
|
|
3919
|
+
const recordedBranch = withBranch[0].branch;
|
|
3920
|
+
const current = await currentBranch(cwd);
|
|
3921
|
+
if (current !== recordedBranch) {
|
|
3922
|
+
return {
|
|
3923
|
+
ok: false,
|
|
3924
|
+
reason: `branch mismatch: run ${runId} was started on "${recordedBranch}", but cwd is currently on "${current}". Switch branches: \`git checkout ${recordedBranch}\``
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
return { ok: true, warnings: gate.warnings };
|
|
3929
|
+
}
|
|
3930
|
+
async function currentBranch(cwd) {
|
|
3931
|
+
try {
|
|
3932
|
+
const { stdout } = await execFileP2(
|
|
3933
|
+
"git",
|
|
3934
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
3935
|
+
{ cwd, timeout: 1e4 }
|
|
3936
|
+
);
|
|
3937
|
+
return stdout.toString().trim();
|
|
3938
|
+
} catch {
|
|
3939
|
+
return "";
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
async function findLatestResumableRun(cwd, planFilter) {
|
|
3943
|
+
const { getPilotDir: getPilotDir2 } = await import("./paths-LT3QQKCF.js");
|
|
3944
|
+
const pilot = await getPilotDir2(cwd);
|
|
3945
|
+
const runsDir = path9.join(pilot, "runs");
|
|
3946
|
+
let entries;
|
|
3947
|
+
try {
|
|
3948
|
+
entries = await fs9.readdir(runsDir);
|
|
3949
|
+
} catch {
|
|
3950
|
+
return null;
|
|
3951
|
+
}
|
|
3952
|
+
const candidates = [];
|
|
3953
|
+
for (const id of entries) {
|
|
3954
|
+
const dbPath = path9.join(runsDir, id, "state.db");
|
|
3955
|
+
let st;
|
|
3956
|
+
try {
|
|
3957
|
+
st = await fs9.stat(dbPath);
|
|
3958
|
+
} catch {
|
|
3959
|
+
continue;
|
|
3960
|
+
}
|
|
3961
|
+
candidates.push({
|
|
3962
|
+
runId: id,
|
|
3963
|
+
dbPath,
|
|
3964
|
+
runDir: path9.join(runsDir, id),
|
|
3965
|
+
mtime: st.mtimeMs
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
3969
|
+
for (const c2 of candidates) {
|
|
3970
|
+
const opened = openStateDb(c2.dbPath);
|
|
3971
|
+
try {
|
|
3972
|
+
if (planFilter !== void 0) {
|
|
3973
|
+
const { getRun: getRun2 } = await import("./runs-QWPL3TKV.js");
|
|
3974
|
+
const run2 = getRun2(opened.db, c2.runId);
|
|
3975
|
+
if (!run2) continue;
|
|
3976
|
+
const matches = run2.plan_path === planFilter || path9.basename(run2.plan_path) === path9.basename(planFilter);
|
|
3977
|
+
if (!matches) continue;
|
|
3978
|
+
}
|
|
3979
|
+
const counts = countByStatus(opened.db, c2.runId);
|
|
3980
|
+
const nonSucceeded = counts.pending + counts.ready + counts.running + counts.failed + counts.blocked + counts.aborted;
|
|
3981
|
+
if (nonSucceeded > 0) {
|
|
3982
|
+
return c2;
|
|
3983
|
+
}
|
|
3984
|
+
} finally {
|
|
3985
|
+
opened.close();
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
void resolveBaseDir;
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
async function resolvePlanFilter(cwd, input) {
|
|
3992
|
+
const { getPlansDir: getPlansDir2 } = await import("./paths-LT3QQKCF.js");
|
|
3993
|
+
if (path9.isAbsolute(input)) return input;
|
|
3994
|
+
const cwdRel = path9.resolve(cwd, input);
|
|
3995
|
+
try {
|
|
3996
|
+
await fs9.stat(cwdRel);
|
|
3997
|
+
return cwdRel;
|
|
3998
|
+
} catch {
|
|
3999
|
+
}
|
|
4000
|
+
const plansDir = await getPlansDir2(cwd);
|
|
4001
|
+
const plansDirRel = path9.join(plansDir, input);
|
|
4002
|
+
try {
|
|
4003
|
+
await fs9.stat(plansDirRel);
|
|
4004
|
+
return plansDirRel;
|
|
4005
|
+
} catch {
|
|
4006
|
+
}
|
|
4007
|
+
if (!/\.(ya?ml)$/i.test(input)) {
|
|
4008
|
+
for (const ext of [".yaml", ".yml"]) {
|
|
4009
|
+
const withExt = path9.join(plansDir, `${input}${ext}`);
|
|
4010
|
+
try {
|
|
4011
|
+
await fs9.stat(withExt);
|
|
4012
|
+
return withExt;
|
|
4013
|
+
} catch {
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
return input;
|
|
4018
|
+
}
|
|
4019
|
+
async function runCleanup2(cleanup) {
|
|
4020
|
+
while (cleanup.length > 0) {
|
|
4021
|
+
const fn = cleanup.pop();
|
|
4022
|
+
try {
|
|
4023
|
+
await fn();
|
|
4024
|
+
} catch {
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4192
4029
|
// src/pilot/cli/status.ts
|
|
4193
|
-
|
|
4030
|
+
import { command as command5, flag as flag4, option as option4, optional as optional5, string as string5 } from "cmd-ts";
|
|
4031
|
+
var statusCmd = command5({
|
|
4194
4032
|
name: "status",
|
|
4195
4033
|
description: "Print the run + task status for a pilot run.",
|
|
4196
4034
|
args: {
|
|
4197
|
-
run:
|
|
4035
|
+
run: option4({
|
|
4198
4036
|
long: "run",
|
|
4199
|
-
type:
|
|
4037
|
+
type: optional5(string5),
|
|
4200
4038
|
description: "Run ID. Defaults to the newest run with a state.db."
|
|
4201
4039
|
}),
|
|
4202
|
-
json:
|
|
4040
|
+
json: flag4({
|
|
4203
4041
|
long: "json",
|
|
4204
4042
|
description: "Emit JSON instead of human-readable text."
|
|
4205
4043
|
})
|
|
@@ -4289,199 +4127,19 @@ function wrap(text, width) {
|
|
|
4289
4127
|
return out;
|
|
4290
4128
|
}
|
|
4291
4129
|
|
|
4292
|
-
// src/pilot/cli/resume.ts
|
|
4293
|
-
import { command as command5, option as option4, optional as optional5, string as string5 } from "cmd-ts";
|
|
4294
|
-
var resumeCmd = command5({
|
|
4295
|
-
name: "resume",
|
|
4296
|
-
description: "Continue a partially-completed pilot run.",
|
|
4297
|
-
args: {
|
|
4298
|
-
run: option4({
|
|
4299
|
-
long: "run",
|
|
4300
|
-
type: optional5(string5),
|
|
4301
|
-
description: "Run ID. Defaults to the newest run."
|
|
4302
|
-
})
|
|
4303
|
-
},
|
|
4304
|
-
handler: async ({ run: run2 }) => {
|
|
4305
|
-
await requirePlugin();
|
|
4306
|
-
const code = await runResume({ runId: run2 });
|
|
4307
|
-
process.exit(code);
|
|
4308
|
-
}
|
|
4309
|
-
});
|
|
4310
|
-
async function runResume(opts) {
|
|
4311
|
-
let discovered;
|
|
4312
|
-
try {
|
|
4313
|
-
discovered = await discoverRun({
|
|
4314
|
-
cwd: process.cwd(),
|
|
4315
|
-
runId: opts.runId
|
|
4316
|
-
});
|
|
4317
|
-
} catch (err) {
|
|
4318
|
-
process.stderr.write(
|
|
4319
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4320
|
-
`
|
|
4321
|
-
);
|
|
4322
|
-
return 1;
|
|
4323
|
-
}
|
|
4324
|
-
const opened = openStateDb(discovered.dbPath);
|
|
4325
|
-
const cleanup = [
|
|
4326
|
-
() => opened.close()
|
|
4327
|
-
];
|
|
4328
|
-
const run2 = getRun(opened.db, discovered.runId);
|
|
4329
|
-
if (run2 === null) {
|
|
4330
|
-
process.stderr.write(
|
|
4331
|
-
`pilot resume: run ${discovered.runId} missing from DB
|
|
4332
|
-
`
|
|
4333
|
-
);
|
|
4334
|
-
await runCleanup2(cleanup);
|
|
4335
|
-
return 1;
|
|
4336
|
-
}
|
|
4337
|
-
const loaded = await loadPlan(run2.plan_path);
|
|
4338
|
-
if (!loaded.ok) {
|
|
4339
|
-
process.stderr.write(
|
|
4340
|
-
`pilot resume: cannot reload plan at ${run2.plan_path} (${loaded.kind})
|
|
4341
|
-
`
|
|
4342
|
-
);
|
|
4343
|
-
for (const e of loaded.errors) {
|
|
4344
|
-
process.stderr.write(` ${e.path}: ${e.message}
|
|
4345
|
-
`);
|
|
4346
|
-
}
|
|
4347
|
-
await runCleanup2(cleanup);
|
|
4348
|
-
return 1;
|
|
4349
|
-
}
|
|
4350
|
-
if (run2.status === "pending") {
|
|
4351
|
-
markRunRunning(opened.db, discovered.runId);
|
|
4352
|
-
} else if (run2.status === "running") {
|
|
4353
|
-
} else {
|
|
4354
|
-
opened.db.run(
|
|
4355
|
-
`UPDATE runs SET status='running', finished_at=NULL WHERE id=?`,
|
|
4356
|
-
[discovered.runId]
|
|
4357
|
-
);
|
|
4358
|
-
}
|
|
4359
|
-
appendEvent(opened.db, {
|
|
4360
|
-
runId: discovered.runId,
|
|
4361
|
-
kind: "run.resumed",
|
|
4362
|
-
payload: { previousStatus: run2.status }
|
|
4363
|
-
});
|
|
4364
|
-
const runDir = await getRunDir(process.cwd(), discovered.runId);
|
|
4365
|
-
return executeRun({
|
|
4366
|
-
db: opened,
|
|
4367
|
-
runId: discovered.runId,
|
|
4368
|
-
plan: loaded.plan,
|
|
4369
|
-
planPath: run2.plan_path,
|
|
4370
|
-
runDir,
|
|
4371
|
-
// Reconstruct the same branch prefix the original `pilot build` used.
|
|
4372
|
-
// The runId segment is what makes branches unique per run; resume MUST
|
|
4373
|
-
// match the original to find the existing worktrees.
|
|
4374
|
-
branchPrefix: deriveBranchPrefix(
|
|
4375
|
-
loaded.plan.branch_prefix,
|
|
4376
|
-
run2.plan_slug,
|
|
4377
|
-
discovered.runId
|
|
4378
|
-
),
|
|
4379
|
-
cleanup
|
|
4380
|
-
});
|
|
4381
|
-
}
|
|
4382
|
-
async function runCleanup2(cleanup) {
|
|
4383
|
-
while (cleanup.length > 0) {
|
|
4384
|
-
const fn = cleanup.pop();
|
|
4385
|
-
try {
|
|
4386
|
-
await fn();
|
|
4387
|
-
} catch {
|
|
4388
|
-
}
|
|
4389
|
-
}
|
|
4390
|
-
}
|
|
4391
|
-
|
|
4392
|
-
// src/pilot/cli/retry.ts
|
|
4393
|
-
import { command as command6, option as option5, optional as optional6, positional as positional4, string as string6, flag as flag4 } from "cmd-ts";
|
|
4394
|
-
var retryCmd = command6({
|
|
4395
|
-
name: "retry",
|
|
4396
|
-
description: "Reset a single task to pending. Optionally also re-run it.",
|
|
4397
|
-
args: {
|
|
4398
|
-
taskId: positional4({
|
|
4399
|
-
type: string6,
|
|
4400
|
-
displayName: "task-id",
|
|
4401
|
-
description: "Task id to reset (e.g. T1)."
|
|
4402
|
-
}),
|
|
4403
|
-
run: option5({
|
|
4404
|
-
long: "run",
|
|
4405
|
-
type: optional6(string6),
|
|
4406
|
-
description: "Run ID. Defaults to the newest run."
|
|
4407
|
-
}),
|
|
4408
|
-
runNow: flag4({
|
|
4409
|
-
long: "run-now",
|
|
4410
|
-
description: "After resetting, immediately run `pilot resume` on the same DB."
|
|
4411
|
-
})
|
|
4412
|
-
},
|
|
4413
|
-
handler: async ({ taskId, run: run2, runNow }) => {
|
|
4414
|
-
await requirePlugin();
|
|
4415
|
-
const code = await runRetry({ taskId, runId: run2, runNow });
|
|
4416
|
-
process.exit(code);
|
|
4417
|
-
}
|
|
4418
|
-
});
|
|
4419
|
-
async function runRetry(opts) {
|
|
4420
|
-
let discovered;
|
|
4421
|
-
try {
|
|
4422
|
-
discovered = await discoverRun({
|
|
4423
|
-
cwd: process.cwd(),
|
|
4424
|
-
runId: opts.runId
|
|
4425
|
-
});
|
|
4426
|
-
} catch (err) {
|
|
4427
|
-
process.stderr.write(
|
|
4428
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4429
|
-
`
|
|
4430
|
-
);
|
|
4431
|
-
return 1;
|
|
4432
|
-
}
|
|
4433
|
-
const opened = openStateDb(discovered.dbPath);
|
|
4434
|
-
try {
|
|
4435
|
-
const task = getTask(opened.db, discovered.runId, opts.taskId);
|
|
4436
|
-
if (task === null) {
|
|
4437
|
-
process.stderr.write(
|
|
4438
|
-
`pilot retry: task ${JSON.stringify(opts.taskId)} not found in run ${discovered.runId}
|
|
4439
|
-
`
|
|
4440
|
-
);
|
|
4441
|
-
return 1;
|
|
4442
|
-
}
|
|
4443
|
-
const previousStatus = task.status;
|
|
4444
|
-
try {
|
|
4445
|
-
markPending(opened.db, discovered.runId, opts.taskId);
|
|
4446
|
-
} catch (err) {
|
|
4447
|
-
process.stderr.write(
|
|
4448
|
-
`pilot retry: ${err instanceof Error ? err.message : String(err)}
|
|
4449
|
-
`
|
|
4450
|
-
);
|
|
4451
|
-
return 1;
|
|
4452
|
-
}
|
|
4453
|
-
appendEvent(opened.db, {
|
|
4454
|
-
runId: discovered.runId,
|
|
4455
|
-
taskId: opts.taskId,
|
|
4456
|
-
kind: "task.retry",
|
|
4457
|
-
payload: { previousStatus }
|
|
4458
|
-
});
|
|
4459
|
-
process.stdout.write(
|
|
4460
|
-
`pilot retry: ${opts.taskId} reset to pending (was ${previousStatus})
|
|
4461
|
-
`
|
|
4462
|
-
);
|
|
4463
|
-
} finally {
|
|
4464
|
-
opened.close();
|
|
4465
|
-
}
|
|
4466
|
-
if (opts.runNow) {
|
|
4467
|
-
return runResume({ runId: discovered.runId });
|
|
4468
|
-
}
|
|
4469
|
-
return 0;
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
4130
|
// src/pilot/cli/logs.ts
|
|
4473
|
-
import { command as
|
|
4474
|
-
var logsCmd =
|
|
4131
|
+
import { command as command6, flag as flag5, option as option5, optional as optional6, positional as positional4, string as string6 } from "cmd-ts";
|
|
4132
|
+
var logsCmd = command6({
|
|
4475
4133
|
name: "logs",
|
|
4476
4134
|
description: "Print structured events for a task.",
|
|
4477
4135
|
args: {
|
|
4478
|
-
taskId:
|
|
4479
|
-
type:
|
|
4136
|
+
taskId: positional4({
|
|
4137
|
+
type: string6,
|
|
4480
4138
|
displayName: "task-id"
|
|
4481
4139
|
}),
|
|
4482
|
-
run:
|
|
4140
|
+
run: option5({
|
|
4483
4141
|
long: "run",
|
|
4484
|
-
type:
|
|
4142
|
+
type: optional6(string6),
|
|
4485
4143
|
description: "Run ID. Defaults to the newest run."
|
|
4486
4144
|
}),
|
|
4487
4145
|
json: flag5({
|
|
@@ -4601,172 +4259,18 @@ function summarizePayload(kind, payload) {
|
|
|
4601
4259
|
return s;
|
|
4602
4260
|
}
|
|
4603
4261
|
|
|
4604
|
-
// src/pilot/cli/worktrees.ts
|
|
4605
|
-
import { command as command8, flag as flag6, option as option7, optional as optional8, string as string8, subcommands } from "cmd-ts";
|
|
4606
|
-
import { promises as fs12 } from "fs";
|
|
4607
|
-
import * as path11 from "path";
|
|
4608
|
-
var listSubcmd = command8({
|
|
4609
|
-
name: "list",
|
|
4610
|
-
description: "List worktrees registered with the repo (filter to pilot ones).",
|
|
4611
|
-
args: {
|
|
4612
|
-
run: option7({
|
|
4613
|
-
long: "run",
|
|
4614
|
-
type: optional8(string8),
|
|
4615
|
-
description: "Run ID for context. Defaults to the newest run."
|
|
4616
|
-
})
|
|
4617
|
-
},
|
|
4618
|
-
handler: async ({ run: run2 }) => {
|
|
4619
|
-
const code = await runWorktreesList({ runId: run2 });
|
|
4620
|
-
process.exit(code);
|
|
4621
|
-
}
|
|
4622
|
-
});
|
|
4623
|
-
var pruneSubcmd = command8({
|
|
4624
|
-
name: "prune",
|
|
4625
|
-
description: "Remove worktrees from succeeded tasks (default) or all (--all).",
|
|
4626
|
-
args: {
|
|
4627
|
-
run: option7({
|
|
4628
|
-
long: "run",
|
|
4629
|
-
type: optional8(string8),
|
|
4630
|
-
description: "Run ID. Defaults to the newest run."
|
|
4631
|
-
}),
|
|
4632
|
-
all: flag6({
|
|
4633
|
-
long: "all",
|
|
4634
|
-
description: "Remove every pilot worktree for this run, even failed/aborted ones."
|
|
4635
|
-
}),
|
|
4636
|
-
dryRun: flag6({
|
|
4637
|
-
long: "dry-run",
|
|
4638
|
-
description: "Print what would be removed without removing."
|
|
4639
|
-
})
|
|
4640
|
-
},
|
|
4641
|
-
handler: async ({ run: run2, all, dryRun }) => {
|
|
4642
|
-
const code = await runWorktreesPrune({ runId: run2, all, dryRun });
|
|
4643
|
-
process.exit(code);
|
|
4644
|
-
}
|
|
4645
|
-
});
|
|
4646
|
-
var worktreesCmd = subcommands({
|
|
4647
|
-
name: "worktrees",
|
|
4648
|
-
description: "Inspect and prune pilot-managed git worktrees.",
|
|
4649
|
-
cmds: {
|
|
4650
|
-
list: listSubcmd,
|
|
4651
|
-
prune: pruneSubcmd
|
|
4652
|
-
}
|
|
4653
|
-
});
|
|
4654
|
-
async function runWorktreesList(opts) {
|
|
4655
|
-
let discovered;
|
|
4656
|
-
try {
|
|
4657
|
-
discovered = await discoverRun({
|
|
4658
|
-
cwd: process.cwd(),
|
|
4659
|
-
runId: opts.runId
|
|
4660
|
-
});
|
|
4661
|
-
} catch (err) {
|
|
4662
|
-
process.stderr.write(
|
|
4663
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4664
|
-
`
|
|
4665
|
-
);
|
|
4666
|
-
return 1;
|
|
4667
|
-
}
|
|
4668
|
-
const all = await gitWorktreeList(process.cwd());
|
|
4669
|
-
const wtBase = path11.join(discovered.runDir, "..");
|
|
4670
|
-
const pilotDir = path11.dirname(path11.dirname(discovered.runDir));
|
|
4671
|
-
const wtPrefix = path11.join(pilotDir, "worktrees", discovered.runId);
|
|
4672
|
-
const filtered = all.filter((w) => w.path.startsWith(wtPrefix));
|
|
4673
|
-
void wtBase;
|
|
4674
|
-
if (filtered.length === 0) {
|
|
4675
|
-
process.stdout.write(
|
|
4676
|
-
`pilot worktrees list: no pilot worktrees for run ${discovered.runId}
|
|
4677
|
-
`
|
|
4678
|
-
);
|
|
4679
|
-
return 0;
|
|
4680
|
-
}
|
|
4681
|
-
for (const w of filtered) {
|
|
4682
|
-
process.stdout.write(
|
|
4683
|
-
`${w.path} ${w.head.slice(0, 7)} ${w.branch ?? "(detached)"}
|
|
4684
|
-
`
|
|
4685
|
-
);
|
|
4686
|
-
}
|
|
4687
|
-
return 0;
|
|
4688
|
-
}
|
|
4689
|
-
async function runWorktreesPrune(opts) {
|
|
4690
|
-
let discovered;
|
|
4691
|
-
try {
|
|
4692
|
-
discovered = await discoverRun({
|
|
4693
|
-
cwd: process.cwd(),
|
|
4694
|
-
runId: opts.runId
|
|
4695
|
-
});
|
|
4696
|
-
} catch (err) {
|
|
4697
|
-
process.stderr.write(
|
|
4698
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4699
|
-
`
|
|
4700
|
-
);
|
|
4701
|
-
return 1;
|
|
4702
|
-
}
|
|
4703
|
-
const opened = openStateDb(discovered.dbPath);
|
|
4704
|
-
let candidates;
|
|
4705
|
-
try {
|
|
4706
|
-
const tasks = listTasks(opened.db, discovered.runId);
|
|
4707
|
-
const run2 = getRun(opened.db, discovered.runId);
|
|
4708
|
-
if (opts.all) {
|
|
4709
|
-
candidates = tasks.map((t) => t.worktree_path).filter((p) => p !== null);
|
|
4710
|
-
} else {
|
|
4711
|
-
const safeStatuses = run2?.status === "completed";
|
|
4712
|
-
candidates = tasks.filter((t) => safeStatuses && t.status === "succeeded").map((t) => t.worktree_path).filter((p) => p !== null);
|
|
4713
|
-
}
|
|
4714
|
-
} finally {
|
|
4715
|
-
opened.close();
|
|
4716
|
-
}
|
|
4717
|
-
const uniq = [...new Set(candidates)];
|
|
4718
|
-
if (uniq.length === 0) {
|
|
4719
|
-
process.stdout.write(
|
|
4720
|
-
`pilot worktrees prune: nothing to prune for run ${discovered.runId}` + (opts.all ? "" : " (use --all to force)") + "\n"
|
|
4721
|
-
);
|
|
4722
|
-
return 0;
|
|
4723
|
-
}
|
|
4724
|
-
if (opts.dryRun) {
|
|
4725
|
-
process.stdout.write("Would remove:\n");
|
|
4726
|
-
for (const p of uniq) process.stdout.write(` ${p}
|
|
4727
|
-
`);
|
|
4728
|
-
return 0;
|
|
4729
|
-
}
|
|
4730
|
-
let removed = 0;
|
|
4731
|
-
let errors = 0;
|
|
4732
|
-
for (const p of uniq) {
|
|
4733
|
-
try {
|
|
4734
|
-
await gitWorktreeRemove({
|
|
4735
|
-
repoPath: process.cwd(),
|
|
4736
|
-
worktreePath: p
|
|
4737
|
-
});
|
|
4738
|
-
try {
|
|
4739
|
-
await fs12.rm(p, { recursive: true, force: true });
|
|
4740
|
-
} catch {
|
|
4741
|
-
}
|
|
4742
|
-
removed++;
|
|
4743
|
-
} catch (err) {
|
|
4744
|
-
errors++;
|
|
4745
|
-
process.stderr.write(
|
|
4746
|
-
`pilot worktrees prune: failed to remove ${p}: ${err instanceof Error ? err.message : String(err)}
|
|
4747
|
-
`
|
|
4748
|
-
);
|
|
4749
|
-
}
|
|
4750
|
-
}
|
|
4751
|
-
process.stdout.write(
|
|
4752
|
-
`pilot worktrees prune: removed ${removed}/${uniq.length} (${errors} errors)
|
|
4753
|
-
`
|
|
4754
|
-
);
|
|
4755
|
-
return errors > 0 ? 1 : 0;
|
|
4756
|
-
}
|
|
4757
|
-
|
|
4758
4262
|
// src/pilot/cli/cost.ts
|
|
4759
|
-
import { command as
|
|
4760
|
-
var costCmd =
|
|
4263
|
+
import { command as command7, flag as flag6, option as option6, optional as optional7, string as string7 } from "cmd-ts";
|
|
4264
|
+
var costCmd = command7({
|
|
4761
4265
|
name: "cost",
|
|
4762
4266
|
description: "Print per-task and total cost for a run.",
|
|
4763
4267
|
args: {
|
|
4764
|
-
run:
|
|
4268
|
+
run: option6({
|
|
4765
4269
|
long: "run",
|
|
4766
|
-
type:
|
|
4270
|
+
type: optional7(string7),
|
|
4767
4271
|
description: "Run ID. Defaults to the newest run."
|
|
4768
4272
|
}),
|
|
4769
|
-
json:
|
|
4273
|
+
json: flag6({
|
|
4770
4274
|
long: "json",
|
|
4771
4275
|
description: "Emit JSON instead of human-readable text."
|
|
4772
4276
|
})
|
|
@@ -4828,8 +4332,8 @@ async function runCost(opts) {
|
|
|
4828
4332
|
}
|
|
4829
4333
|
|
|
4830
4334
|
// src/pilot/cli/plan-dir.ts
|
|
4831
|
-
import { command as
|
|
4832
|
-
var planDirCmd =
|
|
4335
|
+
import { command as command8 } from "cmd-ts";
|
|
4336
|
+
var planDirCmd = command8({
|
|
4833
4337
|
name: "plan-dir",
|
|
4834
4338
|
description: "Print the pilot plans directory for the current worktree (creates it if missing).",
|
|
4835
4339
|
args: {},
|
|
@@ -4849,29 +4353,27 @@ var planDirCmd = command10({
|
|
|
4849
4353
|
});
|
|
4850
4354
|
|
|
4851
4355
|
// src/pilot/cli/index.ts
|
|
4852
|
-
var pilotSubcommand =
|
|
4356
|
+
var pilotSubcommand = subcommands({
|
|
4853
4357
|
name: "pilot",
|
|
4854
4358
|
description: "Pilot subsystem \u2014 plan, validate, build, and manage unattended task runs.",
|
|
4855
4359
|
cmds: {
|
|
4856
4360
|
validate: validateCmd,
|
|
4857
4361
|
plan: planCmd,
|
|
4858
4362
|
build: buildCmd,
|
|
4363
|
+
"build-resume": buildResumeCmd,
|
|
4859
4364
|
status: statusCmd,
|
|
4860
|
-
resume: resumeCmd,
|
|
4861
|
-
retry: retryCmd,
|
|
4862
4365
|
logs: logsCmd,
|
|
4863
|
-
worktrees: worktreesCmd,
|
|
4864
4366
|
cost: costCmd,
|
|
4865
4367
|
"plan-dir": planDirCmd
|
|
4866
4368
|
}
|
|
4867
4369
|
});
|
|
4868
4370
|
|
|
4869
4371
|
// src/cli/cli-update.ts
|
|
4870
|
-
import * as
|
|
4871
|
-
import * as
|
|
4872
|
-
import * as
|
|
4372
|
+
import * as fs10 from "fs";
|
|
4373
|
+
import * as path10 from "path";
|
|
4374
|
+
import * as os3 from "os";
|
|
4873
4375
|
import { spawn as spawn3 } from "child_process";
|
|
4874
|
-
import { fileURLToPath as
|
|
4376
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4875
4377
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
4876
4378
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4877
4379
|
var c = {
|
|
@@ -4896,12 +4398,12 @@ function isMajorBump(current, latest) {
|
|
|
4896
4398
|
return latest.major > current.major;
|
|
4897
4399
|
}
|
|
4898
4400
|
function getStateFilePath() {
|
|
4899
|
-
const cacheHome = process.env["XDG_CACHE_HOME"] ??
|
|
4900
|
-
return
|
|
4401
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path10.join(os3.homedir(), ".cache");
|
|
4402
|
+
return path10.join(cacheHome, "harness-opencode", "cli-update.json");
|
|
4901
4403
|
}
|
|
4902
4404
|
function readState() {
|
|
4903
4405
|
try {
|
|
4904
|
-
const raw =
|
|
4406
|
+
const raw = fs10.readFileSync(getStateFilePath(), "utf8");
|
|
4905
4407
|
return JSON.parse(raw);
|
|
4906
4408
|
} catch {
|
|
4907
4409
|
return null;
|
|
@@ -4910,21 +4412,21 @@ function readState() {
|
|
|
4910
4412
|
function writeState(state) {
|
|
4911
4413
|
try {
|
|
4912
4414
|
const statePath = getStateFilePath();
|
|
4913
|
-
|
|
4914
|
-
|
|
4415
|
+
fs10.mkdirSync(path10.dirname(statePath), { recursive: true });
|
|
4416
|
+
fs10.writeFileSync(statePath, JSON.stringify(state));
|
|
4915
4417
|
} catch {
|
|
4916
4418
|
}
|
|
4917
4419
|
}
|
|
4918
4420
|
function readInstalledVersion() {
|
|
4919
|
-
const here =
|
|
4421
|
+
const here = path10.dirname(fileURLToPath3(import.meta.url));
|
|
4920
4422
|
const candidates = [
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4423
|
+
path10.join(here, "..", "package.json"),
|
|
4424
|
+
path10.join(here, "..", "..", "package.json"),
|
|
4425
|
+
path10.join(here, "package.json")
|
|
4924
4426
|
];
|
|
4925
4427
|
for (const candidate of candidates) {
|
|
4926
4428
|
try {
|
|
4927
|
-
const raw =
|
|
4429
|
+
const raw = fs10.readFileSync(candidate, "utf8");
|
|
4928
4430
|
const parsed = JSON.parse(raw);
|
|
4929
4431
|
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
4930
4432
|
return parsed.version;
|
|
@@ -5050,15 +4552,15 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
|
|
|
5050
4552
|
}
|
|
5051
4553
|
}
|
|
5052
4554
|
var VERSION = "0.1.0";
|
|
5053
|
-
var installCmd =
|
|
4555
|
+
var installCmd = command9({
|
|
5054
4556
|
name: "install",
|
|
5055
4557
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
5056
4558
|
args: {
|
|
5057
|
-
dryRun:
|
|
4559
|
+
dryRun: flag7({
|
|
5058
4560
|
long: "dry-run",
|
|
5059
4561
|
description: "Preview changes without writing."
|
|
5060
4562
|
}),
|
|
5061
|
-
pin:
|
|
4563
|
+
pin: flag7({
|
|
5062
4564
|
long: "pin",
|
|
5063
4565
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
5064
4566
|
})
|
|
@@ -5067,11 +4569,11 @@ var installCmd = command11({
|
|
|
5067
4569
|
await install({ dryRun, pin });
|
|
5068
4570
|
}
|
|
5069
4571
|
});
|
|
5070
|
-
var uninstallCmd =
|
|
4572
|
+
var uninstallCmd = command9({
|
|
5071
4573
|
name: "uninstall",
|
|
5072
4574
|
description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
|
|
5073
4575
|
args: {
|
|
5074
|
-
dryRun:
|
|
4576
|
+
dryRun: flag7({
|
|
5075
4577
|
long: "dry-run",
|
|
5076
4578
|
description: "Preview changes without writing."
|
|
5077
4579
|
})
|
|
@@ -5080,7 +4582,7 @@ var uninstallCmd = command11({
|
|
|
5080
4582
|
uninstall({ dryRun });
|
|
5081
4583
|
}
|
|
5082
4584
|
});
|
|
5083
|
-
var doctorCmd =
|
|
4585
|
+
var doctorCmd = command9({
|
|
5084
4586
|
name: "doctor",
|
|
5085
4587
|
description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
|
|
5086
4588
|
args: {},
|
|
@@ -5088,22 +4590,22 @@ var doctorCmd = command11({
|
|
|
5088
4590
|
doctor();
|
|
5089
4591
|
}
|
|
5090
4592
|
});
|
|
5091
|
-
var planCheckCmd =
|
|
4593
|
+
var planCheckCmd = command9({
|
|
5092
4594
|
name: "plan-check",
|
|
5093
4595
|
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
5094
4596
|
args: {
|
|
5095
|
-
run:
|
|
4597
|
+
run: option7({
|
|
5096
4598
|
long: "run",
|
|
5097
|
-
type:
|
|
4599
|
+
type: optional8(string8),
|
|
5098
4600
|
description: "Print verify commands for pending items, one per line."
|
|
5099
4601
|
}),
|
|
5100
|
-
check:
|
|
4602
|
+
check: option7({
|
|
5101
4603
|
long: "check",
|
|
5102
|
-
type:
|
|
4604
|
+
type: optional8(string8),
|
|
5103
4605
|
description: "Structural validation; exits 1 if any item is invalid."
|
|
5104
4606
|
}),
|
|
5105
4607
|
rest: restPositionals({
|
|
5106
|
-
type:
|
|
4608
|
+
type: string8,
|
|
5107
4609
|
displayName: "plan-path",
|
|
5108
4610
|
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
5109
4611
|
})
|
|
@@ -5120,7 +4622,7 @@ var planCheckCmd = command11({
|
|
|
5120
4622
|
planCheck(legacy);
|
|
5121
4623
|
}
|
|
5122
4624
|
});
|
|
5123
|
-
var planDirCmd2 =
|
|
4625
|
+
var planDirCmd2 = command9({
|
|
5124
4626
|
name: "plan-dir",
|
|
5125
4627
|
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
5126
4628
|
args: {},
|
|
@@ -5139,15 +4641,15 @@ var planDirCmd2 = command11({
|
|
|
5139
4641
|
}
|
|
5140
4642
|
}
|
|
5141
4643
|
});
|
|
5142
|
-
var installPluginCmd =
|
|
4644
|
+
var installPluginCmd = command9({
|
|
5143
4645
|
name: "install-plugin",
|
|
5144
4646
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
5145
4647
|
args: {
|
|
5146
|
-
dryRun:
|
|
4648
|
+
dryRun: flag7({
|
|
5147
4649
|
long: "dry-run",
|
|
5148
4650
|
description: "Preview changes without writing."
|
|
5149
4651
|
}),
|
|
5150
|
-
pin:
|
|
4652
|
+
pin: flag7({
|
|
5151
4653
|
long: "pin",
|
|
5152
4654
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
5153
4655
|
})
|
|
@@ -5156,7 +4658,7 @@ var installPluginCmd = command11({
|
|
|
5156
4658
|
await install({ dryRun, pin });
|
|
5157
4659
|
}
|
|
5158
4660
|
});
|
|
5159
|
-
var cli =
|
|
4661
|
+
var cli = subcommands2({
|
|
5160
4662
|
name: "glrs-oc",
|
|
5161
4663
|
description: "OpenCode agent harness CLI.",
|
|
5162
4664
|
version: VERSION,
|