@ordewell/core 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  DENY_ALL: () => DENY_ALL,
52
52
  EmbeddedNewlineError: () => EmbeddedNewlineError,
53
53
  EnvConfig: () => EnvConfig,
54
+ ExecutableNotFoundError: () => ExecutableNotFoundError,
54
55
  FsPluginStore: () => FsPluginStore,
55
56
  GIT_READONLY_SUBCOMMANDS: () => GIT_READONLY_SUBCOMMANDS,
56
57
  GREP_DEFAULT_HEAD_LIMIT: () => GREP_DEFAULT_HEAD_LIMIT,
@@ -71,14 +72,17 @@ __export(index_exports, {
71
72
  PROVIDER_PRIORITY: () => PROVIDER_PRIORITY,
72
73
  PROVIDER_SHORT_LABEL: () => PROVIDER_SHORT_LABEL,
73
74
  PendingApprovals: () => PendingApprovals,
75
+ PlanEditError: () => PlanEditError,
74
76
  PlanParseError: () => PlanParseError,
75
77
  PlanStore: () => PlanStore,
76
78
  Planner: () => Planner,
79
+ PlannerModelMemory: () => PlannerModelMemory,
77
80
  REFUSED_COMMANDS: () => REFUSED_COMMANDS,
78
81
  RunnerInstallation: () => RunnerInstallation,
79
82
  RunnerRegistry: () => RunnerRegistry,
80
83
  SEARCH_EXCLUSIONS: () => SEARCH_EXCLUSIONS,
81
84
  STATE_DIR: () => STATE_DIR,
85
+ STOPPED_TOOL_RESULT: () => STOPPED_TOOL_RESULT,
82
86
  SYMBOL_LANGUAGES: () => SYMBOL_LANGUAGES,
83
87
  Session: () => Session,
84
88
  SettingsService: () => SettingsService,
@@ -89,9 +93,11 @@ __export(index_exports, {
89
93
  TmuxRunner: () => TmuxRunner,
90
94
  VerdictEngine: () => VerdictEngine,
91
95
  WINDOWS_MAX_COMMAND_LINE: () => WINDOWS_MAX_COMMAND_LINE,
96
+ WorkspaceNotFoundError: () => WorkspaceNotFoundError,
92
97
  addTaskToPlan: () => addTaskToPlan,
93
98
  applyHeadLimit: () => applyHeadLimit,
94
99
  applyTaskOps: () => applyTaskOps,
100
+ assertWorkspaceExists: () => assertWorkspaceExists,
95
101
  augmentPromptWithPriorOutputs: () => augmentPromptWithPriorOutputs,
96
102
  augmentedPath: () => augmentedPath,
97
103
  buildConversationSystemPrompt: () => buildConversationSystemPrompt,
@@ -165,6 +171,7 @@ __export(index_exports, {
165
171
  hasTmux: () => hasTmux,
166
172
  includeGlobFor: () => includeGlobFor,
167
173
  isCliProvider: () => isCliProvider,
174
+ isExecutableResolved: () => isExecutableResolved,
168
175
  isOpenAiProvider: () => isOpenAiProvider,
169
176
  killTree: () => killTree,
170
177
  knownModelId: () => knownModelId,
@@ -819,6 +826,16 @@ function dialectFor(dialect) {
819
826
  const resolved = dialect ?? (process.platform === "win32" ? "cmd" : "posix");
820
827
  return resolved === "cmd" ? CMD_DIALECT : POSIX_DIALECT;
821
828
  }
829
+ function isDevNullTarget(target) {
830
+ if (!target.startsWith("/")) return false;
831
+ const resolved = [];
832
+ for (const seg of target.split("/")) {
833
+ if (seg === "" || seg === ".") continue;
834
+ if (seg === "..") resolved.pop();
835
+ else resolved.push(seg);
836
+ }
837
+ return `/${resolved.join("/")}` === "/dev/null";
838
+ }
822
839
  function matchParen(s, open) {
823
840
  let depth = 0;
824
841
  for (let i = open; i < s.length; i++) {
@@ -829,7 +846,7 @@ function matchParen(s, open) {
829
846
  }
830
847
  function lex(command, nested, dialect) {
831
848
  const segments = [];
832
- let redirect = false;
849
+ let unsafeRedirect;
833
850
  let processSubstitution = false;
834
851
  let unbalanced = false;
835
852
  let tokens = [];
@@ -838,7 +855,19 @@ function lex(command, nested, dialect) {
838
855
  let started = false;
839
856
  let piped = false;
840
857
  let quote = "";
841
- const endToken = () => {
858
+ let redirectTargetMode = false;
859
+ let redirectOperator = "";
860
+ const endToken = (hardBoundary = true) => {
861
+ if (redirectTargetMode) {
862
+ if (!started && !hardBoundary) return;
863
+ if (!unsafeRedirect && !isDevNullTarget(current)) {
864
+ unsafeRedirect = { operator: redirectOperator, target: current };
865
+ }
866
+ redirectTargetMode = false;
867
+ current = "";
868
+ started = false;
869
+ return;
870
+ }
842
871
  if (started) tokens.push(current);
843
872
  current = "";
844
873
  started = false;
@@ -926,9 +955,34 @@ function lex(command, nested, dialect) {
926
955
  i += 2;
927
956
  continue;
928
957
  }
929
- if (c === ">") redirect = true;
958
+ let fd = "";
959
+ if (started && /^[0-9]+$/.test(current)) {
960
+ fd = current;
961
+ current = "";
962
+ started = false;
963
+ } else {
964
+ endToken();
965
+ }
966
+ const doubled = command[i + 1] === c;
967
+ const opLen = doubled ? 2 : 1;
968
+ if (c === ">") {
969
+ const dup = /^&([0-9]+)(?![0-9])/.exec(command.slice(i + opLen));
970
+ if (dup) {
971
+ i += opLen + dup[0].length;
972
+ continue;
973
+ }
974
+ redirectTargetMode = true;
975
+ redirectOperator = fd + c.repeat(opLen);
976
+ }
977
+ i += opLen;
978
+ continue;
979
+ }
980
+ if (c === "&" && command[i + 1] === ">") {
930
981
  endToken();
931
- i += command[i + 1] === c ? 2 : 1;
982
+ const doubled = command[i + 2] === ">";
983
+ redirectTargetMode = true;
984
+ redirectOperator = doubled ? "&>>" : "&>";
985
+ i += doubled ? 3 : 2;
932
986
  continue;
933
987
  }
934
988
  if (c === "(" || c === ")") {
@@ -948,7 +1002,7 @@ function lex(command, nested, dialect) {
948
1002
  continue;
949
1003
  }
950
1004
  if (/\s/.test(c)) {
951
- endToken();
1005
+ endToken(false);
952
1006
  i++;
953
1007
  continue;
954
1008
  }
@@ -958,7 +1012,7 @@ function lex(command, nested, dialect) {
958
1012
  }
959
1013
  if (quote !== "") unbalanced = true;
960
1014
  endSegment(false);
961
- return { segments, redirect, processSubstitution, unbalanced };
1015
+ return { segments, unsafeRedirect, processSubstitution, unbalanced };
962
1016
  }
963
1017
  function binaryName(token, dialect) {
964
1018
  const base = token.split(/[/\\]/).pop() ?? "";
@@ -978,7 +1032,7 @@ function lexAll(command, dialect) {
978
1032
  for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
979
1033
  const inner = lex(queue.shift(), queue, dialect);
980
1034
  all.segments.push(...inner.segments.filter((s) => s.binary));
981
- all.redirect ||= inner.redirect;
1035
+ all.unsafeRedirect ??= inner.unsafeRedirect;
982
1036
  all.processSubstitution ||= inner.processSubstitution;
983
1037
  all.unbalanced ||= inner.unbalanced;
984
1038
  }
@@ -1027,8 +1081,9 @@ function refusalFor(seg) {
1027
1081
  if (seg.binary === "git") {
1028
1082
  const sub = seg.args.find((a) => !a.startsWith("-"));
1029
1083
  if (sub === "branch" || sub === "tag") {
1030
- if (seg.args.some((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a))) {
1031
- return `"git ${sub}" with a delete/move flag mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
1084
+ const flag = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a));
1085
+ if (flag) {
1086
+ return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
1032
1087
  }
1033
1088
  }
1034
1089
  }
@@ -1038,8 +1093,9 @@ function refusalFor(seg) {
1038
1093
  return `find with "${flag}" runs an inner command this classifier cannot inspect. Use the read-only research tools on find's output, or describe the change as a task.`;
1039
1094
  }
1040
1095
  }
1041
- if (seg.binary === "sed" && seg.args.some((a) => a === "-i" || a === "--in-place" || /^-i./.test(a))) {
1042
- return `"sed -i" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
1096
+ const sedInPlace = seg.binary === "sed" ? seg.args.find((a) => a === "-i" || a === "--in-place" || /^-i./.test(a)) : void 0;
1097
+ if (sedInPlace) {
1098
+ return `"sed ${sedInPlace}" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
1043
1099
  }
1044
1100
  if (seg.binary === "awk" && seg.args.some((a) => a === "inplace")) {
1045
1101
  return `"awk -i inplace" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
@@ -1058,7 +1114,7 @@ function classifyCommand(command, opts = {}) {
1058
1114
  const trimmed = command.trim();
1059
1115
  if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
1060
1116
  const dialect = dialectFor(opts.dialect);
1061
- const { segments, redirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
1117
+ const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
1062
1118
  if (unbalanced) {
1063
1119
  return {
1064
1120
  tier: "refuse",
@@ -1073,11 +1129,12 @@ function classifyCommand(command, opts = {}) {
1073
1129
  reason: "Process substitution runs another command this classifier cannot inspect. Run the command on its own and read its output, or describe it as a task."
1074
1130
  };
1075
1131
  }
1076
- if (redirect) {
1132
+ if (unsafeRedirect) {
1133
+ const written = unsafeRedirect.target ? `"${unsafeRedirect.operator} ${unsafeRedirect.target}"` : `"${unsafeRedirect.operator}"`;
1077
1134
  return {
1078
1135
  tier: "refuse",
1079
1136
  scope: "",
1080
- reason: "Output redirection writes files. You are a read-only planner \u2014 read the output instead, or describe the write as a task."
1137
+ reason: `${written} writes to a file. You are a read-only planner \u2014 read the output instead, or describe the write as a task.`
1081
1138
  };
1082
1139
  }
1083
1140
  if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
@@ -1397,7 +1454,7 @@ ${refs.output.trim()}`);
1397
1454
  * anything else asks once and is remembered, and writes are refused outright
1398
1455
  * because a planner that mutates the workspace has stopped being a planner.
1399
1456
  */
1400
- async bash(command) {
1457
+ async bash(command, signal) {
1401
1458
  const { tier, scope, reason } = classifyCommand(command, { dialect: this.researchShell.dialect });
1402
1459
  if (tier === "refuse") {
1403
1460
  return { success: false, output: `Command refused: ${reason}`, truncated: false };
@@ -1420,7 +1477,7 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
1420
1477
  };
1421
1478
  }
1422
1479
  }
1423
- const outcome = await this.execBashImpl(command);
1480
+ const outcome = await this.execBashImpl(command, signal);
1424
1481
  if (outcome.success) return outcome;
1425
1482
  const warning = researchShellWarning(this.researchShell);
1426
1483
  return warning ? { ...outcome, output: `${outcome.output}
@@ -1429,6 +1486,82 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
1429
1486
  }
1430
1487
  };
1431
1488
 
1489
+ // src/services/executeTool.ts
1490
+ var GREP_OUTPUT_MODES = ["content", "files", "count"];
1491
+ function num(value) {
1492
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1493
+ }
1494
+ function bool(value) {
1495
+ return typeof value === "boolean" ? value : void 0;
1496
+ }
1497
+ function str(value) {
1498
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1499
+ }
1500
+ function grepOptions(args) {
1501
+ const mode = str(args.output_mode ?? args.outputMode);
1502
+ return {
1503
+ include: str(args.include),
1504
+ path: str(args.path),
1505
+ outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
1506
+ contextBefore: num(args.context_before ?? args.contextBefore),
1507
+ contextAfter: num(args.context_after ?? args.contextAfter),
1508
+ literal: bool(args.literal),
1509
+ caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
1510
+ headLimit: num(args.head_limit ?? args.headLimit)
1511
+ };
1512
+ }
1513
+ var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
1514
+ async function executeTool(name, args, fs10, fetcher, signal) {
1515
+ if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
1516
+ switch (name) {
1517
+ case "read_file": {
1518
+ const offset = num(args.offset);
1519
+ const limit = num(args.limit);
1520
+ const maxBytes = num(args.maxBytes);
1521
+ return fs10.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
1522
+ }
1523
+ case "read_files": {
1524
+ const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
1525
+ return fs10.readFiles(paths);
1526
+ }
1527
+ case "glob":
1528
+ return fs10.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
1529
+ case "grep":
1530
+ return fs10.grep(String(args.pattern ?? ""), grepOptions(args));
1531
+ case "find_symbol":
1532
+ return fs10.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
1533
+ case "list_dir":
1534
+ return fs10.listDir(String(args.path ?? ""), num(args.depth));
1535
+ case "bash":
1536
+ return fs10.bash(String(args.command ?? ""), signal);
1537
+ case "fetch": {
1538
+ if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
1539
+ const url = String(args.url ?? "");
1540
+ const allowed = await fetcher.confirm(url);
1541
+ if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
1542
+ return fetcher.fetch(url);
1543
+ }
1544
+ case "web_search": {
1545
+ if (!fetcher?.search) {
1546
+ return { success: false, output: "Web search is not available in this environment. If you already know a documentation URL, use the fetch tool instead.", truncated: false };
1547
+ }
1548
+ const query = String(args.query ?? "").trim();
1549
+ if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
1550
+ return fetcher.search(query);
1551
+ }
1552
+ // Budget models hallucinate two kinds of tools: plan-committing ones
1553
+ // (create_task, submit_plan) and implementation ones (create_file,
1554
+ // write_file). Steer both back: you are a planner, tasks run later,
1555
+ // and the commit channel is raw JSON in the reply text.
1556
+ default:
1557
+ return {
1558
+ success: false,
1559
+ output: `Unknown tool: ${name}. You are a PLANNER \u2014 you never create or edit files yourself; the coding agents executing the plan will do that later. Your tools are read-only research tools: read_file, read_files, glob, grep, find_symbol, list_dir, bash, fetch, web_search. To commit the final plan, output it as a raw JSON object with a "tasks" array directly in your reply text (no tool call).`,
1560
+ truncated: false
1561
+ };
1562
+ }
1563
+ }
1564
+
1432
1565
  // src/services/ApprovalPolicy.ts
1433
1566
  function scopeMatches(pattern, scope) {
1434
1567
  if (pattern === scope) return true;
@@ -4315,8 +4448,8 @@ function savePrdMarkdown(workspace, slug2, markdown) {
4315
4448
 
4316
4449
  // src/services/ContextCollector.ts
4317
4450
  var ORDEWELL_CONTEXT_MAX = 8e3;
4318
- async function collectResearchContext(fs9, runners) {
4319
- const collector = new ContextCollector(fs9);
4451
+ async function collectResearchContext(fs10, runners) {
4452
+ const collector = new ContextCollector(fs10);
4320
4453
  let contextStr = "";
4321
4454
  let firstContext = null;
4322
4455
  for (const r of runners) {
@@ -4343,8 +4476,8 @@ ${firstContext.dirStructure}
4343
4476
  return contextStr;
4344
4477
  }
4345
4478
  var ContextCollector = class {
4346
- constructor(fs9, registry) {
4347
- this.fs = fs9;
4479
+ constructor(fs10, registry) {
4480
+ this.fs = fs10;
4348
4481
  this.registry = registry;
4349
4482
  }
4350
4483
  fs;
@@ -4380,80 +4513,6 @@ var ContextCollector = class {
4380
4513
  }
4381
4514
  };
4382
4515
 
4383
- // src/services/executeTool.ts
4384
- var GREP_OUTPUT_MODES = ["content", "files", "count"];
4385
- function num(value) {
4386
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4387
- }
4388
- function bool(value) {
4389
- return typeof value === "boolean" ? value : void 0;
4390
- }
4391
- function str(value) {
4392
- return typeof value === "string" && value.length > 0 ? value : void 0;
4393
- }
4394
- function grepOptions(args) {
4395
- const mode = str(args.output_mode ?? args.outputMode);
4396
- return {
4397
- include: str(args.include),
4398
- path: str(args.path),
4399
- outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
4400
- contextBefore: num(args.context_before ?? args.contextBefore),
4401
- contextAfter: num(args.context_after ?? args.contextAfter),
4402
- literal: bool(args.literal),
4403
- caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
4404
- headLimit: num(args.head_limit ?? args.headLimit)
4405
- };
4406
- }
4407
- async function executeTool(name, args, fs9, fetcher) {
4408
- switch (name) {
4409
- case "read_file": {
4410
- const offset = num(args.offset);
4411
- const limit = num(args.limit);
4412
- const maxBytes = num(args.maxBytes);
4413
- return fs9.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
4414
- }
4415
- case "read_files": {
4416
- const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
4417
- return fs9.readFiles(paths);
4418
- }
4419
- case "glob":
4420
- return fs9.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
4421
- case "grep":
4422
- return fs9.grep(String(args.pattern ?? ""), grepOptions(args));
4423
- case "find_symbol":
4424
- return fs9.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
4425
- case "list_dir":
4426
- return fs9.listDir(String(args.path ?? ""), num(args.depth));
4427
- case "bash":
4428
- return fs9.bash(String(args.command ?? ""));
4429
- case "fetch": {
4430
- if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
4431
- const url = String(args.url ?? "");
4432
- const allowed = await fetcher.confirm(url);
4433
- if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
4434
- return fetcher.fetch(url);
4435
- }
4436
- case "web_search": {
4437
- if (!fetcher?.search) {
4438
- return { success: false, output: "Web search is not available in this environment. If you already know a documentation URL, use the fetch tool instead.", truncated: false };
4439
- }
4440
- const query = String(args.query ?? "").trim();
4441
- if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
4442
- return fetcher.search(query);
4443
- }
4444
- // Budget models hallucinate two kinds of tools: plan-committing ones
4445
- // (create_task, submit_plan) and implementation ones (create_file,
4446
- // write_file). Steer both back: you are a planner, tasks run later,
4447
- // and the commit channel is raw JSON in the reply text.
4448
- default:
4449
- return {
4450
- success: false,
4451
- output: `Unknown tool: ${name}. You are a PLANNER \u2014 you never create or edit files yourself; the coding agents executing the plan will do that later. Your tools are read-only research tools: read_file, read_files, glob, grep, find_symbol, list_dir, bash, fetch, web_search. To commit the final plan, output it as a raw JSON object with a "tasks" array directly in your reply text (no tool call).`,
4452
- truncated: false
4453
- };
4454
- }
4455
- }
4456
-
4457
4516
  // src/services/researchStepSummary.ts
4458
4517
  function ellipsize(text, max) {
4459
4518
  const oneLine = text.replace(/\s+/g, " ").trim();
@@ -4535,23 +4594,23 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
4535
4594
  function refused(output) {
4536
4595
  return { success: false, output, truncated: false };
4537
4596
  }
4538
- function nonPromptingFs(fs9) {
4539
- const withinWorkspace = (p) => resolveWithin(fs9.getWorkspaceRoot(), p ?? ".").inside;
4597
+ function nonPromptingFs(fs10) {
4598
+ const withinWorkspace = (p) => resolveWithin(fs10.getWorkspaceRoot(), p ?? ".").inside;
4540
4599
  const outsideRefusal = (p) => refused(
4541
4600
  `Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
4542
4601
  );
4543
4602
  return {
4544
- getWorkspaceRoot: () => fs9.getWorkspaceRoot(),
4545
- readFile: (p, opts) => withinWorkspace(p) ? fs9.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
4603
+ getWorkspaceRoot: () => fs10.getWorkspaceRoot(),
4604
+ readFile: (p, opts) => withinWorkspace(p) ? fs10.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
4546
4605
  readFiles: (paths) => {
4547
4606
  const escaping = paths.find((p) => !withinWorkspace(p));
4548
- return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs9.readFiles(paths);
4607
+ return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs10.readFiles(paths);
4549
4608
  },
4550
- glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs9.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4551
- grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs9.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4552
- findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs9.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
4553
- listDir: (p, depth) => withinWorkspace(p) ? fs9.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
4554
- bash: async (command) => {
4609
+ glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4610
+ grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
4611
+ findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs10.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
4612
+ listDir: (p, depth) => withinWorkspace(p) ? fs10.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
4613
+ bash: async (command, signal) => {
4555
4614
  const dialect = resolveResearchShell().dialect;
4556
4615
  const { tier, reason } = classifyCommand(command, { dialect });
4557
4616
  if (tier !== "auto") {
@@ -4561,13 +4620,13 @@ function nonPromptingFs(fs9) {
4561
4620
  }
4562
4621
  const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
4563
4622
  if (escaping) return outsideRefusal(escaping);
4564
- return fs9.bash(command);
4623
+ return fs10.bash(command, signal);
4565
4624
  }
4566
4625
  };
4567
4626
  }
4568
4627
  async function runLoop(prompt, deps) {
4569
4628
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
4570
- const fs9 = nonPromptingFs(deps.fs);
4629
+ const fs10 = nonPromptingFs(deps.fs);
4571
4630
  const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
4572
4631
  let turn = await chat.sendMessage(prompt, deps.signal);
4573
4632
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -4584,7 +4643,7 @@ ${turn.text}`;
4584
4643
  if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
4585
4644
  const toolArgs = JSON.stringify(tc.args);
4586
4645
  deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
4587
- const res = await executeTool(tc.name, args, fs9);
4646
+ const res = await executeTool(tc.name, args, fs10, void 0, deps.signal);
4588
4647
  const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
4589
4648
  [... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
4590
4649
  const stepEntry = {
@@ -4699,7 +4758,7 @@ var BaseAiService = class _BaseAiService {
4699
4758
  * steering/failed result and the loop continues sequentially: a subagent can
4700
4759
  * never fail a plan or a turn.
4701
4760
  */
4702
- async executeSpawnAgent(args, fs9, onProgress, signal, subagentId) {
4761
+ async executeSpawnAgent(args, fs10, onProgress, signal, subagentId) {
4703
4762
  if (!this.researchSubagentsEnabled) {
4704
4763
  return { success: false, output: "spawn_research_agent is not available in this session. Continue researching sequentially with your other tools (read_file, glob, grep, list_dir, bash).", truncated: false };
4705
4764
  }
@@ -4713,20 +4772,20 @@ var BaseAiService = class _BaseAiService {
4713
4772
  if (!chat) throw new Error("subagent chats are not available for this provider");
4714
4773
  return chat;
4715
4774
  },
4716
- fs: fs9,
4775
+ fs: fs10,
4717
4776
  signal,
4718
4777
  onProgress: (progress) => onProgress({ ...progress, subagentId })
4719
4778
  });
4720
4779
  }
4721
4780
  /** Collect project context for the planning phase. Shared with the harness backend. */
4722
- static async collectResearchContext(fs9, runners) {
4723
- return collectResearchContext(fs9, runners);
4781
+ static async collectResearchContext(fs10, runners) {
4782
+ return collectResearchContext(fs10, runners);
4724
4783
  }
4725
4784
  /**
4726
4785
  * Execute one round of tool calls and report each through onProgress.
4727
4786
  * Returns the results to feed back plus the log entries produced.
4728
4787
  */
4729
- async executeToolCalls(toolCalls, fs9, onProgress, fetcher, stepIndex, thinking, signal) {
4788
+ async executeToolCalls(toolCalls, fs10, onProgress, fetcher, stepIndex, thinking, signal) {
4730
4789
  const toolResults = [];
4731
4790
  const logEntries = [];
4732
4791
  let resultsText = "";
@@ -4745,18 +4804,18 @@ var BaseAiService = class _BaseAiService {
4745
4804
  const subagentId = (0, import_uuid2.v4)();
4746
4805
  spawnSubagentIds.set(i, subagentId);
4747
4806
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
4748
- precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs9, onProgress, signal, subagentId));
4807
+ precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
4749
4808
  }) : Promise.resolve(),
4750
4809
  parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
4751
4810
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
4752
- precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs9, fetcher));
4811
+ precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs10, fetcher, signal));
4753
4812
  }) : Promise.resolve()
4754
4813
  ]);
4755
4814
  for (const [index, tc] of toolCalls.entries()) {
4756
4815
  const toolArgs = normalizedArgs[index];
4757
4816
  const preResult = precomputed.get(index);
4758
4817
  if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
4759
- const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs9, fetcher);
4818
+ const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs10, fetcher, signal);
4760
4819
  const MAX_RESPONSE_CHARS = 5e4;
4761
4820
  const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
4762
4821
  const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
@@ -4942,7 +5001,7 @@ ${llmOutput}
4942
5001
  * parsed tasks if a plan was emitted mid-loop, or null if the loop
4943
5002
  * exhausted without a valid plan.
4944
5003
  */
4945
- async runResearchLoop(chat, firstMessage, fs9, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
5004
+ async runResearchLoop(chat, firstMessage, fs10, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
4946
5005
  const researchLog = initialLog ? [...initialLog] : [];
4947
5006
  if (!initialLog && userGoal) {
4948
5007
  researchLog.push({
@@ -4969,7 +5028,7 @@ ${llmOutput}
4969
5028
  if (!turn.hasToolCalls) break;
4970
5029
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
4971
5030
  turn.toolCalls,
4972
- fs9,
5031
+ fs10,
4973
5032
  onProgress,
4974
5033
  fetcher,
4975
5034
  step,
@@ -5205,11 +5264,11 @@ ${repairHint}` }] : parts;
5205
5264
  return this.consumePlanStream(result, onToken);
5206
5265
  }
5207
5266
  // --- IAiService implementation ---
5208
- async researchAndPlan(userDescription, runners, modelsByRunner, fs9, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5267
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5209
5268
  this.ensureInit();
5210
5269
  const { autonomousDefault } = modes;
5211
5270
  const genModel = this.getPlanningModel();
5212
- const contextStr = await BaseAiService.collectResearchContext(fs9, runners);
5271
+ const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
5213
5272
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
5214
5273
  const firstMessage = `${buildResearchToolsPrompt()}
5215
5274
 
@@ -5220,17 +5279,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
5220
5279
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
5221
5280
  });
5222
5281
  const researchChat = new GeminiResearchChat(chat);
5223
- const result = await this.runResearchLoop(researchChat, firstMessage, fs9, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
5282
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
5224
5283
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
5225
5284
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
5226
5285
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
5227
5286
  }
5228
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs9, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
5287
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
5229
5288
  this.ensureInit();
5230
5289
  const { autonomousDefault } = modes;
5231
5290
  let ctx = "";
5232
- if (fs9) {
5233
- const collected = await new ContextCollector(fs9).collect(runners[0] ?? "claude-code");
5291
+ if (fs10) {
5292
+ const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
5234
5293
  if (collected.aiflowContext) ctx = `
5235
5294
  <aiflow_context>
5236
5295
  ${collected.aiflowContext}
@@ -5246,12 +5305,12 @@ ${collected.aiflowContext}
5246
5305
  autonomousDefault
5247
5306
  );
5248
5307
  }
5249
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs9, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
5308
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
5250
5309
  this.ensureInit();
5251
5310
  const { autonomousDefault } = modes;
5252
5311
  let aiflowContext;
5253
- if (fs9) {
5254
- const collected = await new ContextCollector(fs9).collect(existingPlan.runners[0] ?? "claude-code");
5312
+ if (fs10) {
5313
+ const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
5255
5314
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
5256
5315
  }
5257
5316
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -5491,12 +5550,12 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
5491
5550
  }
5492
5551
  }
5493
5552
  // --- IAiService implementation ---
5494
- async researchAndPlan(userDescription, runners, modelsByRunner, fs9, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5553
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5495
5554
  this.ensureInit();
5496
5555
  const client = this.getClient();
5497
5556
  const { autonomousDefault, researchSubagents: researchSubagentsEnabled } = modes;
5498
5557
  this.researchSubagentsEnabled = researchSubagentsEnabled;
5499
- const contextStr = await BaseAiService.collectResearchContext(fs9, runners);
5558
+ const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
5500
5559
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
5501
5560
  const userText = `User goal: ${userDescription}`;
5502
5561
  const firstMessage = `${buildResearchToolsPrompt(researchSubagentsEnabled)}
@@ -5514,17 +5573,17 @@ ${userText}`;
5514
5573
  toOpenAiTools(researchSubagentsEnabled),
5515
5574
  (delta) => onProgress({ type: "thinking", text: delta })
5516
5575
  );
5517
- const result = await this.runResearchLoop(researchChat, firstMessage, fs9, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
5576
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
5518
5577
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
5519
5578
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
5520
5579
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
5521
5580
  }
5522
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs9, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5581
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5523
5582
  this.ensureInit();
5524
5583
  const { autonomousDefault } = modes;
5525
5584
  let ctx = "";
5526
- if (fs9) {
5527
- const collected = await new ContextCollector(fs9).collect(runners[0] ?? "claude-code");
5585
+ if (fs10) {
5586
+ const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
5528
5587
  if (collected.aiflowContext) ctx = `
5529
5588
  <aiflow_context>
5530
5589
  ${collected.aiflowContext}
@@ -5540,12 +5599,12 @@ ${collected.aiflowContext}
5540
5599
  autonomousDefault
5541
5600
  );
5542
5601
  }
5543
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs9, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5602
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
5544
5603
  this.ensureInit();
5545
5604
  const { autonomousDefault } = modes;
5546
5605
  let aiflowContext;
5547
- if (fs9) {
5548
- const collected = await new ContextCollector(fs9).collect(existingPlan.runners[0] ?? "claude-code");
5606
+ if (fs10) {
5607
+ const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
5549
5608
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
5550
5609
  }
5551
5610
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -6163,6 +6222,7 @@ var PlanStore = class {
6163
6222
  }
6164
6223
  remove(taskId) {
6165
6224
  if (!this._taskMap.has(taskId)) return;
6225
+ this.unblockDependents(taskId);
6166
6226
  this._planTasks = removeTaskFromPlan(this._planTasks, taskId);
6167
6227
  this.rebuild();
6168
6228
  this.pruneTerminalSets();
@@ -6217,9 +6277,7 @@ ${taskB.description}`,
6217
6277
  const userIds = /* @__PURE__ */ new Set([taskIdA, taskIdB]);
6218
6278
  const sanitized = this._planTasks.filter((t) => !userIds.has(t.id)).map((t) => ({
6219
6279
  ...t,
6220
- dependencies: t.dependencies.map(
6221
- (depId) => depId === taskIdA || depId === taskIdB ? mergedId : depId
6222
- )
6280
+ dependencies: [...new Set(t.dependencies.map((depId) => userIds.has(depId) ? mergedId : depId))]
6223
6281
  }));
6224
6282
  this._planTasks = renumberTasks([...sanitized, merged]);
6225
6283
  this.rebuild();
@@ -6231,11 +6289,12 @@ ${taskB.description}`,
6231
6289
  const original = this._taskMap.get(taskId);
6232
6290
  if (!original) throw new Error(`Task ${taskId} not found`);
6233
6291
  if (!newTaskSpecs.length) throw new Error("Must provide at least one new task spec");
6234
- const newTasks = newTaskSpecs.map(
6235
- (spec, i) => createTask({
6292
+ const finalized = [];
6293
+ newTaskSpecs.forEach((spec, i) => {
6294
+ finalized.push(createTask({
6236
6295
  ...spec,
6237
6296
  order: original.order + i,
6238
- dependencies: i === 0 ? [...original.dependencies] : [newTaskSpecs[0].id ?? `split-${taskId}-0`],
6297
+ dependencies: i === 0 ? [...original.dependencies] : [finalized[i - 1].id],
6239
6298
  type: spec.type ?? original.type,
6240
6299
  assignedRunner: spec.assignedRunner ?? original.assignedRunner,
6241
6300
  assignedModel: spec.assignedModel ?? original.assignedModel,
@@ -6243,17 +6302,7 @@ ${taskB.description}`,
6243
6302
  autonomy: spec.autonomy ?? original.autonomy,
6244
6303
  sliceType: spec.sliceType ?? original.sliceType,
6245
6304
  userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered
6246
- })
6247
- );
6248
- const newTaskIds = /* @__PURE__ */ new Map();
6249
- newTasks.forEach((t, i) => newTaskIds.set(i, t.id));
6250
- const finalized = newTasks.map((t) => {
6251
- const deps = t.dependencies.map((depId) => {
6252
- const idx = newTaskSpecs.findIndex((s) => (s.id ?? "") === depId);
6253
- if (idx >= 0) return newTaskIds.get(idx) ?? depId;
6254
- return depId;
6255
- });
6256
- return { ...t, dependencies: deps };
6305
+ }));
6257
6306
  });
6258
6307
  const tailId = finalized[finalized.length - 1].id;
6259
6308
  const updatedOthers = this._planTasks.filter((t) => t.id !== taskId).map((t) => ({
@@ -6498,6 +6547,15 @@ var TaskOrchestrator = class {
6498
6547
  get isRunning() {
6499
6548
  return this.running || this.startingTaskIds.size > 0 || this.activeTaskSessions.size > 0;
6500
6549
  }
6550
+ /**
6551
+ * A runner is executing *right now*. Narrower than {@link isRunning}, which
6552
+ * also covers the armed-but-idle scheduler `tick()` deliberately leaves
6553
+ * behind when a plan is paused on a user task, a checkpoint or a hold —
6554
+ * nothing is executing then, so nothing is reading the plan mid-mutation.
6555
+ */
6556
+ get hasLiveWork() {
6557
+ return this.activeTaskSessions.size > 0 || this.startingTaskIds.size > 0;
6558
+ }
6501
6559
  get isReviewApproved() {
6502
6560
  return this.reviewApproved;
6503
6561
  }
@@ -6544,18 +6602,31 @@ var TaskOrchestrator = class {
6544
6602
  this.retryCounts.clear();
6545
6603
  this.onHold.clear();
6546
6604
  }
6605
+ /**
6606
+ * Adopt an edited plan without letting go of the run in progress. Everything
6607
+ * the scheduler owns — live sessions, holds, retry counts, the verifier and
6608
+ * the review approval — survives, because `loadPlan` clears all of it and a
6609
+ * mid-run edit is not a new run. Only the tasks change.
6610
+ *
6611
+ * The ids with live sessions are defended: one dropped from the edited plan is
6612
+ * carried over (its verdict still has to land somewhere), and one whose status
6613
+ * the snapshot predates is put back to `in_progress` — adopting the snapshot's
6614
+ * "pending" would offer the scheduler work a runner is already doing.
6615
+ */
6547
6616
  reconcilePlan(newTasks, planRunners = ["claude-code"]) {
6617
+ const adopted = [...newTasks];
6548
6618
  for (const taskId of this.activeTaskSessions.keys()) {
6549
6619
  const task = this.store.get(taskId);
6550
6620
  if (!task) continue;
6551
- const inNewPlan = newTasks.find((t) => t.id === taskId);
6552
- if (!inNewPlan) {
6553
- newTasks.push({ ...task });
6554
- } else if (inNewPlan.status !== "in_progress") {
6555
- console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${inNewPlan.status}" in modified plan, using orchestrator truth`);
6621
+ const at = adopted.findIndex((t) => t.id === taskId);
6622
+ if (at < 0) {
6623
+ adopted.push({ ...task });
6624
+ } else if (adopted[at].status !== "in_progress") {
6625
+ console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${adopted[at].status}" in modified plan, using orchestrator truth`);
6626
+ adopted[at] = { ...adopted[at], status: "in_progress" };
6556
6627
  }
6557
6628
  }
6558
- this.store.load(newTasks, planRunners);
6629
+ this.store.load(adopted, planRunners);
6559
6630
  this.planStatus = "running";
6560
6631
  }
6561
6632
  async start() {
@@ -6676,6 +6747,21 @@ ${output.slice(-3e3)}`);
6676
6747
  this.emit("onTaskChanged");
6677
6748
  await this.tick();
6678
6749
  }
6750
+ /**
6751
+ * Let go of a task that is leaving the plan. A live runner is cancelled
6752
+ * through {@link cancelTask} so the verifier generation is bumped before the
6753
+ * process dies; a spawn still in flight is dropped from `startingTaskIds`,
6754
+ * which is what makes {@link startTask} kill the session it is about to
6755
+ * receive. The id's remaining bookkeeping goes too — a hold or retry count
6756
+ * kept for a task that no longer exists would be inherited by nothing and
6757
+ * leaves `isRunning` reading true forever.
6758
+ */
6759
+ async releaseTask(taskId) {
6760
+ this.startingTaskIds.delete(taskId);
6761
+ if (this.activeTaskSessions.has(taskId)) await this.cancelTask(taskId);
6762
+ this.onHold.delete(taskId);
6763
+ this.retryCounts.delete(taskId);
6764
+ }
6679
6765
  async markTaskComplete(taskId) {
6680
6766
  const task = this.store.get(taskId);
6681
6767
  if (!task || task.status === "completed") return;
@@ -7135,28 +7221,28 @@ function posixBinDirs() {
7135
7221
  ];
7136
7222
  }
7137
7223
  function windowsBinDirs(env, home) {
7138
- const join13 = (...parts) => path5.win32.join(...parts);
7139
- const under = (base, ...parts) => base ? [join13(base, ...parts)] : [];
7224
+ const join14 = (...parts) => path5.win32.join(...parts);
7225
+ const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
7140
7226
  const appData = env.APPDATA;
7141
7227
  const localAppData = env.LOCALAPPDATA;
7142
7228
  const programData = env.ProgramData;
7143
7229
  return [
7144
7230
  // Native per-user installers, PowerShell-driven and otherwise.
7145
- join13(home, ".local", "bin"),
7146
- join13(home, ".opencode", "bin"),
7147
- join13(home, ".claude", "bin"),
7148
- join13(home, ".codex", "bin"),
7231
+ join14(home, ".local", "bin"),
7232
+ join14(home, ".opencode", "bin"),
7233
+ join14(home, ".claude", "bin"),
7234
+ join14(home, ".codex", "bin"),
7149
7235
  ...under(localAppData, "Programs", "claude"),
7150
7236
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
7151
7237
  // literal spelling is kept as a fallback for a host that does not export
7152
7238
  // APPDATA (a service account, a stripped CI container).
7153
7239
  ...under(appData, "npm"),
7154
- join13(home, "AppData", "Roaming", "npm"),
7240
+ join14(home, "AppData", "Roaming", "npm"),
7155
7241
  ...under(localAppData, "pnpm"),
7156
7242
  ...under(localAppData, "Yarn", "bin"),
7157
- join13(home, ".bun", "bin"),
7243
+ join14(home, ".bun", "bin"),
7158
7244
  // Windows package managers.
7159
- join13(home, "scoop", "shims"),
7245
+ join14(home, "scoop", "shims"),
7160
7246
  ...under(env.SCOOP, "shims"),
7161
7247
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
7162
7248
  ...under(programData, "chocolatey", "bin"),
@@ -7166,7 +7252,7 @@ function windowsBinDirs(env, home) {
7166
7252
  ...under(localAppData, "Microsoft", "WindowsApps"),
7167
7253
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
7168
7254
  ...under(localAppData, "Volta", "bin"),
7169
- join13(home, ".volta", "bin")
7255
+ join14(home, ".volta", "bin")
7170
7256
  ];
7171
7257
  }
7172
7258
  function wellKnownBinDirs(deps = {}) {
@@ -7289,6 +7375,26 @@ Install the native executable \u2014 Claude Code's Windows installer, Codex's re
7289
7375
  function hasLineBreak(parts) {
7290
7376
  return parts.some((part) => /[\r\n]/.test(part));
7291
7377
  }
7378
+ var ExecutableNotFoundError = class extends Error {
7379
+ constructor(command, searchedPath) {
7380
+ super(`Could not find "${command}" on PATH.
7381
+
7382
+ Searched: ${searchedPath || "(empty PATH)"}`);
7383
+ this.command = command;
7384
+ this.searchedPath = searchedPath;
7385
+ this.name = "ExecutableNotFoundError";
7386
+ }
7387
+ command;
7388
+ searchedPath;
7389
+ };
7390
+ function isResolvableOnPosixPath(command, PATH, exists) {
7391
+ if (command.includes("/")) return exists(command);
7392
+ return PATH.split(path6.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path6.join(dir, command)));
7393
+ }
7394
+ function isExecutableResolved(command, plan, PATH, deps = {}) {
7395
+ if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
7396
+ return isResolvableOnPosixPath(command, PATH, deps.exists ?? defaultExists2);
7397
+ }
7292
7398
  function defaultExists2(candidate) {
7293
7399
  try {
7294
7400
  return fsSync2.statSync(candidate).isFile();
@@ -8048,6 +8154,10 @@ var ModelDiscovery = class {
8048
8154
  clear() {
8049
8155
  this.cache.clear();
8050
8156
  }
8157
+ /** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
8158
+ getCached(runner) {
8159
+ return this.cache.get(runner);
8160
+ }
8051
8161
  };
8052
8162
 
8053
8163
  // src/services/ModelCatalog.ts
@@ -8366,6 +8476,17 @@ var ModelResolver = class {
8366
8476
  refreshRunnerModels() {
8367
8477
  this.discovery.clear();
8368
8478
  }
8479
+ /**
8480
+ * Synchronous reads of whatever the last `modelsForRunners`/`pickerOptions`
8481
+ * call already cached — never trigger discovery or a fetch. For callers
8482
+ * (e.g. a settings write) that need a catalog without going async.
8483
+ */
8484
+ getCachedRunnerModels(runner) {
8485
+ return this.discovery.getCached(runner) ?? [];
8486
+ }
8487
+ getCachedPickerOptions() {
8488
+ return this.cachedPickerOptions ?? [];
8489
+ }
8369
8490
  invalidate() {
8370
8491
  this.discovery.clear();
8371
8492
  this.cachedPickerOptions = null;
@@ -8493,6 +8614,28 @@ var RunnerInstallation = class {
8493
8614
  // src/services/harness/CliAgentAiService.ts
8494
8615
  var import_child_process5 = require("child_process");
8495
8616
 
8617
+ // src/utils/workspace.ts
8618
+ var fs3 = __toESM(require("fs"));
8619
+ var WorkspaceNotFoundError = class extends Error {
8620
+ constructor(workspace) {
8621
+ super(`Workspace "${workspace}" does not exist.`);
8622
+ this.workspace = workspace;
8623
+ this.name = "WorkspaceNotFoundError";
8624
+ }
8625
+ workspace;
8626
+ };
8627
+ function defaultIsDirectory(candidate) {
8628
+ try {
8629
+ return fs3.statSync(candidate).isDirectory();
8630
+ } catch {
8631
+ return false;
8632
+ }
8633
+ }
8634
+ function assertWorkspaceExists(workspace, deps = {}) {
8635
+ const isDirectory = deps.isDirectory ?? defaultIsDirectory;
8636
+ if (!isDirectory(workspace)) throw new WorkspaceNotFoundError(workspace);
8637
+ }
8638
+
8496
8639
  // src/services/harness/AgentAdapter.ts
8497
8640
  var LineBuffer = class {
8498
8641
  buffer = "";
@@ -8546,6 +8689,7 @@ var StdioAgentAdapter = class {
8546
8689
  async handshake(_opts) {
8547
8690
  }
8548
8691
  async start(opts) {
8692
+ assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
8549
8693
  this.processEnded = new Promise((resolve3) => {
8550
8694
  this.markEnded = resolve3;
8551
8695
  });
@@ -8557,6 +8701,9 @@ var StdioAgentAdapter = class {
8557
8701
  platform: this.deps.platform,
8558
8702
  resolvePath
8559
8703
  });
8704
+ if (!isExecutableResolved(spec.command, launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
8705
+ throw new ExecutableNotFoundError(spec.command, PATH);
8706
+ }
8560
8707
  this.process = this.deps.spawn(launch.file, launch.args, {
8561
8708
  env: this.spawnEnv,
8562
8709
  stdio: ["pipe", "pipe", "pipe"],
@@ -9179,12 +9326,16 @@ var OpenCodeAdapter = class {
9179
9326
  opts = null;
9180
9327
  async start(opts) {
9181
9328
  this.opts = opts;
9329
+ assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
9182
9330
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
9183
9331
  const PATH = await resolvePath();
9184
9332
  const launch = await planDirectLaunch("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
9185
9333
  platform: this.deps.platform,
9186
9334
  resolvePath
9187
9335
  });
9336
+ if (!isExecutableResolved("opencode", launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
9337
+ throw new ExecutableNotFoundError("opencode", PATH);
9338
+ }
9188
9339
  this.process = this.deps.spawn(launch.file, launch.args, {
9189
9340
  env: withPath(process.env, PATH),
9190
9341
  stdio: ["pipe", "pipe", "pipe"],
@@ -9498,7 +9649,9 @@ var CliAgentAiService = class {
9498
9649
  spawn: deps.spawn ?? import_child_process5.spawn,
9499
9650
  fetch: deps.fetch ?? globalThis.fetch,
9500
9651
  resolvePath: deps.resolvePath,
9501
- platform: deps.platform
9652
+ platform: deps.platform,
9653
+ isDirectory: deps.isDirectory,
9654
+ exists: deps.exists
9502
9655
  };
9503
9656
  this.makeAdapter = deps.createAdapter ?? defaultAdapter;
9504
9657
  this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
@@ -9843,8 +9996,8 @@ var CliAgentAiService = class {
9843
9996
  this.lastNativeSessionId = previousSessionId;
9844
9997
  }
9845
9998
  }
9846
- async researchAndPlan(userDescription, runners, modelsByRunner, fs9, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
9847
- const contextStr = await collectResearchContext(fs9, runners);
9999
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
10000
+ const contextStr = await collectResearchContext(fs10, runners);
9848
10001
  const researchLog = [
9849
10002
  { id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
9850
10003
  ];
@@ -10214,16 +10367,16 @@ function executionSummary(tasks) {
10214
10367
  }
10215
10368
 
10216
10369
  // src/utils/sessionStore.ts
10217
- var fs4 = __toESM(require("fs"));
10370
+ var fs5 = __toESM(require("fs"));
10218
10371
  var path10 = __toESM(require("path"));
10219
10372
 
10220
10373
  // src/utils/fsHelpers.ts
10221
- var fs3 = __toESM(require("fs"));
10374
+ var fs4 = __toESM(require("fs"));
10222
10375
  var path9 = __toESM(require("path"));
10223
10376
  var STATE_DIR = ".ordewell";
10224
10377
  function ensureDir(dir) {
10225
- if (!fs3.existsSync(dir)) {
10226
- fs3.mkdirSync(dir, { recursive: true });
10378
+ if (!fs4.existsSync(dir)) {
10379
+ fs4.mkdirSync(dir, { recursive: true });
10227
10380
  }
10228
10381
  }
10229
10382
  function getStateDir(baseDir) {
@@ -10268,16 +10421,16 @@ function saveSession(plan, goal, baseDir, id) {
10268
10421
  };
10269
10422
  const filename = sessionFilename(meta);
10270
10423
  const filePath = path10.join(sessionsDir, filename);
10271
- fs4.writeFileSync(filePath, JSON.stringify(session, null, 2));
10424
+ fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
10272
10425
  return meta;
10273
10426
  }
10274
10427
  function listSessions(baseDir, logger = defaultLogger) {
10275
10428
  const sessionsDir = getSessionsDir(baseDir);
10276
- if (!fs4.existsSync(sessionsDir)) return [];
10277
- const files = fs4.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
10429
+ if (!fs5.existsSync(sessionsDir)) return [];
10430
+ const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
10278
10431
  return files.map((f) => {
10279
10432
  try {
10280
- const raw = fs4.readFileSync(path10.join(sessionsDir, f), "utf-8");
10433
+ const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
10281
10434
  const parsed = JSON.parse(raw);
10282
10435
  if (hasScalarRunner(parsed)) {
10283
10436
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -10292,10 +10445,10 @@ function listSessions(baseDir, logger = defaultLogger) {
10292
10445
  }).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10293
10446
  }
10294
10447
  function findSessionFile(sessionsDir, sessionId, logger) {
10295
- const files = fs4.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
10448
+ const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
10296
10449
  for (const f of files) {
10297
10450
  try {
10298
- const raw = fs4.readFileSync(path10.join(sessionsDir, f), "utf-8");
10451
+ const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
10299
10452
  const session = JSON.parse(raw);
10300
10453
  if (session.meta.id === sessionId) return f;
10301
10454
  } catch (err) {
@@ -10306,11 +10459,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
10306
10459
  }
10307
10460
  function loadSession(sessionId, baseDir, logger = defaultLogger) {
10308
10461
  const sessionsDir = getSessionsDir(baseDir);
10309
- if (!fs4.existsSync(sessionsDir)) return null;
10462
+ if (!fs5.existsSync(sessionsDir)) return null;
10310
10463
  try {
10311
10464
  const file = findSessionFile(sessionsDir, sessionId, logger);
10312
10465
  if (file) {
10313
- const raw = fs4.readFileSync(path10.join(sessionsDir, file), "utf-8");
10466
+ const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
10314
10467
  const parsed = JSON.parse(raw);
10315
10468
  if (hasScalarRunner(parsed)) {
10316
10469
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -10340,11 +10493,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
10340
10493
  }
10341
10494
  function deleteSession(sessionId, baseDir, logger = defaultLogger) {
10342
10495
  const sessionsDir = getSessionsDir(baseDir);
10343
- if (!fs4.existsSync(sessionsDir)) return false;
10496
+ if (!fs5.existsSync(sessionsDir)) return false;
10344
10497
  try {
10345
10498
  const file = findSessionFile(sessionsDir, sessionId, logger);
10346
10499
  if (file) {
10347
- fs4.unlinkSync(path10.join(sessionsDir, file));
10500
+ fs5.unlinkSync(path10.join(sessionsDir, file));
10348
10501
  return true;
10349
10502
  }
10350
10503
  } catch (err) {
@@ -10354,6 +10507,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
10354
10507
  }
10355
10508
 
10356
10509
  // src/services/createSession.ts
10510
+ var PlanEditError = class extends Error {
10511
+ constructor(message) {
10512
+ super(message);
10513
+ this.name = "PlanEditError";
10514
+ }
10515
+ };
10357
10516
  function sessionRuntimeSettings(settings) {
10358
10517
  return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
10359
10518
  }
@@ -10626,12 +10785,41 @@ var Session = class {
10626
10785
  get isExecuting() {
10627
10786
  return this.orchestrator.isRunning;
10628
10787
  }
10788
+ /** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
10789
+ get hasLiveWork() {
10790
+ return this.orchestrator.hasLiveWork;
10791
+ }
10629
10792
  get status() {
10630
10793
  return this.orchestrator.status;
10631
10794
  }
10632
10795
  get sessionConfig() {
10633
10796
  return this.config;
10634
10797
  }
10798
+ /**
10799
+ * Deny every parked approval as soon as a planning turn is aborted, for the
10800
+ * same reason `beginFreshPlan` does it: a prompt raised by a turn nobody is
10801
+ * waiting on has no one left to serve, and denying it unblocks the research
10802
+ * loop instead of stranding it. Without this the request sat out its
10803
+ * five-minute timeout and the loop then carried on as if nothing had
10804
+ * happened — the abort was real, but invisible until long after the user
10805
+ * pressed stop.
10806
+ *
10807
+ * The listener is returned as a disposer rather than left attached: callers
10808
+ * own the signal and may reuse it, and a leaked listener would deny the
10809
+ * *next* turn's prompts the moment that stale signal aborted.
10810
+ */
10811
+ denyApprovalsOnAbort(signal) {
10812
+ if (!signal) return () => {
10813
+ };
10814
+ if (signal.aborted) {
10815
+ this.approvals.clear();
10816
+ return () => {
10817
+ };
10818
+ }
10819
+ const onAbort = () => this.approvals.clear();
10820
+ signal.addEventListener("abort", onAbort);
10821
+ return () => signal.removeEventListener("abort", onAbort);
10822
+ }
10635
10823
  async startExecution() {
10636
10824
  if (!this.plan || !this.store.planTasks.length) throw new Error("No plan to execute");
10637
10825
  this.store.clearLog();
@@ -10654,19 +10842,25 @@ var Session = class {
10654
10842
  const settings = this.settingsFn();
10655
10843
  const { modelAllowlist } = settings;
10656
10844
  const modes = plannerModesFrom(settings, this.config.autonomousMode);
10657
- const plan = await this.planner.generate({
10658
- goal,
10659
- runners: chosenRunners,
10660
- modelsByRunner,
10661
- runnerModes,
10662
- autonomousDefault: this.config.autonomousMode,
10663
- fs: this.fsAdapter,
10664
- fetcher: this.fetcher,
10665
- onProgress: (p) => this.translateProgress(p),
10666
- signal: options?.signal,
10667
- perRunnerAllowlist: modelAllowlist,
10668
- modes
10669
- });
10845
+ const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
10846
+ let plan;
10847
+ try {
10848
+ plan = await this.planner.generate({
10849
+ goal,
10850
+ runners: chosenRunners,
10851
+ modelsByRunner,
10852
+ runnerModes,
10853
+ autonomousDefault: this.config.autonomousMode,
10854
+ fs: this.fsAdapter,
10855
+ fetcher: this.fetcher,
10856
+ onProgress: (p) => this.translateProgress(p),
10857
+ signal: options?.signal,
10858
+ perRunnerAllowlist: modelAllowlist,
10859
+ modes
10860
+ });
10861
+ } finally {
10862
+ releaseAbort();
10863
+ }
10670
10864
  this.plan = plan;
10671
10865
  this.orchestrator.loadPlan(plan.tasks, plan.runners);
10672
10866
  this.store.resetForRun({ preserveCompleted: false });
@@ -10703,23 +10897,28 @@ var Session = class {
10703
10897
  researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
10704
10898
  conversationHistory: [{ role: "user", content: goal, timestamp: now }]
10705
10899
  };
10706
- const turn = await this.aiService.startConversation({
10707
- goal,
10708
- runners: chosenRunners,
10709
- modelsByRunner: filteredModels,
10710
- runnerModes,
10711
- autonomousDefault: this.config.autonomousMode,
10712
- grillMeEnabled: grillMeEnabled ?? false,
10713
- prdEnabled: prdEnabled ?? false,
10714
- reviewEnabled: reviewEnabled ?? false,
10715
- verificationEnabled: verificationEnabled ?? false,
10716
- researchSubagentsEnabled: researchSubagentsEnabled ?? false,
10717
- fs: this.fsAdapter,
10718
- fetcher: this.fetcher,
10719
- onProgress: (p) => this.translateProgress(p),
10720
- signal: options?.signal
10721
- });
10722
- return this.settleTurn(turn, options);
10900
+ const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
10901
+ try {
10902
+ const turn = await this.aiService.startConversation({
10903
+ goal,
10904
+ runners: chosenRunners,
10905
+ modelsByRunner: filteredModels,
10906
+ runnerModes,
10907
+ autonomousDefault: this.config.autonomousMode,
10908
+ grillMeEnabled: grillMeEnabled ?? false,
10909
+ prdEnabled: prdEnabled ?? false,
10910
+ reviewEnabled: reviewEnabled ?? false,
10911
+ verificationEnabled: verificationEnabled ?? false,
10912
+ researchSubagentsEnabled: researchSubagentsEnabled ?? false,
10913
+ fs: this.fsAdapter,
10914
+ fetcher: this.fetcher,
10915
+ onProgress: (p) => this.translateProgress(p),
10916
+ signal: options?.signal
10917
+ });
10918
+ return await this.settleTurn(turn, options);
10919
+ } finally {
10920
+ releaseAbort();
10921
+ }
10723
10922
  }
10724
10923
  /**
10725
10924
  * Every subsequent user reply in the planning conversation — grill-me
@@ -10737,21 +10936,26 @@ var Session = class {
10737
10936
  ${userMessage}` : userMessage;
10738
10937
  const aiService = this.aiService;
10739
10938
  const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
10740
- let turn = canContinueLive ? await aiService.continueConversation(
10741
- outgoing,
10742
- (p) => this.translateProgress(p),
10743
- options?.signal
10744
- ) : await this.resumeConversation(outgoing, priorHistory, options);
10745
- if ((turn.kind === "task_ops" || turn.kind === "plan") && this.isExecuting) {
10746
- this.queueMessage(userMessage);
10747
- this.plan.queuedMessages = this.getQueuedMessages();
10748
- turn = {
10749
- kind: "message",
10750
- text: `Execution is running, so I queued your change \u2014 it will be applied between task batches (${this.queuedCount} queued).`,
10751
- researchLog: turn.researchLog
10752
- };
10939
+ const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
10940
+ try {
10941
+ let turn = canContinueLive ? await aiService.continueConversation(
10942
+ outgoing,
10943
+ (p) => this.translateProgress(p),
10944
+ options?.signal
10945
+ ) : await this.resumeConversation(outgoing, priorHistory, options);
10946
+ if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
10947
+ this.queueMessage(userMessage);
10948
+ this.plan.queuedMessages = this.getQueuedMessages();
10949
+ turn = {
10950
+ kind: "message",
10951
+ text: `Execution is running, so I queued your change \u2014 it will be applied between task batches (${this.queuedCount} queued).`,
10952
+ researchLog: turn.researchLog
10953
+ };
10954
+ }
10955
+ return await this.settleTurn(turn, options);
10956
+ } finally {
10957
+ releaseAbort();
10753
10958
  }
10754
- return this.settleTurn(turn, options);
10755
10959
  }
10756
10960
  /**
10757
10961
  * Drive a planner turn to a persisted, broadcast outcome. Task edits apply
@@ -10790,7 +10994,10 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
10790
10994
  maxRepairs: 2,
10791
10995
  onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
10792
10996
  });
10793
- if ("plan" in settled) return settled.plan;
10997
+ if ("plan" in settled) {
10998
+ await this.orchestrator.tick();
10999
+ return settled.plan;
11000
+ }
10794
11001
  return this.applyConversationTurn(settled.turn);
10795
11002
  }
10796
11003
  /**
@@ -10805,9 +11012,9 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
10805
11012
  const lines = this.store.planTasks.map(
10806
11013
  (t) => `#${t.order} id=${t.id} "${t.title}" [${t.status}] runner:${t.assignedRunner}${t.assignedModel ? ` model:${t.assignedModel.modelId}` : ""} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`
10807
11014
  );
10808
- const running = this.store.planTasks.filter((t) => t.status === "in_progress");
10809
- const execNote = this.isExecuting ? `
10810
- Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${running.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
11015
+ const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
11016
+ const execNote = this.hasLiveWork ? `
11017
+ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
10811
11018
  const modelLines = [];
10812
11019
  for (const runner of this.plan.runners) {
10813
11020
  const models = this.modelsCache[runner] ?? [];
@@ -10849,8 +11056,12 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
10849
11056
  this.plan.researchLog = [...this.plan.researchLog ?? [], ...turn.researchLog];
10850
11057
  const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
10851
11058
  const coerced = coerceAssignments(result.tasks, allowlist, this.plan.runners, this.modelsCache);
10852
- this.orchestrator.loadPlan(coerced, this.plan.runners);
10853
- this.store.resetForRun();
11059
+ if (this.orchestrator.isRunning) {
11060
+ this.orchestrator.reconcilePlan(coerced, this.plan.runners);
11061
+ } else {
11062
+ this.orchestrator.loadPlan(coerced, this.plan.runners);
11063
+ this.store.resetForRun();
11064
+ }
10854
11065
  this.appendTranscript("assistant", content, now);
10855
11066
  return true;
10856
11067
  },
@@ -11086,8 +11297,33 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
11086
11297
  notify();
11087
11298
  return this.plan;
11088
11299
  }
11089
- updateTask(taskId, changes) {
11090
- return this.mutatePlan(
11300
+ /**
11301
+ * The direct-edit seam: {@link mutatePlan} plus the reschedule every
11302
+ * structural edit owes an armed scheduler. Nothing else wakes one after a
11303
+ * hand edit — the queue-drain path never runs, because a direct edit never
11304
+ * queues — so a task the edit just unblocked would sit ready and never
11305
+ * start. `tick()` no-ops while the scheduler is idle, so this costs nothing
11306
+ * during plain planning. The planner-driven path re-ticks in
11307
+ * {@link settleTurn} instead; it must not tick twice.
11308
+ */
11309
+ async editPlan(op, notify) {
11310
+ const plan = this.mutatePlan(op, notify);
11311
+ if (!plan) return null;
11312
+ await this.orchestrator.tick();
11313
+ return plan;
11314
+ }
11315
+ /**
11316
+ * Patch one task's fields. A hand-set dependency list is the one patch that
11317
+ * can leave the plan unschedulable, so it goes through the same
11318
+ * {@link canSetDependencies} guard the planner's pickers use rather than a
11319
+ * second copy of the rule — and throws, so the surface can say why.
11320
+ */
11321
+ async updateTask(taskId, changes) {
11322
+ if (changes.dependencies) {
11323
+ const check = canSetDependencies(this.store.planTasks, taskId, changes.dependencies);
11324
+ if (!check.ok) throw new PlanEditError(check.error ?? "Those dependencies are not valid");
11325
+ }
11326
+ return this.editPlan(
11091
11327
  () => Boolean(this.store.update(taskId, changes)),
11092
11328
  () => this.broadcast({ type: "task_updated", taskId, changes })
11093
11329
  );
@@ -11097,8 +11333,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
11097
11333
  * because a runner change is never a single-field edit: the task's model,
11098
11334
  * thinking effort and mode are all scoped to its runner, so they are
11099
11335
  * re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
11100
- * That needs discovery, which is async — hence a method of its own rather
11101
- * than a branch inside the sync `updateTask`.
11336
+ * That needs discovery, which is why it is not a branch inside `updateTask`.
11102
11337
  *
11103
11338
  * The runner is also admitted into `plan.runners` — see {@link admitRunner}.
11104
11339
  */
@@ -11110,7 +11345,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
11110
11345
  const catalog = await this.catalogFor(runner);
11111
11346
  const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner));
11112
11347
  if (Object.keys(changes).length === 0) return this.plan;
11113
- return this.mutatePlan(() => {
11348
+ return this.editPlan(() => {
11114
11349
  if (!this.store.update(taskId, changes)) return false;
11115
11350
  this.admitRunner(runner, catalog.models);
11116
11351
  return true;
@@ -11157,24 +11392,29 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
11157
11392
  if (models.length > 0) this.modelsCache = { ...this.modelsCache, [runner]: models };
11158
11393
  }
11159
11394
  /**
11160
- * Replace one task's dependency list. Separate from {@link updateTask}
11161
- * because a hand-edited graph is the one task edit that can leave a plan
11162
- * unschedulable: `canSetDependencies` is the guard, and it lives behind this
11163
- * one method so the API and both surfaces' pickers reject the same edits
11164
- * rather than each carrying a copy of the rule. Throws so a surface can say
11165
- * why the edit was refused.
11395
+ * Replace one task's dependency list — the named entry point the surfaces'
11396
+ * dependency pickers call. The guard itself lives in {@link updateTask}, so
11397
+ * a dependency list arriving as a plain field patch is rejected by the same
11398
+ * rule instead of slipping past it.
11166
11399
  */
11167
- setTaskDependencies(taskId, dependencies) {
11400
+ async setTaskDependencies(taskId, dependencies) {
11168
11401
  if (!this.plan) return null;
11169
- const check = canSetDependencies(this.store.planTasks, taskId, dependencies);
11170
- if (!check.ok) throw new Error(check.error ?? "Those dependencies are not valid");
11171
11402
  return this.updateTask(taskId, { dependencies });
11172
11403
  }
11173
11404
  async completeTask(taskId) {
11174
11405
  await this.markTaskComplete(taskId);
11175
11406
  }
11176
- removeTask(taskId) {
11177
- return this.mutatePlan(() => (this.store.remove(taskId), true));
11407
+ /**
11408
+ * Delete one task. A running task is cancelled first: the plan can drop it
11409
+ * either way, but nothing can reach its runner afterwards — the tmux session
11410
+ * outlives the plan and the orchestrator keeps counting it as active. The
11411
+ * planner-driven path refuses instead (see {@link applyTaskOps}); a user
11412
+ * deleting their own task means it.
11413
+ */
11414
+ async removeTask(taskId) {
11415
+ if (!this.plan || !this.store.get(taskId)) return null;
11416
+ await this.orchestrator.releaseTask(taskId);
11417
+ return this.editPlan(() => (this.store.remove(taskId), true));
11178
11418
  }
11179
11419
  /**
11180
11420
  * Add one task, filling in whatever the caller left unset. A task with no
@@ -11194,20 +11434,20 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
11194
11434
  const runner = draft.assignedRunner ?? this.plan.runners[0];
11195
11435
  const dependencies = (draft.dependencies ?? []).filter((id) => this.store.get(id));
11196
11436
  const catalog = draft.type === "user" ? null : await this.catalogFor(runner);
11197
- return this.mutatePlan(() => {
11437
+ return this.editPlan(() => {
11198
11438
  this.store.add({ ...draft, ...catalog ? runnerAssignment(this.allowedCatalog(catalog, runner), draft) : {}, assignedRunner: runner, dependencies });
11199
11439
  if (catalog) this.admitRunner(runner, catalog.models);
11200
11440
  return true;
11201
11441
  });
11202
11442
  }
11203
- mergeTasks(taskIdA, taskIdB) {
11204
- return this.mutatePlan(() => (this.store.merge(taskIdA, taskIdB), true));
11443
+ async mergeTasks(taskIdA, taskIdB) {
11444
+ return this.editPlan(() => (this.store.merge(taskIdA, taskIdB), true));
11205
11445
  }
11206
- mergeMultipleTasks(taskIds) {
11207
- return this.mutatePlan(() => (this.store.mergeMultiple(taskIds), true));
11446
+ async mergeMultipleTasks(taskIds) {
11447
+ return this.editPlan(() => (this.store.mergeMultiple(taskIds), true));
11208
11448
  }
11209
- splitTask(taskId, newTasks) {
11210
- return this.mutatePlan(() => (this.store.split(taskId, newTasks), true));
11449
+ async splitTask(taskId, newTasks) {
11450
+ return this.editPlan(() => (this.store.split(taskId, newTasks), true));
11211
11451
  }
11212
11452
  /**
11213
11453
  * Planner-driven merge: validate compatibility up front, then ask the planner
@@ -12079,13 +12319,13 @@ var TmuxRunner = class extends AbstractRunner {
12079
12319
  };
12080
12320
 
12081
12321
  // src/plugins/RunnerRegistry.ts
12082
- var fs6 = __toESM(require("fs"));
12322
+ var fs7 = __toESM(require("fs"));
12083
12323
  var path12 = __toESM(require("path"));
12084
12324
  var os5 = __toESM(require("os"));
12085
12325
  var import_child_process9 = require("child_process");
12086
12326
 
12087
12327
  // src/plugins/FsPluginStore.ts
12088
- var fs5 = __toESM(require("fs"));
12328
+ var fs6 = __toESM(require("fs"));
12089
12329
  var path11 = __toESM(require("path"));
12090
12330
  var os4 = __toESM(require("os"));
12091
12331
  function userPluginsDir() {
@@ -12098,9 +12338,9 @@ function isValidManifest(obj) {
12098
12338
  }
12099
12339
  function loadManifestFromDir(dir) {
12100
12340
  const manifestPath = path11.join(dir, "manifest.json");
12101
- if (!fs5.existsSync(manifestPath)) return null;
12341
+ if (!fs6.existsSync(manifestPath)) return null;
12102
12342
  try {
12103
- const raw = fs5.readFileSync(manifestPath, "utf8");
12343
+ const raw = fs6.readFileSync(manifestPath, "utf8");
12104
12344
  const parsed = JSON.parse(raw);
12105
12345
  if (!isValidManifest(parsed)) return null;
12106
12346
  return parsed;
@@ -12110,29 +12350,29 @@ function loadManifestFromDir(dir) {
12110
12350
  }
12111
12351
  function copyDirSync(src, dest) {
12112
12352
  ensureDir(dest);
12113
- const entries = fs5.readdirSync(src, { withFileTypes: true });
12353
+ const entries = fs6.readdirSync(src, { withFileTypes: true });
12114
12354
  for (const entry of entries) {
12115
12355
  const srcPath = path11.join(src, entry.name);
12116
12356
  const destPath = path11.join(dest, entry.name);
12117
12357
  if (entry.isDirectory()) {
12118
12358
  copyDirSync(srcPath, destPath);
12119
12359
  } else {
12120
- fs5.copyFileSync(srcPath, destPath);
12360
+ fs6.copyFileSync(srcPath, destPath);
12121
12361
  }
12122
12362
  }
12123
12363
  }
12124
12364
  function rmDirSync(dir) {
12125
- if (!fs5.existsSync(dir)) return;
12126
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
12365
+ if (!fs6.existsSync(dir)) return;
12366
+ const entries = fs6.readdirSync(dir, { withFileTypes: true });
12127
12367
  for (const entry of entries) {
12128
12368
  const p = path11.join(dir, entry.name);
12129
12369
  if (entry.isDirectory()) {
12130
12370
  rmDirSync(p);
12131
12371
  } else {
12132
- fs5.unlinkSync(p);
12372
+ fs6.unlinkSync(p);
12133
12373
  }
12134
12374
  }
12135
- fs5.rmdirSync(dir);
12375
+ fs6.rmdirSync(dir);
12136
12376
  }
12137
12377
  var FsPluginStore = class {
12138
12378
  getUserPluginsDir() {
@@ -12140,10 +12380,10 @@ var FsPluginStore = class {
12140
12380
  }
12141
12381
  listUserPluginDirs() {
12142
12382
  const dir = userPluginsDir();
12143
- if (!fs5.existsSync(dir)) return [];
12383
+ if (!fs6.existsSync(dir)) return [];
12144
12384
  let entries;
12145
12385
  try {
12146
- entries = fs5.readdirSync(dir, { withFileTypes: true });
12386
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
12147
12387
  } catch {
12148
12388
  return [];
12149
12389
  }
@@ -12162,20 +12402,20 @@ var FsPluginStore = class {
12162
12402
  ensureDir(dir);
12163
12403
  }
12164
12404
  writeFile(filePath, content) {
12165
- fs5.writeFileSync(filePath, content);
12405
+ fs6.writeFileSync(filePath, content);
12166
12406
  }
12167
12407
  readFile(filePath) {
12168
12408
  try {
12169
- return fs5.readFileSync(filePath, "utf8");
12409
+ return fs6.readFileSync(filePath, "utf8");
12170
12410
  } catch {
12171
12411
  return null;
12172
12412
  }
12173
12413
  }
12174
12414
  dirExists(path15) {
12175
- return fs5.existsSync(path15) && fs5.statSync(path15).isDirectory();
12415
+ return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
12176
12416
  }
12177
12417
  exists(path15) {
12178
- return fs5.existsSync(path15);
12418
+ return fs6.existsSync(path15);
12179
12419
  }
12180
12420
  };
12181
12421
 
@@ -12644,20 +12884,20 @@ echo "Running ${name} with prompt: $prompt"
12644
12884
  };
12645
12885
  function fsReaddirSync(dir) {
12646
12886
  try {
12647
- return fs6.readdirSync(dir);
12887
+ return fs7.readdirSync(dir);
12648
12888
  } catch {
12649
12889
  return [];
12650
12890
  }
12651
12891
  }
12652
12892
  function fsChmodSync(fpath, mode) {
12653
12893
  try {
12654
- fs6.chmodSync(fpath, mode);
12894
+ fs7.chmodSync(fpath, mode);
12655
12895
  } catch {
12656
12896
  }
12657
12897
  }
12658
12898
 
12659
12899
  // src/utils/stateStore.ts
12660
- var fs7 = __toESM(require("fs"));
12900
+ var fs8 = __toESM(require("fs"));
12661
12901
  var path13 = __toESM(require("path"));
12662
12902
  var STATE_FILE = "state.json";
12663
12903
  function getStatePath(baseDir) {
@@ -12667,14 +12907,14 @@ function saveState(plan, baseDir) {
12667
12907
  const statePath = getStatePath(baseDir ?? process.cwd());
12668
12908
  ensureDir(path13.dirname(statePath));
12669
12909
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
12670
- fs7.writeFileSync(statePath, JSON.stringify(plan, null, 2));
12910
+ fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
12671
12911
  }
12672
12912
  function loadState(baseDir, logger = defaultLogger) {
12673
12913
  const dir = baseDir ?? process.cwd();
12674
12914
  const statePath = getStatePath(dir);
12675
- if (!fs7.existsSync(statePath)) return null;
12915
+ if (!fs8.existsSync(statePath)) return null;
12676
12916
  try {
12677
- const raw = fs7.readFileSync(statePath, "utf-8");
12917
+ const raw = fs8.readFileSync(statePath, "utf-8");
12678
12918
  const parsed = JSON.parse(raw);
12679
12919
  if (!Array.isArray(parsed.runners)) {
12680
12920
  logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
@@ -12689,15 +12929,15 @@ function loadState(baseDir, logger = defaultLogger) {
12689
12929
  function clearState(baseDir) {
12690
12930
  const dir = baseDir ?? process.cwd();
12691
12931
  const statePath = getStatePath(dir);
12692
- if (fs7.existsSync(statePath)) fs7.unlinkSync(statePath);
12932
+ if (fs8.existsSync(statePath)) fs8.unlinkSync(statePath);
12693
12933
  }
12694
12934
  function stateExists(baseDir) {
12695
12935
  const dir = baseDir ?? process.cwd();
12696
- return fs7.existsSync(getStatePath(dir));
12936
+ return fs8.existsSync(getStatePath(dir));
12697
12937
  }
12698
12938
 
12699
12939
  // src/services/SettingsService.ts
12700
- var fs8 = __toESM(require("fs"));
12940
+ var fs9 = __toESM(require("fs"));
12701
12941
  var path14 = __toESM(require("path"));
12702
12942
  var os6 = __toESM(require("os"));
12703
12943
  var DEFAULTS = {
@@ -12713,6 +12953,17 @@ function getSettingsPath() {
12713
12953
  if (override && override.trim()) return override.trim();
12714
12954
  return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
12715
12955
  }
12956
+ function readPlannerModels(raw) {
12957
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
12958
+ const out = {};
12959
+ for (const [provider, entry] of Object.entries(raw)) {
12960
+ if (!entry || typeof entry !== "object") continue;
12961
+ const { model, effort } = entry;
12962
+ if (typeof model !== "string" || !model.trim()) continue;
12963
+ out[provider] = typeof effort === "string" && effort.trim() ? { model, effort } : { model };
12964
+ }
12965
+ return Object.keys(out).length > 0 ? out : void 0;
12966
+ }
12716
12967
  var SettingsService = class {
12717
12968
  filePath;
12718
12969
  cache = null;
@@ -12733,7 +12984,7 @@ var SettingsService = class {
12733
12984
  }
12734
12985
  fileMtimeMs() {
12735
12986
  try {
12736
- return fs8.statSync(this.filePath).mtimeMs;
12987
+ return fs9.statSync(this.filePath).mtimeMs;
12737
12988
  } catch {
12738
12989
  return null;
12739
12990
  }
@@ -12806,6 +13057,26 @@ var SettingsService = class {
12806
13057
  }
12807
13058
  this.persist();
12808
13059
  }
13060
+ getPlannerModel(provider) {
13061
+ return this.getAll().plannerModels?.[provider];
13062
+ }
13063
+ setPlannerModel(provider, entry) {
13064
+ this.getAll();
13065
+ if (entry === void 0) {
13066
+ if (this.cache.plannerModels) {
13067
+ delete this.cache.plannerModels[provider];
13068
+ if (Object.keys(this.cache.plannerModels).length === 0) {
13069
+ this.cache.plannerModels = void 0;
13070
+ }
13071
+ }
13072
+ } else {
13073
+ if (!this.cache.plannerModels) {
13074
+ this.cache.plannerModels = {};
13075
+ }
13076
+ this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
13077
+ }
13078
+ this.persist();
13079
+ }
12809
13080
  getEnabledRunners() {
12810
13081
  return this.getAll().enabledRunners;
12811
13082
  }
@@ -12816,8 +13087,8 @@ var SettingsService = class {
12816
13087
  }
12817
13088
  load() {
12818
13089
  try {
12819
- if (fs8.existsSync(this.filePath)) {
12820
- const raw = JSON.parse(fs8.readFileSync(this.filePath, "utf-8"));
13090
+ if (fs9.existsSync(this.filePath)) {
13091
+ const raw = JSON.parse(fs9.readFileSync(this.filePath, "utf-8"));
12821
13092
  const settings = {
12822
13093
  grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
12823
13094
  tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
@@ -12831,6 +13102,10 @@ var SettingsService = class {
12831
13102
  if (raw.modelAllowlist !== void 0) {
12832
13103
  settings.modelAllowlist = raw.modelAllowlist;
12833
13104
  }
13105
+ const plannerModels = readPlannerModels(raw.plannerModels);
13106
+ if (plannerModels) {
13107
+ settings.plannerModels = plannerModels;
13108
+ }
12834
13109
  if (Array.isArray(raw.enabledRunners)) {
12835
13110
  settings.enabledRunners = raw.enabledRunners.map(String);
12836
13111
  }
@@ -12842,13 +13117,48 @@ var SettingsService = class {
12842
13117
  }
12843
13118
  persist() {
12844
13119
  const dir = path14.dirname(this.filePath);
12845
- if (!fs8.existsSync(dir)) {
12846
- fs8.mkdirSync(dir, { recursive: true });
13120
+ if (!fs9.existsSync(dir)) {
13121
+ fs9.mkdirSync(dir, { recursive: true });
12847
13122
  }
12848
- fs8.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
13123
+ fs9.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
12849
13124
  this.cachedMtimeMs = this.fileMtimeMs();
12850
13125
  }
12851
13126
  };
13127
+
13128
+ // src/services/PlannerModelMemory.ts
13129
+ var PlannerModelMemory = class {
13130
+ constructor(store) {
13131
+ this.store = store;
13132
+ }
13133
+ store;
13134
+ /** Record what the user picked for `provider`; a blank model forgets it instead. */
13135
+ remember(provider, model, effort) {
13136
+ const id = model.trim();
13137
+ if (!id) {
13138
+ this.store.setPlannerModel(provider, void 0);
13139
+ return;
13140
+ }
13141
+ const level = effort?.trim();
13142
+ this.store.setPlannerModel(provider, level ? { model: id, effort: level } : { model: id });
13143
+ }
13144
+ /**
13145
+ * What the model and effort should become when `provider` takes over the
13146
+ * planner, given the models it actually serves. The `source` is what the
13147
+ * three surfaces word their notice from — restored, defaulted, or nothing.
13148
+ */
13149
+ recall(provider, catalog) {
13150
+ const remembered = this.store.getPlannerModel(provider);
13151
+ const match = remembered && catalog.find((c) => c.id === remembered.model);
13152
+ if (remembered && match) {
13153
+ const level = remembered.effort ?? "";
13154
+ const survives = (match.variants ?? []).some((v) => v.id === level);
13155
+ return { model: match.id, effort: survives ? level : "", source: "remembered" };
13156
+ }
13157
+ const first = catalog[0];
13158
+ if (first) return { model: first.id, effort: "", source: "catalog-default" };
13159
+ return { model: "", effort: "", source: "none" };
13160
+ }
13161
+ };
12852
13162
  // Annotate the CommonJS export names for ESM import in node:
12853
13163
  0 && (module.exports = {
12854
13164
  ALL_PROVIDERS,
@@ -12872,6 +13182,7 @@ var SettingsService = class {
12872
13182
  DENY_ALL,
12873
13183
  EmbeddedNewlineError,
12874
13184
  EnvConfig,
13185
+ ExecutableNotFoundError,
12875
13186
  FsPluginStore,
12876
13187
  GIT_READONLY_SUBCOMMANDS,
12877
13188
  GREP_DEFAULT_HEAD_LIMIT,
@@ -12892,14 +13203,17 @@ var SettingsService = class {
12892
13203
  PROVIDER_PRIORITY,
12893
13204
  PROVIDER_SHORT_LABEL,
12894
13205
  PendingApprovals,
13206
+ PlanEditError,
12895
13207
  PlanParseError,
12896
13208
  PlanStore,
12897
13209
  Planner,
13210
+ PlannerModelMemory,
12898
13211
  REFUSED_COMMANDS,
12899
13212
  RunnerInstallation,
12900
13213
  RunnerRegistry,
12901
13214
  SEARCH_EXCLUSIONS,
12902
13215
  STATE_DIR,
13216
+ STOPPED_TOOL_RESULT,
12903
13217
  SYMBOL_LANGUAGES,
12904
13218
  Session,
12905
13219
  SettingsService,
@@ -12910,9 +13224,11 @@ var SettingsService = class {
12910
13224
  TmuxRunner,
12911
13225
  VerdictEngine,
12912
13226
  WINDOWS_MAX_COMMAND_LINE,
13227
+ WorkspaceNotFoundError,
12913
13228
  addTaskToPlan,
12914
13229
  applyHeadLimit,
12915
13230
  applyTaskOps,
13231
+ assertWorkspaceExists,
12916
13232
  augmentPromptWithPriorOutputs,
12917
13233
  augmentedPath,
12918
13234
  buildConversationSystemPrompt,
@@ -12986,6 +13302,7 @@ var SettingsService = class {
12986
13302
  hasTmux,
12987
13303
  includeGlobFor,
12988
13304
  isCliProvider,
13305
+ isExecutableResolved,
12989
13306
  isOpenAiProvider,
12990
13307
  killTree,
12991
13308
  knownModelId,