@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +29 -4
  3. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +26 -1
  4. package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +37 -0
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +33 -0
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +32 -0
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +15 -20
  8. package/dist/vendor/harness-opencode/dist/chunk-57EOY72Y.js +174 -0
  9. package/dist/vendor/harness-opencode/dist/chunk-5TAMY7P6.js +67 -0
  10. package/dist/vendor/harness-opencode/dist/chunk-BKTFWXLG.js +204 -0
  11. package/dist/vendor/harness-opencode/dist/{chunk-XCZ3NOXR.js → chunk-CZMAJISX.js} +28 -0
  12. package/dist/vendor/harness-opencode/dist/chunk-KB7M7JXU.js +145 -0
  13. package/dist/vendor/harness-opencode/dist/chunk-RNRCXQ65.js +56 -0
  14. package/dist/vendor/harness-opencode/dist/{chunk-VVMP6QWS.js → chunk-WBBN7OVN.js} +162 -2
  15. package/dist/vendor/harness-opencode/dist/cli.js +964 -1383
  16. package/dist/vendor/harness-opencode/dist/index.js +2 -2
  17. package/dist/vendor/harness-opencode/dist/install-X5KEANRB.js +13 -0
  18. package/dist/vendor/harness-opencode/dist/paths-LT3QQKCF.js +18 -0
  19. package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.d.ts +1 -0
  20. package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.js +228 -0
  21. package/dist/vendor/harness-opencode/dist/pilot-config-7LJZ23YK.js +55 -0
  22. package/dist/vendor/harness-opencode/dist/runs-QWPL3TKV.js +18 -0
  23. package/dist/vendor/harness-opencode/dist/safety-gate-WM3EWOCY.js +10 -0
  24. package/dist/vendor/harness-opencode/dist/setup-hook-FHTXMAQL.js +88 -0
  25. package/dist/vendor/harness-opencode/dist/skills/adr/SKILL.md +328 -0
  26. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/SKILL.md +41 -10
  27. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +27 -0
  28. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/qa-expectations.md +120 -0
  29. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/self-review.md +1 -1
  30. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/touches-scope.md +34 -0
  31. package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/verify-design.md +81 -13
  32. package/dist/vendor/harness-opencode/dist/tasks-KJ3WN2KY.js +32 -0
  33. package/dist/vendor/harness-opencode/package.json +1 -1
  34. package/package.json +1 -1
  35. 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-XCZ3NOXR.js";
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-VVMP6QWS.js";
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 command11,
16
- flag as flag8,
17
- option as option9,
18
- optional as optional10,
19
- positional as positional6,
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 string10,
22
- subcommands as subcommands3,
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(command12) {
159
+ function cmd(command10) {
119
160
  try {
120
- return execSync(command12, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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 as subcommands2 } from "cmd-ts";
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
- }).strict();
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 = path4.resolve(absPath);
502
+ const resolved = path3.resolve(absPath);
550
503
  let raw;
551
504
  try {
552
- raw = await fs4.readFile(resolved, "utf8");
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 = path6.resolve(input);
985
- let stat2;
845
+ const resolved = path4.resolve(input);
846
+ let stat;
986
847
  try {
987
- stat2 = await fs6.stat(resolved);
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 (stat2.isFile()) return resolved;
994
- if (stat2.isDirectory()) return findNewestYaml(resolved);
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 fs6.readdir(dir);
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 = path6.join(dir, name);
1020
- let stat2;
880
+ const full = path4.join(dir, name);
881
+ let stat;
1021
882
  try {
1022
- stat2 = await fs6.stat(full);
883
+ stat = await fs4.stat(full);
1023
884
  } catch {
1024
885
  continue;
1025
886
  }
1026
- const mtime = stat2.mtimeMs;
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 path6.join(dir, newest.name);
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 fs7 } from "fs";
1041
- import * as path7 from "path";
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 fs7.readdir(dir);
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 = path7.join(dir, name);
1002
+ const full = path5.join(dir, name);
1142
1003
  try {
1143
- const st = await fs7.stat(full);
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((resolve5) => {
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) => resolve5(code ?? 1));
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
- resolve5(1);
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 path9 from "path";
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(path13) {
1381
- const db = new Database(path13, { create: true });
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 (path13 !== ":memory:") {
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(path13)}: ${err instanceof Error ? err.message : String(err)}`
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 as execFile2 } from "child_process";
1647
- import * as fs8 from "fs";
1648
- import * as path8 from "path";
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
- fs8.mkdirSync(path8.dirname(options.serverLogPath), { recursive: true });
1678
- fs8.writeFileSync(
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
- return {
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((resolve5, reject) => {
1439
+ await new Promise((resolve6, reject) => {
1729
1440
  const controller = new AbortController();
1730
1441
  const timer = setTimeout(() => controller.abort(), 5e3);
1731
- execFile2(
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
- resolve5();
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((resolve5) => {
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
- resolve5(result);
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/worktree/git.ts
1965
- import { execFile as execFile3 } from "child_process";
1966
- function execFileP2(file, args, opts = {}) {
1967
- const { cwd, timeoutMs = 3e4, env } = opts;
1968
- return new Promise((resolve5, reject) => {
1969
- const controller = new AbortController();
1970
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1971
- execFile3(
1972
- file,
1973
- args,
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 command12 of commands) {
2549
- const result = await runOne(command12, options);
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(command12, options) {
2561
- if (typeof command12 !== "string" || command12.length === 0) {
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", command12], {
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: command12 });
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((resolve5) => {
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
- resolve5({ code: code2, signal: signal2 });
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: command12 });
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: command12,
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: command12,
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.worktree, args.sinceSha);
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
- return { ok: false, changed, violators: [...changed] };
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([...args.allowed], { dot: true });
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 ?? 3;
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
- const cwd = process.cwd();
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 slot;
2933
- let prepared;
2396
+ let sinceSha;
2934
2397
  try {
2935
- slot = deps.pool.acquire();
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 = `worktree prepare failed: ${errorMessage2(err)}`;
2944
- try {
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: "prepare", reason: reason2 }
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: prepared.path }
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 bus = deps.busFactory(prepared.path);
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: prepared.branch,
3008
- worktreePath: prepared.path
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: prepared.branch, worktreePath: prepared.path }
2488
+ payload: { sessionId, branch: "", worktreePath: cwd }
3028
2489
  });
3029
2490
  const ctx = {
3030
2491
  planName: deps.plan.name,
3031
- branch: prepared.branch,
3032
- worktreePath: prepared.path,
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
- deps.pool.preserveOnFailure(slot);
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: prepared.path },
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
- deps.pool.preserveOnFailure(slot);
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
- deps.pool.preserveOnFailure(slot);
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: prepared.path,
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
- worktree: prepared.path,
3223
- sinceSha: prepared.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
- worktree: prepared.path,
3268
- message: commitMessage,
3269
- authorName: deps.authorName,
3270
- authorEmail: deps.authorEmail
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 reason2 = `commit failed: ${errorMessage2(err)}`;
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 fs10 } from "fs";
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: ulid2 } = await import("ulid");
3477
- const runId = ulid2();
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 = path9.isAbsolute(input.flag) ? input.flag : path9.resolve(cwd, input.flag);
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 (path9.isAbsolute(input.positional)) {
3160
+ if (path7.isAbsolute(input.positional)) {
3615
3161
  candidates.push(input.positional);
3616
3162
  } else {
3617
- candidates.push(path9.resolve(cwd, input.positional));
3618
- candidates.push(path9.join(plansDir2, input.positional));
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(path9.join(plansDir2, `${input.positional}.yaml`));
3621
- candidates.push(path9.join(plansDir2, `${input.positional}.yml`));
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 fs10.stat(p);
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 fs10.readdir(dir);
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 fs10.stat(path9.join(dir, name));
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 ? path9.join(dir, newest.name) : null;
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 fs10.readdir(plansDir);
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 = path9.join(plansDir, name);
3245
+ const full = path7.join(plansDir, name);
3700
3246
  try {
3701
- const st = await fs10.stat(full);
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
- stderrWriter(`[${formatTs3(clock())}] ${line}
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 ${Math.round(ms / 1e3)}s`);
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 ${Math.round(ms / 1e3)}s`);
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 = path9.basename(planPath, path9.extname(planPath)) || deriveSlug(plan.name);
3514
+ const base = path7.basename(planPath, path7.extname(planPath)) || deriveSlug(plan.name);
3919
3515
  const dir = await getPlansDir(cwd);
3920
- const entries = await fs10.readdir(dir).catch(() => []);
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) => path9.basename(n, path9.extname(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 ? Math.round((t.finished_at - t.started_at) / 1e3) : 0;
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}s attempts: ${t.attempts}
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/status.ts
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 fs11 } from "fs";
4018
- import * as path10 from "path";
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 fs11.stat(cwdDbPath);
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 fs11.readdir(base);
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 = path10.join(
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 stat2 = await fs11.stat(path10.join(base, folder));
4052
- if (!stat2.isDirectory()) continue;
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 fs11.stat(candidateDbPath);
4059
- const candidateRunDir = path10.join(
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 = path10.join(pilot, "runs");
3679
+ const runsDir = path8.join(pilot, "runs");
4080
3680
  let entries;
4081
3681
  try {
4082
- entries = await fs11.readdir(runsDir);
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 = path10.join(runsDir, id, "state.db");
3690
+ const dbPath = path8.join(runsDir, id, "state.db");
4091
3691
  let st;
4092
3692
  try {
4093
- st = await fs11.stat(dbPath);
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: path10.join(runsDir, newest.id)
3709
+ runDir: path8.join(runsDir, newest.id)
4110
3710
  };
4111
3711
  }
4112
3712
 
4113
- // src/pilot/cli/status.ts
4114
- var statusCmd = command4({
4115
- name: "status",
4116
- description: "Print the run + task status for a pilot run.",
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 with a state.db."
3722
+ description: "Run ID to resume. Defaults to the newest resumable run matching --plan (or interactive picker if multiple exist)."
4122
3723
  }),
4123
- json: flag3({
4124
- long: "json",
4125
- description: "Emit JSON instead of human-readable text."
4126
- })
4127
- },
4128
- handler: async ({ run: run2, json }) => {
4129
- const code = await runStatus({ runId: run2, json });
4130
- process.exit(code);
4131
- }
4132
- });
4133
- async function runStatus(opts) {
4134
- let discovered;
4135
- try {
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 ({ run: run2 }) => {
3739
+ handler: async (args) => {
4226
3740
  await requirePlugin();
4227
- const code = await runResume({ runId: run2 });
3741
+ const code = await runBuildResume(args);
4228
3742
  process.exit(code);
4229
3743
  }
4230
3744
  });
4231
- async function runResume(opts) {
4232
- let discovered;
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
- discovered = await discoverRun({
4235
- cwd: process.cwd(),
4236
- runId: opts.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
- `${err instanceof Error ? err.message : String(err)}
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(discovered.dbPath);
4246
- const cleanup = [
4247
- () => opened.close()
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: run ${discovered.runId} missing from DB
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 loaded = await loadPlan(run2.plan_path);
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: cannot reload plan at ${run2.plan_path} (${loaded.kind})
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
- for (const e of loaded.errors) {
4265
- process.stderr.write(` ${e.path}: ${e.message}
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
- if (run2.status === "pending") {
4272
- markRunRunning(opened.db, discovered.runId);
4273
- } else if (run2.status === "running") {
4274
- } else {
4275
- opened.db.run(
4276
- `UPDATE runs SET status='running', finished_at=NULL WHERE id=?`,
4277
- [discovered.runId]
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: discovered.runId,
3879
+ runId,
4282
3880
  kind: "run.resumed",
4283
- payload: { previousStatus: run2.status }
3881
+ payload: {
3882
+ resetTaskIds: resetIds,
3883
+ skippedSucceeded: counts.succeeded
3884
+ }
4284
3885
  });
4285
- const runDir = await getRunDir(process.cwd(), discovered.runId);
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: discovered.runId,
4289
- plan: loaded.plan,
4290
- planPath: run2.plan_path,
3895
+ runId,
3896
+ plan,
3897
+ planPath,
4291
3898
  runDir,
4292
- // Reconstruct the same branch prefix the original `pilot build` used.
4293
- // The runId segment is what makes branches unique per run; resume MUST
4294
- // match the original to find the existing worktrees.
4295
- branchPrefix: deriveBranchPrefix(
4296
- loaded.plan.branch_prefix,
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/retry.ts
4314
- import { command as command6, option as option5, optional as optional6, positional as positional4, string as string6, flag as flag4 } from "cmd-ts";
4315
- var retryCmd = command6({
4316
- name: "retry",
4317
- description: "Reset a single task to pending. Optionally also re-run it.",
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
- taskId: positional4({
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: optional6(string6),
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
- runNow: flag4({
4330
- long: "run-now",
4331
- description: "After resetting, immediately run `pilot resume` on the same DB."
4040
+ json: flag4({
4041
+ long: "json",
4042
+ description: "Emit JSON instead of human-readable text."
4332
4043
  })
4333
4044
  },
4334
- handler: async ({ taskId, run: run2, runNow }) => {
4335
- await requirePlugin();
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 runRetry(opts) {
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 task = getTask(opened.db, discovered.runId, opts.taskId);
4357
- if (task === null) {
4066
+ const run2 = getRun(opened.db, discovered.runId);
4067
+ if (run2 === null) {
4358
4068
  process.stderr.write(
4359
- `pilot retry: task ${JSON.stringify(opts.taskId)} not found in run ${discovered.runId}
4069
+ `pilot status: run ${JSON.stringify(discovered.runId)} not in DB
4360
4070
  `
4361
4071
  );
4362
4072
  return 1;
4363
4073
  }
4364
- const previousStatus = task.status;
4365
- try {
4366
- markPending(opened.db, discovered.runId, opts.taskId);
4367
- } catch (err) {
4368
- process.stderr.write(
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 1;
4080
+ return 0;
4373
4081
  }
4374
- appendEvent(opened.db, {
4375
- runId: discovered.runId,
4376
- taskId: opts.taskId,
4377
- kind: "task.retry",
4378
- payload: { previousStatus }
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
- if (opts.runNow) {
4388
- return runResume({ runId: discovered.runId });
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
- return 0;
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 command7, flag as flag5, option as option6, optional as optional7, positional as positional5, string as string7 } from "cmd-ts";
4395
- var logsCmd = command7({
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: positional5({
4400
- type: string7,
4136
+ taskId: positional4({
4137
+ type: string6,
4401
4138
  displayName: "task-id"
4402
4139
  }),
4403
- run: option6({
4140
+ run: option5({
4404
4141
  long: "run",
4405
- type: optional7(string7),
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 command9, flag as flag7, option as option8, optional as optional9, string as string9 } from "cmd-ts";
4681
- var costCmd = command9({
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: option8({
4268
+ run: option6({
4686
4269
  long: "run",
4687
- type: optional9(string9),
4270
+ type: optional7(string7),
4688
4271
  description: "Run ID. Defaults to the newest run."
4689
4272
  }),
4690
- json: flag7({
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 command10 } from "cmd-ts";
4753
- var planDirCmd = command10({
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 = subcommands2({
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 fs13 from "fs";
4792
- import * as path12 from "path";
4793
- import * as os5 from "os";
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 fileURLToPath2 } from "url";
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"] ?? path12.join(os5.homedir(), ".cache");
4821
- return path12.join(cacheHome, "harness-opencode", "cli-update.json");
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 = fs13.readFileSync(getStateFilePath(), "utf8");
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
- fs13.mkdirSync(path12.dirname(statePath), { recursive: true });
4835
- fs13.writeFileSync(statePath, JSON.stringify(state));
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 = path12.dirname(fileURLToPath2(import.meta.url));
4421
+ const here = path10.dirname(fileURLToPath3(import.meta.url));
4841
4422
  const candidates = [
4842
- path12.join(here, "..", "package.json"),
4843
- path12.join(here, "..", "..", "package.json"),
4844
- path12.join(here, "package.json")
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 = fs13.readFileSync(candidate, "utf8");
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 = command11({
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: flag8({
4559
+ dryRun: flag7({
4979
4560
  long: "dry-run",
4980
4561
  description: "Preview changes without writing."
4981
4562
  }),
4982
- pin: flag8({
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 = command11({
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: flag8({
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 = command11({
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 = command11({
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: option9({
4597
+ run: option7({
5017
4598
  long: "run",
5018
- type: optional10(string10),
4599
+ type: optional8(string8),
5019
4600
  description: "Print verify commands for pending items, one per line."
5020
4601
  }),
5021
- check: option9({
4602
+ check: option7({
5022
4603
  long: "check",
5023
- type: optional10(string10),
4604
+ type: optional8(string8),
5024
4605
  description: "Structural validation; exits 1 if any item is invalid."
5025
4606
  }),
5026
4607
  rest: restPositionals({
5027
- type: string10,
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 = command11({
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 = command11({
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: flag8({
4648
+ dryRun: flag7({
5068
4649
  long: "dry-run",
5069
4650
  description: "Preview changes without writing."
5070
4651
  }),
5071
- pin: flag8({
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 = subcommands3({
4661
+ var cli = subcommands2({
5081
4662
  name: "glrs-oc",
5082
4663
  description: "OpenCode agent harness CLI.",
5083
4664
  version: VERSION,