@glrs-dev/cli 0.1.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 +18 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +29 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +26 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +37 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +33 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +32 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +15 -20
- 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-XCZ3NOXR.js → chunk-CZMAJISX.js} +28 -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/{chunk-VVMP6QWS.js → chunk-WBBN7OVN.js} +162 -2
- package/dist/vendor/harness-opencode/dist/cli.js +964 -1383
- package/dist/vendor/harness-opencode/dist/index.js +2 -2
- package/dist/vendor/harness-opencode/dist/install-X5KEANRB.js +13 -0
- 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 +41 -10
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +27 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/qa-expectations.md +120 -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 +81 -13
- 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/install-4EYR56OR.js +0 -9
|
@@ -2,24 +2,65 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createAgents,
|
|
4
4
|
validateModelOverride
|
|
5
|
-
} from "./chunk-
|
|
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
|
-
} from "./chunk-
|
|
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(),
|
|
@@ -515,7 +442,33 @@ var PlanSchema = z.object({
|
|
|
515
442
|
defaults: DefaultsSchema,
|
|
516
443
|
milestones: z.array(MilestoneSchema).default([]),
|
|
517
444
|
tasks: z.array(TaskSchema).min(1, "plan must declare at least one task")
|
|
518
|
-
}).
|
|
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
|
+
});
|
|
519
472
|
function parsePlan(input) {
|
|
520
473
|
const result = PlanSchema.safeParse(input);
|
|
521
474
|
if (result.success) {
|
|
@@ -546,10 +499,10 @@ async function loadPlan(absPath) {
|
|
|
546
499
|
if (typeof absPath !== "string") {
|
|
547
500
|
throw new TypeError(`loadPlan: expected string path, got ${typeof absPath}`);
|
|
548
501
|
}
|
|
549
|
-
const resolved =
|
|
502
|
+
const resolved = path3.resolve(absPath);
|
|
550
503
|
let raw;
|
|
551
504
|
try {
|
|
552
|
-
raw = await
|
|
505
|
+
raw = await fs3.readFile(resolved, "utf8");
|
|
553
506
|
} catch (err) {
|
|
554
507
|
return {
|
|
555
508
|
ok: false,
|
|
@@ -803,98 +756,6 @@ function findTouchConflicts(tasks) {
|
|
|
803
756
|
return conflicts;
|
|
804
757
|
}
|
|
805
758
|
|
|
806
|
-
// src/pilot/paths.ts
|
|
807
|
-
import { promises as fs5 } from "fs";
|
|
808
|
-
import * as os4 from "os";
|
|
809
|
-
import * as path5 from "path";
|
|
810
|
-
function expandTilde2(p) {
|
|
811
|
-
if (p === "~") return os4.homedir();
|
|
812
|
-
if (p.startsWith("~/")) return path5.join(os4.homedir(), p.slice(2));
|
|
813
|
-
return p;
|
|
814
|
-
}
|
|
815
|
-
function resolveBaseDir() {
|
|
816
|
-
const pilotEnv = process.env.GLORIOUS_PILOT_DIR;
|
|
817
|
-
if (pilotEnv) return expandTilde2(pilotEnv);
|
|
818
|
-
const planEnv = process.env.GLORIOUS_PLAN_DIR;
|
|
819
|
-
if (planEnv) {
|
|
820
|
-
return path5.dirname(expandTilde2(planEnv));
|
|
821
|
-
}
|
|
822
|
-
return path5.join(os4.homedir(), ".glorious", "opencode");
|
|
823
|
-
}
|
|
824
|
-
function padWorker(n) {
|
|
825
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
826
|
-
throw new RangeError(`worker index must be a non-negative integer, got ${n}`);
|
|
827
|
-
}
|
|
828
|
-
return n.toString().padStart(2, "0");
|
|
829
|
-
}
|
|
830
|
-
async function getPilotDir(cwd) {
|
|
831
|
-
const base = resolveBaseDir();
|
|
832
|
-
const repoFolder = await getRepoFolder(cwd);
|
|
833
|
-
const dir = path5.join(base, repoFolder, "pilot");
|
|
834
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
835
|
-
return dir;
|
|
836
|
-
}
|
|
837
|
-
async function getPlansDir(cwd) {
|
|
838
|
-
const pilot = await getPilotDir(cwd);
|
|
839
|
-
const dir = path5.join(pilot, "plans");
|
|
840
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
841
|
-
return dir;
|
|
842
|
-
}
|
|
843
|
-
async function getRunDir(cwd, runId) {
|
|
844
|
-
if (!isSafeRunId(runId)) {
|
|
845
|
-
throw new Error(
|
|
846
|
-
`getRunDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
const pilot = await getPilotDir(cwd);
|
|
850
|
-
const dir = path5.join(pilot, "runs", runId);
|
|
851
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
852
|
-
return dir;
|
|
853
|
-
}
|
|
854
|
-
async function getWorktreeDir(cwd, runId, n) {
|
|
855
|
-
if (!isSafeRunId(runId)) {
|
|
856
|
-
throw new Error(
|
|
857
|
-
`getWorktreeDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
const pilot = await getPilotDir(cwd);
|
|
861
|
-
const parent = path5.join(pilot, "worktrees", runId);
|
|
862
|
-
await fs5.mkdir(parent, { recursive: true });
|
|
863
|
-
return path5.join(parent, padWorker(n));
|
|
864
|
-
}
|
|
865
|
-
async function getStateDbPath(cwd, runId) {
|
|
866
|
-
const runDir = await getRunDir(cwd, runId);
|
|
867
|
-
return path5.join(runDir, "state.db");
|
|
868
|
-
}
|
|
869
|
-
async function getWorkerJsonlPath(cwd, runId, n) {
|
|
870
|
-
const runDir = await getRunDir(cwd, runId);
|
|
871
|
-
const workersDir = path5.join(runDir, "workers");
|
|
872
|
-
await fs5.mkdir(workersDir, { recursive: true });
|
|
873
|
-
return path5.join(workersDir, `${padWorker(n)}.jsonl`);
|
|
874
|
-
}
|
|
875
|
-
async function getTaskJsonlPath(cwd, runId, taskId) {
|
|
876
|
-
if (!isSafeRunId(runId)) {
|
|
877
|
-
throw new Error(
|
|
878
|
-
`getTaskJsonlPath: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
if (!isSafeTaskId(taskId)) {
|
|
882
|
-
throw new Error(
|
|
883
|
-
`getTaskJsonlPath: taskId ${JSON.stringify(taskId)} is not a safe filesystem segment`
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
const runDir = await getRunDir(cwd, runId);
|
|
887
|
-
const taskDir = path5.join(runDir, "tasks", taskId);
|
|
888
|
-
await fs5.mkdir(taskDir, { recursive: true });
|
|
889
|
-
return path5.join(taskDir, "session.jsonl");
|
|
890
|
-
}
|
|
891
|
-
function isSafeRunId(runId) {
|
|
892
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(runId);
|
|
893
|
-
}
|
|
894
|
-
function isSafeTaskId(taskId) {
|
|
895
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
759
|
// src/pilot/cli/validate.ts
|
|
899
760
|
var validateCmd = command({
|
|
900
761
|
name: "validate",
|
|
@@ -981,17 +842,17 @@ async function runValidate(opts) {
|
|
|
981
842
|
}
|
|
982
843
|
async function resolvePlanPath(input) {
|
|
983
844
|
if (input !== void 0 && input.length > 0) {
|
|
984
|
-
const resolved =
|
|
985
|
-
let
|
|
845
|
+
const resolved = path4.resolve(input);
|
|
846
|
+
let stat;
|
|
986
847
|
try {
|
|
987
|
-
|
|
848
|
+
stat = await fs4.stat(resolved);
|
|
988
849
|
} catch (err) {
|
|
989
850
|
throw new Error(
|
|
990
851
|
`cannot stat ${JSON.stringify(resolved)}: ${err instanceof Error ? err.message : String(err)}`
|
|
991
852
|
);
|
|
992
853
|
}
|
|
993
|
-
if (
|
|
994
|
-
if (
|
|
854
|
+
if (stat.isFile()) return resolved;
|
|
855
|
+
if (stat.isDirectory()) return findNewestYaml(resolved);
|
|
995
856
|
throw new Error(
|
|
996
857
|
`${JSON.stringify(resolved)} is neither a file nor a directory`
|
|
997
858
|
);
|
|
@@ -1002,7 +863,7 @@ async function resolvePlanPath(input) {
|
|
|
1002
863
|
async function findNewestYaml(dir) {
|
|
1003
864
|
let entries;
|
|
1004
865
|
try {
|
|
1005
|
-
entries = await
|
|
866
|
+
entries = await fs4.readdir(dir);
|
|
1006
867
|
} catch (err) {
|
|
1007
868
|
throw new Error(
|
|
1008
869
|
`cannot read directory ${JSON.stringify(dir)}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1016,14 +877,14 @@ async function findNewestYaml(dir) {
|
|
|
1016
877
|
}
|
|
1017
878
|
let newest = null;
|
|
1018
879
|
for (const name of yamls) {
|
|
1019
|
-
const full =
|
|
1020
|
-
let
|
|
880
|
+
const full = path4.join(dir, name);
|
|
881
|
+
let stat;
|
|
1021
882
|
try {
|
|
1022
|
-
|
|
883
|
+
stat = await fs4.stat(full);
|
|
1023
884
|
} catch {
|
|
1024
885
|
continue;
|
|
1025
886
|
}
|
|
1026
|
-
const mtime =
|
|
887
|
+
const mtime = stat.mtimeMs;
|
|
1027
888
|
if (newest === null || mtime > newest.mtime) {
|
|
1028
889
|
newest = { name, mtime };
|
|
1029
890
|
}
|
|
@@ -1031,14 +892,14 @@ async function findNewestYaml(dir) {
|
|
|
1031
892
|
if (newest === null) {
|
|
1032
893
|
throw new Error(`no readable *.yaml files in ${JSON.stringify(dir)}`);
|
|
1033
894
|
}
|
|
1034
|
-
return
|
|
895
|
+
return path4.join(dir, newest.name);
|
|
1035
896
|
}
|
|
1036
897
|
|
|
1037
898
|
// src/pilot/cli/plan.ts
|
|
1038
899
|
import { command as command2, optional as optional2, positional as positional2, string as string2, option } from "cmd-ts";
|
|
1039
900
|
import { spawn } from "child_process";
|
|
1040
|
-
import { promises as
|
|
1041
|
-
import * as
|
|
901
|
+
import { promises as fs5 } from "fs";
|
|
902
|
+
import * as path5 from "path";
|
|
1042
903
|
var PLANNER_AGENT = "pilot-planner";
|
|
1043
904
|
var planCmd = command2({
|
|
1044
905
|
name: "plan",
|
|
@@ -1132,15 +993,15 @@ async function snapshotYamls(dir) {
|
|
|
1132
993
|
const out = /* @__PURE__ */ new Map();
|
|
1133
994
|
let entries;
|
|
1134
995
|
try {
|
|
1135
|
-
entries = await
|
|
996
|
+
entries = await fs5.readdir(dir);
|
|
1136
997
|
} catch {
|
|
1137
998
|
return out;
|
|
1138
999
|
}
|
|
1139
1000
|
for (const name of entries) {
|
|
1140
1001
|
if (!name.endsWith(".yaml") && !name.endsWith(".yml")) continue;
|
|
1141
|
-
const full =
|
|
1002
|
+
const full = path5.join(dir, name);
|
|
1142
1003
|
try {
|
|
1143
|
-
const st = await
|
|
1004
|
+
const st = await fs5.stat(full);
|
|
1144
1005
|
out.set(full, st.mtimeMs);
|
|
1145
1006
|
} catch {
|
|
1146
1007
|
}
|
|
@@ -1164,18 +1025,18 @@ function pickNewestNew(before, after) {
|
|
|
1164
1025
|
return { path: pool[0].path, mtimeMs: pool[0].mtimeMs };
|
|
1165
1026
|
}
|
|
1166
1027
|
function spawnTui(args) {
|
|
1167
|
-
return new Promise((
|
|
1028
|
+
return new Promise((resolve6) => {
|
|
1168
1029
|
const child = spawn(args.bin, args.args, {
|
|
1169
1030
|
cwd: args.cwd,
|
|
1170
1031
|
stdio: "inherit"
|
|
1171
1032
|
});
|
|
1172
|
-
child.on("exit", (code) =>
|
|
1033
|
+
child.on("exit", (code) => resolve6(code ?? 1));
|
|
1173
1034
|
child.on("error", (err) => {
|
|
1174
1035
|
process.stderr.write(
|
|
1175
1036
|
`pilot plan: failed to spawn ${args.bin}: ${err.message}
|
|
1176
1037
|
`
|
|
1177
1038
|
);
|
|
1178
|
-
|
|
1039
|
+
resolve6(1);
|
|
1179
1040
|
});
|
|
1180
1041
|
});
|
|
1181
1042
|
}
|
|
@@ -1190,7 +1051,7 @@ import {
|
|
|
1190
1051
|
string as string3,
|
|
1191
1052
|
number as cmdNumber
|
|
1192
1053
|
} from "cmd-ts";
|
|
1193
|
-
import * as
|
|
1054
|
+
import * as path7 from "path";
|
|
1194
1055
|
|
|
1195
1056
|
// src/pilot/plan/slug.ts
|
|
1196
1057
|
var MAX_SLUG_LENGTH = 50;
|
|
@@ -1377,18 +1238,18 @@ function splitStatements(sql) {
|
|
|
1377
1238
|
}
|
|
1378
1239
|
|
|
1379
1240
|
// src/pilot/state/db.ts
|
|
1380
|
-
function openStateDb(
|
|
1381
|
-
const db = new Database(
|
|
1241
|
+
function openStateDb(path11) {
|
|
1242
|
+
const db = new Database(path11, { create: true });
|
|
1382
1243
|
try {
|
|
1383
1244
|
db.run("PRAGMA foreign_keys = ON");
|
|
1384
|
-
if (
|
|
1245
|
+
if (path11 !== ":memory:") {
|
|
1385
1246
|
db.run("PRAGMA journal_mode = WAL");
|
|
1386
1247
|
db.run("PRAGMA synchronous = NORMAL");
|
|
1387
1248
|
}
|
|
1388
1249
|
} catch (err) {
|
|
1389
1250
|
db.close();
|
|
1390
1251
|
throw new Error(
|
|
1391
|
-
`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)}`
|
|
1392
1253
|
);
|
|
1393
1254
|
}
|
|
1394
1255
|
let newlyApplied;
|
|
@@ -1405,180 +1266,6 @@ function openStateDb(path13) {
|
|
|
1405
1266
|
};
|
|
1406
1267
|
}
|
|
1407
1268
|
|
|
1408
|
-
// src/pilot/state/runs.ts
|
|
1409
|
-
import { ulid } from "ulid";
|
|
1410
|
-
function createRun(db, args) {
|
|
1411
|
-
const id = ulid();
|
|
1412
|
-
const now = args.now ?? Date.now();
|
|
1413
|
-
db.run(
|
|
1414
|
-
`INSERT INTO runs (id, plan_path, plan_slug, started_at, status)
|
|
1415
|
-
VALUES (?, ?, ?, ?, 'pending')`,
|
|
1416
|
-
[id, args.planPath, args.slug, now]
|
|
1417
|
-
);
|
|
1418
|
-
void args.plan;
|
|
1419
|
-
return id;
|
|
1420
|
-
}
|
|
1421
|
-
function markRunRunning(db, runId) {
|
|
1422
|
-
const cur = getRun(db, runId);
|
|
1423
|
-
if (!cur) throw new Error(`markRunRunning: run ${JSON.stringify(runId)} not found`);
|
|
1424
|
-
if (cur.status === "running") return;
|
|
1425
|
-
if (cur.status !== "pending") {
|
|
1426
|
-
throw new Error(
|
|
1427
|
-
`markRunRunning: cannot move run ${JSON.stringify(runId)} from ${cur.status} to running`
|
|
1428
|
-
);
|
|
1429
|
-
}
|
|
1430
|
-
db.run("UPDATE runs SET status='running' WHERE id=?", [runId]);
|
|
1431
|
-
}
|
|
1432
|
-
function markRunFinished(db, runId, status, now = Date.now()) {
|
|
1433
|
-
if (status !== "completed" && status !== "aborted" && status !== "failed") {
|
|
1434
|
-
throw new Error(
|
|
1435
|
-
`markRunFinished: ${JSON.stringify(status)} is not a terminal status`
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
const cur = getRun(db, runId);
|
|
1439
|
-
if (!cur) {
|
|
1440
|
-
throw new Error(`markRunFinished: run ${JSON.stringify(runId)} not found`);
|
|
1441
|
-
}
|
|
1442
|
-
db.run("UPDATE runs SET status=?, finished_at=? WHERE id=?", [status, now, runId]);
|
|
1443
|
-
}
|
|
1444
|
-
function getRun(db, runId) {
|
|
1445
|
-
const row = db.query("SELECT * FROM runs WHERE id=?").get(runId);
|
|
1446
|
-
return row;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// src/pilot/state/tasks.ts
|
|
1450
|
-
function upsertFromPlan(db, runId, plan) {
|
|
1451
|
-
const stmt = db.prepare(
|
|
1452
|
-
`INSERT OR IGNORE INTO tasks (run_id, task_id, status) VALUES (?, ?, 'pending')`
|
|
1453
|
-
);
|
|
1454
|
-
const tx = db.transaction(() => {
|
|
1455
|
-
for (const t of plan.tasks) {
|
|
1456
|
-
stmt.run(runId, t.id);
|
|
1457
|
-
}
|
|
1458
|
-
});
|
|
1459
|
-
tx();
|
|
1460
|
-
}
|
|
1461
|
-
function markReady(db, runId, taskId) {
|
|
1462
|
-
requireStatus(db, runId, taskId, ["pending"], "ready");
|
|
1463
|
-
db.run(
|
|
1464
|
-
"UPDATE tasks SET status='ready' WHERE run_id=? AND task_id=?",
|
|
1465
|
-
[runId, taskId]
|
|
1466
|
-
);
|
|
1467
|
-
}
|
|
1468
|
-
function markRunning(db, args) {
|
|
1469
|
-
requireStatus(db, args.runId, args.taskId, ["ready"], "running");
|
|
1470
|
-
const now = args.now ?? Date.now();
|
|
1471
|
-
db.run(
|
|
1472
|
-
`UPDATE tasks
|
|
1473
|
-
SET status='running',
|
|
1474
|
-
attempts = attempts + 1,
|
|
1475
|
-
session_id = ?,
|
|
1476
|
-
branch = ?,
|
|
1477
|
-
worktree_path = ?,
|
|
1478
|
-
started_at = COALESCE(started_at, ?)
|
|
1479
|
-
WHERE run_id=? AND task_id=?`,
|
|
1480
|
-
[args.sessionId, args.branch, args.worktreePath, now, args.runId, args.taskId]
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
function markSucceeded(db, runId, taskId, now = Date.now()) {
|
|
1484
|
-
requireStatus(db, runId, taskId, ["running"], "succeeded");
|
|
1485
|
-
db.run(
|
|
1486
|
-
`UPDATE tasks
|
|
1487
|
-
SET status='succeeded', finished_at=?, last_error=NULL
|
|
1488
|
-
WHERE run_id=? AND task_id=?`,
|
|
1489
|
-
[now, runId, taskId]
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
function markFailed(db, runId, taskId, reason, now = Date.now()) {
|
|
1493
|
-
requireStatus(db, runId, taskId, ["running", "ready"], "failed");
|
|
1494
|
-
db.run(
|
|
1495
|
-
`UPDATE tasks
|
|
1496
|
-
SET status='failed', finished_at=?, last_error=?
|
|
1497
|
-
WHERE run_id=? AND task_id=?`,
|
|
1498
|
-
[now, reason, runId, taskId]
|
|
1499
|
-
);
|
|
1500
|
-
}
|
|
1501
|
-
function markBlocked(db, runId, taskId, reason) {
|
|
1502
|
-
requireStatus(db, runId, taskId, ["pending", "ready"], "blocked");
|
|
1503
|
-
db.run(
|
|
1504
|
-
`UPDATE tasks
|
|
1505
|
-
SET status='blocked', last_error=?
|
|
1506
|
-
WHERE run_id=? AND task_id=?`,
|
|
1507
|
-
[reason, runId, taskId]
|
|
1508
|
-
);
|
|
1509
|
-
}
|
|
1510
|
-
function markAborted(db, runId, taskId, reason, now = Date.now()) {
|
|
1511
|
-
requireStatus(db, runId, taskId, ["running", "ready"], "aborted");
|
|
1512
|
-
db.run(
|
|
1513
|
-
`UPDATE tasks
|
|
1514
|
-
SET status='aborted', finished_at=?, last_error=?
|
|
1515
|
-
WHERE run_id=? AND task_id=?`,
|
|
1516
|
-
[now, reason, runId, taskId]
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
function markPending(db, runId, taskId) {
|
|
1520
|
-
const cur = getTask(db, runId, taskId);
|
|
1521
|
-
if (!cur) {
|
|
1522
|
-
throw new Error(
|
|
1523
|
-
`markPending: task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
db.run(
|
|
1527
|
-
`UPDATE tasks
|
|
1528
|
-
SET status='pending',
|
|
1529
|
-
session_id=NULL,
|
|
1530
|
-
branch=NULL,
|
|
1531
|
-
worktree_path=NULL,
|
|
1532
|
-
started_at=NULL,
|
|
1533
|
-
finished_at=NULL,
|
|
1534
|
-
last_error=NULL
|
|
1535
|
-
WHERE run_id=? AND task_id=?`,
|
|
1536
|
-
[runId, taskId]
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
function setCostUsd(db, runId, taskId, costUsd) {
|
|
1540
|
-
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1541
|
-
throw new RangeError(`setCostUsd: invalid cost ${costUsd}`);
|
|
1542
|
-
}
|
|
1543
|
-
db.run(
|
|
1544
|
-
"UPDATE tasks SET cost_usd=? WHERE run_id=? AND task_id=?",
|
|
1545
|
-
[costUsd, runId, taskId]
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
function getTask(db, runId, taskId) {
|
|
1549
|
-
return db.query("SELECT * FROM tasks WHERE run_id=? AND task_id=?").get(runId, taskId);
|
|
1550
|
-
}
|
|
1551
|
-
function listTasks(db, runId) {
|
|
1552
|
-
return db.query("SELECT * FROM tasks WHERE run_id=? ORDER BY task_id").all(runId);
|
|
1553
|
-
}
|
|
1554
|
-
function countByStatus(db, runId) {
|
|
1555
|
-
const rows = db.query("SELECT status, COUNT(*) as n FROM tasks WHERE run_id=? GROUP BY status").all(runId);
|
|
1556
|
-
const out = {
|
|
1557
|
-
pending: 0,
|
|
1558
|
-
ready: 0,
|
|
1559
|
-
running: 0,
|
|
1560
|
-
succeeded: 0,
|
|
1561
|
-
failed: 0,
|
|
1562
|
-
blocked: 0,
|
|
1563
|
-
aborted: 0
|
|
1564
|
-
};
|
|
1565
|
-
for (const r of rows) out[r.status] = r.n;
|
|
1566
|
-
return out;
|
|
1567
|
-
}
|
|
1568
|
-
function requireStatus(db, runId, taskId, expected, intended) {
|
|
1569
|
-
const row = getTask(db, runId, taskId);
|
|
1570
|
-
if (!row) {
|
|
1571
|
-
throw new Error(
|
|
1572
|
-
`task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
1573
|
-
);
|
|
1574
|
-
}
|
|
1575
|
-
if (!expected.includes(row.status)) {
|
|
1576
|
-
throw new Error(
|
|
1577
|
-
`cannot move task ${JSON.stringify(taskId)} from ${row.status} to ${intended} (expected one of: ${expected.join(", ")})`
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
1269
|
// src/pilot/state/events.ts
|
|
1583
1270
|
function appendEvent(db, args) {
|
|
1584
1271
|
const ts = args.now ?? Date.now();
|
|
@@ -1643,9 +1330,10 @@ function tryParseJson(s) {
|
|
|
1643
1330
|
}
|
|
1644
1331
|
|
|
1645
1332
|
// src/pilot/opencode/server.ts
|
|
1646
|
-
import { execFile
|
|
1647
|
-
import * as
|
|
1648
|
-
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";
|
|
1649
1337
|
import {
|
|
1650
1338
|
createOpencodeServer,
|
|
1651
1339
|
createOpencodeClient
|
|
@@ -1657,7 +1345,7 @@ async function startOpencodeServer(options = {}) {
|
|
|
1657
1345
|
const port = options.port ?? DEFAULT_PORT;
|
|
1658
1346
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
1659
1347
|
await ensureOpencodeOnPath();
|
|
1660
|
-
const serverConfig = buildPilotServerConfig();
|
|
1348
|
+
const serverConfig = buildPilotServerConfig(options.runContext);
|
|
1661
1349
|
void options.cwd;
|
|
1662
1350
|
let server;
|
|
1663
1351
|
try {
|
|
@@ -1674,8 +1362,8 @@ async function startOpencodeServer(options = {}) {
|
|
|
1674
1362
|
}
|
|
1675
1363
|
if (options.serverLogPath) {
|
|
1676
1364
|
try {
|
|
1677
|
-
|
|
1678
|
-
|
|
1365
|
+
fs6.mkdirSync(path6.dirname(options.serverLogPath), { recursive: true });
|
|
1366
|
+
fs6.writeFileSync(
|
|
1679
1367
|
options.serverLogPath,
|
|
1680
1368
|
`# pilot opencode server spawn ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1681
1369
|
# url=${server.url} hostname=${hostname} port=${port} timeoutMs=${timeoutMs}
|
|
@@ -1701,15 +1389,38 @@ async function startOpencodeServer(options = {}) {
|
|
|
1701
1389
|
};
|
|
1702
1390
|
return { url: server.url, client, shutdown };
|
|
1703
1391
|
}
|
|
1704
|
-
function buildPilotServerConfig() {
|
|
1392
|
+
function buildPilotServerConfig(runContext) {
|
|
1705
1393
|
const agents = createAgents();
|
|
1706
1394
|
const pilotAgents = {};
|
|
1707
1395
|
for (const name of ["pilot-builder", "pilot-planner"]) {
|
|
1708
1396
|
if (name in agents) pilotAgents[name] = agents[name];
|
|
1709
1397
|
}
|
|
1710
|
-
|
|
1398
|
+
const config = {
|
|
1711
1399
|
agent: pilotAgents
|
|
1712
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;
|
|
1713
1424
|
}
|
|
1714
1425
|
function resolveTimeoutMs(explicit) {
|
|
1715
1426
|
if (typeof explicit === "number" && explicit > 0) return explicit;
|
|
@@ -1725,10 +1436,10 @@ function resolveTimeoutMs(explicit) {
|
|
|
1725
1436
|
return DEFAULT_STARTUP_TIMEOUT_MS;
|
|
1726
1437
|
}
|
|
1727
1438
|
async function ensureOpencodeOnPath() {
|
|
1728
|
-
await new Promise((
|
|
1439
|
+
await new Promise((resolve6, reject) => {
|
|
1729
1440
|
const controller = new AbortController();
|
|
1730
1441
|
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1731
|
-
|
|
1442
|
+
execFile(
|
|
1732
1443
|
"opencode",
|
|
1733
1444
|
["--version"],
|
|
1734
1445
|
{ signal: controller.signal, encoding: "utf8" },
|
|
@@ -1742,7 +1453,7 @@ async function ensureOpencodeOnPath() {
|
|
|
1742
1453
|
);
|
|
1743
1454
|
return;
|
|
1744
1455
|
}
|
|
1745
|
-
|
|
1456
|
+
resolve6();
|
|
1746
1457
|
}
|
|
1747
1458
|
);
|
|
1748
1459
|
});
|
|
@@ -1822,7 +1533,7 @@ var EventBus = class {
|
|
|
1822
1533
|
waitForIdle(sessionId, options = {}) {
|
|
1823
1534
|
const stallMs = options.stallMs ?? 60 * 60 * 1e3;
|
|
1824
1535
|
const errorIsFatal = options.errorIsFatal ?? true;
|
|
1825
|
-
return new Promise((
|
|
1536
|
+
return new Promise((resolve6) => {
|
|
1826
1537
|
let settled = false;
|
|
1827
1538
|
let stallTimer = null;
|
|
1828
1539
|
let unsubscribe = () => {
|
|
@@ -1835,7 +1546,7 @@ var EventBus = class {
|
|
|
1835
1546
|
if (stallTimer) clearTimeout(stallTimer);
|
|
1836
1547
|
unsubscribe();
|
|
1837
1548
|
removeAbortListener();
|
|
1838
|
-
|
|
1549
|
+
resolve6(result);
|
|
1839
1550
|
};
|
|
1840
1551
|
const armStallTimer = () => {
|
|
1841
1552
|
if (stallTimer) clearTimeout(stallTimer);
|
|
@@ -1961,415 +1672,17 @@ function isEventLike(v) {
|
|
|
1961
1672
|
return typeof o.type === "string" && typeof o.properties === "object" && o.properties !== null;
|
|
1962
1673
|
}
|
|
1963
1674
|
|
|
1964
|
-
// src/pilot/
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
signal: controller.signal,
|
|
1976
|
-
cwd,
|
|
1977
|
-
encoding: "utf8",
|
|
1978
|
-
env,
|
|
1979
|
-
// Increase maxBuffer — git diff/log output can exceed the
|
|
1980
|
-
// 1MB default on large repos.
|
|
1981
|
-
maxBuffer: 16 * 1024 * 1024
|
|
1982
|
-
},
|
|
1983
|
-
(err, stdout, stderr) => {
|
|
1984
|
-
clearTimeout(timer);
|
|
1985
|
-
if (err) {
|
|
1986
|
-
const msg = `${err.message}${stderr ? `
|
|
1987
|
-
stderr:
|
|
1988
|
-
${stderr}` : ""}`;
|
|
1989
|
-
reject(new Error(msg));
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
resolve5({ stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
1993
|
-
}
|
|
1994
|
-
);
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
function assertSafeArg(s, label) {
|
|
1998
|
-
if (typeof s !== "string" || s.length === 0) {
|
|
1999
|
-
throw new TypeError(`${label}: expected non-empty string, got ${JSON.stringify(s)}`);
|
|
2000
|
-
}
|
|
2001
|
-
if (s.includes("\0")) {
|
|
2002
|
-
throw new TypeError(`${label}: contains null byte: ${JSON.stringify(s)}`);
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
async function headSha(repoOrWorktree) {
|
|
2006
|
-
assertSafeArg(repoOrWorktree, "headSha repo");
|
|
2007
|
-
const { stdout } = await execFileP2("git", [
|
|
2008
|
-
"-C",
|
|
2009
|
-
repoOrWorktree,
|
|
2010
|
-
"rev-parse",
|
|
2011
|
-
"HEAD"
|
|
2012
|
-
]);
|
|
2013
|
-
return stdout.trim();
|
|
2014
|
-
}
|
|
2015
|
-
async function gitWorktreeAdd(args) {
|
|
2016
|
-
assertSafeArg(args.repoPath, "repoPath");
|
|
2017
|
-
assertSafeArg(args.worktreePath, "worktreePath");
|
|
2018
|
-
assertSafeArg(args.commitIsh, "commitIsh");
|
|
2019
|
-
const cmd2 = ["-C", args.repoPath, "worktree", "add"];
|
|
2020
|
-
if (args.branch !== void 0) {
|
|
2021
|
-
assertSafeArg(args.branch, "branch");
|
|
2022
|
-
cmd2.push("-B", args.branch);
|
|
2023
|
-
}
|
|
2024
|
-
cmd2.push(args.worktreePath, args.commitIsh);
|
|
2025
|
-
await execFileP2("git", cmd2);
|
|
2026
|
-
}
|
|
2027
|
-
async function gitWorktreeRemove(args) {
|
|
2028
|
-
assertSafeArg(args.repoPath, "repoPath");
|
|
2029
|
-
assertSafeArg(args.worktreePath, "worktreePath");
|
|
2030
|
-
try {
|
|
2031
|
-
await execFileP2("git", [
|
|
2032
|
-
"-C",
|
|
2033
|
-
args.repoPath,
|
|
2034
|
-
"worktree",
|
|
2035
|
-
"remove",
|
|
2036
|
-
"--force",
|
|
2037
|
-
args.worktreePath
|
|
2038
|
-
]);
|
|
2039
|
-
} catch (err) {
|
|
2040
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2041
|
-
if (/is not a working tree|worktree.*does not exist/i.test(msg)) {
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
throw err;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
async function gitWorktreeList(repoPath) {
|
|
2048
|
-
assertSafeArg(repoPath, "repoPath");
|
|
2049
|
-
const { stdout } = await execFileP2("git", [
|
|
2050
|
-
"-C",
|
|
2051
|
-
repoPath,
|
|
2052
|
-
"worktree",
|
|
2053
|
-
"list",
|
|
2054
|
-
"--porcelain"
|
|
2055
|
-
]);
|
|
2056
|
-
const records = [];
|
|
2057
|
-
let cur = null;
|
|
2058
|
-
for (const line of stdout.split("\n")) {
|
|
2059
|
-
if (line.length === 0) {
|
|
2060
|
-
if (cur && cur.path) records.push(finalizeWorktreeInfo(cur));
|
|
2061
|
-
cur = null;
|
|
2062
|
-
continue;
|
|
2063
|
-
}
|
|
2064
|
-
if (cur === null) cur = {};
|
|
2065
|
-
const [keyRaw, ...rest] = line.split(" ");
|
|
2066
|
-
const value = rest.join(" ");
|
|
2067
|
-
switch (keyRaw) {
|
|
2068
|
-
case "worktree":
|
|
2069
|
-
cur.path = value;
|
|
2070
|
-
break;
|
|
2071
|
-
case "HEAD":
|
|
2072
|
-
cur.head = value;
|
|
2073
|
-
break;
|
|
2074
|
-
case "branch":
|
|
2075
|
-
cur.branch = value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
|
|
2076
|
-
break;
|
|
2077
|
-
case "detached":
|
|
2078
|
-
cur.branch = null;
|
|
2079
|
-
break;
|
|
2080
|
-
case "bare":
|
|
2081
|
-
cur.bare = true;
|
|
2082
|
-
break;
|
|
2083
|
-
default:
|
|
2084
|
-
break;
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
if (cur && cur.path) records.push(finalizeWorktreeInfo(cur));
|
|
2088
|
-
return records;
|
|
2089
|
-
}
|
|
2090
|
-
function finalizeWorktreeInfo(p) {
|
|
2091
|
-
return {
|
|
2092
|
-
path: p.path,
|
|
2093
|
-
head: p.head ?? "",
|
|
2094
|
-
branch: p.branch ?? null,
|
|
2095
|
-
bare: p.bare ?? false
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
async function checkoutFreshBranch(args) {
|
|
2099
|
-
assertSafeArg(args.worktree, "worktree");
|
|
2100
|
-
assertSafeArg(args.branch, "branch");
|
|
2101
|
-
assertSafeArg(args.base, "base");
|
|
2102
|
-
await execFileP2("git", [
|
|
2103
|
-
"-C",
|
|
2104
|
-
args.worktree,
|
|
2105
|
-
"checkout",
|
|
2106
|
-
"-B",
|
|
2107
|
-
args.branch,
|
|
2108
|
-
args.base
|
|
2109
|
-
]);
|
|
2110
|
-
}
|
|
2111
|
-
async function cleanWorktree(worktree) {
|
|
2112
|
-
assertSafeArg(worktree, "worktree");
|
|
2113
|
-
await execFileP2("git", ["-C", worktree, "reset", "--hard"]);
|
|
2114
|
-
await execFileP2("git", ["-C", worktree, "clean", "-fdx"]);
|
|
2115
|
-
}
|
|
2116
|
-
async function commitAll(args) {
|
|
2117
|
-
assertSafeArg(args.worktree, "worktree");
|
|
2118
|
-
if (typeof args.message !== "string" || args.message.length === 0) {
|
|
2119
|
-
throw new TypeError("commitAll: message must be non-empty");
|
|
2120
|
-
}
|
|
2121
|
-
await execFileP2("git", ["-C", args.worktree, "add", "-A"]);
|
|
2122
|
-
const env = { ...process.env };
|
|
2123
|
-
if (args.authorName) env.GIT_AUTHOR_NAME = args.authorName;
|
|
2124
|
-
if (args.authorEmail) env.GIT_AUTHOR_EMAIL = args.authorEmail;
|
|
2125
|
-
if (args.authorName) env.GIT_COMMITTER_NAME = args.authorName;
|
|
2126
|
-
if (args.authorEmail) env.GIT_COMMITTER_EMAIL = args.authorEmail;
|
|
2127
|
-
await execFileP2("git", ["-C", args.worktree, "commit", "-m", args.message], {
|
|
2128
|
-
env
|
|
2129
|
-
});
|
|
2130
|
-
return headSha(args.worktree);
|
|
2131
|
-
}
|
|
2132
|
-
async function diffNamesSince(worktree, sinceSha) {
|
|
2133
|
-
assertSafeArg(worktree, "worktree");
|
|
2134
|
-
assertSafeArg(sinceSha, "sinceSha");
|
|
2135
|
-
const sets = await Promise.all([
|
|
2136
|
-
runDiffNames(worktree, ["diff", "--name-only", `${sinceSha}..HEAD`]),
|
|
2137
|
-
runDiffNames(worktree, ["diff", "--name-only", "--cached"]),
|
|
2138
|
-
runDiffNames(worktree, ["diff", "--name-only"]),
|
|
2139
|
-
runDiffNames(worktree, [
|
|
2140
|
-
"ls-files",
|
|
2141
|
-
"--others",
|
|
2142
|
-
"--exclude-standard"
|
|
2143
|
-
])
|
|
2144
|
-
]);
|
|
2145
|
-
const all = /* @__PURE__ */ new Set();
|
|
2146
|
-
for (const s of sets) for (const p of s) all.add(p);
|
|
2147
|
-
return [...all].sort();
|
|
2148
|
-
}
|
|
2149
|
-
async function runDiffNames(worktree, args) {
|
|
2150
|
-
const { stdout } = await execFileP2("git", ["-C", worktree, ...args]);
|
|
2151
|
-
return stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
// src/pilot/worktree/pool.ts
|
|
2155
|
-
import { promises as fs9 } from "fs";
|
|
2156
|
-
var WorktreePool = class {
|
|
2157
|
-
repoPath;
|
|
2158
|
-
worktreeDirOf;
|
|
2159
|
-
slots = /* @__PURE__ */ new Map();
|
|
2160
|
-
/**
|
|
2161
|
-
* Slots that were preserved on failure. No longer reachable via
|
|
2162
|
-
* `acquire` — they stay here for `shutdown` (so `keepPreserved=false`
|
|
2163
|
-
* can still clean them up) and `inspect` (so debug tooling sees them).
|
|
2164
|
-
*
|
|
2165
|
-
* When a slot is preserved and a subsequent `acquire(n)` happens, the
|
|
2166
|
-
* current live slot at index `n` is MOVED here, and `slots` gets a
|
|
2167
|
-
* fresh stub at `n` with a bumped `retryCounter`. This is what
|
|
2168
|
-
* prevents a single failed task from poisoning every downstream task
|
|
2169
|
-
* (the pre-v0.2 bug: one preserve → all subsequent `prepare` calls
|
|
2170
|
-
* threw "slot N is preserved").
|
|
2171
|
-
*/
|
|
2172
|
-
retiredSlots = [];
|
|
2173
|
-
/**
|
|
2174
|
-
* Per-index retry counter. Bumps every time `acquire` retires a
|
|
2175
|
-
* preserved slot. Read by `prepare` to decide whether the worktree
|
|
2176
|
-
* path needs a `-<counter>` suffix (for retried slots) or the bare
|
|
2177
|
-
* `worktreeDirOf(n)` path (first-ever use — back-compat with the
|
|
2178
|
-
* existing on-disk layout).
|
|
2179
|
-
*/
|
|
2180
|
-
retryCounter = /* @__PURE__ */ new Map();
|
|
2181
|
-
workerCount;
|
|
2182
|
-
/**
|
|
2183
|
-
* Set of workers currently held by an `acquire`. v0.1 only ever holds
|
|
2184
|
-
* one at a time, but the structure scales to v0.3.
|
|
2185
|
-
*/
|
|
2186
|
-
busy = /* @__PURE__ */ new Set();
|
|
2187
|
-
constructor(opts) {
|
|
2188
|
-
this.repoPath = opts.repoPath;
|
|
2189
|
-
this.worktreeDirOf = opts.worktreeDir;
|
|
2190
|
-
const requested = opts.workerCount ?? 1;
|
|
2191
|
-
if (requested > 1) {
|
|
2192
|
-
process.stderr.write(
|
|
2193
|
-
`[pilot] WorktreePool: workerCount=${requested} requested, but v0.1 supports only 1 \u2014 clamping.
|
|
2194
|
-
`
|
|
2195
|
-
);
|
|
2196
|
-
}
|
|
2197
|
-
this.workerCount = 1;
|
|
2198
|
-
}
|
|
2199
|
-
/**
|
|
2200
|
-
* Acquire a worker slot. Returns the live slot for the given worker
|
|
2201
|
-
* index, or a fresh stub if the current live slot was preserved on
|
|
2202
|
-
* failure.
|
|
2203
|
-
*
|
|
2204
|
-
* v0.1 always uses slot 0. First call returns a fresh stub. If that
|
|
2205
|
-
* slot is later `preserveOnFailure`'d, the next `acquire()` retires
|
|
2206
|
-
* the preserved slot into `retiredSlots`, bumps the retry counter,
|
|
2207
|
-
* and mints a new stub at index 0. The old slot stays on disk (for
|
|
2208
|
-
* operator inspection) but is no longer the pool's live slot.
|
|
2209
|
-
*/
|
|
2210
|
-
acquire() {
|
|
2211
|
-
for (let n = 0; n < this.workerCount; n++) {
|
|
2212
|
-
if (this.busy.has(n)) continue;
|
|
2213
|
-
this.busy.add(n);
|
|
2214
|
-
const existing = this.slots.get(n);
|
|
2215
|
-
if (existing && existing.preserved) {
|
|
2216
|
-
this.retiredSlots.push(existing);
|
|
2217
|
-
this.slots.delete(n);
|
|
2218
|
-
this.retryCounter.set(n, (this.retryCounter.get(n) ?? 0) + 1);
|
|
2219
|
-
} else if (existing) {
|
|
2220
|
-
return existing;
|
|
2221
|
-
}
|
|
2222
|
-
const stub = {
|
|
2223
|
-
index: n,
|
|
2224
|
-
path: "",
|
|
2225
|
-
// filled by prepare
|
|
2226
|
-
prepared: false,
|
|
2227
|
-
preserved: false
|
|
2228
|
-
};
|
|
2229
|
-
this.slots.set(n, stub);
|
|
2230
|
-
return stub;
|
|
2231
|
-
}
|
|
2232
|
-
throw new Error(
|
|
2233
|
-
`WorktreePool.acquire: no free worker slots (workerCount=${this.workerCount}, busy=${[...this.busy].join(",")})`
|
|
2234
|
-
);
|
|
2235
|
-
}
|
|
2236
|
-
/**
|
|
2237
|
-
* Prepare a worktree for the given task. Idempotent: on first call,
|
|
2238
|
-
* runs `git worktree add`; on subsequent calls, recycles the existing
|
|
2239
|
-
* worktree (clean + checkout fresh branch).
|
|
2240
|
-
*
|
|
2241
|
-
* Returns the SHA at HEAD post-prepare. The worker records this as
|
|
2242
|
-
* `sinceSha` for the post-task `enforceTouches` diff.
|
|
2243
|
-
*
|
|
2244
|
-
* `branchPrefix` typically = `pilot/<plan-slug>`; the actual branch
|
|
2245
|
-
* is `<branchPrefix>/<taskId>`. `base` is the commit-ish the branch
|
|
2246
|
-
* is created from — usually the main branch's HEAD or a specific
|
|
2247
|
-
* sha if reproducibility matters.
|
|
2248
|
-
*
|
|
2249
|
-
* For retried slots (i.e. `retryCounter[n] > 0`), the resolved path
|
|
2250
|
-
* gets a `-<counter>` suffix so retries don't collide with the
|
|
2251
|
-
* preserved predecessor on disk.
|
|
2252
|
-
*/
|
|
2253
|
-
async prepare(args) {
|
|
2254
|
-
if (args.slot.preserved) {
|
|
2255
|
-
throw new Error(
|
|
2256
|
-
`WorktreePool.prepare: slot ${args.slot.index} is preserved (failed task awaiting cleanup); cannot reuse`
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
const branch = `${args.branchPrefix}/${args.taskId}`;
|
|
2260
|
-
if (!args.slot.prepared) {
|
|
2261
|
-
const basePath = await this.worktreeDirOf(args.slot.index);
|
|
2262
|
-
const counter = this.retryCounter.get(args.slot.index) ?? 0;
|
|
2263
|
-
const wtPath = counter > 0 ? `${basePath}-${counter}` : basePath;
|
|
2264
|
-
args.slot.path = wtPath;
|
|
2265
|
-
try {
|
|
2266
|
-
await fs9.stat(wtPath);
|
|
2267
|
-
await gitWorktreeRemove({
|
|
2268
|
-
repoPath: this.repoPath,
|
|
2269
|
-
worktreePath: wtPath
|
|
2270
|
-
});
|
|
2271
|
-
await fs9.rm(wtPath, { recursive: true, force: true });
|
|
2272
|
-
} catch {
|
|
2273
|
-
}
|
|
2274
|
-
await gitWorktreeAdd({
|
|
2275
|
-
repoPath: this.repoPath,
|
|
2276
|
-
worktreePath: wtPath,
|
|
2277
|
-
commitIsh: args.base,
|
|
2278
|
-
branch
|
|
2279
|
-
});
|
|
2280
|
-
args.slot.prepared = true;
|
|
2281
|
-
} else {
|
|
2282
|
-
await cleanWorktree(args.slot.path);
|
|
2283
|
-
await checkoutFreshBranch({
|
|
2284
|
-
worktree: args.slot.path,
|
|
2285
|
-
branch,
|
|
2286
|
-
base: args.base
|
|
2287
|
-
});
|
|
2288
|
-
}
|
|
2289
|
-
const sinceSha = await headSha(args.slot.path);
|
|
2290
|
-
return { sinceSha, branch, path: args.slot.path };
|
|
2291
|
-
}
|
|
2292
|
-
/**
|
|
2293
|
-
* Release a slot back to the pool — slot becomes available for
|
|
2294
|
-
* `acquire` again. Call after a clean task completion (commit
|
|
2295
|
-
* succeeded, no preserved state needed).
|
|
2296
|
-
*
|
|
2297
|
-
* Does NOT clean the worktree — the next `prepare` call will reset
|
|
2298
|
-
* it. If you want eager cleanup (e.g. before a long idle), call
|
|
2299
|
-
* `cleanWorktree(slot.path)` separately.
|
|
2300
|
-
*/
|
|
2301
|
-
release(slot) {
|
|
2302
|
-
if (!this.busy.has(slot.index)) {
|
|
2303
|
-
throw new Error(
|
|
2304
|
-
`WorktreePool.release: slot ${slot.index} is not held`
|
|
2305
|
-
);
|
|
2306
|
-
}
|
|
2307
|
-
this.busy.delete(slot.index);
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Preserve a slot's state on failure. The slot is marked preserved
|
|
2311
|
-
* and removed from the busy set. Unlike pre-v0.2 behaviour, the
|
|
2312
|
-
* next `acquire()` call retires this slot into `retiredSlots` and
|
|
2313
|
-
* mints a fresh stub — so a single failure doesn't cascade-block
|
|
2314
|
-
* the rest of the run.
|
|
2315
|
-
*
|
|
2316
|
-
* The CLI's `pilot worktrees prune` (Phase G6) remains the path to
|
|
2317
|
-
* permanently remove preserved slots from disk.
|
|
2318
|
-
*/
|
|
2319
|
-
preserveOnFailure(slot) {
|
|
2320
|
-
slot.preserved = true;
|
|
2321
|
-
this.busy.delete(slot.index);
|
|
2322
|
-
}
|
|
2323
|
-
/**
|
|
2324
|
-
* Tear down all worktrees managed by this pool — BOTH live and
|
|
2325
|
-
* retired. Called at end of `pilot build` (whether success or
|
|
2326
|
-
* failure). Preserved slots are skipped when `keepPreserved` is
|
|
2327
|
-
* true (the default) — those are the user's to inspect.
|
|
2328
|
-
*/
|
|
2329
|
-
async shutdown(args = {}) {
|
|
2330
|
-
const keepPreserved = args.keepPreserved ?? true;
|
|
2331
|
-
const errors = [];
|
|
2332
|
-
const all = [...this.slots.values(), ...this.retiredSlots];
|
|
2333
|
-
for (const slot of all) {
|
|
2334
|
-
if (slot.preserved && keepPreserved) continue;
|
|
2335
|
-
if (!slot.prepared || slot.path === "") continue;
|
|
2336
|
-
try {
|
|
2337
|
-
await gitWorktreeRemove({
|
|
2338
|
-
repoPath: this.repoPath,
|
|
2339
|
-
worktreePath: slot.path
|
|
2340
|
-
});
|
|
2341
|
-
} catch (err) {
|
|
2342
|
-
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
if (errors.length > 0) {
|
|
2346
|
-
throw new Error(
|
|
2347
|
-
`WorktreePool.shutdown: ${errors.length} worktree removal(s) failed:
|
|
2348
|
-
` + errors.map((e) => e.message).join("\n---\n")
|
|
2349
|
-
);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
/**
|
|
2353
|
-
* Inspect current slots (for tests / `pilot worktrees list`). Returns
|
|
2354
|
-
* live slots followed by retired slots, in insertion order within
|
|
2355
|
-
* each group.
|
|
2356
|
-
*/
|
|
2357
|
-
inspect() {
|
|
2358
|
-
return [...this.slots.values(), ...this.retiredSlots];
|
|
2359
|
-
}
|
|
2360
|
-
};
|
|
2361
|
-
|
|
2362
|
-
// src/pilot/scheduler/ready-set.ts
|
|
2363
|
-
function makeScheduler(args) {
|
|
2364
|
-
const { db, runId, plan } = args;
|
|
2365
|
-
const planById = /* @__PURE__ */ new Map();
|
|
2366
|
-
for (const t of plan.tasks) planById.set(t.id, t);
|
|
2367
|
-
const dependentsOf = /* @__PURE__ */ new Map();
|
|
2368
|
-
for (const t of plan.tasks) {
|
|
2369
|
-
for (const dep of t.depends_on) {
|
|
2370
|
-
const list = dependentsOf.get(dep);
|
|
2371
|
-
if (list) list.push(t.id);
|
|
2372
|
-
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]);
|
|
2373
1686
|
}
|
|
2374
1687
|
}
|
|
2375
1688
|
return {
|
|
@@ -2435,6 +1748,8 @@ function depsSatisfied(db, runId, task) {
|
|
|
2435
1748
|
|
|
2436
1749
|
// src/pilot/worker/worker.ts
|
|
2437
1750
|
import * as fsSync from "fs";
|
|
1751
|
+
import { execFile as execFileCb } from "child_process";
|
|
1752
|
+
import { promisify as promisifyUtil } from "util";
|
|
2438
1753
|
|
|
2439
1754
|
// src/pilot/opencode/prompts.ts
|
|
2440
1755
|
function kickoffPrompt(task, ctx) {
|
|
@@ -2489,6 +1804,18 @@ function kickoffPrompt(task, ctx) {
|
|
|
2489
1804
|
);
|
|
2490
1805
|
}
|
|
2491
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
|
+
);
|
|
2492
1819
|
if (task.context !== void 0 && task.context.trim().length > 0) {
|
|
2493
1820
|
sections.push(``, `## Context`, ``, task.context.trim());
|
|
2494
1821
|
}
|
|
@@ -2545,8 +1872,8 @@ var DEFAULT_OUTPUT_CAP_BYTES = 256 * 1024;
|
|
|
2545
1872
|
var TRUNCATION_NOTICE = "\n[pilot] verify output truncated\n";
|
|
2546
1873
|
async function runVerify(commands, options) {
|
|
2547
1874
|
const results = [];
|
|
2548
|
-
for (const
|
|
2549
|
-
const result = await runOne(
|
|
1875
|
+
for (const command10 of commands) {
|
|
1876
|
+
const result = await runOne(command10, options);
|
|
2550
1877
|
results.push(result);
|
|
2551
1878
|
if (!result.ok) {
|
|
2552
1879
|
return { ok: false, results, failure: result };
|
|
@@ -2557,8 +1884,8 @@ async function runVerify(commands, options) {
|
|
|
2557
1884
|
results
|
|
2558
1885
|
};
|
|
2559
1886
|
}
|
|
2560
|
-
async function runOne(
|
|
2561
|
-
if (typeof
|
|
1887
|
+
async function runOne(command10, options) {
|
|
1888
|
+
if (typeof command10 !== "string" || command10.length === 0) {
|
|
2562
1889
|
throw new TypeError(`runOne: command must be a non-empty string`);
|
|
2563
1890
|
}
|
|
2564
1891
|
if (typeof options.cwd !== "string" || options.cwd.length === 0) {
|
|
@@ -2574,7 +1901,7 @@ async function runOne(command12, options) {
|
|
|
2574
1901
|
stdout: { partial: "" },
|
|
2575
1902
|
stderr: { partial: "" }
|
|
2576
1903
|
};
|
|
2577
|
-
const child = spawn2("bash", ["-c",
|
|
1904
|
+
const child = spawn2("bash", ["-c", command10], {
|
|
2578
1905
|
cwd: options.cwd,
|
|
2579
1906
|
env: options.env ?? process.env,
|
|
2580
1907
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2619,18 +1946,18 @@ async function runOne(command12, options) {
|
|
|
2619
1946
|
const lines = combined.split("\n");
|
|
2620
1947
|
state.partial = lines.pop();
|
|
2621
1948
|
for (const line of lines) {
|
|
2622
|
-
options.onLine({ stream, line, command:
|
|
1949
|
+
options.onLine({ stream, line, command: command10 });
|
|
2623
1950
|
}
|
|
2624
1951
|
}
|
|
2625
1952
|
};
|
|
2626
1953
|
child.stdout?.on("data", (c2) => handleChunk("stdout", c2));
|
|
2627
1954
|
child.stderr?.on("data", (c2) => handleChunk("stderr", c2));
|
|
2628
|
-
const { code, signal } = await new Promise((
|
|
1955
|
+
const { code, signal } = await new Promise((resolve6) => {
|
|
2629
1956
|
let resolved = false;
|
|
2630
1957
|
const finalize = (code2, signal2) => {
|
|
2631
1958
|
if (resolved) return;
|
|
2632
1959
|
resolved = true;
|
|
2633
|
-
|
|
1960
|
+
resolve6({ code: code2, signal: signal2 });
|
|
2634
1961
|
};
|
|
2635
1962
|
child.on("error", (err) => {
|
|
2636
1963
|
if (!truncated) {
|
|
@@ -2650,7 +1977,7 @@ async function runOne(command12, options) {
|
|
|
2650
1977
|
for (const stream of ["stdout", "stderr"]) {
|
|
2651
1978
|
const partial = streamState[stream].partial;
|
|
2652
1979
|
if (partial.length > 0) {
|
|
2653
|
-
options.onLine({ stream, line: partial, command:
|
|
1980
|
+
options.onLine({ stream, line: partial, command: command10 });
|
|
2654
1981
|
}
|
|
2655
1982
|
}
|
|
2656
1983
|
}
|
|
@@ -2659,7 +1986,7 @@ async function runOne(command12, options) {
|
|
|
2659
1986
|
if (code === 0 && !timedOut && !aborted) {
|
|
2660
1987
|
return {
|
|
2661
1988
|
ok: true,
|
|
2662
|
-
command:
|
|
1989
|
+
command: command10,
|
|
2663
1990
|
exitCode: 0,
|
|
2664
1991
|
output,
|
|
2665
1992
|
durationMs
|
|
@@ -2667,7 +1994,7 @@ async function runOne(command12, options) {
|
|
|
2667
1994
|
}
|
|
2668
1995
|
return {
|
|
2669
1996
|
ok: false,
|
|
2670
|
-
command:
|
|
1997
|
+
command: command10,
|
|
2671
1998
|
exitCode: code ?? -1,
|
|
2672
1999
|
signal,
|
|
2673
2000
|
timedOut,
|
|
@@ -2695,15 +2022,66 @@ function killTree(child) {
|
|
|
2695
2022
|
|
|
2696
2023
|
// src/pilot/verify/touches.ts
|
|
2697
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
|
+
}
|
|
2698
2065
|
async function enforceTouches(args) {
|
|
2699
|
-
const changed = await diffNamesSince(args.
|
|
2066
|
+
const changed = await diffNamesSince(args.cwd, args.sinceSha);
|
|
2700
2067
|
if (changed.length === 0) {
|
|
2701
2068
|
return { ok: true, changed: [] };
|
|
2702
2069
|
}
|
|
2070
|
+
const combined = [
|
|
2071
|
+
...args.allowed,
|
|
2072
|
+
...args.tolerate ?? [],
|
|
2073
|
+
...DEFAULT_TOLERATE
|
|
2074
|
+
];
|
|
2703
2075
|
if (args.allowed.length === 0) {
|
|
2704
|
-
|
|
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 };
|
|
2705
2083
|
}
|
|
2706
|
-
const matchAllowed = picomatch2(
|
|
2084
|
+
const matchAllowed = picomatch2(combined, { dot: true });
|
|
2707
2085
|
const violators = changed.filter((p) => !matchAllowed(p));
|
|
2708
2086
|
if (violators.length === 0) return { ok: true, changed };
|
|
2709
2087
|
return { ok: false, changed, violators };
|
|
@@ -2858,10 +2236,71 @@ function isTextPart(v) {
|
|
|
2858
2236
|
}
|
|
2859
2237
|
|
|
2860
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
|
+
}
|
|
2861
2278
|
async function runWorker(deps) {
|
|
2862
2279
|
const attempted = [];
|
|
2863
|
-
const maxAttempts = deps.maxAttempts ??
|
|
2280
|
+
const maxAttempts = deps.maxAttempts ?? 5;
|
|
2864
2281
|
const stallMs = deps.stallMs ?? 60 * 60 * 1e3;
|
|
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
|
+
}
|
|
2865
2304
|
while (true) {
|
|
2866
2305
|
if (deps.abortSignal?.aborted) {
|
|
2867
2306
|
return { aborted: true, attempted };
|
|
@@ -2871,7 +2310,14 @@ async function runWorker(deps) {
|
|
|
2871
2310
|
return { aborted: false, attempted };
|
|
2872
2311
|
}
|
|
2873
2312
|
attempted.push(pick.task.id);
|
|
2874
|
-
await runOneTask(deps, pick.task, { maxAttempts, stallMs });
|
|
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 };
|
|
2320
|
+
}
|
|
2875
2321
|
const row = getTask(deps.db, deps.runId, pick.task.id);
|
|
2876
2322
|
if (row && (row.status === "failed" || row.status === "aborted")) {
|
|
2877
2323
|
const blocked = deps.scheduler.cascadeFail(
|
|
@@ -2922,38 +2368,42 @@ function openForensics(args) {
|
|
|
2922
2368
|
};
|
|
2923
2369
|
}
|
|
2924
2370
|
async function runOneTask(deps, task, opts) {
|
|
2925
|
-
|
|
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;
|
|
2926
2390
|
appendEvent(deps.db, {
|
|
2927
2391
|
runId: deps.runId,
|
|
2928
2392
|
taskId: task.id,
|
|
2929
2393
|
kind: "task.started",
|
|
2930
2394
|
payload: {}
|
|
2931
2395
|
});
|
|
2932
|
-
let
|
|
2933
|
-
let prepared;
|
|
2396
|
+
let sinceSha;
|
|
2934
2397
|
try {
|
|
2935
|
-
|
|
2936
|
-
prepared = await deps.pool.prepare({
|
|
2937
|
-
slot,
|
|
2938
|
-
taskId: task.id,
|
|
2939
|
-
branchPrefix: deps.branchPrefix,
|
|
2940
|
-
base: deps.base
|
|
2941
|
-
});
|
|
2398
|
+
sinceSha = await headSha(cwd);
|
|
2942
2399
|
} catch (err) {
|
|
2943
|
-
const reason2 = `
|
|
2944
|
-
|
|
2945
|
-
const row = getTask(deps.db, deps.runId, task.id);
|
|
2946
|
-
if (row?.status === "pending") {
|
|
2947
|
-
deps.scheduler.next();
|
|
2948
|
-
}
|
|
2949
|
-
markFailed(deps.db, deps.runId, task.id, reason2);
|
|
2950
|
-
} catch {
|
|
2951
|
-
}
|
|
2400
|
+
const reason2 = `headSha failed: ${errorMessage2(err)}`;
|
|
2401
|
+
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
2952
2402
|
appendEvent(deps.db, {
|
|
2953
2403
|
runId: deps.runId,
|
|
2954
2404
|
taskId: task.id,
|
|
2955
2405
|
kind: "task.failed",
|
|
2956
|
-
payload: { phase: "
|
|
2406
|
+
payload: { phase: "headSha", reason: reason2 }
|
|
2957
2407
|
});
|
|
2958
2408
|
return;
|
|
2959
2409
|
}
|
|
@@ -2961,7 +2411,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
2961
2411
|
try {
|
|
2962
2412
|
const created = await deps.client.session.create({
|
|
2963
2413
|
body: { title: `pilot/${deps.runId}/${task.id}` },
|
|
2964
|
-
query: { directory:
|
|
2414
|
+
query: { directory: cwd }
|
|
2965
2415
|
});
|
|
2966
2416
|
if (!created.data?.id) {
|
|
2967
2417
|
throw new Error(`session.create returned no id`);
|
|
@@ -2969,7 +2419,6 @@ async function runOneTask(deps, task, opts) {
|
|
|
2969
2419
|
sessionId = created.data.id;
|
|
2970
2420
|
} catch (err) {
|
|
2971
2421
|
const reason2 = `session.create failed: ${errorMessage2(err)}`;
|
|
2972
|
-
deps.pool.preserveOnFailure(slot);
|
|
2973
2422
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
2974
2423
|
appendEvent(deps.db, {
|
|
2975
2424
|
runId: deps.runId,
|
|
@@ -2979,7 +2428,14 @@ async function runOneTask(deps, task, opts) {
|
|
|
2979
2428
|
});
|
|
2980
2429
|
return;
|
|
2981
2430
|
}
|
|
2982
|
-
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);
|
|
2983
2439
|
await new Promise((r) => setTimeout(r, 200));
|
|
2984
2440
|
const disposeBus = async () => {
|
|
2985
2441
|
try {
|
|
@@ -2998,18 +2454,23 @@ async function runOneTask(deps, task, opts) {
|
|
|
2998
2454
|
if (forensics) forensics.dispose();
|
|
2999
2455
|
void disposeBus();
|
|
3000
2456
|
};
|
|
2457
|
+
const unregisterSessionSafe = async () => {
|
|
2458
|
+
try {
|
|
2459
|
+
await unregisterSession({ runDir, sessionId });
|
|
2460
|
+
} catch {
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
3001
2463
|
const forensicsCounters = () => forensics ? forensics.counters() : { lastEventTs: null, eventCount: 0 };
|
|
3002
2464
|
try {
|
|
3003
2465
|
markRunning(deps.db, {
|
|
3004
2466
|
runId: deps.runId,
|
|
3005
2467
|
taskId: task.id,
|
|
3006
2468
|
sessionId,
|
|
3007
|
-
branch:
|
|
3008
|
-
worktreePath:
|
|
2469
|
+
branch: "",
|
|
2470
|
+
worktreePath: cwd
|
|
3009
2471
|
});
|
|
3010
2472
|
} catch (err) {
|
|
3011
2473
|
disposeForensics();
|
|
3012
|
-
deps.pool.preserveOnFailure(slot);
|
|
3013
2474
|
const reason2 = `markRunning failed: ${errorMessage2(err)}`;
|
|
3014
2475
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3015
2476
|
appendEvent(deps.db, {
|
|
@@ -3024,12 +2485,12 @@ async function runOneTask(deps, task, opts) {
|
|
|
3024
2485
|
runId: deps.runId,
|
|
3025
2486
|
taskId: task.id,
|
|
3026
2487
|
kind: "task.session.created",
|
|
3027
|
-
payload: { sessionId, branch:
|
|
2488
|
+
payload: { sessionId, branch: "", worktreePath: cwd }
|
|
3028
2489
|
});
|
|
3029
2490
|
const ctx = {
|
|
3030
2491
|
planName: deps.plan.name,
|
|
3031
|
-
branch:
|
|
3032
|
-
worktreePath:
|
|
2492
|
+
branch: "",
|
|
2493
|
+
worktreePath: cwd,
|
|
3033
2494
|
milestone: task.milestone,
|
|
3034
2495
|
verifyAfterEach: deps.plan.defaults.verify_after_each,
|
|
3035
2496
|
verifyMilestone: task.milestone !== void 0 ? deps.plan.milestones.find((m) => m.name === task.milestone)?.verify ?? [] : []
|
|
@@ -3037,8 +2498,50 @@ async function runOneTask(deps, task, opts) {
|
|
|
3037
2498
|
const allVerify = [
|
|
3038
2499
|
...task.verify,
|
|
3039
2500
|
...deps.plan.defaults.verify_after_each,
|
|
3040
|
-
...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
|
+
)
|
|
3041
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
|
+
}
|
|
3042
2545
|
let lastFailure = null;
|
|
3043
2546
|
let stopReason = null;
|
|
3044
2547
|
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
@@ -3046,7 +2549,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3046
2549
|
await abortSession(deps, sessionId);
|
|
3047
2550
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal");
|
|
3048
2551
|
disposeForensics();
|
|
3049
|
-
|
|
2552
|
+
await unregisterSessionSafe();
|
|
3050
2553
|
appendEvent(deps.db, {
|
|
3051
2554
|
runId: deps.runId,
|
|
3052
2555
|
taskId: task.id,
|
|
@@ -3076,7 +2579,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3076
2579
|
try {
|
|
3077
2580
|
await deps.client.session.promptAsync({
|
|
3078
2581
|
path: { id: sessionId },
|
|
3079
|
-
query: { directory:
|
|
2582
|
+
query: { directory: cwd },
|
|
3080
2583
|
body: {
|
|
3081
2584
|
agent: task.agent ?? deps.plan.defaults.agent,
|
|
3082
2585
|
parts: [{ type: "text", text: promptText }]
|
|
@@ -3086,7 +2589,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3086
2589
|
unsubStop();
|
|
3087
2590
|
const reason2 = `promptAsync failed: ${errorMessage2(err)}`;
|
|
3088
2591
|
disposeForensics();
|
|
3089
|
-
|
|
2592
|
+
await unregisterSessionSafe();
|
|
3090
2593
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3091
2594
|
appendEvent(deps.db, {
|
|
3092
2595
|
runId: deps.runId,
|
|
@@ -3106,7 +2609,7 @@ async function runOneTask(deps, task, opts) {
|
|
|
3106
2609
|
await abortSession(deps, sessionId);
|
|
3107
2610
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal");
|
|
3108
2611
|
disposeForensics();
|
|
3109
|
-
|
|
2612
|
+
await unregisterSessionSafe();
|
|
3110
2613
|
appendEvent(deps.db, {
|
|
3111
2614
|
runId: deps.runId,
|
|
3112
2615
|
taskId: task.id,
|
|
@@ -3124,8 +2627,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3124
2627
|
} catch {
|
|
3125
2628
|
}
|
|
3126
2629
|
disposeForensics();
|
|
2630
|
+
await unregisterSessionSafe();
|
|
3127
2631
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3128
|
-
deps.pool.preserveOnFailure(slot);
|
|
3129
2632
|
appendEvent(deps.db, {
|
|
3130
2633
|
runId: deps.runId,
|
|
3131
2634
|
taskId: task.id,
|
|
@@ -3143,8 +2646,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3143
2646
|
if (idleResult.kind === "session-error") {
|
|
3144
2647
|
const reason2 = `session error: ${JSON.stringify(idleResult.properties)}`;
|
|
3145
2648
|
disposeForensics();
|
|
2649
|
+
await unregisterSessionSafe();
|
|
3146
2650
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3147
|
-
deps.pool.preserveOnFailure(slot);
|
|
3148
2651
|
appendEvent(deps.db, {
|
|
3149
2652
|
runId: deps.runId,
|
|
3150
2653
|
taskId: task.id,
|
|
@@ -3159,8 +2662,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3159
2662
|
}
|
|
3160
2663
|
if (stopReason !== null) {
|
|
3161
2664
|
disposeForensics();
|
|
2665
|
+
await unregisterSessionSafe();
|
|
3162
2666
|
markFailedSafe(deps.db, deps.runId, task.id, stopReason);
|
|
3163
|
-
deps.pool.preserveOnFailure(slot);
|
|
3164
2667
|
appendEvent(deps.db, {
|
|
3165
2668
|
runId: deps.runId,
|
|
3166
2669
|
taskId: task.id,
|
|
@@ -3170,9 +2673,10 @@ async function runOneTask(deps, task, opts) {
|
|
|
3170
2673
|
return;
|
|
3171
2674
|
}
|
|
3172
2675
|
const verifyResult = await runVerify(allVerify, {
|
|
3173
|
-
cwd
|
|
2676
|
+
cwd,
|
|
3174
2677
|
abortSignal: deps.abortSignal,
|
|
3175
|
-
onLine: deps.onVerifyLine
|
|
2678
|
+
onLine: deps.onVerifyLine,
|
|
2679
|
+
env: process.env
|
|
3176
2680
|
});
|
|
3177
2681
|
if (!verifyResult.ok) {
|
|
3178
2682
|
lastFailure = {
|
|
@@ -3190,20 +2694,21 @@ async function runOneTask(deps, task, opts) {
|
|
|
3190
2694
|
command: lastFailure.command,
|
|
3191
2695
|
exitCode: lastFailure.exitCode,
|
|
3192
2696
|
timedOut: verifyResult.failure.timedOut,
|
|
3193
|
-
aborted: verifyResult.failure.aborted
|
|
2697
|
+
aborted: verifyResult.failure.aborted,
|
|
2698
|
+
output: verifyResult.failure.output.slice(-2048)
|
|
3194
2699
|
}
|
|
3195
2700
|
});
|
|
3196
2701
|
if (verifyResult.failure.aborted) {
|
|
3197
2702
|
disposeForensics();
|
|
2703
|
+
await unregisterSessionSafe();
|
|
3198
2704
|
markAbortedSafe(deps.db, deps.runId, task.id, "abort signal during verify");
|
|
3199
|
-
deps.pool.preserveOnFailure(slot);
|
|
3200
2705
|
return;
|
|
3201
2706
|
}
|
|
3202
2707
|
if (attempt < opts.maxAttempts) continue;
|
|
3203
2708
|
const reason2 = `verify failed after ${opts.maxAttempts} attempts: ${lastFailure.command} \u2192 exit ${lastFailure.exitCode}`;
|
|
3204
2709
|
disposeForensics();
|
|
2710
|
+
await unregisterSessionSafe();
|
|
3205
2711
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3206
|
-
deps.pool.preserveOnFailure(slot);
|
|
3207
2712
|
appendEvent(deps.db, {
|
|
3208
2713
|
runId: deps.runId,
|
|
3209
2714
|
taskId: task.id,
|
|
@@ -3219,9 +2724,10 @@ async function runOneTask(deps, task, opts) {
|
|
|
3219
2724
|
payload: { attempt }
|
|
3220
2725
|
});
|
|
3221
2726
|
const touches = await enforceTouches({
|
|
3222
|
-
|
|
3223
|
-
sinceSha
|
|
3224
|
-
allowed: task.touches
|
|
2727
|
+
cwd,
|
|
2728
|
+
sinceSha,
|
|
2729
|
+
allowed: task.touches,
|
|
2730
|
+
tolerate: task.tolerate
|
|
3225
2731
|
});
|
|
3226
2732
|
if (!touches.ok) {
|
|
3227
2733
|
lastFailure = {
|
|
@@ -3239,8 +2745,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3239
2745
|
if (attempt < opts.maxAttempts) continue;
|
|
3240
2746
|
const reason2 = `touches violation after ${opts.maxAttempts} attempts: ${touches.violators.join(", ")}`;
|
|
3241
2747
|
disposeForensics();
|
|
2748
|
+
await unregisterSessionSafe();
|
|
3242
2749
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3243
|
-
deps.pool.preserveOnFailure(slot);
|
|
3244
2750
|
appendEvent(deps.db, {
|
|
3245
2751
|
runId: deps.runId,
|
|
3246
2752
|
taskId: task.id,
|
|
@@ -3251,8 +2757,8 @@ async function runOneTask(deps, task, opts) {
|
|
|
3251
2757
|
}
|
|
3252
2758
|
if (touches.changed.length === 0) {
|
|
3253
2759
|
disposeForensics();
|
|
2760
|
+
await unregisterSessionSafe();
|
|
3254
2761
|
markSucceeded(deps.db, deps.runId, task.id);
|
|
3255
|
-
deps.pool.release(slot);
|
|
3256
2762
|
appendEvent(deps.db, {
|
|
3257
2763
|
runId: deps.runId,
|
|
3258
2764
|
taskId: task.id,
|
|
@@ -3263,15 +2769,15 @@ async function runOneTask(deps, task, opts) {
|
|
|
3263
2769
|
}
|
|
3264
2770
|
try {
|
|
3265
2771
|
const commitMessage = `${task.id}: ${task.title}`;
|
|
3266
|
-
const sha = await commitAll(
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
2772
|
+
const sha = await commitAll(
|
|
2773
|
+
cwd,
|
|
2774
|
+
commitMessage,
|
|
2775
|
+
deps.authorName,
|
|
2776
|
+
deps.authorEmail
|
|
2777
|
+
);
|
|
3272
2778
|
disposeForensics();
|
|
2779
|
+
await unregisterSessionSafe();
|
|
3273
2780
|
markSucceeded(deps.db, deps.runId, task.id);
|
|
3274
|
-
deps.pool.release(slot);
|
|
3275
2781
|
appendEvent(deps.db, {
|
|
3276
2782
|
runId: deps.runId,
|
|
3277
2783
|
taskId: task.id,
|
|
@@ -3280,23 +2786,36 @@ async function runOneTask(deps, task, opts) {
|
|
|
3280
2786
|
});
|
|
3281
2787
|
return;
|
|
3282
2788
|
} catch (err) {
|
|
3283
|
-
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)}`;
|
|
3284
2803
|
disposeForensics();
|
|
2804
|
+
await unregisterSessionSafe();
|
|
3285
2805
|
markFailedSafe(deps.db, deps.runId, task.id, reason2);
|
|
3286
|
-
deps.pool.preserveOnFailure(slot);
|
|
3287
2806
|
appendEvent(deps.db, {
|
|
3288
2807
|
runId: deps.runId,
|
|
3289
2808
|
taskId: task.id,
|
|
3290
2809
|
kind: "task.failed",
|
|
3291
|
-
payload: { phase: "commit", reason: reason2 }
|
|
2810
|
+
payload: { phase: "commit", reason: reason2, attempts: opts.maxAttempts }
|
|
3292
2811
|
});
|
|
3293
2812
|
return;
|
|
3294
2813
|
}
|
|
3295
2814
|
}
|
|
3296
2815
|
const reason = "worker loop exited unexpectedly";
|
|
3297
2816
|
disposeForensics();
|
|
2817
|
+
await unregisterSessionSafe();
|
|
3298
2818
|
markFailedSafe(deps.db, deps.runId, task.id, reason);
|
|
3299
|
-
deps.pool.preserveOnFailure(slot);
|
|
3300
2819
|
appendEvent(deps.db, {
|
|
3301
2820
|
runId: deps.runId,
|
|
3302
2821
|
taskId: task.id,
|
|
@@ -3368,7 +2887,7 @@ function errorMessage2(err) {
|
|
|
3368
2887
|
}
|
|
3369
2888
|
|
|
3370
2889
|
// src/pilot/cli/build.ts
|
|
3371
|
-
import { promises as
|
|
2890
|
+
import { promises as fs7 } from "fs";
|
|
3372
2891
|
var buildCmd = command3({
|
|
3373
2892
|
name: "build",
|
|
3374
2893
|
description: "Execute a pilot.yaml plan via the worker loop.",
|
|
@@ -3473,8 +2992,8 @@ async function runBuild(opts) {
|
|
|
3473
2992
|
const opened = openStateDb(":memory:");
|
|
3474
2993
|
opened.close();
|
|
3475
2994
|
const cleanup = [];
|
|
3476
|
-
const { ulid
|
|
3477
|
-
const runId =
|
|
2995
|
+
const { ulid } = await import("ulid");
|
|
2996
|
+
const runId = ulid();
|
|
3478
2997
|
const dbPath = await getStateDbPath(cwd, runId);
|
|
3479
2998
|
const runDir = await getRunDir(cwd, runId);
|
|
3480
2999
|
const branchPrefix = deriveBranchPrefix(plan.branch_prefix, slug, runId);
|
|
@@ -3493,6 +3012,49 @@ async function runBuild(opts) {
|
|
|
3493
3012
|
kind: "run.started",
|
|
3494
3013
|
payload: { planPath, slug, runDir, branchPrefix }
|
|
3495
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
|
+
}
|
|
3496
3058
|
return executeRun({
|
|
3497
3059
|
db: real,
|
|
3498
3060
|
runId,
|
|
@@ -3510,10 +3072,17 @@ async function executeRun(args) {
|
|
|
3510
3072
|
const { db, runId, plan, planPath, runDir, branchPrefix, cleanup } = args;
|
|
3511
3073
|
const cwd = process.cwd();
|
|
3512
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);
|
|
3513
3077
|
let server;
|
|
3514
3078
|
try {
|
|
3515
3079
|
server = await startOpencodeServer({
|
|
3516
|
-
port: args.opencodePort ?? 0
|
|
3080
|
+
port: args.opencodePort ?? 0,
|
|
3081
|
+
runContext: {
|
|
3082
|
+
runDir: runDirForMcp,
|
|
3083
|
+
dbPath: dbPathForMcp,
|
|
3084
|
+
runId
|
|
3085
|
+
}
|
|
3517
3086
|
});
|
|
3518
3087
|
} catch (err) {
|
|
3519
3088
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -3530,28 +3099,7 @@ async function executeRun(args) {
|
|
|
3530
3099
|
}
|
|
3531
3100
|
cleanup.push(() => server.shutdown());
|
|
3532
3101
|
const busFactory = (directory) => new EventBus(server.client, directory);
|
|
3533
|
-
const pool = new WorktreePool({
|
|
3534
|
-
repoPath: cwd,
|
|
3535
|
-
worktreeDir: async (n) => getWorktreeDir(cwd, runId, n)
|
|
3536
|
-
});
|
|
3537
|
-
cleanup.push(() => pool.shutdown({ keepPreserved: true }));
|
|
3538
3102
|
const scheduler = makeScheduler({ db: db.db, runId, plan });
|
|
3539
|
-
let base;
|
|
3540
|
-
try {
|
|
3541
|
-
base = await headSha(cwd);
|
|
3542
|
-
} catch (err) {
|
|
3543
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
3544
|
-
process.stderr.write(`pilot: cannot resolve HEAD sha: ${reason}
|
|
3545
|
-
`);
|
|
3546
|
-
appendEvent(db.db, {
|
|
3547
|
-
runId,
|
|
3548
|
-
kind: "run.error",
|
|
3549
|
-
payload: { phase: "head-sha", reason }
|
|
3550
|
-
});
|
|
3551
|
-
markRunFinished(db.db, runId, "failed");
|
|
3552
|
-
await runCleanup(cleanup);
|
|
3553
|
-
return 1;
|
|
3554
|
-
}
|
|
3555
3103
|
const aborter = new AbortController();
|
|
3556
3104
|
const sigintHandler = () => aborter.abort("SIGINT");
|
|
3557
3105
|
process.once("SIGINT", sigintHandler);
|
|
@@ -3559,28 +3107,26 @@ async function executeRun(args) {
|
|
|
3559
3107
|
process.off("SIGINT", sigintHandler);
|
|
3560
3108
|
});
|
|
3561
3109
|
if (args.quiet !== true) {
|
|
3110
|
+
stderrWriter(
|
|
3111
|
+
`pilot build: run ${runId} started (${plan.tasks.length} tasks)
|
|
3112
|
+
`
|
|
3113
|
+
);
|
|
3562
3114
|
const unsubLogger = startStreamingLogger({
|
|
3563
3115
|
stderrWriter,
|
|
3564
3116
|
runId,
|
|
3565
3117
|
totalTasks: plan.tasks.length,
|
|
3566
|
-
subscribe: subscribeToEvents
|
|
3118
|
+
subscribe: subscribeToEvents,
|
|
3119
|
+
db: db.db
|
|
3567
3120
|
});
|
|
3568
3121
|
cleanup.push(() => unsubLogger());
|
|
3569
|
-
stderrWriter(
|
|
3570
|
-
`pilot build: run ${runId} started (${plan.tasks.length} tasks)
|
|
3571
|
-
`
|
|
3572
|
-
);
|
|
3573
3122
|
}
|
|
3574
3123
|
const result = await runWorker({
|
|
3575
3124
|
db: db.db,
|
|
3576
3125
|
runId,
|
|
3577
3126
|
plan,
|
|
3578
3127
|
scheduler,
|
|
3579
|
-
pool,
|
|
3580
3128
|
client: server.client,
|
|
3581
3129
|
busFactory,
|
|
3582
|
-
branchPrefix,
|
|
3583
|
-
base,
|
|
3584
3130
|
abortSignal: aborter.signal
|
|
3585
3131
|
});
|
|
3586
3132
|
const counts = countByStatus(db.db, runId);
|
|
@@ -3599,7 +3145,7 @@ async function executeRun(args) {
|
|
|
3599
3145
|
}
|
|
3600
3146
|
async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
3601
3147
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
3602
|
-
const resolved =
|
|
3148
|
+
const resolved = path7.isAbsolute(input.flag) ? input.flag : path7.resolve(cwd, input.flag);
|
|
3603
3149
|
if (await isFile(resolved)) {
|
|
3604
3150
|
return { kind: "ok", path: resolved };
|
|
3605
3151
|
}
|
|
@@ -3611,14 +3157,14 @@ async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
|
3611
3157
|
if (input.positional !== void 0 && input.positional.length > 0) {
|
|
3612
3158
|
const plansDir2 = await getPlansDir(cwd);
|
|
3613
3159
|
const candidates = [];
|
|
3614
|
-
if (
|
|
3160
|
+
if (path7.isAbsolute(input.positional)) {
|
|
3615
3161
|
candidates.push(input.positional);
|
|
3616
3162
|
} else {
|
|
3617
|
-
candidates.push(
|
|
3618
|
-
candidates.push(
|
|
3163
|
+
candidates.push(path7.resolve(cwd, input.positional));
|
|
3164
|
+
candidates.push(path7.join(plansDir2, input.positional));
|
|
3619
3165
|
if (!/\.(ya?ml)$/i.test(input.positional)) {
|
|
3620
|
-
candidates.push(
|
|
3621
|
-
candidates.push(
|
|
3166
|
+
candidates.push(path7.join(plansDir2, `${input.positional}.yaml`));
|
|
3167
|
+
candidates.push(path7.join(plansDir2, `${input.positional}.yml`));
|
|
3622
3168
|
}
|
|
3623
3169
|
}
|
|
3624
3170
|
for (const c2 of candidates) {
|
|
@@ -3652,7 +3198,7 @@ async function resolvePlanPathSmart(input, cwd, readPlanSelection) {
|
|
|
3652
3198
|
}
|
|
3653
3199
|
async function isFile(p) {
|
|
3654
3200
|
try {
|
|
3655
|
-
const st = await
|
|
3201
|
+
const st = await fs7.stat(p);
|
|
3656
3202
|
return st.isFile();
|
|
3657
3203
|
} catch {
|
|
3658
3204
|
return false;
|
|
@@ -3661,7 +3207,7 @@ async function isFile(p) {
|
|
|
3661
3207
|
async function findNewestYaml2(dir) {
|
|
3662
3208
|
let entries;
|
|
3663
3209
|
try {
|
|
3664
|
-
entries = await
|
|
3210
|
+
entries = await fs7.readdir(dir);
|
|
3665
3211
|
} catch {
|
|
3666
3212
|
return null;
|
|
3667
3213
|
}
|
|
@@ -3672,7 +3218,7 @@ async function findNewestYaml2(dir) {
|
|
|
3672
3218
|
let newest = null;
|
|
3673
3219
|
for (const name of yamls) {
|
|
3674
3220
|
try {
|
|
3675
|
-
const st = await
|
|
3221
|
+
const st = await fs7.stat(path7.join(dir, name));
|
|
3676
3222
|
if (newest === null || st.mtimeMs > newest.mtime) {
|
|
3677
3223
|
newest = { name, mtime: st.mtimeMs };
|
|
3678
3224
|
}
|
|
@@ -3680,13 +3226,13 @@ async function findNewestYaml2(dir) {
|
|
|
3680
3226
|
continue;
|
|
3681
3227
|
}
|
|
3682
3228
|
}
|
|
3683
|
-
return newest ?
|
|
3229
|
+
return newest ? path7.join(dir, newest.name) : null;
|
|
3684
3230
|
}
|
|
3685
3231
|
async function defaultReadPlanSelection(cwd) {
|
|
3686
3232
|
const plansDir = await getPlansDir(cwd);
|
|
3687
3233
|
let entries;
|
|
3688
3234
|
try {
|
|
3689
|
-
entries = await
|
|
3235
|
+
entries = await fs7.readdir(plansDir);
|
|
3690
3236
|
} catch {
|
|
3691
3237
|
return void 0;
|
|
3692
3238
|
}
|
|
@@ -3696,9 +3242,9 @@ async function defaultReadPlanSelection(cwd) {
|
|
|
3696
3242
|
if (yamls.length === 0) return void 0;
|
|
3697
3243
|
const stats = await Promise.all(
|
|
3698
3244
|
yamls.map(async (name) => {
|
|
3699
|
-
const full =
|
|
3245
|
+
const full = path7.join(plansDir, name);
|
|
3700
3246
|
try {
|
|
3701
|
-
const st = await
|
|
3247
|
+
const st = await fs7.stat(full);
|
|
3702
3248
|
return { name, full, mtime: st.mtimeMs };
|
|
3703
3249
|
} catch {
|
|
3704
3250
|
return null;
|
|
@@ -3749,6 +3295,13 @@ function relativeTimeFromNow(thenMs) {
|
|
|
3749
3295
|
const d = Math.round(h / 24);
|
|
3750
3296
|
return `${d}d ago`;
|
|
3751
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
|
+
}
|
|
3752
3305
|
function isExitPromptError2(err) {
|
|
3753
3306
|
return err !== null && typeof err === "object" && "name" in err && err.name === "ExitPromptError";
|
|
3754
3307
|
}
|
|
@@ -3772,8 +3325,9 @@ function startStreamingLogger(args) {
|
|
|
3772
3325
|
return `${hh}:${mm}:${ss}`;
|
|
3773
3326
|
};
|
|
3774
3327
|
const write = (line) => {
|
|
3775
|
-
|
|
3776
|
-
|
|
3328
|
+
const msg = `[${formatTs3(clock())}] ${line}
|
|
3329
|
+
`;
|
|
3330
|
+
stderrWriter(msg);
|
|
3777
3331
|
};
|
|
3778
3332
|
const writeRaw = (line) => {
|
|
3779
3333
|
stderrWriter(`${line}
|
|
@@ -3806,6 +3360,11 @@ function startStreamingLogger(args) {
|
|
|
3806
3360
|
write(
|
|
3807
3361
|
`task.verify.failed ${id ?? "?"} attempt ${p.attempt}/${p.of} (${p.command} \u2192 exit ${p.exitCode}${timedOutSuffix})`
|
|
3808
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
|
+
}
|
|
3809
3368
|
} else {
|
|
3810
3369
|
write(`task.verify.failed ${id ?? "?"}`);
|
|
3811
3370
|
}
|
|
@@ -3814,14 +3373,14 @@ function startStreamingLogger(args) {
|
|
|
3814
3373
|
case "task.succeeded": {
|
|
3815
3374
|
succeeded += 1;
|
|
3816
3375
|
const ms = id !== null ? event.ts - (taskStart.get(id) ?? event.ts) : 0;
|
|
3817
|
-
write(`task.succeeded ${id ?? "?"} in ${
|
|
3376
|
+
write(`task.succeeded ${id ?? "?"} in ${formatDuration(ms)}`);
|
|
3818
3377
|
write(`run.progress ${succeeded}/${totalTasks} succeeded`);
|
|
3819
3378
|
break;
|
|
3820
3379
|
}
|
|
3821
3380
|
case "task.failed": {
|
|
3822
3381
|
failed += 1;
|
|
3823
3382
|
const ms = id !== null ? event.ts - (taskStart.get(id) ?? event.ts) : 0;
|
|
3824
|
-
write(`task.failed ${id ?? "?"} in ${
|
|
3383
|
+
write(`task.failed ${id ?? "?"} in ${formatDuration(ms)}`);
|
|
3825
3384
|
const detail = extractPhaseReason(event.payload);
|
|
3826
3385
|
if (detail !== null) {
|
|
3827
3386
|
writeRaw(` \u2192 ${detail.phase}: ${truncate(detail.reason, 200)}`);
|
|
@@ -3886,6 +3445,12 @@ function startStreamingLogger(args) {
|
|
|
3886
3445
|
case "task.touches.violation":
|
|
3887
3446
|
write(`task.touches.violation ${id ?? "?"}`);
|
|
3888
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
|
+
}
|
|
3889
3454
|
// Other kinds (task.session.created, run.*) are intentionally
|
|
3890
3455
|
// suppressed — too chatty for stdout. `pilot logs` carries the
|
|
3891
3456
|
// full trace.
|
|
@@ -3893,8 +3458,39 @@ function startStreamingLogger(args) {
|
|
|
3893
3458
|
break;
|
|
3894
3459
|
}
|
|
3895
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
|
+
}
|
|
3896
3491
|
return () => {
|
|
3897
3492
|
flushBlockedSummary();
|
|
3493
|
+
if (progressPollTimer) clearInterval(progressPollTimer);
|
|
3898
3494
|
unsub();
|
|
3899
3495
|
};
|
|
3900
3496
|
}
|
|
@@ -3915,11 +3511,11 @@ function deriveBranchPrefix(planBranchPrefix, slug, runId) {
|
|
|
3915
3511
|
return `${base}/${runId}`;
|
|
3916
3512
|
}
|
|
3917
3513
|
async function deriveUniqueSlug(plan, planPath, cwd) {
|
|
3918
|
-
const base =
|
|
3514
|
+
const base = path7.basename(planPath, path7.extname(planPath)) || deriveSlug(plan.name);
|
|
3919
3515
|
const dir = await getPlansDir(cwd);
|
|
3920
|
-
const entries = await
|
|
3516
|
+
const entries = await fs7.readdir(dir).catch(() => []);
|
|
3921
3517
|
const existingSlugs = new Set(
|
|
3922
|
-
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)))
|
|
3923
3519
|
);
|
|
3924
3520
|
existingSlugs.delete(base);
|
|
3925
3521
|
return resolveUniqueSlug(base, existingSlugs);
|
|
@@ -3957,14 +3553,14 @@ Failed tasks (${failed.length}):
|
|
|
3957
3553
|
const { phase, reason } = resolveFailureDetail(db, runId, t);
|
|
3958
3554
|
const session = t.session_id ?? "(none \u2014 failed before session.create)";
|
|
3959
3555
|
const worktree = t.worktree_path ?? "(none)";
|
|
3960
|
-
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";
|
|
3961
3557
|
process.stdout.write(
|
|
3962
3558
|
` ${t.task_id}
|
|
3963
3559
|
phase: ${phase}
|
|
3964
3560
|
reason: ${truncateSummary(reason, 300)}
|
|
3965
3561
|
session: ${session}
|
|
3966
3562
|
worktree: ${worktree}
|
|
3967
|
-
elapsed: ${elapsed}
|
|
3563
|
+
elapsed: ${elapsed} attempts: ${t.attempts}
|
|
3968
3564
|
|
|
3969
3565
|
`
|
|
3970
3566
|
);
|
|
@@ -4010,12 +3606,16 @@ async function runCleanup(cleanup) {
|
|
|
4010
3606
|
}
|
|
4011
3607
|
}
|
|
4012
3608
|
|
|
4013
|
-
// src/pilot/cli/
|
|
4014
|
-
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";
|
|
4015
3615
|
|
|
4016
3616
|
// src/pilot/cli/discover.ts
|
|
4017
|
-
import { promises as
|
|
4018
|
-
import * as
|
|
3617
|
+
import { promises as fs8 } from "fs";
|
|
3618
|
+
import * as path8 from "path";
|
|
4019
3619
|
async function discoverRun(args) {
|
|
4020
3620
|
const cwd = args.cwd;
|
|
4021
3621
|
if (args.runId !== void 0 && args.runId.length > 0) {
|
|
@@ -4023,7 +3623,7 @@ async function discoverRun(args) {
|
|
|
4023
3623
|
const cwdDbPath = await getStateDbPath(cwd, args.runId);
|
|
4024
3624
|
tried.push(cwdDbPath);
|
|
4025
3625
|
try {
|
|
4026
|
-
await
|
|
3626
|
+
await fs8.stat(cwdDbPath);
|
|
4027
3627
|
const runDir = await getRunDir(cwd, args.runId);
|
|
4028
3628
|
return { runId: args.runId, dbPath: cwdDbPath, runDir };
|
|
4029
3629
|
} catch {
|
|
@@ -4031,14 +3631,14 @@ async function discoverRun(args) {
|
|
|
4031
3631
|
const base = resolveBaseDir();
|
|
4032
3632
|
let repoFolders;
|
|
4033
3633
|
try {
|
|
4034
|
-
repoFolders = await
|
|
3634
|
+
repoFolders = await fs8.readdir(base);
|
|
4035
3635
|
} catch {
|
|
4036
3636
|
throw new Error(
|
|
4037
3637
|
`pilot: no state.db for run ${JSON.stringify(args.runId)} (looked at ${tried.join(", ")}; base ${base} does not exist)`
|
|
4038
3638
|
);
|
|
4039
3639
|
}
|
|
4040
3640
|
for (const folder of repoFolders) {
|
|
4041
|
-
const candidateDbPath =
|
|
3641
|
+
const candidateDbPath = path8.join(
|
|
4042
3642
|
base,
|
|
4043
3643
|
folder,
|
|
4044
3644
|
"pilot",
|
|
@@ -4048,15 +3648,15 @@ async function discoverRun(args) {
|
|
|
4048
3648
|
);
|
|
4049
3649
|
if (tried.includes(candidateDbPath)) continue;
|
|
4050
3650
|
try {
|
|
4051
|
-
const
|
|
4052
|
-
if (!
|
|
3651
|
+
const stat = await fs8.stat(path8.join(base, folder));
|
|
3652
|
+
if (!stat.isDirectory()) continue;
|
|
4053
3653
|
} catch {
|
|
4054
3654
|
continue;
|
|
4055
3655
|
}
|
|
4056
3656
|
tried.push(candidateDbPath);
|
|
4057
3657
|
try {
|
|
4058
|
-
await
|
|
4059
|
-
const candidateRunDir =
|
|
3658
|
+
await fs8.stat(candidateDbPath);
|
|
3659
|
+
const candidateRunDir = path8.join(
|
|
4060
3660
|
base,
|
|
4061
3661
|
folder,
|
|
4062
3662
|
"pilot",
|
|
@@ -4076,10 +3676,10 @@ async function discoverRun(args) {
|
|
|
4076
3676
|
);
|
|
4077
3677
|
}
|
|
4078
3678
|
const pilot = await getPilotDir(cwd);
|
|
4079
|
-
const runsDir =
|
|
3679
|
+
const runsDir = path8.join(pilot, "runs");
|
|
4080
3680
|
let entries;
|
|
4081
3681
|
try {
|
|
4082
|
-
entries = await
|
|
3682
|
+
entries = await fs8.readdir(runsDir);
|
|
4083
3683
|
} catch {
|
|
4084
3684
|
throw new Error(
|
|
4085
3685
|
`pilot: no runs found at ${runsDir} (run \`pilot build\` first)`
|
|
@@ -4087,10 +3687,10 @@ async function discoverRun(args) {
|
|
|
4087
3687
|
}
|
|
4088
3688
|
let newest = null;
|
|
4089
3689
|
for (const id of entries) {
|
|
4090
|
-
const dbPath =
|
|
3690
|
+
const dbPath = path8.join(runsDir, id, "state.db");
|
|
4091
3691
|
let st;
|
|
4092
3692
|
try {
|
|
4093
|
-
st = await
|
|
3693
|
+
st = await fs8.stat(dbPath);
|
|
4094
3694
|
} catch {
|
|
4095
3695
|
continue;
|
|
4096
3696
|
}
|
|
@@ -4106,200 +3706,316 @@ async function discoverRun(args) {
|
|
|
4106
3706
|
return {
|
|
4107
3707
|
runId: newest.id,
|
|
4108
3708
|
dbPath: newest.dbPath,
|
|
4109
|
-
runDir:
|
|
3709
|
+
runDir: path8.join(runsDir, newest.id)
|
|
4110
3710
|
};
|
|
4111
3711
|
}
|
|
4112
3712
|
|
|
4113
|
-
// src/pilot/cli/
|
|
4114
|
-
var
|
|
4115
|
-
|
|
4116
|
-
|
|
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.",
|
|
4117
3718
|
args: {
|
|
4118
3719
|
run: option3({
|
|
4119
3720
|
long: "run",
|
|
4120
3721
|
type: optional4(string4),
|
|
4121
|
-
description: "Run ID. Defaults to the newest run
|
|
3722
|
+
description: "Run ID to resume. Defaults to the newest resumable run matching --plan (or interactive picker if multiple exist)."
|
|
4122
3723
|
}),
|
|
4123
|
-
|
|
4124
|
-
long: "
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
})
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
discovered = await discoverRun({
|
|
4137
|
-
cwd: process.cwd(),
|
|
4138
|
-
runId: opts.runId
|
|
4139
|
-
});
|
|
4140
|
-
} catch (err) {
|
|
4141
|
-
process.stderr.write(
|
|
4142
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4143
|
-
`
|
|
4144
|
-
);
|
|
4145
|
-
return 1;
|
|
4146
|
-
}
|
|
4147
|
-
const opened = openStateDb(discovered.dbPath);
|
|
4148
|
-
try {
|
|
4149
|
-
const run2 = getRun(opened.db, discovered.runId);
|
|
4150
|
-
if (run2 === null) {
|
|
4151
|
-
process.stderr.write(
|
|
4152
|
-
`pilot status: run ${JSON.stringify(discovered.runId)} not in DB
|
|
4153
|
-
`
|
|
4154
|
-
);
|
|
4155
|
-
return 1;
|
|
4156
|
-
}
|
|
4157
|
-
const tasks = listTasks(opened.db, discovered.runId);
|
|
4158
|
-
const counts = countByStatus(opened.db, discovered.runId);
|
|
4159
|
-
if (opts.json) {
|
|
4160
|
-
process.stdout.write(
|
|
4161
|
-
JSON.stringify({ run: run2, tasks, counts }, null, 2) + "\n"
|
|
4162
|
-
);
|
|
4163
|
-
return 0;
|
|
4164
|
-
}
|
|
4165
|
-
const lines = [];
|
|
4166
|
-
lines.push(`Run ${run2.id}: ${run2.status}`);
|
|
4167
|
-
lines.push(` plan: ${run2.plan_path}`);
|
|
4168
|
-
lines.push(` slug: ${run2.plan_slug}`);
|
|
4169
|
-
lines.push(
|
|
4170
|
-
` started: ${formatTs(run2.started_at)} finished: ${run2.finished_at !== null ? formatTs(run2.finished_at) : "--"}`
|
|
4171
|
-
);
|
|
4172
|
-
lines.push(
|
|
4173
|
-
` counts: succeeded=${counts.succeeded} failed=${counts.failed} blocked=${counts.blocked} aborted=${counts.aborted} pending=${counts.pending} ready=${counts.ready} running=${counts.running}`
|
|
4174
|
-
);
|
|
4175
|
-
lines.push("");
|
|
4176
|
-
lines.push(` Tasks (${tasks.length}):`);
|
|
4177
|
-
for (const t of tasks) {
|
|
4178
|
-
const cost = `$${t.cost_usd.toFixed(2)}`;
|
|
4179
|
-
const branch = t.branch ?? "-";
|
|
4180
|
-
const baseLine = ` ${t.task_id.padEnd(12)} [${t.status.padEnd(9)}] attempts=${t.attempts} cost=${cost} branch=${branch}`;
|
|
4181
|
-
lines.push(baseLine);
|
|
4182
|
-
if (t.last_error) {
|
|
4183
|
-
for (const wrapped of wrap(`last_error: ${t.last_error}`, 76)) {
|
|
4184
|
-
lines.push(` ${wrapped}`);
|
|
4185
|
-
}
|
|
4186
|
-
}
|
|
4187
|
-
}
|
|
4188
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
4189
|
-
return 0;
|
|
4190
|
-
} finally {
|
|
4191
|
-
opened.close();
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4194
|
-
function formatTs(ms) {
|
|
4195
|
-
return new Date(ms).toISOString();
|
|
4196
|
-
}
|
|
4197
|
-
function wrap(text, width) {
|
|
4198
|
-
if (text.length <= width) return [text];
|
|
4199
|
-
const out = [];
|
|
4200
|
-
let cur = "";
|
|
4201
|
-
for (const word of text.split(" ")) {
|
|
4202
|
-
if ((cur + " " + word).trim().length > width) {
|
|
4203
|
-
out.push(cur);
|
|
4204
|
-
cur = word;
|
|
4205
|
-
} else {
|
|
4206
|
-
cur = (cur + " " + word).trim();
|
|
4207
|
-
}
|
|
4208
|
-
}
|
|
4209
|
-
if (cur.length > 0) out.push(cur);
|
|
4210
|
-
return out;
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
// src/pilot/cli/resume.ts
|
|
4214
|
-
import { command as command5, option as option4, optional as optional5, string as string5 } from "cmd-ts";
|
|
4215
|
-
var resumeCmd = command5({
|
|
4216
|
-
name: "resume",
|
|
4217
|
-
description: "Continue a partially-completed pilot run.",
|
|
4218
|
-
args: {
|
|
4219
|
-
run: option4({
|
|
4220
|
-
long: "run",
|
|
4221
|
-
type: optional5(string5),
|
|
4222
|
-
description: "Run ID. Defaults to the newest run."
|
|
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."
|
|
4223
3737
|
})
|
|
4224
3738
|
},
|
|
4225
|
-
handler: async (
|
|
3739
|
+
handler: async (args) => {
|
|
4226
3740
|
await requirePlugin();
|
|
4227
|
-
const code = await
|
|
3741
|
+
const code = await runBuildResume(args);
|
|
4228
3742
|
process.exit(code);
|
|
4229
3743
|
}
|
|
4230
3744
|
});
|
|
4231
|
-
async function
|
|
4232
|
-
|
|
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;
|
|
4233
3751
|
try {
|
|
4234
|
-
|
|
4235
|
-
cwd:
|
|
4236
|
-
runId
|
|
4237
|
-
|
|
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
|
+
}
|
|
4238
3772
|
} catch (err) {
|
|
4239
3773
|
process.stderr.write(
|
|
4240
|
-
|
|
3774
|
+
`pilot build-resume: ${err instanceof Error ? err.message : String(err)}
|
|
4241
3775
|
`
|
|
4242
3776
|
);
|
|
4243
3777
|
return 1;
|
|
4244
3778
|
}
|
|
4245
|
-
const opened = openStateDb(
|
|
4246
|
-
const cleanup = [
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
const run2 = getRun(opened.db, discovered.runId);
|
|
3779
|
+
const opened = openStateDb(dbPath);
|
|
3780
|
+
const cleanup = [];
|
|
3781
|
+
cleanup.push(() => opened.close());
|
|
3782
|
+
const run2 = getRun(opened.db, runId);
|
|
4250
3783
|
if (run2 === null) {
|
|
4251
3784
|
process.stderr.write(
|
|
4252
|
-
`pilot resume:
|
|
3785
|
+
`pilot build-resume: state.db exists at ${dbPath} but has no row for ${runId}
|
|
4253
3786
|
`
|
|
4254
3787
|
);
|
|
4255
3788
|
await runCleanup2(cleanup);
|
|
4256
3789
|
return 1;
|
|
4257
3790
|
}
|
|
4258
|
-
const
|
|
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);
|
|
4259
3803
|
if (!loaded.ok) {
|
|
4260
3804
|
process.stderr.write(
|
|
4261
|
-
`pilot resume:
|
|
3805
|
+
`pilot build-resume: plan at ${planPath} failed to load:
|
|
3806
|
+
` + loaded.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n") + `
|
|
4262
3807
|
`
|
|
4263
3808
|
);
|
|
4264
|
-
|
|
4265
|
-
|
|
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}
|
|
4266
3816
|
`);
|
|
4267
|
-
}
|
|
4268
3817
|
await runCleanup2(cleanup);
|
|
4269
3818
|
return 1;
|
|
4270
3819
|
}
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
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
|
+
`
|
|
4278
3874
|
);
|
|
3875
|
+
await runCleanup2(cleanup);
|
|
3876
|
+
return 1;
|
|
4279
3877
|
}
|
|
4280
3878
|
appendEvent(opened.db, {
|
|
4281
|
-
runId
|
|
3879
|
+
runId,
|
|
4282
3880
|
kind: "run.resumed",
|
|
4283
|
-
payload: {
|
|
3881
|
+
payload: {
|
|
3882
|
+
resetTaskIds: resetIds,
|
|
3883
|
+
skippedSucceeded: counts.succeeded
|
|
3884
|
+
}
|
|
4284
3885
|
});
|
|
4285
|
-
|
|
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);
|
|
4286
3893
|
return executeRun({
|
|
4287
3894
|
db: opened,
|
|
4288
|
-
runId
|
|
4289
|
-
plan
|
|
4290
|
-
planPath
|
|
3895
|
+
runId,
|
|
3896
|
+
plan,
|
|
3897
|
+
planPath,
|
|
4291
3898
|
runDir,
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
run2.plan_slug,
|
|
4298
|
-
discovered.runId
|
|
4299
|
-
),
|
|
4300
|
-
cleanup
|
|
3899
|
+
branchPrefix,
|
|
3900
|
+
cleanup,
|
|
3901
|
+
opencodePort: opts.opencodePort,
|
|
3902
|
+
quiet: opts.quiet,
|
|
3903
|
+
stderrWriter
|
|
4301
3904
|
});
|
|
4302
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
|
+
}
|
|
4303
4019
|
async function runCleanup2(cleanup) {
|
|
4304
4020
|
while (cleanup.length > 0) {
|
|
4305
4021
|
const fn = cleanup.pop();
|
|
@@ -4310,34 +4026,28 @@ async function runCleanup2(cleanup) {
|
|
|
4310
4026
|
}
|
|
4311
4027
|
}
|
|
4312
4028
|
|
|
4313
|
-
// src/pilot/cli/
|
|
4314
|
-
import { command as
|
|
4315
|
-
var
|
|
4316
|
-
name: "
|
|
4317
|
-
description: "
|
|
4029
|
+
// src/pilot/cli/status.ts
|
|
4030
|
+
import { command as command5, flag as flag4, option as option4, optional as optional5, string as string5 } from "cmd-ts";
|
|
4031
|
+
var statusCmd = command5({
|
|
4032
|
+
name: "status",
|
|
4033
|
+
description: "Print the run + task status for a pilot run.",
|
|
4318
4034
|
args: {
|
|
4319
|
-
|
|
4320
|
-
type: string6,
|
|
4321
|
-
displayName: "task-id",
|
|
4322
|
-
description: "Task id to reset (e.g. T1)."
|
|
4323
|
-
}),
|
|
4324
|
-
run: option5({
|
|
4035
|
+
run: option4({
|
|
4325
4036
|
long: "run",
|
|
4326
|
-
type:
|
|
4327
|
-
description: "Run ID. Defaults to the newest run."
|
|
4037
|
+
type: optional5(string5),
|
|
4038
|
+
description: "Run ID. Defaults to the newest run with a state.db."
|
|
4328
4039
|
}),
|
|
4329
|
-
|
|
4330
|
-
long: "
|
|
4331
|
-
description: "
|
|
4040
|
+
json: flag4({
|
|
4041
|
+
long: "json",
|
|
4042
|
+
description: "Emit JSON instead of human-readable text."
|
|
4332
4043
|
})
|
|
4333
4044
|
},
|
|
4334
|
-
handler: async ({
|
|
4335
|
-
await
|
|
4336
|
-
const code = await runRetry({ taskId, runId: run2, runNow });
|
|
4045
|
+
handler: async ({ run: run2, json }) => {
|
|
4046
|
+
const code = await runStatus({ runId: run2, json });
|
|
4337
4047
|
process.exit(code);
|
|
4338
4048
|
}
|
|
4339
4049
|
});
|
|
4340
|
-
async function
|
|
4050
|
+
async function runStatus(opts) {
|
|
4341
4051
|
let discovered;
|
|
4342
4052
|
try {
|
|
4343
4053
|
discovered = await discoverRun({
|
|
@@ -4353,56 +4063,83 @@ async function runRetry(opts) {
|
|
|
4353
4063
|
}
|
|
4354
4064
|
const opened = openStateDb(discovered.dbPath);
|
|
4355
4065
|
try {
|
|
4356
|
-
const
|
|
4357
|
-
if (
|
|
4066
|
+
const run2 = getRun(opened.db, discovered.runId);
|
|
4067
|
+
if (run2 === null) {
|
|
4358
4068
|
process.stderr.write(
|
|
4359
|
-
`pilot
|
|
4069
|
+
`pilot status: run ${JSON.stringify(discovered.runId)} not in DB
|
|
4360
4070
|
`
|
|
4361
4071
|
);
|
|
4362
4072
|
return 1;
|
|
4363
4073
|
}
|
|
4364
|
-
const
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
`pilot retry: ${err instanceof Error ? err.message : String(err)}
|
|
4370
|
-
`
|
|
4074
|
+
const tasks = listTasks(opened.db, discovered.runId);
|
|
4075
|
+
const counts = countByStatus(opened.db, discovered.runId);
|
|
4076
|
+
if (opts.json) {
|
|
4077
|
+
process.stdout.write(
|
|
4078
|
+
JSON.stringify({ run: run2, tasks, counts }, null, 2) + "\n"
|
|
4371
4079
|
);
|
|
4372
|
-
return
|
|
4080
|
+
return 0;
|
|
4373
4081
|
}
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
process.stdout.write(
|
|
4381
|
-
`pilot retry: ${opts.taskId} reset to pending (was ${previousStatus})
|
|
4382
|
-
`
|
|
4082
|
+
const lines = [];
|
|
4083
|
+
lines.push(`Run ${run2.id}: ${run2.status}`);
|
|
4084
|
+
lines.push(` plan: ${run2.plan_path}`);
|
|
4085
|
+
lines.push(` slug: ${run2.plan_slug}`);
|
|
4086
|
+
lines.push(
|
|
4087
|
+
` started: ${formatTs(run2.started_at)} finished: ${run2.finished_at !== null ? formatTs(run2.finished_at) : "--"}`
|
|
4383
4088
|
);
|
|
4089
|
+
lines.push(
|
|
4090
|
+
` counts: succeeded=${counts.succeeded} failed=${counts.failed} blocked=${counts.blocked} aborted=${counts.aborted} pending=${counts.pending} ready=${counts.ready} running=${counts.running}`
|
|
4091
|
+
);
|
|
4092
|
+
lines.push("");
|
|
4093
|
+
lines.push(` Tasks (${tasks.length}):`);
|
|
4094
|
+
for (const t of tasks) {
|
|
4095
|
+
const cost = `$${t.cost_usd.toFixed(2)}`;
|
|
4096
|
+
const branch = t.branch ?? "-";
|
|
4097
|
+
const baseLine = ` ${t.task_id.padEnd(12)} [${t.status.padEnd(9)}] attempts=${t.attempts} cost=${cost} branch=${branch}`;
|
|
4098
|
+
lines.push(baseLine);
|
|
4099
|
+
if (t.last_error) {
|
|
4100
|
+
for (const wrapped of wrap(`last_error: ${t.last_error}`, 76)) {
|
|
4101
|
+
lines.push(` ${wrapped}`);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
4106
|
+
return 0;
|
|
4384
4107
|
} finally {
|
|
4385
4108
|
opened.close();
|
|
4386
4109
|
}
|
|
4387
|
-
|
|
4388
|
-
|
|
4110
|
+
}
|
|
4111
|
+
function formatTs(ms) {
|
|
4112
|
+
return new Date(ms).toISOString();
|
|
4113
|
+
}
|
|
4114
|
+
function wrap(text, width) {
|
|
4115
|
+
if (text.length <= width) return [text];
|
|
4116
|
+
const out = [];
|
|
4117
|
+
let cur = "";
|
|
4118
|
+
for (const word of text.split(" ")) {
|
|
4119
|
+
if ((cur + " " + word).trim().length > width) {
|
|
4120
|
+
out.push(cur);
|
|
4121
|
+
cur = word;
|
|
4122
|
+
} else {
|
|
4123
|
+
cur = (cur + " " + word).trim();
|
|
4124
|
+
}
|
|
4389
4125
|
}
|
|
4390
|
-
|
|
4126
|
+
if (cur.length > 0) out.push(cur);
|
|
4127
|
+
return out;
|
|
4391
4128
|
}
|
|
4392
4129
|
|
|
4393
4130
|
// src/pilot/cli/logs.ts
|
|
4394
|
-
import { command as
|
|
4395
|
-
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({
|
|
4396
4133
|
name: "logs",
|
|
4397
4134
|
description: "Print structured events for a task.",
|
|
4398
4135
|
args: {
|
|
4399
|
-
taskId:
|
|
4400
|
-
type:
|
|
4136
|
+
taskId: positional4({
|
|
4137
|
+
type: string6,
|
|
4401
4138
|
displayName: "task-id"
|
|
4402
4139
|
}),
|
|
4403
|
-
run:
|
|
4140
|
+
run: option5({
|
|
4404
4141
|
long: "run",
|
|
4405
|
-
type:
|
|
4142
|
+
type: optional6(string6),
|
|
4406
4143
|
description: "Run ID. Defaults to the newest run."
|
|
4407
4144
|
}),
|
|
4408
4145
|
json: flag5({
|
|
@@ -4522,172 +4259,18 @@ function summarizePayload(kind, payload) {
|
|
|
4522
4259
|
return s;
|
|
4523
4260
|
}
|
|
4524
4261
|
|
|
4525
|
-
// src/pilot/cli/worktrees.ts
|
|
4526
|
-
import { command as command8, flag as flag6, option as option7, optional as optional8, string as string8, subcommands } from "cmd-ts";
|
|
4527
|
-
import { promises as fs12 } from "fs";
|
|
4528
|
-
import * as path11 from "path";
|
|
4529
|
-
var listSubcmd = command8({
|
|
4530
|
-
name: "list",
|
|
4531
|
-
description: "List worktrees registered with the repo (filter to pilot ones).",
|
|
4532
|
-
args: {
|
|
4533
|
-
run: option7({
|
|
4534
|
-
long: "run",
|
|
4535
|
-
type: optional8(string8),
|
|
4536
|
-
description: "Run ID for context. Defaults to the newest run."
|
|
4537
|
-
})
|
|
4538
|
-
},
|
|
4539
|
-
handler: async ({ run: run2 }) => {
|
|
4540
|
-
const code = await runWorktreesList({ runId: run2 });
|
|
4541
|
-
process.exit(code);
|
|
4542
|
-
}
|
|
4543
|
-
});
|
|
4544
|
-
var pruneSubcmd = command8({
|
|
4545
|
-
name: "prune",
|
|
4546
|
-
description: "Remove worktrees from succeeded tasks (default) or all (--all).",
|
|
4547
|
-
args: {
|
|
4548
|
-
run: option7({
|
|
4549
|
-
long: "run",
|
|
4550
|
-
type: optional8(string8),
|
|
4551
|
-
description: "Run ID. Defaults to the newest run."
|
|
4552
|
-
}),
|
|
4553
|
-
all: flag6({
|
|
4554
|
-
long: "all",
|
|
4555
|
-
description: "Remove every pilot worktree for this run, even failed/aborted ones."
|
|
4556
|
-
}),
|
|
4557
|
-
dryRun: flag6({
|
|
4558
|
-
long: "dry-run",
|
|
4559
|
-
description: "Print what would be removed without removing."
|
|
4560
|
-
})
|
|
4561
|
-
},
|
|
4562
|
-
handler: async ({ run: run2, all, dryRun }) => {
|
|
4563
|
-
const code = await runWorktreesPrune({ runId: run2, all, dryRun });
|
|
4564
|
-
process.exit(code);
|
|
4565
|
-
}
|
|
4566
|
-
});
|
|
4567
|
-
var worktreesCmd = subcommands({
|
|
4568
|
-
name: "worktrees",
|
|
4569
|
-
description: "Inspect and prune pilot-managed git worktrees.",
|
|
4570
|
-
cmds: {
|
|
4571
|
-
list: listSubcmd,
|
|
4572
|
-
prune: pruneSubcmd
|
|
4573
|
-
}
|
|
4574
|
-
});
|
|
4575
|
-
async function runWorktreesList(opts) {
|
|
4576
|
-
let discovered;
|
|
4577
|
-
try {
|
|
4578
|
-
discovered = await discoverRun({
|
|
4579
|
-
cwd: process.cwd(),
|
|
4580
|
-
runId: opts.runId
|
|
4581
|
-
});
|
|
4582
|
-
} catch (err) {
|
|
4583
|
-
process.stderr.write(
|
|
4584
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4585
|
-
`
|
|
4586
|
-
);
|
|
4587
|
-
return 1;
|
|
4588
|
-
}
|
|
4589
|
-
const all = await gitWorktreeList(process.cwd());
|
|
4590
|
-
const wtBase = path11.join(discovered.runDir, "..");
|
|
4591
|
-
const pilotDir = path11.dirname(path11.dirname(discovered.runDir));
|
|
4592
|
-
const wtPrefix = path11.join(pilotDir, "worktrees", discovered.runId);
|
|
4593
|
-
const filtered = all.filter((w) => w.path.startsWith(wtPrefix));
|
|
4594
|
-
void wtBase;
|
|
4595
|
-
if (filtered.length === 0) {
|
|
4596
|
-
process.stdout.write(
|
|
4597
|
-
`pilot worktrees list: no pilot worktrees for run ${discovered.runId}
|
|
4598
|
-
`
|
|
4599
|
-
);
|
|
4600
|
-
return 0;
|
|
4601
|
-
}
|
|
4602
|
-
for (const w of filtered) {
|
|
4603
|
-
process.stdout.write(
|
|
4604
|
-
`${w.path} ${w.head.slice(0, 7)} ${w.branch ?? "(detached)"}
|
|
4605
|
-
`
|
|
4606
|
-
);
|
|
4607
|
-
}
|
|
4608
|
-
return 0;
|
|
4609
|
-
}
|
|
4610
|
-
async function runWorktreesPrune(opts) {
|
|
4611
|
-
let discovered;
|
|
4612
|
-
try {
|
|
4613
|
-
discovered = await discoverRun({
|
|
4614
|
-
cwd: process.cwd(),
|
|
4615
|
-
runId: opts.runId
|
|
4616
|
-
});
|
|
4617
|
-
} catch (err) {
|
|
4618
|
-
process.stderr.write(
|
|
4619
|
-
`${err instanceof Error ? err.message : String(err)}
|
|
4620
|
-
`
|
|
4621
|
-
);
|
|
4622
|
-
return 1;
|
|
4623
|
-
}
|
|
4624
|
-
const opened = openStateDb(discovered.dbPath);
|
|
4625
|
-
let candidates;
|
|
4626
|
-
try {
|
|
4627
|
-
const tasks = listTasks(opened.db, discovered.runId);
|
|
4628
|
-
const run2 = getRun(opened.db, discovered.runId);
|
|
4629
|
-
if (opts.all) {
|
|
4630
|
-
candidates = tasks.map((t) => t.worktree_path).filter((p) => p !== null);
|
|
4631
|
-
} else {
|
|
4632
|
-
const safeStatuses = run2?.status === "completed";
|
|
4633
|
-
candidates = tasks.filter((t) => safeStatuses && t.status === "succeeded").map((t) => t.worktree_path).filter((p) => p !== null);
|
|
4634
|
-
}
|
|
4635
|
-
} finally {
|
|
4636
|
-
opened.close();
|
|
4637
|
-
}
|
|
4638
|
-
const uniq = [...new Set(candidates)];
|
|
4639
|
-
if (uniq.length === 0) {
|
|
4640
|
-
process.stdout.write(
|
|
4641
|
-
`pilot worktrees prune: nothing to prune for run ${discovered.runId}` + (opts.all ? "" : " (use --all to force)") + "\n"
|
|
4642
|
-
);
|
|
4643
|
-
return 0;
|
|
4644
|
-
}
|
|
4645
|
-
if (opts.dryRun) {
|
|
4646
|
-
process.stdout.write("Would remove:\n");
|
|
4647
|
-
for (const p of uniq) process.stdout.write(` ${p}
|
|
4648
|
-
`);
|
|
4649
|
-
return 0;
|
|
4650
|
-
}
|
|
4651
|
-
let removed = 0;
|
|
4652
|
-
let errors = 0;
|
|
4653
|
-
for (const p of uniq) {
|
|
4654
|
-
try {
|
|
4655
|
-
await gitWorktreeRemove({
|
|
4656
|
-
repoPath: process.cwd(),
|
|
4657
|
-
worktreePath: p
|
|
4658
|
-
});
|
|
4659
|
-
try {
|
|
4660
|
-
await fs12.rm(p, { recursive: true, force: true });
|
|
4661
|
-
} catch {
|
|
4662
|
-
}
|
|
4663
|
-
removed++;
|
|
4664
|
-
} catch (err) {
|
|
4665
|
-
errors++;
|
|
4666
|
-
process.stderr.write(
|
|
4667
|
-
`pilot worktrees prune: failed to remove ${p}: ${err instanceof Error ? err.message : String(err)}
|
|
4668
|
-
`
|
|
4669
|
-
);
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
process.stdout.write(
|
|
4673
|
-
`pilot worktrees prune: removed ${removed}/${uniq.length} (${errors} errors)
|
|
4674
|
-
`
|
|
4675
|
-
);
|
|
4676
|
-
return errors > 0 ? 1 : 0;
|
|
4677
|
-
}
|
|
4678
|
-
|
|
4679
4262
|
// src/pilot/cli/cost.ts
|
|
4680
|
-
import { command as
|
|
4681
|
-
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({
|
|
4682
4265
|
name: "cost",
|
|
4683
4266
|
description: "Print per-task and total cost for a run.",
|
|
4684
4267
|
args: {
|
|
4685
|
-
run:
|
|
4268
|
+
run: option6({
|
|
4686
4269
|
long: "run",
|
|
4687
|
-
type:
|
|
4270
|
+
type: optional7(string7),
|
|
4688
4271
|
description: "Run ID. Defaults to the newest run."
|
|
4689
4272
|
}),
|
|
4690
|
-
json:
|
|
4273
|
+
json: flag6({
|
|
4691
4274
|
long: "json",
|
|
4692
4275
|
description: "Emit JSON instead of human-readable text."
|
|
4693
4276
|
})
|
|
@@ -4749,8 +4332,8 @@ async function runCost(opts) {
|
|
|
4749
4332
|
}
|
|
4750
4333
|
|
|
4751
4334
|
// src/pilot/cli/plan-dir.ts
|
|
4752
|
-
import { command as
|
|
4753
|
-
var planDirCmd =
|
|
4335
|
+
import { command as command8 } from "cmd-ts";
|
|
4336
|
+
var planDirCmd = command8({
|
|
4754
4337
|
name: "plan-dir",
|
|
4755
4338
|
description: "Print the pilot plans directory for the current worktree (creates it if missing).",
|
|
4756
4339
|
args: {},
|
|
@@ -4770,29 +4353,27 @@ var planDirCmd = command10({
|
|
|
4770
4353
|
});
|
|
4771
4354
|
|
|
4772
4355
|
// src/pilot/cli/index.ts
|
|
4773
|
-
var pilotSubcommand =
|
|
4356
|
+
var pilotSubcommand = subcommands({
|
|
4774
4357
|
name: "pilot",
|
|
4775
4358
|
description: "Pilot subsystem \u2014 plan, validate, build, and manage unattended task runs.",
|
|
4776
4359
|
cmds: {
|
|
4777
4360
|
validate: validateCmd,
|
|
4778
4361
|
plan: planCmd,
|
|
4779
4362
|
build: buildCmd,
|
|
4363
|
+
"build-resume": buildResumeCmd,
|
|
4780
4364
|
status: statusCmd,
|
|
4781
|
-
resume: resumeCmd,
|
|
4782
|
-
retry: retryCmd,
|
|
4783
4365
|
logs: logsCmd,
|
|
4784
|
-
worktrees: worktreesCmd,
|
|
4785
4366
|
cost: costCmd,
|
|
4786
4367
|
"plan-dir": planDirCmd
|
|
4787
4368
|
}
|
|
4788
4369
|
});
|
|
4789
4370
|
|
|
4790
4371
|
// src/cli/cli-update.ts
|
|
4791
|
-
import * as
|
|
4792
|
-
import * as
|
|
4793
|
-
import * as
|
|
4372
|
+
import * as fs10 from "fs";
|
|
4373
|
+
import * as path10 from "path";
|
|
4374
|
+
import * as os3 from "os";
|
|
4794
4375
|
import { spawn as spawn3 } from "child_process";
|
|
4795
|
-
import { fileURLToPath as
|
|
4376
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4796
4377
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
4797
4378
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4798
4379
|
var c = {
|
|
@@ -4817,12 +4398,12 @@ function isMajorBump(current, latest) {
|
|
|
4817
4398
|
return latest.major > current.major;
|
|
4818
4399
|
}
|
|
4819
4400
|
function getStateFilePath() {
|
|
4820
|
-
const cacheHome = process.env["XDG_CACHE_HOME"] ??
|
|
4821
|
-
return
|
|
4401
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path10.join(os3.homedir(), ".cache");
|
|
4402
|
+
return path10.join(cacheHome, "harness-opencode", "cli-update.json");
|
|
4822
4403
|
}
|
|
4823
4404
|
function readState() {
|
|
4824
4405
|
try {
|
|
4825
|
-
const raw =
|
|
4406
|
+
const raw = fs10.readFileSync(getStateFilePath(), "utf8");
|
|
4826
4407
|
return JSON.parse(raw);
|
|
4827
4408
|
} catch {
|
|
4828
4409
|
return null;
|
|
@@ -4831,21 +4412,21 @@ function readState() {
|
|
|
4831
4412
|
function writeState(state) {
|
|
4832
4413
|
try {
|
|
4833
4414
|
const statePath = getStateFilePath();
|
|
4834
|
-
|
|
4835
|
-
|
|
4415
|
+
fs10.mkdirSync(path10.dirname(statePath), { recursive: true });
|
|
4416
|
+
fs10.writeFileSync(statePath, JSON.stringify(state));
|
|
4836
4417
|
} catch {
|
|
4837
4418
|
}
|
|
4838
4419
|
}
|
|
4839
4420
|
function readInstalledVersion() {
|
|
4840
|
-
const here =
|
|
4421
|
+
const here = path10.dirname(fileURLToPath3(import.meta.url));
|
|
4841
4422
|
const candidates = [
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4423
|
+
path10.join(here, "..", "package.json"),
|
|
4424
|
+
path10.join(here, "..", "..", "package.json"),
|
|
4425
|
+
path10.join(here, "package.json")
|
|
4845
4426
|
];
|
|
4846
4427
|
for (const candidate of candidates) {
|
|
4847
4428
|
try {
|
|
4848
|
-
const raw =
|
|
4429
|
+
const raw = fs10.readFileSync(candidate, "utf8");
|
|
4849
4430
|
const parsed = JSON.parse(raw);
|
|
4850
4431
|
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
4851
4432
|
return parsed.version;
|
|
@@ -4971,15 +4552,15 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
|
|
|
4971
4552
|
}
|
|
4972
4553
|
}
|
|
4973
4554
|
var VERSION = "0.1.0";
|
|
4974
|
-
var installCmd =
|
|
4555
|
+
var installCmd = command9({
|
|
4975
4556
|
name: "install",
|
|
4976
4557
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
4977
4558
|
args: {
|
|
4978
|
-
dryRun:
|
|
4559
|
+
dryRun: flag7({
|
|
4979
4560
|
long: "dry-run",
|
|
4980
4561
|
description: "Preview changes without writing."
|
|
4981
4562
|
}),
|
|
4982
|
-
pin:
|
|
4563
|
+
pin: flag7({
|
|
4983
4564
|
long: "pin",
|
|
4984
4565
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
4985
4566
|
})
|
|
@@ -4988,11 +4569,11 @@ var installCmd = command11({
|
|
|
4988
4569
|
await install({ dryRun, pin });
|
|
4989
4570
|
}
|
|
4990
4571
|
});
|
|
4991
|
-
var uninstallCmd =
|
|
4572
|
+
var uninstallCmd = command9({
|
|
4992
4573
|
name: "uninstall",
|
|
4993
4574
|
description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
|
|
4994
4575
|
args: {
|
|
4995
|
-
dryRun:
|
|
4576
|
+
dryRun: flag7({
|
|
4996
4577
|
long: "dry-run",
|
|
4997
4578
|
description: "Preview changes without writing."
|
|
4998
4579
|
})
|
|
@@ -5001,7 +4582,7 @@ var uninstallCmd = command11({
|
|
|
5001
4582
|
uninstall({ dryRun });
|
|
5002
4583
|
}
|
|
5003
4584
|
});
|
|
5004
|
-
var doctorCmd =
|
|
4585
|
+
var doctorCmd = command9({
|
|
5005
4586
|
name: "doctor",
|
|
5006
4587
|
description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
|
|
5007
4588
|
args: {},
|
|
@@ -5009,22 +4590,22 @@ var doctorCmd = command11({
|
|
|
5009
4590
|
doctor();
|
|
5010
4591
|
}
|
|
5011
4592
|
});
|
|
5012
|
-
var planCheckCmd =
|
|
4593
|
+
var planCheckCmd = command9({
|
|
5013
4594
|
name: "plan-check",
|
|
5014
4595
|
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
5015
4596
|
args: {
|
|
5016
|
-
run:
|
|
4597
|
+
run: option7({
|
|
5017
4598
|
long: "run",
|
|
5018
|
-
type:
|
|
4599
|
+
type: optional8(string8),
|
|
5019
4600
|
description: "Print verify commands for pending items, one per line."
|
|
5020
4601
|
}),
|
|
5021
|
-
check:
|
|
4602
|
+
check: option7({
|
|
5022
4603
|
long: "check",
|
|
5023
|
-
type:
|
|
4604
|
+
type: optional8(string8),
|
|
5024
4605
|
description: "Structural validation; exits 1 if any item is invalid."
|
|
5025
4606
|
}),
|
|
5026
4607
|
rest: restPositionals({
|
|
5027
|
-
type:
|
|
4608
|
+
type: string8,
|
|
5028
4609
|
displayName: "plan-path",
|
|
5029
4610
|
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
5030
4611
|
})
|
|
@@ -5041,7 +4622,7 @@ var planCheckCmd = command11({
|
|
|
5041
4622
|
planCheck(legacy);
|
|
5042
4623
|
}
|
|
5043
4624
|
});
|
|
5044
|
-
var planDirCmd2 =
|
|
4625
|
+
var planDirCmd2 = command9({
|
|
5045
4626
|
name: "plan-dir",
|
|
5046
4627
|
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
5047
4628
|
args: {},
|
|
@@ -5060,15 +4641,15 @@ var planDirCmd2 = command11({
|
|
|
5060
4641
|
}
|
|
5061
4642
|
}
|
|
5062
4643
|
});
|
|
5063
|
-
var installPluginCmd =
|
|
4644
|
+
var installPluginCmd = command9({
|
|
5064
4645
|
name: "install-plugin",
|
|
5065
4646
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
5066
4647
|
args: {
|
|
5067
|
-
dryRun:
|
|
4648
|
+
dryRun: flag7({
|
|
5068
4649
|
long: "dry-run",
|
|
5069
4650
|
description: "Preview changes without writing."
|
|
5070
4651
|
}),
|
|
5071
|
-
pin:
|
|
4652
|
+
pin: flag7({
|
|
5072
4653
|
long: "pin",
|
|
5073
4654
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
5074
4655
|
})
|
|
@@ -5077,7 +4658,7 @@ var installPluginCmd = command11({
|
|
|
5077
4658
|
await install({ dryRun, pin });
|
|
5078
4659
|
}
|
|
5079
4660
|
});
|
|
5080
|
-
var cli =
|
|
4661
|
+
var cli = subcommands2({
|
|
5081
4662
|
name: "glrs-oc",
|
|
5082
4663
|
description: "OpenCode agent harness CLI.",
|
|
5083
4664
|
version: VERSION,
|