@ordewell/core 0.6.3 → 0.7.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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/{IFileSystem-DQEKyC6H.d.ts → IFileSystem-D4ADfTrX.d.ts} +1 -1
  3. package/dist/{IFileSystem-Qj2MWHn_.d.mts → IFileSystem-DgpX0BU7.d.mts} +1 -1
  4. package/dist/{ModeResolver-ByNpC0EP.d.ts → ModeResolver-BEcRAz1c.d.ts} +3 -3
  5. package/dist/{ModeResolver-rqaY_4Ex.d.mts → ModeResolver-Cv01ZDd9.d.mts} +3 -3
  6. package/dist/{Task-xqdxugt2.d.mts → Task-CoKhvBa1.d.mts} +275 -2
  7. package/dist/{Task-xqdxugt2.d.ts → Task-CoKhvBa1.d.ts} +275 -2
  8. package/dist/{chunk-M4KXBEFJ.mjs → chunk-J3YSTPCE.mjs} +41 -41
  9. package/dist/chunk-J3YSTPCE.mjs.map +1 -0
  10. package/dist/{chunk-I6U3PVHL.mjs → chunk-LSGY7AIZ.mjs} +74 -23
  11. package/dist/chunk-LSGY7AIZ.mjs.map +1 -0
  12. package/dist/{chunk-JRZPSNRO.mjs → chunk-YU4UKKKI.mjs} +1 -1
  13. package/dist/chunk-YU4UKKKI.mjs.map +1 -0
  14. package/dist/index.d.mts +250 -34
  15. package/dist/index.d.ts +250 -34
  16. package/dist/index.js +2011 -670
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +1650 -365
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/order-labels.d.mts +2 -1
  21. package/dist/order-labels.d.ts +2 -1
  22. package/dist/{parsing-DDZ1rUYa.d.mts → parsing-B4OCHH1_.d.mts} +24 -3
  23. package/dist/{parsing-COPG8-Ep.d.ts → parsing-DMdOq-BV.d.ts} +24 -3
  24. package/dist/parsing.d.mts +4 -3
  25. package/dist/parsing.d.ts +4 -3
  26. package/dist/parsing.js +36 -38
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +2 -2
  29. package/dist/{plan-utils-Br8UAoiu.d.ts → plan-utils-BdYPRBno.d.ts} +28 -3
  30. package/dist/{plan-utils-DMgajMvd.d.mts → plan-utils-CD1Xu65s.d.mts} +28 -3
  31. package/dist/plan-utils.d.mts +4 -3
  32. package/dist/plan-utils.d.ts +4 -3
  33. package/dist/plan-utils.js +111 -58
  34. package/dist/plan-utils.js.map +1 -1
  35. package/dist/plan-utils.mjs +8 -2
  36. package/dist/testing.d.mts +11 -2
  37. package/dist/testing.d.ts +11 -2
  38. package/dist/testing.js +17 -0
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +17 -0
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +4 -2
  43. package/dist/chunk-I6U3PVHL.mjs.map +0 -1
  44. package/dist/chunk-JRZPSNRO.mjs.map +0 -1
  45. package/dist/chunk-M4KXBEFJ.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -79,11 +79,15 @@ __export(index_exports, {
79
79
  NO_TURN: () => NO_TURN,
80
80
  OPENCODE_MANIFEST: () => OPENCODE_MANIFEST,
81
81
  ORCHESTRATOR_SHORTCUTS: () => ORCHESTRATOR_SHORTCUTS,
82
+ ORDEWELL_MCP_PATH: () => ORDEWELL_MCP_PATH,
83
+ ORDEWELL_MCP_SERVER_NAME: () => ORDEWELL_MCP_SERVER_NAME,
82
84
  ORDEWELL_SETTABLE_ENV: () => ORDEWELL_SETTABLE_ENV,
83
85
  OUTPUT_LINES_DEFAULT: () => OUTPUT_LINES_DEFAULT,
84
86
  OUTPUT_LINES_MAX: () => OUTPUT_LINES_MAX,
85
87
  OpenAiService: () => OpenAiService,
86
88
  OpenCodeAdapter: () => OpenCodeAdapter,
89
+ OrdewellMcpServer: () => OrdewellMcpServer,
90
+ PLANNER_TOOLS: () => PLANNER_TOOLS,
87
91
  PLAN_ENVELOPE_KEY: () => PLAN_ENVELOPE_KEY,
88
92
  PLUGIN_NAME_PATTERN: () => PLUGIN_NAME_PATTERN,
89
93
  PROVIDER_CREDENTIAL_ENV: () => PROVIDER_CREDENTIAL_ENV,
@@ -120,6 +124,7 @@ __export(index_exports, {
120
124
  TASK_QUERY_FIELDS: () => TASK_QUERY_FIELDS,
121
125
  TASK_QUERY_PROTOCOL: () => TASK_QUERY_PROTOCOL,
122
126
  TASK_QUERY_REMINDER: () => TASK_QUERY_REMINDER,
127
+ TASK_TOOLS: () => TASK_TOOLS,
123
128
  TRUNCATED_PLAN_REPAIR_INSTRUCTION: () => TRUNCATED_PLAN_REPAIR_INSTRUCTION,
124
129
  TaskControlError: () => TaskControlError,
125
130
  TaskLogRecorder: () => TaskLogRecorder,
@@ -203,10 +208,14 @@ __export(index_exports, {
203
208
  daemonTokenPath: () => daemonTokenPath,
204
209
  defaultLogger: () => defaultLogger,
205
210
  definitionPattern: () => definitionPattern,
211
+ defuseMarkers: () => defuseMarkers,
206
212
  deleteSession: () => deleteSession,
207
213
  dependencyCandidates: () => dependencyCandidates,
208
214
  dependentsOf: () => dependentsOf,
209
215
  describeMergeResult: () => describeMergeResult,
216
+ diffRows: () => diffRows,
217
+ diffStat: () => diffStat,
218
+ diffSummary: () => diffSummary,
210
219
  digestTaskLog: () => digestTaskLog,
211
220
  discoverGeminiModels: () => discoverGeminiModels,
212
221
  drainNext: () => drainNext,
@@ -269,6 +278,7 @@ __export(index_exports, {
269
278
  loadState: () => loadState,
270
279
  looksLikePlanAttempt: () => looksLikePlanAttempt,
271
280
  mapAgentTool: () => mapAgentTool,
281
+ mcpClientConfig: () => mcpClientConfig,
272
282
  migrateLegacyPlan: () => migrateLegacyPlan,
273
283
  migrateOldConfigDir: () => migrateOldConfigDir,
274
284
  migratePlanIsolation: () => migratePlanIsolation,
@@ -284,6 +294,7 @@ __export(index_exports, {
284
294
  opensWithJsonObject: () => opensWithJsonObject,
285
295
  outputLines: () => outputLines,
286
296
  outputPreview: () => outputPreview,
297
+ ownerOnlyConfigFile: () => ownerOnlyConfigFile,
287
298
  parseAutonomyLevel: () => parseAutonomyLevel,
288
299
  parseMaxParallel: () => parseMaxParallel,
289
300
  parsePartialPlan: () => parsePartialPlan,
@@ -342,6 +353,7 @@ __export(index_exports, {
342
353
  serializeTaskStatus: () => serializeTaskStatus,
343
354
  sessionDataDir: () => sessionDataDir,
344
355
  sessionRuntimeSettings: () => sessionRuntimeSettings,
356
+ sharedMcpServer: () => sharedMcpServer,
345
357
  stateExists: () => stateExists,
346
358
  stopTurn: () => stopTurn,
347
359
  stripAnsi: () => stripAnsi,
@@ -375,6 +387,7 @@ __export(index_exports, {
375
387
  usageLine: () => usageLine,
376
388
  validateModifiedPlan: () => validateModifiedPlan,
377
389
  validatePlanModification: () => validatePlanModification,
390
+ validatePlanTasks: () => validatePlanTasks,
378
391
  warningsText: () => warningsText,
379
392
  wellKnownBinDirs: () => wellKnownBinDirs,
380
393
  windowsCommandLine: () => windowsCommandLine,
@@ -709,13 +722,13 @@ function validateModifiedPlan(original, modified) {
709
722
  return warnings;
710
723
  }
711
724
  function warningsText(w) {
712
- const lines = [];
713
- if (w.deletedCompleted.length) lines.push(`Completed tasks deleted: ${w.deletedCompleted.join(", ")}`);
714
- if (w.changedCompleted.length) lines.push(`Completed tasks modified: ${w.changedCompleted.join(", ")}`);
715
- if (w.deletedInProgress.length) lines.push(`In-progress tasks deleted: ${w.deletedInProgress.join(", ")}`);
716
- if (w.modifiedInProgress.length) lines.push(`In-progress tasks modified: ${w.modifiedInProgress.join(", ")}`);
717
- if (w.brokenDependencies.length) lines.push(`Broken dependencies: ${w.brokenDependencies.join(", ")}`);
718
- return lines.length > 0 ? lines.join("\n") : null;
725
+ const lines2 = [];
726
+ if (w.deletedCompleted.length) lines2.push(`Completed tasks deleted: ${w.deletedCompleted.join(", ")}`);
727
+ if (w.changedCompleted.length) lines2.push(`Completed tasks modified: ${w.changedCompleted.join(", ")}`);
728
+ if (w.deletedInProgress.length) lines2.push(`In-progress tasks deleted: ${w.deletedInProgress.join(", ")}`);
729
+ if (w.modifiedInProgress.length) lines2.push(`In-progress tasks modified: ${w.modifiedInProgress.join(", ")}`);
730
+ if (w.brokenDependencies.length) lines2.push(`Broken dependencies: ${w.brokenDependencies.join(", ")}`);
731
+ return lines2.length > 0 ? lines2.join("\n") : null;
719
732
  }
720
733
 
721
734
  // src/models/Usage.ts
@@ -775,14 +788,14 @@ function capLine(line) {
775
788
  return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
776
789
  }
777
790
  function trimToolOutput(output) {
778
- const lines = output.split("\n");
779
- const long = lines.some((l) => l.length > MAX_LINE_CHARS);
780
- if (lines.length <= HEAD_LINES + TAIL_LINES) return long ? { output: lines.map(capLine).join("\n") } : { output };
781
- const omittedLines = lines.length - HEAD_LINES - TAIL_LINES;
791
+ const lines2 = output.split("\n");
792
+ const long = lines2.some((l) => l.length > MAX_LINE_CHARS);
793
+ if (lines2.length <= HEAD_LINES + TAIL_LINES) return long ? { output: lines2.map(capLine).join("\n") } : { output };
794
+ const omittedLines = lines2.length - HEAD_LINES - TAIL_LINES;
782
795
  const kept = [
783
- ...lines.slice(0, HEAD_LINES),
796
+ ...lines2.slice(0, HEAD_LINES),
784
797
  `\u2026 ${omittedLines} line${omittedLines === 1 ? "" : "s"} omitted \u2026`,
785
- ...lines.slice(-TAIL_LINES)
798
+ ...lines2.slice(-TAIL_LINES)
786
799
  ];
787
800
  return { output: kept.map(capLine).join("\n"), omittedLines };
788
801
  }
@@ -862,15 +875,15 @@ function digestTaskLog(events, maxCalls) {
862
875
  else if (event.type === "text") lastText = event.text;
863
876
  else if (event.type === "error") lastError = event.message;
864
877
  }
865
- const lines = [];
878
+ const lines2 = [];
866
879
  for (const event of events) {
867
880
  if (event.type !== "tool_call") continue;
868
881
  const outcome = outcomes.get(event.id);
869
882
  const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
870
- lines.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
883
+ lines2.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
871
884
  }
872
- const omitted = lines.length - maxCalls;
873
- const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines.slice(omitted)] : lines;
885
+ const omitted = lines2.length - maxCalls;
886
+ const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines2.slice(omitted)] : lines2;
874
887
  const gap = () => out.length > 0 ? [""] : [];
875
888
  const said2 = lastText?.trim();
876
889
  if (said2) {
@@ -908,9 +921,9 @@ var SEARCH_EXCLUSIONS = [
908
921
  ];
909
922
 
910
923
  // src/interfaces/IApproval.ts
911
- function toApprovalDecision(answer) {
912
- if (typeof answer === "boolean") return { decision: answer ? "allow" : "deny" };
913
- return answer;
924
+ function toApprovalDecision(answer2) {
925
+ if (typeof answer2 === "boolean") return { decision: answer2 ? "allow" : "deny" };
926
+ return answer2;
914
927
  }
915
928
  function isGranted(decision) {
916
929
  return decision.decision !== "deny";
@@ -2987,7 +3000,10 @@ var WRAPPER_FAMILY = {
2987
3000
  // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
2988
3001
  // name arrives as its first argument.
2989
3002
  busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
2990
- command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
3003
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] },
3004
+ // Runs the named shell builtin, so `builtin eval …` is an `eval`. Classified
3005
+ // on its own name it only asked, and a grant for `builtin echo` covered it.
3006
+ builtin: {}
2991
3007
  };
2992
3008
  var XARGS_SPEC = {
2993
3009
  valueFlags: [
@@ -3717,6 +3733,9 @@ function refusalFor(seg) {
3717
3733
  if (seg.binary === "source" || seg.binary === ".") {
3718
3734
  return `"${seg.binary}" runs a file's contents as commands, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
3719
3735
  }
3736
+ if (seg.binary === "enable" && seg.args.some((a) => /^-[a-zA-Z]*f/.test(a))) {
3737
+ return `"enable -f" loads a shared library into the shell, which runs its code. Use the read-only research tools, or describe it as a task.`;
3738
+ }
3720
3739
  if (SHELL_KEYWORDS.includes(seg.binary)) {
3721
3740
  return `"${seg.binary}" is a shell keyword or compound-command opener. This classifier only inspects the command that runs first, and a keyword hides the real one from it. Run the inner command directly, or describe it as a task.`;
3722
3741
  }
@@ -3827,6 +3846,9 @@ function classifyCommand(command, opts = {}) {
3827
3846
  }
3828
3847
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
3829
3848
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
3849
+ if (nonAuto.some((u) => u.seg.binary === "cd" || u.seg.binary !== "command" && SHELL_STATE_COMMANDS.has(u.seg.binary))) {
3850
+ return { tier: "ask", scope: trimmed };
3851
+ }
3830
3852
  const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
3831
3853
  return { tier: "ask", scope };
3832
3854
  }
@@ -4447,12 +4469,12 @@ var ApprovalPolicy = class {
4447
4469
  if (this.generation === gen) this.inFlight.delete(openKey);
4448
4470
  });
4449
4471
  this.inFlight.set(openKey, pending);
4450
- const answer = await pending;
4472
+ const answer2 = await pending;
4451
4473
  if (this.generation === gen) {
4452
- if (answer) open.forEach((s) => this.granted.add(s));
4474
+ if (answer2) open.forEach((s) => this.granted.add(s));
4453
4475
  else this.refused.add(key);
4454
4476
  }
4455
- return decide(answer, "asked");
4477
+ return decide(answer2, "asked");
4456
4478
  }
4457
4479
  /** Scopes the user has granted this session — for display and for persistence. */
4458
4480
  grantedScopes() {
@@ -4513,10 +4535,10 @@ var PendingApprovals = class {
4513
4535
  * settled. "Allow for this task" on a request that did not offer it is a
4514
4536
  * plain allow: no grant is made that the requester never proposed.
4515
4537
  */
4516
- resolve(id, answer) {
4538
+ resolve(id, answer2) {
4517
4539
  const entry = this.entries.get(id);
4518
4540
  if (!entry) return false;
4519
- const decision = toApprovalDecision(answer);
4541
+ const decision = toApprovalDecision(answer2);
4520
4542
  entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
4521
4543
  return true;
4522
4544
  }
@@ -4561,6 +4583,10 @@ var KNOWN_TOOLS = {
4561
4583
  list: "list_dir",
4562
4584
  fetch: "fetch"
4563
4585
  };
4586
+ var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "patch", "add", "update", "delete", "filechange"]);
4587
+ function isFileEditTool(name) {
4588
+ return FILE_EDIT_TOOLS.has(name.trim().toLowerCase().replace(/[_-]/g, ""));
4589
+ }
4564
4590
  function mapAgentTool(name) {
4565
4591
  const normalized = name.trim().toLowerCase().replace(/[_-]/g, "");
4566
4592
  const direct = KNOWN_TOOLS[normalized] ?? KNOWN_TOOLS[name.trim().toLowerCase()];
@@ -4569,25 +4595,25 @@ function mapAgentTool(name) {
4569
4595
  }
4570
4596
  return { tool: "agent_tool", toolLabel: name };
4571
4597
  }
4572
- function normalizeAgentArgs(tool, args) {
4598
+ function normalizeAgentArgs(tool2, args) {
4573
4599
  const out = { ...args };
4574
4600
  const alias = (from, to) => {
4575
4601
  if (out[to] === void 0 && out[from] !== void 0) out[to] = out[from];
4576
4602
  };
4577
- if (tool === "read_file" || tool === "list_dir") {
4603
+ if (tool2 === "read_file" || tool2 === "list_dir") {
4578
4604
  alias("filePath", "path");
4579
4605
  alias("file_path", "path");
4580
4606
  alias("target", "path");
4581
4607
  }
4582
- if (tool === "bash") {
4608
+ if (tool2 === "bash") {
4583
4609
  alias("cmd", "command");
4584
4610
  alias("script", "command");
4585
4611
  if (typeof out.command !== "string" && Array.isArray(out.command)) {
4586
4612
  out.command = out.command.join(" ");
4587
4613
  }
4588
4614
  }
4589
- if (tool === "fetch") alias("uri", "url");
4590
- if (tool === "web_search") alias("q", "query");
4615
+ if (tool2 === "fetch") alias("uri", "url");
4616
+ if (tool2 === "web_search") alias("q", "query");
4591
4617
  return out;
4592
4618
  }
4593
4619
 
@@ -4756,13 +4782,13 @@ function renderTerminalOutput(raw) {
4756
4782
  function renderCleanCapture(raw, doneToken) {
4757
4783
  const rendered = renderTerminalOutput(raw).replace(/[\s─-▟]+$/gm, "");
4758
4784
  if (!doneToken) return rendered.trim();
4759
- const lines = rendered.split("\n");
4785
+ const lines2 = rendered.split("\n");
4760
4786
  const flat = (s) => flattenTerminalOutput(s);
4761
- for (let i = 0; i < lines.length; i++) {
4762
- const own = flat(lines[i]).includes(doneToken);
4763
- if (own) return lines.slice(0, i).join("\n").trim();
4764
- const window = flat(lines[i]) + (i + 1 < lines.length ? flat(lines[i + 1]) : "");
4765
- if (window.includes(doneToken)) return lines.slice(0, i + 1).join("\n").trim();
4787
+ for (let i = 0; i < lines2.length; i++) {
4788
+ const own = flat(lines2[i]).includes(doneToken);
4789
+ if (own) return lines2.slice(0, i).join("\n").trim();
4790
+ const window = flat(lines2[i]) + (i + 1 < lines2.length ? flat(lines2[i + 1]) : "");
4791
+ if (window.includes(doneToken)) return lines2.slice(0, i + 1).join("\n").trim();
4766
4792
  }
4767
4793
  return rendered.trim();
4768
4794
  }
@@ -4793,9 +4819,9 @@ function oneLine(text2) {
4793
4819
  return text2.replace(/\s+/g, " ").trim();
4794
4820
  }
4795
4821
  function firstLineOf(command) {
4796
- const lines = command.trim().split(/\r?\n/);
4797
- const more = lines.length - 1;
4798
- return more > 0 ? `${lines[0]} \u2026 +${more} line${more === 1 ? "" : "s"}` : lines[0];
4822
+ const lines2 = command.trim().split(/\r?\n/);
4823
+ const more = lines2.length - 1;
4824
+ return more > 0 ? `${lines2[0]} \u2026 +${more} line${more === 1 ? "" : "s"}` : lines2[0];
4799
4825
  }
4800
4826
  function text(value) {
4801
4827
  return typeof value === "string" ? value : "";
@@ -4811,8 +4837,8 @@ function hintArg(args) {
4811
4837
  const firstText = Object.values(args).find((v) => typeof v === "string" && v.trim() !== "");
4812
4838
  return firstText ? oneLine(firstText) : "";
4813
4839
  }
4814
- function keyArgOf(tool, args) {
4815
- switch (tool) {
4840
+ function keyArgOf(tool2, args) {
4841
+ switch (tool2) {
4816
4842
  case "bash":
4817
4843
  return firstLineOf(text(args.command));
4818
4844
  case "read_file":
@@ -4836,20 +4862,62 @@ function keyArgOf(tool, args) {
4836
4862
  return hintArg(args);
4837
4863
  }
4838
4864
  }
4839
- function toolHeadline(tool, args, toolLabel) {
4840
- const name = toolLabel?.trim() || DISPLAY_NAMES[tool] || tool;
4865
+ function toolHeadline(tool2, args, toolLabel) {
4866
+ const name = toolLabel?.trim() || DISPLAY_NAMES[tool2] || tool2;
4841
4867
  const parsed = parseArgs(args);
4842
- return { name, keyArg: parsed ? keyArgOf(tool, parsed) : oneLine(args) };
4868
+ return { name, keyArg: parsed ? keyArgOf(tool2, parsed) : oneLine(args) };
4843
4869
  }
4844
4870
  function outputLines(output) {
4845
- const lines = output.replace(/\r\n?/g, "\n").replace(ANSI_OR_CTRL_RE, "").split("\n");
4846
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
4847
- return lines;
4871
+ const lines2 = output.replace(/\r\n?/g, "\n").replace(ANSI_OR_CTRL_RE, "").split("\n");
4872
+ while (lines2.length > 0 && lines2[lines2.length - 1].trim() === "") lines2.pop();
4873
+ return lines2;
4848
4874
  }
4849
4875
  function outputPreview(output, maxLines) {
4850
- const lines = outputLines(output);
4851
- const shown = lines.slice(0, Math.max(0, maxLines));
4852
- return { lines: shown, hiddenLineCount: lines.length - shown.length };
4876
+ const lines2 = outputLines(output);
4877
+ const shown = lines2.slice(0, Math.max(0, maxLines));
4878
+ return { lines: shown, hiddenLineCount: lines2.length - shown.length };
4879
+ }
4880
+ function diffStat(output) {
4881
+ const lines2 = outputLines(output);
4882
+ let added = 0;
4883
+ let removed = 0;
4884
+ for (const line of lines2) {
4885
+ if (line.startsWith("+")) added += 1;
4886
+ else if (line.startsWith("-")) removed += 1;
4887
+ else if (!(line.startsWith("@@") || line.startsWith(" ") || line === "" || line.startsWith("\\"))) return null;
4888
+ }
4889
+ return added + removed > 0 ? { added, removed } : null;
4890
+ }
4891
+ var HUNK_RANGE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
4892
+ function diffRows(output) {
4893
+ const rows = [];
4894
+ let oldLine = 1;
4895
+ let newLine = 1;
4896
+ for (const line of outputLines(output)) {
4897
+ const range2 = HUNK_RANGE.exec(line);
4898
+ if (range2) {
4899
+ if (rows.length > 0) rows.push({ kind: "gap", text: "" });
4900
+ oldLine = Number(range2[1]);
4901
+ newLine = Number(range2[2]);
4902
+ } else if (line.startsWith("+")) {
4903
+ rows.push({ kind: "added", line: newLine++, text: line.slice(1) });
4904
+ } else if (line.startsWith("-")) {
4905
+ rows.push({ kind: "removed", line: oldLine++, text: line.slice(1) });
4906
+ } else if (!line.startsWith("\\")) {
4907
+ rows.push({ kind: "context", line: newLine, text: line.slice(1) });
4908
+ oldLine += 1;
4909
+ newLine += 1;
4910
+ }
4911
+ }
4912
+ return rows;
4913
+ }
4914
+ function lines(count) {
4915
+ return `${count} line${count === 1 ? "" : "s"}`;
4916
+ }
4917
+ function diffSummary({ added, removed }) {
4918
+ if (!removed) return `Added ${lines(added)}`;
4919
+ if (!added) return `Removed ${lines(removed)}`;
4920
+ return `Added ${lines(added)}, removed ${lines(removed)}`;
4853
4921
  }
4854
4922
 
4855
4923
  // src/conversation/records.ts
@@ -4878,17 +4946,18 @@ var STATUS_OF_OUTCOME = {
4878
4946
  not_executed: "interrupted"
4879
4947
  };
4880
4948
  function finishedTool(call, output, outcome) {
4881
- return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output, outputLineCount: outputLines(output).length };
4949
+ const diff = outcome === "success" && isFileEditTool(call.toolLabel ?? call.tool) ? diffStat(output) : null;
4950
+ return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output, outputLineCount: outputLines(output).length, ...diff ? { diff } : {} };
4882
4951
  }
4883
4952
  function settledTool(call, step) {
4884
4953
  return finishedTool(call, step.result, step.outcome);
4885
4954
  }
4886
- function pendingTool(id, { tool, toolLabel, args, toolCallId, turnId }) {
4955
+ function pendingTool(id, { tool: tool2, toolLabel, args, toolCallId, turnId }) {
4887
4956
  return {
4888
4957
  type: "tool",
4889
4958
  id,
4890
- tool,
4891
- headline: toolHeadline(tool, args, toolLabel),
4959
+ tool: tool2,
4960
+ headline: toolHeadline(tool2, args, toolLabel),
4892
4961
  args,
4893
4962
  status: "pending",
4894
4963
  output: "",
@@ -5078,12 +5147,12 @@ function thinkingWhole(view, { text: text2, subagentId }) {
5078
5147
  return streamWhole(placed, lane, thinkingStream(subagentId), text2, (id) => thinkingBlock(id, blockText(text2), false, subagentId));
5079
5148
  }
5080
5149
  function announceTool(view, { id: toolCallId, name, args, subagentId }) {
5081
- const { tool, toolLabel } = mapAgentTool(name);
5150
+ const { tool: tool2, toolLabel } = mapAgentTool(name);
5082
5151
  const [placed, lane] = laneOf(view, subagentId);
5083
5152
  return appendOutput(placed, lane, (id) => {
5084
- const call = pendingTool(id, { tool, toolLabel, args, toolCallId });
5153
+ const call = pendingTool(id, { tool: tool2, toolLabel, args, toolCallId });
5085
5154
  const parsed = parseArgs2(args);
5086
- return parsed ? { ...call, headline: toolHeadline(tool, JSON.stringify(normalizeAgentArgs(tool, parsed)), toolLabel) } : call;
5155
+ return parsed ? { ...call, headline: toolHeadline(tool2, JSON.stringify(normalizeAgentArgs(tool2, parsed)), toolLabel) } : call;
5087
5156
  });
5088
5157
  }
5089
5158
  function parseArgs2(args) {
@@ -5123,21 +5192,21 @@ function reportUsage(view, { record }) {
5123
5192
  const next = { ...view, usage };
5124
5193
  return isMeasured(usage.totals) ? setUsageLine(next, usageLine(usage)) : next;
5125
5194
  }
5126
- function runnerToolSubject(tool, args) {
5127
- const { tool: mapped, toolLabel } = mapAgentTool(tool);
5195
+ function runnerToolSubject(tool2, args) {
5196
+ const { tool: mapped, toolLabel } = mapAgentTool(tool2);
5128
5197
  const parsed = parseArgs2(args);
5129
5198
  const headline = toolHeadline(mapped, parsed ? JSON.stringify(normalizeAgentArgs(mapped, parsed)) : args, toolLabel);
5130
5199
  return headline.keyArg ? `${headline.name}(${headline.keyArg})` : headline.name;
5131
5200
  }
5132
- function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId }) {
5201
+ function requestApproval(view, { approvalId, tool: tool2, args, allowForTask, toolCallId }) {
5133
5202
  if (view.blocks.some((b) => b.type === "approval" && b.approvalId === approvalId)) return view;
5134
5203
  return append(view, (id) => ({
5135
5204
  type: "approval",
5136
5205
  id,
5137
5206
  approvalId,
5138
5207
  kind: "runner_tool",
5139
- subject: runnerToolSubject(tool, args),
5140
- scope: tool,
5208
+ subject: runnerToolSubject(tool2, args),
5209
+ scope: tool2,
5141
5210
  status: "pending",
5142
5211
  ...allowForTask ? { allowForTask } : {},
5143
5212
  ...toolCallId ? { toolCallId } : {}
@@ -5431,9 +5500,9 @@ function applyHeadLimit(stdout, headLimit) {
5431
5500
  }
5432
5501
  function formatSearchOutput(capped, root, opts) {
5433
5502
  if (capped.rows.length === 0) return opts.emptyMessage;
5434
- const sep2 = opts.sep ?? import_node_path.default.sep;
5435
- const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
5436
- const body = prefix === sep2 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
5503
+ const sep3 = opts.sep ?? import_node_path.default.sep;
5504
+ const prefix = root === sep3 || root.endsWith(sep3) ? root : `${root}${sep3}`;
5505
+ const body = prefix === sep3 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
5437
5506
  if (!capped.truncated) return body;
5438
5507
  const hint = opts.hint ? ` ${opts.hint}` : "";
5439
5508
  return `${body}
@@ -6483,28 +6552,28 @@ function posixBinDirs() {
6483
6552
  ];
6484
6553
  }
6485
6554
  function windowsBinDirs(env, home) {
6486
- const join26 = (...parts) => path5.win32.join(...parts);
6487
- const under = (base, ...parts) => base ? [join26(base, ...parts)] : [];
6555
+ const join27 = (...parts) => path5.win32.join(...parts);
6556
+ const under = (base, ...parts) => base ? [join27(base, ...parts)] : [];
6488
6557
  const appData = env.APPDATA;
6489
6558
  const localAppData = env.LOCALAPPDATA;
6490
6559
  const programData = env.ProgramData;
6491
6560
  return [
6492
6561
  // Native per-user installers, PowerShell-driven and otherwise.
6493
- join26(home, ".local", "bin"),
6494
- join26(home, ".opencode", "bin"),
6495
- join26(home, ".claude", "bin"),
6496
- join26(home, ".codex", "bin"),
6562
+ join27(home, ".local", "bin"),
6563
+ join27(home, ".opencode", "bin"),
6564
+ join27(home, ".claude", "bin"),
6565
+ join27(home, ".codex", "bin"),
6497
6566
  ...under(localAppData, "Programs", "claude"),
6498
6567
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
6499
6568
  // literal spelling is kept as a fallback for a host that does not export
6500
6569
  // APPDATA (a service account, a stripped CI container).
6501
6570
  ...under(appData, "npm"),
6502
- join26(home, "AppData", "Roaming", "npm"),
6571
+ join27(home, "AppData", "Roaming", "npm"),
6503
6572
  ...under(localAppData, "pnpm"),
6504
6573
  ...under(localAppData, "Yarn", "bin"),
6505
- join26(home, ".bun", "bin"),
6574
+ join27(home, ".bun", "bin"),
6506
6575
  // Windows package managers.
6507
- join26(home, "scoop", "shims"),
6576
+ join27(home, "scoop", "shims"),
6508
6577
  ...under(env.SCOOP, "shims"),
6509
6578
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
6510
6579
  ...under(programData, "chocolatey", "bin"),
@@ -6514,7 +6583,7 @@ function windowsBinDirs(env, home) {
6514
6583
  ...under(localAppData, "Microsoft", "WindowsApps"),
6515
6584
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
6516
6585
  ...under(localAppData, "Volta", "bin"),
6517
- join26(home, ".volta", "bin")
6586
+ join27(home, ".volta", "bin")
6518
6587
  ];
6519
6588
  }
6520
6589
  function wellKnownBinDirs(deps = {}) {
@@ -7002,18 +7071,18 @@ var GitWorktreeIsolation = class {
7002
7071
  */
7003
7072
  sharedPaths(workspaceRoot, isolated) {
7004
7073
  if (isolated.includes(SELF_REPO)) return [];
7005
- const shared = [];
7074
+ const shared2 = [];
7006
7075
  const walk = (rel) => {
7007
7076
  for (const name of listDir(path9.join(workspaceRoot, rel))) {
7008
7077
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
7009
7078
  const child = rel ? `${rel}/${name}` : name;
7010
7079
  if (isolated.includes(child)) continue;
7011
7080
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
7012
- else if (resolves(path9.join(workspaceRoot, child))) shared.push(child);
7081
+ else if (resolves(path9.join(workspaceRoot, child))) shared2.push(child);
7013
7082
  }
7014
7083
  };
7015
7084
  walk("");
7016
- return shared;
7085
+ return shared2;
7017
7086
  }
7018
7087
  prepare(task, run) {
7019
7088
  return this.admin(run.workspaceRoot, async () => {
@@ -7827,13 +7896,14 @@ function filteredBuildModes(modes, autonomousDefault) {
7827
7896
  return buildModes;
7828
7897
  }
7829
7898
  function autonomyLevelLabel(autonomous) {
7830
- return autonomous ? "Full auto" : "Auto";
7899
+ return autonomous ? "Full" : "Guarded";
7831
7900
  }
7832
7901
  function parseAutonomyLevel(arg) {
7833
7902
  switch (arg?.trim().toLowerCase()) {
7834
7903
  case "full":
7835
7904
  case "on":
7836
7905
  return true;
7906
+ case "guarded":
7837
7907
  case "auto":
7838
7908
  case "off":
7839
7909
  return false;
@@ -7842,7 +7912,7 @@ function parseAutonomyLevel(arg) {
7842
7912
  }
7843
7913
  }
7844
7914
  function buildModeGuide(runnerModes, autonomousDefault = true) {
7845
- const lines = [];
7915
+ const lines2 = [];
7846
7916
  for (const [runnerId, modes] of Object.entries(runnerModes)) {
7847
7917
  if (!modes || modes.length === 0) continue;
7848
7918
  const filtered = filteredBuildModes(modes, autonomousDefault);
@@ -7851,13 +7921,13 @@ function buildModeGuide(runnerModes, autonomousDefault = true) {
7851
7921
  const tag = isDefaultMode(m, autonomousDefault) ? " (DEFAULT)" : "";
7852
7922
  return `${m.id}${tag} (${m.description})`;
7853
7923
  }).join(", ");
7854
- lines.push(`- ${runnerId}: ${modeStr}`);
7924
+ lines2.push(`- ${runnerId}: ${modeStr}`);
7855
7925
  }
7856
- if (lines.length === 0) return "";
7926
+ if (lines2.length === 0) return "";
7857
7927
  return [
7858
7928
  `Autonomy level: ${autonomyLevelLabel(autonomousDefault)}.`,
7859
7929
  "AVAILABLE MODES PER RUNNER:",
7860
- ...lines,
7930
+ ...lines2,
7861
7931
  "",
7862
7932
  'For each AI task, use the mode marked (DEFAULT) unless the task needs a different compatible mode listed above. Avoid the "plan" mode (read-only) unless the user explicitly requested analysis-only.'
7863
7933
  ].join("\n");
@@ -8098,10 +8168,12 @@ var TASK_QUERY_FIELDS = [
8098
8168
  "userStoriesCovered",
8099
8169
  "output"
8100
8170
  ];
8171
+ var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
8101
8172
  var OUTPUT_LINES_DEFAULT = 80;
8102
8173
  var OUTPUT_LINES_MAX = 400;
8103
8174
  var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
8104
8175
  var ANSWER_TAIL_RESERVE = 4e3;
8176
+ var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
8105
8177
  function textHasTaskQuery(text2) {
8106
8178
  return text2.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
8107
8179
  }
@@ -8177,6 +8249,7 @@ function taskQuerySignature(query) {
8177
8249
  return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
8178
8250
  }
8179
8251
  var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
8252
+ var TASK_READ_ANSWER_OR_EDIT = "You have now read everything you asked for. Do not read again this turn: answer the user in prose, or call edit_plan for the change you came to make.";
8180
8253
  var TASK_QUERY_PROTOCOL = [
8181
8254
  "READING A TASK BEFORE YOU EDIT IT:",
8182
8255
  "The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
@@ -8188,7 +8261,16 @@ var TASK_QUERY_PROTOCOL = [
8188
8261
  "Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
8189
8262
  "Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
8190
8263
  ];
8264
+ var TASK_READ_TOOLS_PROTOCOL = [
8265
+ "READING A TASK BEFORE YOU EDIT IT:",
8266
+ "The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. Read with these tools; they change nothing:",
8267
+ '- task_query: "tasks" are one or more task references ("<id or #order>"). Ask for everything you need in ONE call. "fields" is optional (omit it to get description, prompt, userSteps, verdict, outputSummary and userStoriesCovered). "catalog": true also returns every runner with its models, thinking-effort variants and modes.',
8268
+ `- task_output: the recent output of a task that is running right now \u2014 the way to diagnose a task that looks stuck mid-execution. "lines" (default 80, at most 400) sets how many; pass an answer's "nextOffset" back as "since" to read only what follows. A task that is not running answers with its verdict, output summary and a digest of its last attempt.`,
8269
+ "Three reads per user message; after that every answer also tells you to land the turn, and after six they are refused. Do not ask the same question twice.",
8270
+ "Once a plan exists, change it with edit_plan \u2014 every turn shows you its operations. Never reply with taskOps or taskQuery JSON."
8271
+ ];
8191
8272
  var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
8273
+ var TASK_READ_TOOLS_REMINDER = "- To READ a task in full (prompt, user steps, verdict, user stories), a running task's output, or the whole model/mode catalog before editing, call task_query or task_output \u2014 they change nothing \u2014 and then call edit_plan with your ops.";
8192
8274
  function findTask(tasks, ref) {
8193
8275
  const byId = tasks.find((t) => t.id === ref);
8194
8276
  if (byId) return byId;
@@ -8202,13 +8284,13 @@ function findTask(tasks, ref) {
8202
8284
  function fitTail(text2, budget) {
8203
8285
  if (text2.length <= budget) return { text: text2, trimmed: false };
8204
8286
  if (budget <= 0) return { text: "", trimmed: true };
8205
- const lines = text2.split("\n");
8287
+ const lines2 = text2.split("\n");
8206
8288
  const kept = [];
8207
8289
  let total = 0;
8208
- for (let i = lines.length - 1; i >= 0; i--) {
8209
- const cost = lines[i].length + (kept.length > 0 ? 1 : 0);
8290
+ for (let i = lines2.length - 1; i >= 0; i--) {
8291
+ const cost = lines2[i].length + (kept.length > 0 ? 1 : 0);
8210
8292
  if (total + cost > budget) break;
8211
- kept.unshift(lines[i]);
8293
+ kept.unshift(lines2[i]);
8212
8294
  total += cost;
8213
8295
  }
8214
8296
  if (kept.length === 0) return { text: text2.slice(text2.length - budget), trimmed: true };
@@ -8259,34 +8341,34 @@ function renderField(task, field, ctx) {
8259
8341
  }
8260
8342
  }
8261
8343
  function renderTask(task, fields, ctx) {
8262
- const lines = [
8344
+ const lines2 = [
8263
8345
  `#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
8264
8346
  ...fields.filter((f) => f !== "output").flatMap((f) => renderField(task, f, ctx)),
8265
8347
  ...fields.includes("output") ? renderLiveOutput(task.id, ctx) : [],
8266
8348
  ""
8267
8349
  ];
8268
- ctx.used += lines.reduce((n, l) => n + l.length + 1, 0);
8269
- return lines;
8350
+ ctx.used += lines2.reduce((n, l) => n + l.length + 1, 0);
8351
+ return lines2;
8270
8352
  }
8271
8353
  function renderCatalog(catalog) {
8272
- const lines = [];
8354
+ const lines2 = [];
8273
8355
  for (const runner of catalog.runners) {
8274
8356
  const models = catalog.models[runner] ?? [];
8275
- lines.push(`${runner} models:`);
8276
- if (models.length === 0) lines.push(" (none discovered)");
8357
+ lines2.push(`${runner} models:`);
8358
+ if (models.length === 0) lines2.push(" (none discovered)");
8277
8359
  for (const m of models) {
8278
8360
  const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
8279
- lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
8361
+ lines2.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
8280
8362
  }
8281
8363
  const modes = catalog.modes[runner] ?? [];
8282
- lines.push(`${runner} task modes:`);
8283
- if (modes.length === 0) lines.push(" (none declared)");
8364
+ lines2.push(`${runner} task modes:`);
8365
+ if (modes.length === 0) lines2.push(" (none declared)");
8284
8366
  const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
8285
8367
  for (const mode of modes) {
8286
- lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
8368
+ lines2.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
8287
8369
  }
8288
8370
  }
8289
- return lines;
8371
+ return lines2;
8290
8372
  }
8291
8373
  function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
8292
8374
  const fields = query.fields ?? TASK_QUERY_FIELDS;
@@ -8320,7 +8402,7 @@ function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
8320
8402
 
8321
8403
  // src/services/PlanPrompts.ts
8322
8404
  function buildResearchToolsPrompt() {
8323
- const lines = [
8405
+ const lines2 = [
8324
8406
  "You have access to the following tools to explore the workspace:",
8325
8407
  "",
8326
8408
  "read_file(path: string, offset?: number, limit?: number) - Read file contents with line numbers and optional pagination. offset is a 0-based line number (default 0). limit is max lines (default 2000). Output lines are prefixed with their line numbers. When a file has more lines than requested, a hint shows the offset to use next. Files larger than ~1 MB are rejected \u2014 use grep instead.",
@@ -8340,7 +8422,7 @@ function buildResearchToolsPrompt() {
8340
8422
  'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
8341
8423
  "The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
8342
8424
  ];
8343
- return lines.join("\n");
8425
+ return lines2.join("\n");
8344
8426
  }
8345
8427
  function buildSubagentSystemPrompt() {
8346
8428
  return [
@@ -8357,18 +8439,18 @@ function buildSubagentSystemPrompt() {
8357
8439
  ].join("\n");
8358
8440
  }
8359
8441
  function buildModeGuideForRunners(runners, _autonomousDefault) {
8360
- const lines = ['Assign a "taskMode" to each AI task based on the runner:'];
8442
+ const lines2 = ['Assign a "taskMode" to each AI task based on the runner:'];
8361
8443
  for (const r of runners) {
8362
8444
  if (r === "claude-code") {
8363
- lines.push('- For claude-code: "acceptEdits" (edit automatically, recommended), "auto" (a classifier approves or blocks each action), "default" (ask before edits), "plan" (read-only analysis), "bypassPermissions" (skip all prompts, CI only).');
8445
+ lines2.push('- For claude-code: "acceptEdits" (edit automatically, recommended), "auto" (a classifier approves or blocks each action), "default" (ask before edits), "plan" (read-only analysis), "bypassPermissions" (skip all prompts, CI only).');
8364
8446
  } else if (r === "opencode") {
8365
- lines.push('- For opencode: "build" (full access agent), "plan" (read-only analysis).');
8447
+ lines2.push('- For opencode: "build" (full access agent), "plan" (read-only analysis).');
8366
8448
  } else {
8367
- lines.push(`- For ${r}: use the modes from the runner's manifest.`);
8449
+ lines2.push(`- For ${r}: use the modes from the runner's manifest.`);
8368
8450
  }
8369
8451
  }
8370
- lines.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
8371
- return lines.join("\n");
8452
+ lines2.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
8453
+ return lines2.join("\n");
8372
8454
  }
8373
8455
  function buildModeExamplesForRunners(runners) {
8374
8456
  return runners.map((r) => {
@@ -8397,7 +8479,7 @@ function verificationModeBlock() {
8397
8479
  ].join("\n");
8398
8480
  }
8399
8481
  function researchPhaseBlock(harnessMode) {
8400
- const shared = [
8482
+ const shared2 = [
8401
8483
  "RESEARCH PHASE:",
8402
8484
  "- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
8403
8485
  ];
@@ -8411,7 +8493,7 @@ function researchPhaseBlock(harnessMode) {
8411
8493
  "- Read key source files to understand architecture"
8412
8494
  ];
8413
8495
  return [
8414
- ...shared,
8496
+ ...shared2,
8415
8497
  ...toolSpecific,
8416
8498
  "- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
8417
8499
  "- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
@@ -8473,28 +8555,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
8473
8555
  verificationBlock
8474
8556
  );
8475
8557
  }
8558
+ var PLANNER_TOOLS_CATALOG = [
8559
+ "RUNNERS, MODELS AND MODES:",
8560
+ "Which runners, models and modes you may assign can change while you plan: the user may enable a runner or edit the model allowlist at any time. Read them just before you submit, never from memory:",
8561
+ "1. Call list_runners: the enabled runners, each with its task modes and default mode.",
8562
+ "2. Call list_models for each runner you will assign: its allowed models, with their thinking-effort variants.",
8563
+ `3. Call submit_plan with the whole plan. "assignedRunner" must be a runner list_runners returned, "assignedModel.modelId" a modelId list_models returned for that runner, "taskMode" one of that runner's modes.`,
8564
+ '4. If submit_plan returns errors, fix exactly what each one names and call it again in the same reply. If it reports "coerced" changes, tell the user what was changed.',
8565
+ ""
8566
+ ];
8476
8567
  function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
8568
+ const tools = variant.plannerTools ?? false;
8477
8569
  return [
8478
- "You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans as JSON. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.",
8570
+ `You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans ${tools ? "through the submit_plan tool" : "as JSON"}. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.`,
8479
8571
  "",
8480
8572
  "WORKFLOW:",
8481
8573
  "1. Explore the workspace with tools to understand the codebase.",
8482
8574
  "2. Ask clarifying questions grounded in your findings. Reference actual files. When the goal is vague, or a decision would materially change the outcome (storage engine, library choice, scope, API shape), ask the user BEFORE planning \u2014 never silently assume. Clear, fully-specified goals need no questions.",
8483
8575
  "3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
8484
- '4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
8576
+ tools ? "4. After the user confirms the outline, call list_runners and list_models, then submit the final task plan with submit_plan." : '4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
8485
8577
  "",
8486
8578
  researchPhaseBlock(variant.harness ?? false),
8487
8579
  verificationBlock,
8488
8580
  "",
8489
8581
  "OUTLINE PHASE:",
8490
- "- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to JSON.",
8582
+ `- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to ${tools ? "submit_plan" : "JSON"}.`,
8491
8583
  "- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
8492
8584
  '- Example: "1. Set up database schema (src/db/schema.ts) \u2014 creates tables for users and sessions\n2. Add auth middleware (src/auth/middleware.ts) \u2014 validates JWTs on protected routes\n3. Build login page (src/ui/Login.tsx) \u2014 form component with validation"',
8493
8585
  "- The user may reply with adjustments. Revise the outline and re-present it.",
8494
- "- When the user confirms the outline, emit the task plan JSON.",
8586
+ tools ? "- When the user confirms the outline, submit the plan with submit_plan." : "- When the user confirms the outline, emit the task plan JSON.",
8495
8587
  "",
8496
8588
  "PLAN FORMAT:",
8497
- "Generate a task plan using this JSON format:",
8589
+ tools ? 'submit_plan takes {"tasks": [...]}, every task in this shape:' : "Generate a task plan using this JSON format:",
8498
8590
  "",
8499
8591
  "{",
8500
8592
  ' "tasks": [',
@@ -8508,8 +8600,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8508
8600
  ' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
8509
8601
  ' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
8510
8602
  ` "assignedModel": { "modelId": "model-id", "modelLabel": "Model Label", "thinkingEffort": "a variant id from that model's variants list \u2014 omit if the model has none" },`,
8511
- ' "assignedRunner": "' + runners.join("|") + '",',
8512
- ' "taskMode": "' + modeExamples + '",',
8603
+ ' "assignedRunner": "' + (tools ? "a runner id from list_runners" : runners.join("|")) + '",',
8604
+ ' "taskMode": "' + (tools ? "one of that runner's modes from list_runners" : modeExamples) + '",',
8513
8605
  ' "autonomy": "AFK|HITL",',
8514
8606
  ' "sliceType": "AFK|HITL",',
8515
8607
  ' "ops": true,',
@@ -8541,8 +8633,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8541
8633
  ...opsTaskSection(true),
8542
8634
  "",
8543
8635
  "RULES:",
8544
- "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
8545
- "- Emit the outline BEFORE the JSON. Do not skip the outline.",
8636
+ ...tools ? [
8637
+ "- Submit the plan ONLY through submit_plan. Never write it as JSON in your reply.",
8638
+ "- Show the outline BEFORE submitting. Do not skip the outline."
8639
+ ] : [
8640
+ "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
8641
+ "- Emit the outline BEFORE the JSON. Do not skip the outline."
8642
+ ],
8546
8643
  "- Reference specific files and patterns from your research in task prompts.",
8547
8644
  ...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
8548
8645
  "",
@@ -8550,23 +8647,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8550
8647
  "- When suggesting libraries, frameworks, or patterns, first verify they already exist in the codebase. NEVER assume a library is available just because it is well-known. Check package.json, imports, or surrounding files first.",
8551
8648
  "- Follow the existing code style, naming conventions, and architectural patterns of the project.",
8552
8649
  "",
8553
- "MODEL ASSIGNMENT:",
8554
- "Available models:",
8555
- modelsJson,
8556
- "",
8557
- runnerInstruction(runners),
8650
+ ...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
8558
8651
  "MODEL SELECTION GUIDELINES:",
8559
8652
  "- Use stronger models for complex refactoring, architecture changes, security-critical code.",
8560
8653
  "- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
8561
- `- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list above (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
8654
+ `- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list ${tools ? "from list_models" : "above"} (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
8562
8655
  "",
8563
8656
  "TASK MODE:",
8564
- modeGuide,
8657
+ tools ? `Use each runner's default mode from list_runners unless the task needs a different mode it lists. Avoid "plan" (read-only) unless the user asked for analysis only.` : modeGuide,
8565
8658
  "",
8566
- // Shared by both variants on purpose: the read channel is a text envelope
8567
- // exactly so a harness planner, which Ordewell cannot hand tools to, speaks
8568
- // the same protocol as an API-backed one (ADR-0009).
8569
- ...TASK_QUERY_PROTOCOL,
8659
+ // The envelope is shared by both variants on purpose: a harness planner
8660
+ // Ordewell could not hand tools to speaks the same protocol as an
8661
+ // API-backed one (ADR-0009). One it did hand them to reads through them.
8662
+ ...tools ? TASK_READ_TOOLS_PROTOCOL : TASK_QUERY_PROTOCOL,
8570
8663
  "",
8571
8664
  context ? `PROJECT CONTEXT:
8572
8665
  ${context}
@@ -8808,25 +8901,25 @@ function executionLogBlock(executionLog) {
8808
8901
  if (executionLog.length === 0) return "";
8809
8902
  const completed = executionLog.filter((t) => t.status === "completed");
8810
8903
  const failed = executionLog.filter((t) => t.status === "failed");
8811
- const lines = ["EXECUTION LOG SUMMARY:"];
8812
- lines.push(`Total entries: ${executionLog.length} (${completed.length} completed, ${failed.length} failed)`);
8904
+ const lines2 = ["EXECUTION LOG SUMMARY:"];
8905
+ lines2.push(`Total entries: ${executionLog.length} (${completed.length} completed, ${failed.length} failed)`);
8813
8906
  if (completed.length > 0) {
8814
- lines.push("");
8815
- lines.push("COMPLETED TASKS:");
8907
+ lines2.push("");
8908
+ lines2.push("COMPLETED TASKS:");
8816
8909
  for (const t of completed) {
8817
8910
  const verdict = t.verdict ? ` \u2014 ${t.verdict.outcome.toUpperCase()}: ${t.verdict.reason}` : "";
8818
- lines.push(`- [${t.id}] ${t.title}${verdict}`);
8911
+ lines2.push(`- [${t.id}] ${t.title}${verdict}`);
8819
8912
  }
8820
8913
  }
8821
8914
  if (failed.length > 0) {
8822
- lines.push("");
8823
- lines.push("FAILED TASKS (eligible for retry):");
8915
+ lines2.push("");
8916
+ lines2.push("FAILED TASKS (eligible for retry):");
8824
8917
  for (const t of failed) {
8825
8918
  const verdict = t.verdict ? ` \u2014 ${t.verdict.reason}` : "";
8826
- lines.push(`- [${t.id}] ${t.title} (retry #${t.retryCount})${verdict}`);
8919
+ lines2.push(`- [${t.id}] ${t.title} (retry #${t.retryCount})${verdict}`);
8827
8920
  }
8828
8921
  }
8829
- return lines.join("\n");
8922
+ return lines2.join("\n");
8830
8923
  }
8831
8924
  function pendingEditRulesBlock() {
8832
8925
  return [
@@ -8869,7 +8962,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
8869
8962
  sections.push('Return the COMPLETE modified pending tasks as a JSON object with a single "tasks" array. Include all pending tasks, not just the modified ones.\nDo NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object.');
8870
8963
  return sections.join("\n");
8871
8964
  }
8872
- function buildMergePrompt(taskIds, tasks) {
8965
+ function buildMergePrompt(taskIds, tasks, tools = false) {
8873
8966
  const idSet = new Set(taskIds);
8874
8967
  const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
8875
8968
  const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
@@ -8877,22 +8970,22 @@ function buildMergePrompt(taskIds, tasks) {
8877
8970
  `Merge these tasks into ONE combined task: ${refs}.`,
8878
8971
  "Write a clear combined title, description, and prompt that cover all of their work.",
8879
8972
  "The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
8880
- 'Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models listed in <available_models> above. Prefer the strongest model if the merged work is complex.',
8881
- 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
8882
- ` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
8973
+ `Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. Prefer the strongest model if the merged work is complex.`,
8974
+ tools ? 'Call edit_plan with a single "merge" op:' : 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
8975
+ ` {"${tools ? "ops" : "taskOps"}":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
8883
8976
  `If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
8884
8977
  ].join("\n");
8885
8978
  }
8886
- function buildSplitPrompt(taskId, tasks) {
8979
+ function buildSplitPrompt(taskId, tasks, tools = false) {
8887
8980
  const task = tasks.find((t) => t.id === taskId);
8888
8981
  if (!task) return `Split task ${taskId} into smaller tasks.`;
8889
8982
  return [
8890
8983
  `Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
8891
8984
  "Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
8892
8985
  "The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
8893
- 'Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models listed in <available_models> above. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).',
8894
- 'Reply with ONLY a taskOps JSON object using a single "split" op:',
8895
- ` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
8986
+ `Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).`,
8987
+ tools ? 'Call edit_plan with a single "split" op:' : 'Reply with ONLY a taskOps JSON object using a single "split" op:',
8988
+ ` {"${tools ? "ops" : "taskOps"}":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
8896
8989
  `If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
8897
8990
  ].join("\n");
8898
8991
  }
@@ -8975,25 +9068,37 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = tr
8975
9068
  const detail = err instanceof Error ? err.message : String(err);
8976
9069
  throw new PlanParseError(`Plan response was not valid JSON: ${detail}`, raw);
8977
9070
  }
8978
- if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.tasks)) {
8979
- throw new PlanParseError("Invalid plan: missing tasks array", raw);
9071
+ const result = validatePlanTasks(parsed, runners, runnerModes, autonomousDefault);
9072
+ if (result.ok) return result.tasks;
9073
+ const [first] = result.errors;
9074
+ throw new PlanParseError(first.message, raw, { semantic: first.taskId !== void 0 });
9075
+ }
9076
+ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = true) {
9077
+ const rawTasks = isRecord(obj) ? obj.tasks : void 0;
9078
+ if (!Array.isArray(rawTasks)) {
9079
+ return { ok: false, errors: [{ field: "tasks", message: "Invalid plan: missing tasks array" }] };
8980
9080
  }
8981
- const rawTasks = parsed.tasks;
8982
9081
  if (rawTasks.length === 0) {
8983
- throw new PlanParseError("Plan contained no tasks", raw);
9082
+ return { ok: false, errors: [{ field: "tasks", message: "Plan contained no tasks" }] };
8984
9083
  }
8985
- const tasks = rawTasks.map((t) => parseTask(t, runners, runnerModes, autonomousDefault));
8986
- validateVerticalSliceShape(tasks, raw);
9084
+ const tasks = rawTasks.map((t) => parseTask(asRecord(t), runners, runnerModes, autonomousDefault));
9085
+ const errors = verticalSliceErrors(tasks);
8987
9086
  for (const t of tasks) {
8988
9087
  if (!runners.includes(t.assignedRunner)) {
8989
- throw new PlanParseError(
8990
- `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`,
8991
- raw,
8992
- { semantic: true }
8993
- );
9088
+ errors.push({
9089
+ taskId: t.id,
9090
+ field: "assignedRunner",
9091
+ message: `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
9092
+ });
8994
9093
  }
8995
9094
  }
8996
- return tasks;
9095
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, tasks };
9096
+ }
9097
+ function isRecord(value) {
9098
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9099
+ }
9100
+ function asRecord(value) {
9101
+ return isRecord(value) ? value : {};
8997
9102
  }
8998
9103
  function looksLikePlanAttempt(raw) {
8999
9104
  const { matches, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
@@ -9018,12 +9123,12 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
9018
9123
  // description/title — same fallback TaskOps.applyTaskOps uses for `add`, so
9019
9124
  // a task never silently ends up unschedulable (getReadyTasks requires prompt).
9020
9125
  prompt: raw.prompt ? String(raw.prompt) : description || title || void 0,
9021
- userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map((s) => ({
9126
+ userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map(asRecord).map((s) => ({
9022
9127
  order: Number(s.order ?? 0),
9023
9128
  instruction: String(s.instruction ?? ""),
9024
9129
  completed: false
9025
9130
  })) : void 0,
9026
- subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(s, runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
9131
+ subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(asRecord(s), runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
9027
9132
  assignedModel: raw.assignedModel ? {
9028
9133
  modelId: String(raw.assignedModel.modelId ?? ""),
9029
9134
  modelLabel: String(raw.assignedModel.modelLabel ?? ""),
@@ -9044,42 +9149,28 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
9044
9149
  ops: !inherited && taskType === "ai" && raw.ops === true
9045
9150
  });
9046
9151
  }
9047
- function validateVerticalSliceShape(tasks, raw, parentTitle) {
9152
+ function verticalSliceErrors(tasks, parentTitle) {
9048
9153
  const where = (t) => parentTitle ? `subtask "${t.title}" of "${parentTitle}"` : `Task "${t.title}"`;
9049
- const semantic = { semantic: true };
9154
+ const errors = [];
9050
9155
  for (const t of tasks) {
9156
+ const fail = (field, message) => errors.push({ taskId: t.id, field, message });
9051
9157
  if (!t.sliceType) {
9052
- throw new PlanParseError(
9053
- `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`,
9054
- raw,
9055
- semantic
9056
- );
9158
+ fail("sliceType", `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`);
9057
9159
  }
9058
9160
  if (t.type === "ai" && !t.autonomy) {
9059
- throw new PlanParseError(
9060
- `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`,
9061
- raw,
9062
- semantic
9063
- );
9161
+ fail("autonomy", `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`);
9064
9162
  }
9065
9163
  if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
9066
- throw new PlanParseError(
9067
- `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`,
9068
- raw,
9069
- semantic
9070
- );
9164
+ fail("userSteps", `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
9071
9165
  }
9072
- if (t.type === "user" && t.sliceType !== "HITL") {
9073
- throw new PlanParseError(
9074
- `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`,
9075
- raw,
9076
- semantic
9077
- );
9166
+ if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
9167
+ fail("sliceType", `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
9078
9168
  }
9079
9169
  if (t.subtasks.length > 0) {
9080
- validateVerticalSliceShape(t.subtasks, raw, t.title);
9170
+ errors.push(...verticalSliceErrors(t.subtasks, t.title));
9081
9171
  }
9082
9172
  }
9173
+ return errors;
9083
9174
  }
9084
9175
  function checkUniqueIds(ctx) {
9085
9176
  const seen = /* @__PURE__ */ new Set();
@@ -9382,11 +9473,11 @@ var UPDATABLE_FIELDS = [
9382
9473
  "sliceType",
9383
9474
  "ops"
9384
9475
  ];
9385
- function taskOpsProtocol(executionNote = "") {
9476
+ function taskOpsProtocol(executionNote = "", tools = false) {
9386
9477
  const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
9387
9478
  return [
9388
- "- To modify specific tasks, reply with ONLY this JSON object:",
9389
- ' {"taskOps": [',
9479
+ tools ? "- To modify specific tasks, call edit_plan, never a JSON reply. Its argument is:" : "- To modify specific tasks, reply with ONLY this JSON object:",
9480
+ tools ? ' {"ops": [' : ' {"taskOps": [',
9390
9481
  ` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
9391
9482
  ' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"ops"?},"handle"?:"<name>"},',
9392
9483
  ' {"op":"remove","taskId":"<id or #order>"},',
@@ -9395,11 +9486,12 @@ function taskOpsProtocol(executionNote = "") {
9395
9486
  ' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
9396
9487
  ` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
9397
9488
  " ]}",
9398
- '- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
9489
+ tools ? "- For sweeping changes, you may instead call submit_plan with the full plan." : '- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
9399
9490
  'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
9400
9491
  'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
9401
- 'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
9492
+ `When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models>"}.`,
9402
9493
  'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
9494
+ ...tools ? ["edit_plan checks the batch when you call it and answers with what it will change, or with the error to fix \u2014 call it again in the same reply. The edit is applied when your reply ends; edits you make in one reply join one batch."] : [],
9403
9495
  'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + executionNote,
9404
9496
  'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
9405
9497
  '"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
@@ -9631,21 +9723,21 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9631
9723
  const handleOwner = /* @__PURE__ */ new Map();
9632
9724
  const handleId = /* @__PURE__ */ new Map();
9633
9725
  for (const [i, op] of ops.entries()) {
9634
- const handle = op.handle;
9635
- if (handle === void 0) continue;
9636
- if (typeof handle !== "string" || !handle.trim()) {
9726
+ const handle2 = op.handle;
9727
+ if (handle2 === void 0) continue;
9728
+ if (typeof handle2 !== "string" || !handle2.trim()) {
9637
9729
  errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
9638
9730
  continue;
9639
9731
  }
9640
- if (handleOwner.has(handle)) {
9641
- errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
9732
+ if (handleOwner.has(handle2)) {
9733
+ errors.push(`op ${i + 1} (${op.op}): handle "${handle2}" is already used earlier in this batch`);
9642
9734
  continue;
9643
9735
  }
9644
- if (refCollidesWithExisting(handle, originalFlatAll)) {
9645
- errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
9736
+ if (refCollidesWithExisting(handle2, originalFlatAll)) {
9737
+ errors.push(`op ${i + 1} (${op.op}): handle "${handle2}" collides with an existing task reference`);
9646
9738
  continue;
9647
9739
  }
9648
- handleOwner.set(handle, i);
9740
+ handleOwner.set(handle2, i);
9649
9741
  }
9650
9742
  if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
9651
9743
  const resolveDeps = (deps, opIndex) => {
@@ -10012,15 +10104,15 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
10012
10104
 
10013
10105
  // src/services/PlanRepair.ts
10014
10106
  async function repairLoop(opts) {
10015
- let reply = await opts.first();
10107
+ let reply2 = await opts.first();
10016
10108
  for (let repairs = 0; ; repairs++) {
10017
- const verdict = await opts.interpret(reply);
10109
+ const verdict = await opts.interpret(reply2);
10018
10110
  if ("done" in verdict) return verdict.done;
10019
10111
  if (repairs >= opts.maxRepairs) {
10020
- return opts.onExhausted({ reply, errors: verdict.retry.errors, cause: verdict.retry.cause });
10112
+ return opts.onExhausted({ reply: reply2, errors: verdict.retry.errors, cause: verdict.retry.cause });
10021
10113
  }
10022
10114
  const { corrective } = verdict.retry;
10023
- reply = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
10115
+ reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
10024
10116
  }
10025
10117
  }
10026
10118
  var JSON_REPAIR_INSTRUCTION = "Your previous response could not be parsed as JSON. Re-send ONLY the JSON object \u2014 no prose before or after, no explanation, no markdown code fences.";
@@ -10136,34 +10228,34 @@ async function settleReply(opts) {
10136
10228
  }
10137
10229
  if (attempt.failure !== void 0) return { done: message(attempt.failure) };
10138
10230
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
10139
- const reply = classifyPlannerReply(attempt.text, opts.classify);
10140
- switch (reply.kind) {
10231
+ const reply2 = classifyPlannerReply(attempt.text, opts.classify);
10232
+ switch (reply2.kind) {
10141
10233
  case "plan":
10142
- return { done: { kind: "plan", tasks: reply.tasks, text: said(attempt), researchLog } };
10234
+ return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
10143
10235
  case "task_ops":
10144
- return { done: { kind: "task_ops", ops: reply.ops, text: said(attempt), researchLog } };
10236
+ return { done: { kind: "task_ops", ops: reply2.ops, text: said(attempt), researchLog } };
10145
10237
  // A read is answered by the conversation, which owns the plan and the
10146
10238
  // catalog, so it leaves here the way a plan or an edit does.
10147
10239
  case "task_query":
10148
- return { done: { kind: "task_query", query: reply.query, text: said(attempt), researchLog } };
10240
+ return { done: { kind: "task_query", query: reply2.query, text: said(attempt), researchLog } };
10149
10241
  case "prose":
10150
10242
  return { done: asProse(attempt) };
10151
10243
  }
10152
10244
  if (opts.signal?.aborted) return { done: asProse(attempt) };
10153
- const errors = [reply.error.message];
10154
- switch (reply.kind) {
10245
+ const errors = [reply2.error.message];
10246
+ switch (reply2.kind) {
10155
10247
  case "broken_task_ops":
10156
- return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply.error.message), cause: reply.error } };
10248
+ return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
10157
10249
  case "broken_task_query":
10158
- return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply.error.message), cause: reply.error } };
10250
+ return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
10159
10251
  case "broken_plan": {
10160
- const corrective = reply.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply.error.message);
10161
- return { retry: { errors, corrective, cause: reply.error } };
10252
+ const corrective = reply2.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply2.error.message);
10253
+ return { retry: { errors, corrective, cause: reply2.error } };
10162
10254
  }
10163
10255
  }
10164
10256
  },
10165
10257
  maxRepairs: MAX_JSON_REPAIRS,
10166
- onExhausted: ({ reply }) => asProse(reply)
10258
+ onExhausted: ({ reply: reply2 }) => asProse(reply2)
10167
10259
  });
10168
10260
  }
10169
10261
  var said = (attempt) => attempt.fullText ?? attempt.text;
@@ -10390,8 +10482,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
10390
10482
  const blocks = [{ tool: null, lines: [] }];
10391
10483
  for (const line of resultsText.split("\n")) {
10392
10484
  const m = headerRe.exec(line);
10393
- const tool = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
10394
- if (tool) blocks.push({ tool, lines: [line] });
10485
+ const tool2 = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
10486
+ if (tool2) blocks.push({ tool: tool2, lines: [line] });
10395
10487
  else blocks[blocks.length - 1].lines.push(line);
10396
10488
  }
10397
10489
  const texts = blocks.map((b) => b.lines.join("\n"));
@@ -10485,9 +10577,9 @@ function ellipsize(text2, max) {
10485
10577
  const oneLine2 = text2.replace(/\s+/g, " ").trim();
10486
10578
  return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
10487
10579
  }
10488
- function summarizeToolCall(tool, argsJson, toolLabel) {
10489
- const display = toolLabel?.trim() || tool;
10490
- if (tool === "agent_tool") {
10580
+ function summarizeToolCall(tool2, argsJson, toolLabel) {
10581
+ const display = toolLabel?.trim() || tool2;
10582
+ if (tool2 === "agent_tool") {
10491
10583
  try {
10492
10584
  const args = JSON.parse(argsJson);
10493
10585
  const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
@@ -10496,7 +10588,7 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
10496
10588
  }
10497
10589
  return display;
10498
10590
  }
10499
- if (tool === "spawn_research_agent") {
10591
+ if (tool2 === "spawn_research_agent") {
10500
10592
  try {
10501
10593
  const args = JSON.parse(argsJson);
10502
10594
  const prompt = typeof args.prompt === "string" ? args.prompt : "";
@@ -10508,19 +10600,19 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
10508
10600
  }
10509
10601
  try {
10510
10602
  const args = JSON.parse(argsJson);
10511
- if (tool === "find_symbol" && args.symbol) {
10603
+ if (tool2 === "find_symbol" && args.symbol) {
10512
10604
  return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
10513
10605
  }
10514
- if (tool === "web_search" && args.query) {
10606
+ if (tool2 === "web_search" && args.query) {
10515
10607
  return `web_search "${ellipsize(String(args.query), 50)}"`;
10516
10608
  }
10517
- if (tool === "fetch" && args.url) {
10609
+ if (tool2 === "fetch" && args.url) {
10518
10610
  return `fetch ${ellipsize(String(args.url), 60)}`;
10519
10611
  }
10520
- if (tool === "glob" && args.pattern) {
10612
+ if (tool2 === "glob" && args.pattern) {
10521
10613
  return `glob ${ellipsize(String(args.pattern), 50)}`;
10522
10614
  }
10523
- if (tool === "grep" && args.pattern) {
10615
+ if (tool2 === "grep" && args.pattern) {
10524
10616
  const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
10525
10617
  const scope = args.include ? ` in ${args.include}` : "";
10526
10618
  return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
@@ -10953,9 +11045,9 @@ ${llmOutput}
10953
11045
  return { tasks: null, researchLog, researchResults };
10954
11046
  }
10955
11047
  const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
10956
- const reply = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
10957
- if (reply.kind === "plan") {
10958
- return { tasks: reply.tasks, researchLog, researchResults };
11048
+ const reply2 = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
11049
+ if (reply2.kind === "plan") {
11050
+ return { tasks: reply2.tasks, researchLog, researchResults };
10959
11051
  }
10960
11052
  if (!turn.hasToolCalls) break;
10961
11053
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
@@ -11672,7 +11764,7 @@ ${collected.aiflowContext}
11672
11764
  };
11673
11765
 
11674
11766
  // src/services/TaskOrchestrator.ts
11675
- var path14 = __toESM(require("path"));
11767
+ var path15 = __toESM(require("path"));
11676
11768
 
11677
11769
  // src/services/promptAugment.ts
11678
11770
  var MAX_TAIL_CHARS = 500;
@@ -11759,7 +11851,7 @@ function renderPlanMap(planTasks, currentTaskId, opts) {
11759
11851
  const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
11760
11852
  const { window, omitted } = pickWindow(sorted, currentIdx, max);
11761
11853
  const currentTask = sorted.find((r) => r.task.id === currentTaskId)?.task;
11762
- const lines = window.map((row) => {
11854
+ const lines2 = window.map((row) => {
11763
11855
  const isCurrent = row.task.id === currentTaskId;
11764
11856
  const tag = planMapStatus(row.task, isCurrent);
11765
11857
  const order = row.parent ? row.label : row.label.padStart(2, " ");
@@ -11776,7 +11868,7 @@ You are running as: ${currentTask.assignedRunner}` : "";
11776
11868
  "",
11777
11869
  "This is for context only \u2014 do ONLY the task marked `\u2190 you are here`. Future tasks will handle their own scope; do not preempt them.",
11778
11870
  "",
11779
- lines.join("\n") + footer + runnerNote
11871
+ lines2.join("\n") + footer + runnerNote
11780
11872
  ].join("\n");
11781
11873
  }
11782
11874
  function renderPreviousAttempt(output) {
@@ -11790,10 +11882,14 @@ function renderPreviousAttempt(output) {
11790
11882
  tail
11791
11883
  ].join("\n");
11792
11884
  }
11793
- function renderCompletionMarker(task) {
11885
+ function renderCompletionMarker(task, completionTool = false) {
11886
+ const howto = `Build it by writing \`<<<ORDEWELL_\` immediately followed by \`DONE_${task.completionMarker}>>>\` \u2014 joined into a single unbroken token, with no space, quote, or any other character between the two parts.`;
11887
+ if (!completionTool) return `
11888
+
11889
+ When you have fully completed this task, print one final line containing only the completion marker. ${howto}`;
11794
11890
  return `
11795
11891
 
11796
- When you have fully completed this task, print one final line containing only the completion marker. Build it by writing \`<<<ORDEWELL_\` immediately followed by \`DONE_${task.completionMarker}>>>\` \u2014 joined into a single unbroken token, with no space, quote, or any other character between the two parts.`;
11892
+ When you have fully completed this task, call the \`task_complete\` tool with status \`done\` and a summary of what you did; the tasks that depend on this one are given that summary. If you cannot complete it, call \`task_complete\` with status \`blocked\` or \`failed\` and the reason instead, and print no marker. After a \`done\` call, or if the tool is not available to you, also print one final line containing only the completion marker. ${howto}`;
11797
11893
  }
11798
11894
  function renderTddInstruction() {
11799
11895
  return [
@@ -11814,7 +11910,21 @@ function renderTddInstruction() {
11814
11910
  ].join("\n");
11815
11911
  }
11816
11912
  var CHECKPOINT_MARKER_HOWTO = "Build it by writing `<<<ORDEWELL_` immediately followed by `CHECKPOINT:` \u2014 no space, quote, or any other character between those two parts \u2014 then a brief summary of what you are about to do and why human input is needed, closed with `>>>`";
11817
- function renderCheckpointInstruction() {
11913
+ function renderCheckpointInstruction(checkpointTool = false) {
11914
+ if (checkpointTool) {
11915
+ return [
11916
+ "## Human-in-the-loop checkpoints",
11917
+ "",
11918
+ "When you reach a decision point that requires human judgment \u2014 before destructive",
11919
+ "operations, after major design decisions, or when multiple viable paths exist \u2014 pause",
11920
+ "and request input:",
11921
+ "",
11922
+ "1. Call the `checkpoint` tool with a brief summary of what you are about to do and why human input is needed. The call waits for the human, and its result is their answer",
11923
+ "2. If the result is `continue`: proceed with the action you described",
11924
+ "3. If the result starts with `rejected:`: the rest is why. Adjust your approach and call `checkpoint` again if needed",
11925
+ `4. If the \`checkpoint\` tool is not available to you, or the call fails, fall back to the marker: print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}`
11926
+ ].join("\n");
11927
+ }
11818
11928
  return [
11819
11929
  "## Human-in-the-loop checkpoints",
11820
11930
  "",
@@ -11846,9 +11956,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
11846
11956
  blocks.push(renderTddInstruction());
11847
11957
  }
11848
11958
  if (isHitlTask(task)) {
11849
- blocks.push(renderCheckpointInstruction());
11959
+ blocks.push(renderCheckpointInstruction(opts?.completionTool));
11850
11960
  }
11851
- const marker2 = renderCompletionMarker(task);
11961
+ const marker2 = renderCompletionMarker(task, opts?.completionTool);
11852
11962
  if (blocks.length === 0) return basePrompt + marker2;
11853
11963
  return `${blocks.join("\n\n")}
11854
11964
 
@@ -11859,12 +11969,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
11859
11969
  opts.ops ? "(Ordewell) You are continuing this task in the same session, in the same checkout. What your earlier attempt did outside the repository was not undone: check what already exists before acting again." : "(Ordewell) You are continuing this task in the same session. Your working directory was recreated from the integration branch: work from your earlier attempt is there only if it landed, so check the files before relying on them."
11860
11970
  ];
11861
11971
  if (isHitlTask(task)) {
11862
- reminder.push(`If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
11972
+ reminder.push(opts.completionTool ? `If you reach a decision that needs human judgment, call the \`checkpoint\` tool: its result is \`continue\` or \`rejected: <why>\`. Only if the tool is not available to you, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.` : `If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
11863
11973
  }
11864
11974
  return `${message.trim()}
11865
11975
 
11866
11976
  ---
11867
- ${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
11977
+ ${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
11868
11978
  }
11869
11979
 
11870
11980
  // src/services/VerdictEngine.ts
@@ -11882,6 +11992,13 @@ var VerdictEngine = class {
11882
11992
  pausedSessions = /* @__PURE__ */ new Map();
11883
11993
  listeners = [];
11884
11994
  checkpointListeners = [];
11995
+ withdrawnListeners = [];
11996
+ /**
11997
+ * The open `checkpoint` tool call per task (ADR-0022, V5): settling it is how
11998
+ * an answer reaches a runner that asked through the tool, where the marker's
11999
+ * answer is typed into the session instead.
12000
+ */
12001
+ toolCheckpoints = /* @__PURE__ */ new Map();
11885
12002
  idleListeners = [];
11886
12003
  /**
11887
12004
  * Per-task generation. Replaced on every watch(), clear() and verdict.
@@ -11912,6 +12029,9 @@ var VerdictEngine = class {
11912
12029
  onCheckpoint(listener) {
11913
12030
  this.checkpointListeners.push(listener);
11914
12031
  }
12032
+ onCheckpointWithdrawn(listener) {
12033
+ this.withdrawnListeners.push(listener);
12034
+ }
11915
12035
  onIdleChange(listener) {
11916
12036
  this.idleListeners.push(listener);
11917
12037
  }
@@ -11973,6 +12093,11 @@ ${line}
11973
12093
  }
11974
12094
  approveCheckpoint(taskId) {
11975
12095
  this.resumeIdle(taskId);
12096
+ const toolCall = this.toolCheckpoints.get(taskId);
12097
+ if (toolCall) {
12098
+ toolCall({ kind: "continue" });
12099
+ return;
12100
+ }
11976
12101
  const session = this.pausedSessions.get(taskId);
11977
12102
  if (session) {
11978
12103
  session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
@@ -11981,6 +12106,11 @@ ${line}
11981
12106
  }
11982
12107
  rejectCheckpoint(taskId, reason) {
11983
12108
  this.resumeIdle(taskId);
12109
+ const toolCall = this.toolCheckpoints.get(taskId);
12110
+ if (toolCall) {
12111
+ toolCall({ kind: "rejected", reason });
12112
+ return;
12113
+ }
11984
12114
  const session = this.pausedSessions.get(taskId);
11985
12115
  if (session) {
11986
12116
  session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
@@ -11991,7 +12121,10 @@ ${line}
11991
12121
  * Attach to a spawned session: scan the output tail for the task's completion
11992
12122
  * marker (delivering a verdict immediately while leaving interactive sessions
11993
12123
  * open), scan for checkpoint markers, and on exit produce a failed verdict
11994
- * when the marker was never observed.
12124
+ * when the marker was never observed. A structured session's `task_complete`
12125
+ * call is evidence too (ADR-0022, V2): whichever signal comes first decides.
12126
+ *
12127
+ * Returns the attempt's generation, what {@link signalComplete} is checked against.
11995
12128
  */
11996
12129
  watch(task, session) {
11997
12130
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
@@ -12021,6 +12154,8 @@ ${line}
12021
12154
  else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
12022
12155
  else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
12023
12156
  });
12157
+ session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
12158
+ session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
12024
12159
  }
12025
12160
  session.onExit((exitCode) => {
12026
12161
  if (this.generations.get(task.id) !== gen) return;
@@ -12029,6 +12164,52 @@ ${line}
12029
12164
  const verdict = this.decide(task, exitCode);
12030
12165
  for (const l of this.listeners) l(task.id, verdict);
12031
12166
  });
12167
+ return gen;
12168
+ }
12169
+ /**
12170
+ * The runner's own `task_complete` call (ADR-0022, V1/V3): settles the
12171
+ * attempt exactly as the marker does, unless that attempt is no longer the
12172
+ * task's current one or another signal already settled it.
12173
+ */
12174
+ signalComplete(taskId, generation, report) {
12175
+ if (this.generations.get(taskId) !== generation) return;
12176
+ this.forget(taskId);
12177
+ this.bumpGeneration(taskId);
12178
+ const verdict = reportedVerdict(report);
12179
+ for (const l of this.listeners) l(taskId, verdict);
12180
+ }
12181
+ /**
12182
+ * The runner's `checkpoint` call (ADR-0022, V5): raised through the same
12183
+ * listeners as the marker, so the task waits on the user the same way, and
12184
+ * settled by {@link approveCheckpoint} or {@link rejectCheckpoint}. It is
12185
+ * withdrawn, never left hanging, once the attempt is over or the call goes.
12186
+ */
12187
+ raiseCheckpoint(taskId, generation, question, signal) {
12188
+ if (this.generations.get(taskId) !== generation || signal.aborted) {
12189
+ return Promise.resolve({ kind: "withdrawn", why: "this attempt has ended." });
12190
+ }
12191
+ if (this.toolCheckpoints.has(taskId)) {
12192
+ return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
12193
+ }
12194
+ return new Promise((resolve7) => {
12195
+ const settle = (answer2) => {
12196
+ signal.removeEventListener("abort", onAbort);
12197
+ if (this.toolCheckpoints.get(taskId) === settle) this.toolCheckpoints.delete(taskId);
12198
+ resolve7(answer2);
12199
+ };
12200
+ const onAbort = () => {
12201
+ settle({ kind: "withdrawn", why: "the call was cancelled." });
12202
+ if (this.generations.get(taskId) !== generation) return;
12203
+ this.resumeIdle(taskId);
12204
+ for (const l of this.withdrawnListeners) l(taskId);
12205
+ };
12206
+ signal.addEventListener("abort", onAbort, { once: true });
12207
+ this.toolCheckpoints.set(taskId, settle);
12208
+ for (const l of this.checkpointListeners) l(taskId, question.trim());
12209
+ });
12210
+ }
12211
+ withdrawToolCheckpoint(taskId) {
12212
+ this.toolCheckpoints.get(taskId)?.({ kind: "withdrawn", why: "this attempt has ended." });
12032
12213
  }
12033
12214
  approvalOpened(taskId, approvalId) {
12034
12215
  const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
@@ -12062,6 +12243,7 @@ ${line}
12062
12243
  this.markerTails.delete(taskId);
12063
12244
  this.checkpointCarry.delete(taskId);
12064
12245
  this.pausedSessions.delete(taskId);
12246
+ this.withdrawToolCheckpoint(taskId);
12065
12247
  this.idlePaused.delete(taskId);
12066
12248
  this.openApprovals.delete(taskId);
12067
12249
  this.clearIdle(taskId);
@@ -12097,6 +12279,7 @@ ${line}
12097
12279
  this.markerTails.clear();
12098
12280
  this.checkpointCarry.clear();
12099
12281
  this.pausedSessions.clear();
12282
+ for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
12100
12283
  for (const timer of this.idleTimers.values()) clearTimeout(timer);
12101
12284
  this.idleTimers.clear();
12102
12285
  this.idleSince.clear();
@@ -12150,6 +12333,28 @@ ${line}
12150
12333
  };
12151
12334
  }
12152
12335
  };
12336
+ function reportedVerdict(report) {
12337
+ const checks = [
12338
+ {
12339
+ name: "task_complete",
12340
+ passed: report.status === "done",
12341
+ skipped: false,
12342
+ detail: `the runner called task_complete with status "${report.status}"`
12343
+ },
12344
+ {
12345
+ name: "completion_marker",
12346
+ passed: report.status === "done",
12347
+ skipped: true,
12348
+ detail: "bypassed \u2014 the runner reported through task_complete"
12349
+ }
12350
+ ];
12351
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
12352
+ if (report.status === "done") {
12353
+ return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt };
12354
+ }
12355
+ const why = report.reason?.trim() || "no reason given";
12356
+ return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt };
12357
+ }
12153
12358
 
12154
12359
  // src/services/transcriptCapture.ts
12155
12360
  var import_fs = require("fs");
@@ -12313,10 +12518,10 @@ async function fromDirenv(cwd, deps) {
12313
12518
  }
12314
12519
  return { env, blocked: null };
12315
12520
  }
12316
- function findUp(start, relative5, deps) {
12521
+ function findUp(start, relative6, deps) {
12317
12522
  let dir = path10.resolve(start);
12318
12523
  for (; ; ) {
12319
- const candidate = path10.join(dir, relative5);
12524
+ const candidate = path10.join(dir, relative6);
12320
12525
  if (deps.readFile(candidate) !== null) return candidate;
12321
12526
  const parent = path10.dirname(dir);
12322
12527
  if (parent === dir) return null;
@@ -12522,10 +12727,10 @@ function codexFinal(home, query, maxChars) {
12522
12727
  function codexLastAssistant(file, query) {
12523
12728
  const raw = (0, import_fs.readFileSync)(file, "utf8");
12524
12729
  if (!raw.includes(query.marker)) return null;
12525
- const lines = raw.split("\n").filter((l) => l.trim());
12730
+ const lines2 = raw.split("\n").filter((l) => l.trim());
12526
12731
  let sawCwd = false;
12527
12732
  let last = null;
12528
- for (const line of lines) {
12733
+ for (const line of lines2) {
12529
12734
  let rec;
12530
12735
  try {
12531
12736
  rec = JSON.parse(line);
@@ -12565,6 +12770,11 @@ var BufferedTaskOutputSource = class {
12565
12770
  session.onExit(() => {
12566
12771
  capture.exited = true;
12567
12772
  });
12773
+ if (isStructuredSession(session)) {
12774
+ session.onTaskComplete(({ summary }) => {
12775
+ if (capture.attached && summary.trim()) capture.reported = summary.trim();
12776
+ });
12777
+ }
12568
12778
  }
12569
12779
  detach(taskId) {
12570
12780
  const capture = this.captures.get(taskId);
@@ -12575,6 +12785,8 @@ var BufferedTaskOutputSource = class {
12575
12785
  this.captures.clear();
12576
12786
  }
12577
12787
  async finalText(attempt, doneToken) {
12788
+ const reported = this.captures.get(attempt.taskId)?.reported;
12789
+ if (reported) return defuseMarkers(reported);
12578
12790
  if (attempt.cwd && attempt.completionMarker) {
12579
12791
  const transcript = await this.transcripts.finalAssistantText({
12580
12792
  runner: attempt.runner,
@@ -12592,8 +12804,8 @@ var BufferedTaskOutputSource = class {
12592
12804
  if (!capture) return null;
12593
12805
  const total = capture.dropped + capture.raw.length;
12594
12806
  const from = Math.min(Math.max(0, (opts.sinceOffset ?? 0) - capture.dropped), capture.raw.length);
12595
- const lines = renderCleanCapture(capture.raw.slice(from)).split("\n");
12596
- const kept = opts.maxLines > 0 ? lines.slice(-opts.maxLines) : [];
12807
+ const lines2 = renderCleanCapture(capture.raw.slice(from)).split("\n");
12808
+ const kept = opts.maxLines > 0 ? lines2.slice(-opts.maxLines) : [];
12597
12809
  return {
12598
12810
  text: kept.join("\n"),
12599
12811
  nextOffset: total,
@@ -13414,8 +13626,8 @@ var IsolationRunController = class {
13414
13626
  this.resolvers = {};
13415
13627
  this.reportedCopies.clear();
13416
13628
  this.listener.changed();
13417
- const shared = sharedPathsNotice(this.run);
13418
- if (shared) this.tell("info", shared);
13629
+ const shared2 = sharedPathsNotice(this.run);
13630
+ if (shared2) this.tell("info", shared2);
13419
13631
  }
13420
13632
  reportCopies(copied) {
13421
13633
  const fresh = copied.filter((p) => !this.reportedCopies.has(p));
@@ -14587,166 +14799,724 @@ ${tail}` : ""}`;
14587
14799
  }
14588
14800
  };
14589
14801
 
14590
- // src/services/harness/ClaudeCodeAdapter.ts
14591
- var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
14592
- var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
14593
- var PROTOCOL_ARGS = [
14594
- "-p",
14595
- "--input-format",
14596
- "stream-json",
14597
- "--output-format",
14598
- "stream-json",
14599
- "--verbose",
14600
- "--include-partial-messages"
14601
- ];
14602
- var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
14603
- var FOLLOW_ON_TURN_GRACE_MS = 5e3;
14604
- var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
14605
- var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
14606
- function usageRecord(usage, model, subagentId) {
14607
- const record = {
14608
- source: "claude-code",
14609
- ...partedPromptUsage({ uncached: usage.input_tokens, cacheRead: usage.cache_read_input_tokens, cacheWrite: usage.cache_creation_input_tokens })
14610
- };
14611
- if (model) record.model = model;
14612
- if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
14613
- if (subagentId) record.subagentId = subagentId;
14614
- return record;
14615
- }
14616
- function subagentFinalCall(result) {
14617
- if (typeof result !== "object" || result === null) return null;
14618
- const { usage, resolvedModel } = result;
14619
- if (typeof usage !== "object" || usage === null) return null;
14620
- return { usage, model: typeof resolvedModel === "string" ? resolvedModel : void 0 };
14802
+ // src/services/harness/fileDiff.ts
14803
+ function markedLines(text2, mark) {
14804
+ if (!text2) return text2;
14805
+ const body = text2.endsWith("\n") ? text2.slice(0, -1) : text2;
14806
+ return `${body.split("\n").map((line) => `${mark}${line}`).join("\n")}${body === text2 ? "" : "\n"}`;
14621
14807
  }
14622
- function notificationOutcome(status) {
14623
- if (status === "completed") return "done";
14624
- if (status === "killed" || status === "stopped") return "stopped";
14625
- return "failed";
14808
+ var HUNK_HEADER = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/;
14809
+ function hunksOf(diff) {
14810
+ const kept = [];
14811
+ let oldLeft = 0;
14812
+ let newLeft = 0;
14813
+ for (const line of diff.split("\n")) {
14814
+ if (oldLeft > 0 || newLeft > 0 || line.startsWith("\\")) {
14815
+ kept.push(line);
14816
+ if (line.startsWith("\\")) continue;
14817
+ if (!line.startsWith("+")) oldLeft -= 1;
14818
+ if (!line.startsWith("-")) newLeft -= 1;
14819
+ continue;
14820
+ }
14821
+ const header = HUNK_HEADER.exec(line);
14822
+ if (!header) continue;
14823
+ kept.push(line);
14824
+ oldLeft = header[1] === void 0 ? 1 : Number(header[1]);
14825
+ newLeft = header[2] === void 0 ? 1 : Number(header[2]);
14826
+ }
14827
+ return kept.length ? `${kept.join("\n")}${diff.endsWith("\n") ? "\n" : ""}` : diff;
14626
14828
  }
14627
- function blocksOf(msg) {
14628
- return Array.isArray(msg.message?.content) ? msg.message.content : [];
14829
+ function range(start, lines2) {
14830
+ return lines2 === 1 ? `${start}` : `${start},${lines2}`;
14629
14831
  }
14630
- function flattenContent(content) {
14631
- if (typeof content === "string") return content;
14632
- if (Array.isArray(content)) {
14633
- return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").filter(Boolean).join("\n");
14832
+ function structuredPatchText(patch) {
14833
+ if (!Array.isArray(patch)) return "";
14834
+ let text2 = "";
14835
+ for (const hunk of patch) {
14836
+ if (!hunk || typeof hunk !== "object") return "";
14837
+ const { oldStart, oldLines, newStart, newLines, lines: lines2 } = hunk;
14838
+ if (typeof oldStart !== "number" || typeof oldLines !== "number" || typeof newStart !== "number" || typeof newLines !== "number") return "";
14839
+ if (!Array.isArray(lines2) || !lines2.every((line) => typeof line === "string")) return "";
14840
+ text2 += `@@ -${range(oldStart, oldLines)} +${range(newStart, newLines)} @@
14841
+ ${lines2.map((line) => `${line}
14842
+ `).join("")}`;
14634
14843
  }
14635
- return "";
14844
+ return text2;
14636
14845
  }
14637
- var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14638
- agentId = "claude-code";
14639
- interruptCount = 0;
14640
- /** Interrupts sent and not yet acknowledged, by request id. */
14641
- pendingControl = /* @__PURE__ */ new Map();
14642
- /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
14643
- interruptRequested = false;
14644
- /** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
14645
- openPermissions = /* @__PURE__ */ new Map();
14646
- /** Whether this turn has already emitted reply text — see {@link handleLine}. */
14647
- turnHasText = false;
14648
- /** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
14649
- pendingBreak = false;
14650
- /** The model answering the planner's current message, as its `message_start` named it. */
14651
- plannerModel;
14652
- /**
14653
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
14654
- * the CLI restores the resumed session's running total, and what Ordewell
14655
- * already counted of it is not ours to know here.
14656
- */
14657
- reportedCostUsd = 0;
14658
- /**
14659
- * Subagents started and not yet finished, keyed by the `Agent` call's id.
14660
- * Kept across turns: a backgrounded one reports after its turn has ended.
14661
- */
14662
- openSubagents = /* @__PURE__ */ new Map();
14663
- /** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
14664
- countedSubagentMessages = /* @__PURE__ */ new Set();
14665
- /** Background tasks (shells, agents) the CLI reports as still running, as of its last `background_tasks_changed`. */
14666
- backgroundTaskCount = 0;
14667
- /**
14668
- * A task's turn whose `result` arrived while background work was still open.
14669
- * The CLI reports that result when the model stops talking, then opens a turn
14670
- * of its own when the work finishes; a turn ended at the first result would
14671
- * lose everything said after it, the completion marker included.
14672
- */
14673
- resultHeld = false;
14674
- followOnTimer = null;
14675
- /** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
14676
- requestedMode = null;
14677
- /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
14678
- pendingResume = null;
14679
- spawnSpec(opts) {
14680
- if (opts.kind === "task") return this.taskSpawnSpec(opts);
14681
- const args = [
14682
- ...PROTOCOL_ARGS,
14683
- // The read-only guarantee, enforced at spawn rather than by prompt.
14684
- "--permission-mode",
14685
- "plan",
14686
- "--disallowedTools",
14687
- DISALLOWED_TOOLS.join(","),
14688
- "--append-system-prompt",
14689
- opts.systemPrompt
14690
- ];
14691
- if (opts.model) args.push("--model", opts.model);
14692
- if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
14693
- if (opts.resumeSessionId) {
14694
- args.push("--resume", opts.resumeSessionId);
14695
- this.reportedCostUsd = void 0;
14696
- this.pendingResume = opts.resumeSessionId;
14846
+
14847
+ // src/services/mcp/OrdewellMcpServer.ts
14848
+ var import_http = require("http");
14849
+ var import_crypto4 = require("crypto");
14850
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
14851
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
14852
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
14853
+
14854
+ // src/utils/daemonToken.ts
14855
+ var import_crypto3 = require("crypto");
14856
+ var import_fs3 = require("fs");
14857
+ var import_path2 = require("path");
14858
+
14859
+ // src/utils/globalDataDir.ts
14860
+ var fs7 = __toESM(require("fs"));
14861
+ var path14 = __toESM(require("path"));
14862
+ var os5 = __toESM(require("os"));
14863
+ function globalDataDir() {
14864
+ return path14.join(os5.homedir(), ".ordewell");
14865
+ }
14866
+ function copyDirSync(src, dest) {
14867
+ fs7.mkdirSync(dest, { recursive: true });
14868
+ const entries = fs7.readdirSync(src, { withFileTypes: true });
14869
+ for (const entry of entries) {
14870
+ const srcPath = path14.join(src, entry.name);
14871
+ const destPath = path14.join(dest, entry.name);
14872
+ if (entry.isDirectory()) {
14873
+ copyDirSync(srcPath, destPath);
14874
+ } else {
14875
+ fs7.copyFileSync(srcPath, destPath);
14697
14876
  }
14698
- return { command: "claude", args };
14699
14877
  }
14700
- /**
14701
- * A task's run: the manifest decides what its mode and effort mean
14702
- * (ADR-0001), and this adds only the protocol around them. No tool list and
14703
- * no system prompt — the task's prompt is its first turn, as on the terminal
14704
- * transport. `--permission-prompt-tool stdio` routes the questions the mode
14705
- * leaves open to the control channel, where the adapter must answer them;
14706
- * without it `-p` refuses them silently and nothing can ever surface one.
14707
- */
14708
- taskSpawnSpec(opts) {
14709
- this.requestedMode = opts.flags.permissionMode;
14710
- const args = [
14711
- ...PROTOCOL_ARGS,
14712
- "--permission-prompt-tool",
14713
- "stdio",
14714
- "--permission-mode",
14715
- opts.flags.permissionMode,
14716
- "--disallowedTools",
14717
- TASK_DISALLOWED_TOOLS.join(","),
14718
- ...opts.flags.effort ? claudeThinkingArgs(opts.flags.effort) : []
14719
- ];
14720
- if (opts.model) args.push("--model", opts.model);
14721
- if (opts.resumeSessionId) {
14722
- args.push("--resume", opts.resumeSessionId);
14723
- this.reportedCostUsd = void 0;
14724
- this.pendingResume = opts.resumeSessionId;
14878
+ }
14879
+ var migrated = false;
14880
+ function migrateOldConfigDir() {
14881
+ if (migrated) return;
14882
+ migrated = true;
14883
+ const oldDir = path14.join(os5.homedir(), ".config", "ordewell");
14884
+ if (!fs7.existsSync(oldDir)) return;
14885
+ const newDir = globalDataDir();
14886
+ const liftFile = (name) => {
14887
+ const oldFile = path14.join(oldDir, name);
14888
+ const newFile = path14.join(newDir, name);
14889
+ if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
14890
+ fs7.mkdirSync(newDir, { recursive: true });
14891
+ fs7.copyFileSync(oldFile, newFile);
14725
14892
  }
14726
- return { command: "claude", args };
14893
+ };
14894
+ liftFile("settings.json");
14895
+ liftFile(".env");
14896
+ const oldPlugins = path14.join(oldDir, "plugins");
14897
+ const newPlugins = path14.join(newDir, "plugins");
14898
+ if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
14899
+ copyDirSync(oldPlugins, newPlugins);
14727
14900
  }
14728
- /**
14729
- * Claude Code's soft interrupt: the turn stops, the process and its session
14730
- * stay. The CLI acknowledges on the control channel, then closes the turn
14731
- * with an `error_during_execution` result, which {@link handleLine} reports
14732
- * as an interrupted `turn_end`.
14733
- */
14734
- interrupt(timeoutMs) {
14735
- if (!this.process) return Promise.resolve(false);
14736
- this.interruptCount += 1;
14737
- const requestId = `ordewell-interrupt-${this.interruptCount}`;
14738
- this.interruptRequested = true;
14739
- return new Promise((resolve7) => {
14740
- const settle = (ok) => {
14901
+ }
14902
+
14903
+ // src/utils/privateFile.ts
14904
+ var import_fs2 = require("fs");
14905
+ var import_path = require("path");
14906
+ var import_crypto2 = require("crypto");
14907
+ var DIR_MODE = 448;
14908
+ var FILE_MODE = 384;
14909
+ var POSIX_MODES = process.platform !== "win32";
14910
+ function ensurePrivateDir(dir) {
14911
+ if (POSIX_MODES) {
14912
+ (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
14913
+ return;
14914
+ }
14915
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
14916
+ }
14917
+ function writePrivateFile(filePath, content) {
14918
+ const dir = (0, import_path.dirname)(filePath);
14919
+ ensurePrivateDir(dir);
14920
+ const tempPath = (0, import_path.join)(dir, `.${(0, import_path.basename)(filePath)}.${(0, import_crypto2.randomBytes)(8).toString("hex")}.tmp`);
14921
+ const fd = (0, import_fs2.openSync)(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
14922
+ try {
14923
+ (0, import_fs2.writeSync)(fd, content);
14924
+ } finally {
14925
+ (0, import_fs2.closeSync)(fd);
14926
+ }
14927
+ try {
14928
+ (0, import_fs2.renameSync)(tempPath, filePath);
14929
+ } catch (err) {
14930
+ try {
14931
+ (0, import_fs2.unlinkSync)(tempPath);
14932
+ } catch {
14933
+ }
14934
+ throw err;
14935
+ }
14936
+ }
14937
+
14938
+ // src/utils/daemonToken.ts
14939
+ var BEARER_PREFIX = "bearer ";
14940
+ var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
14941
+ var DAEMON_SUBPROTOCOL = "ordewell.v1";
14942
+ function configDir() {
14943
+ return globalDataDir();
14944
+ }
14945
+ function daemonTokenPath(port) {
14946
+ return (0, import_path2.join)(configDir(), `server-${port}.token`);
14947
+ }
14948
+ function mintDaemonToken(port) {
14949
+ const token = (0, import_crypto3.randomBytes)(32).toString("base64url");
14950
+ const file = daemonTokenPath(port);
14951
+ writePrivateFile(file, token);
14952
+ return { token, file };
14953
+ }
14954
+ function readDaemonToken(port) {
14955
+ try {
14956
+ const token = (0, import_fs3.readFileSync)(daemonTokenPath(port), "utf8").trim();
14957
+ return token === "" ? void 0 : token;
14958
+ } catch {
14959
+ return void 0;
14960
+ }
14961
+ }
14962
+ function clearDaemonToken(port) {
14963
+ try {
14964
+ (0, import_fs3.unlinkSync)(daemonTokenPath(port));
14965
+ } catch {
14966
+ }
14967
+ }
14968
+ function bearerHeaderValue(token) {
14969
+ return `Bearer ${token}`;
14970
+ }
14971
+ function tokenSubprotocols(token) {
14972
+ return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
14973
+ }
14974
+ function extractPresentedToken(carriers) {
14975
+ const authorization = carriers.authorization;
14976
+ if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
14977
+ const value = authorization.slice(BEARER_PREFIX.length).trim();
14978
+ if (value !== "") return value;
14979
+ }
14980
+ for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
14981
+ const value = offered.trim();
14982
+ if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
14983
+ const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
14984
+ if (token !== "") return token;
14985
+ }
14986
+ return void 0;
14987
+ }
14988
+ function tokensMatch(presented, expected) {
14989
+ if (presented === void 0) return false;
14990
+ const a = Buffer.from(presented, "utf8");
14991
+ const b = Buffer.from(expected, "utf8");
14992
+ if (a.length !== b.length) return false;
14993
+ return (0, import_crypto3.timingSafeEqual)(a, b);
14994
+ }
14995
+
14996
+ // src/services/mcp/tools.ts
14997
+ var import_v4 = require("zod/v4");
14998
+ var taskCompleteInput = import_v4.z.object({
14999
+ status: import_v4.z.enum(["done", "blocked", "failed"]).describe("'done' only when the task is fully complete. 'blocked' or 'failed' end the attempt without passing."),
15000
+ summary: import_v4.z.string().describe("What was done, handed to the tasks that depend on this one."),
15001
+ reason: import_v4.z.string().optional().describe("Why the task is blocked or failed. Expected for anything but 'done'.")
15002
+ });
15003
+ var checkpointInput = import_v4.z.object({
15004
+ question: import_v4.z.string().min(1).describe("The question for the user. The call returns their answer.")
15005
+ });
15006
+ var listRunnersInput = import_v4.z.object({});
15007
+ var listModelsInput = import_v4.z.object({
15008
+ runner: import_v4.z.string().min(1).describe("A runner id from list_runners.")
15009
+ });
15010
+ var taskRef = import_v4.z.union([import_v4.z.string().min(1), import_v4.z.number().int().min(0)]).describe('A task id, "#order", a bare order, or a title.');
15011
+ var modelAssignment = import_v4.z.looseObject({
15012
+ modelId: import_v4.z.string(),
15013
+ modelLabel: import_v4.z.string().optional(),
15014
+ thinkingEffort: import_v4.z.string().optional().describe("A variant id from that model's variants list.")
15015
+ });
15016
+ var planTask = import_v4.z.looseObject({
15017
+ id: import_v4.z.string().optional(),
15018
+ order: import_v4.z.number().optional(),
15019
+ title: import_v4.z.string(),
15020
+ description: import_v4.z.string().optional(),
15021
+ type: import_v4.z.enum(["ai", "user"]).optional(),
15022
+ dependencies: import_v4.z.array(import_v4.z.string()).optional(),
15023
+ prompt: import_v4.z.string().optional(),
15024
+ userSteps: import_v4.z.array(import_v4.z.looseObject({ order: import_v4.z.number().optional(), instruction: import_v4.z.string() })).optional(),
15025
+ assignedRunner: import_v4.z.string().optional(),
15026
+ assignedModel: modelAssignment.optional(),
15027
+ taskMode: import_v4.z.string().optional(),
15028
+ autonomy: import_v4.z.enum(["AFK", "HITL"]).optional(),
15029
+ sliceType: import_v4.z.enum(["AFK", "HITL"]).optional(),
15030
+ ops: import_v4.z.boolean().optional(),
15031
+ userStoriesCovered: import_v4.z.array(import_v4.z.string()).optional(),
15032
+ subtasks: import_v4.z.array(import_v4.z.record(import_v4.z.string(), import_v4.z.unknown())).optional()
15033
+ });
15034
+ var submitPlanInput = import_v4.z.object({
15035
+ tasks: import_v4.z.array(planTask).min(1).describe("The whole plan, in order.")
15036
+ });
15037
+ var taskFields = import_v4.z.record(import_v4.z.string(), import_v4.z.unknown());
15038
+ var handle = import_v4.z.string().optional();
15039
+ var taskOp = import_v4.z.discriminatedUnion("op", [
15040
+ import_v4.z.object({ op: import_v4.z.literal("update"), taskId: taskRef, changes: taskFields }),
15041
+ import_v4.z.object({ op: import_v4.z.literal("add"), task: taskFields, handle }),
15042
+ import_v4.z.object({ op: import_v4.z.literal("remove"), taskId: taskRef }),
15043
+ import_v4.z.object({ op: import_v4.z.literal("reorder"), taskIds: import_v4.z.array(taskRef) }),
15044
+ import_v4.z.object({ op: import_v4.z.literal("merge"), taskIds: import_v4.z.array(taskRef), merged: taskFields, handle }),
15045
+ import_v4.z.object({ op: import_v4.z.literal("split"), taskId: taskRef, parts: import_v4.z.array(taskFields), handle }),
15046
+ import_v4.z.object({ op: import_v4.z.literal("rearm"), taskId: taskRef, changes: taskFields.optional() })
15047
+ ]);
15048
+ var editPlanInput = import_v4.z.object({
15049
+ ops: import_v4.z.array(taskOp).min(1).describe("Applied as one batch; every ref resolves against the plan before any op runs.")
15050
+ });
15051
+ var taskQueryInput = import_v4.z.object({
15052
+ tasks: import_v4.z.array(taskRef).optional(),
15053
+ fields: import_v4.z.array(import_v4.z.enum(TASK_QUERY_TASK_FIELDS)).optional().describe("Omit to read every field. A task's output is read with task_output."),
15054
+ catalog: import_v4.z.boolean().optional().describe("Also return every runner with its models, thinking-effort variants and modes.")
15055
+ }).refine((q) => (q.tasks?.length ?? 0) > 0 || q.catalog === true, { message: "Name at least one task, or set catalog: true." });
15056
+ var taskOutputInput = import_v4.z.object({
15057
+ task: taskRef,
15058
+ lines: import_v4.z.number().int().min(1).optional().describe(`How many lines to return; the default is 80 and anything over ${OUTPUT_LINES_MAX} is cut to ${OUTPUT_LINES_MAX}.`),
15059
+ since: import_v4.z.number().int().min(0).optional().describe("A previous answer's nextOffset: return only what came after it.")
15060
+ });
15061
+ function checkpointReply(answer2) {
15062
+ switch (answer2.kind) {
15063
+ case "continue":
15064
+ return { text: "continue" };
15065
+ case "rejected":
15066
+ return { text: `rejected: ${answer2.reason}` };
15067
+ case "withdrawn":
15068
+ return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
15069
+ }
15070
+ }
15071
+ function tool(name, description, input, pick, annotations) {
15072
+ return {
15073
+ name,
15074
+ description,
15075
+ inputSchema: { ...import_v4.z.toJSONSchema(input, { io: "input" }), type: "object" },
15076
+ ...annotations ? { annotations } : {},
15077
+ async call(handler, args, context) {
15078
+ const run = pick(handler);
15079
+ if (!run) return { text: `${name} is not available in this session.`, isError: true };
15080
+ const parsed = input.safeParse(args ?? {});
15081
+ if (!parsed.success) return { text: `Invalid ${name} arguments: ${import_v4.z.prettifyError(parsed.error)}`, isError: true };
15082
+ return run(parsed.data, context);
15083
+ }
15084
+ };
15085
+ }
15086
+ var TASK_TOOLS = [
15087
+ tool(
15088
+ "task_complete",
15089
+ "Report that this task has ended, and how. Call it once, as your last action.",
15090
+ taskCompleteInput,
15091
+ (h) => h.taskComplete?.bind(h)
15092
+ ),
15093
+ tool(
15094
+ "checkpoint",
15095
+ "Ask the user a question and wait for the answer, which is this call's result.",
15096
+ checkpointInput,
15097
+ (h) => h.checkpoint?.bind(h)
15098
+ )
15099
+ ];
15100
+ var PLANNER_READ_ONLY = { readOnlyHint: true };
15101
+ var PLANNER_TOOLS = [
15102
+ tool(
15103
+ "list_runners",
15104
+ "List the runners enabled right now, each with its modes and default mode. Call it just before submit_plan.",
15105
+ listRunnersInput,
15106
+ (h) => h.listRunners?.bind(h),
15107
+ PLANNER_READ_ONLY
15108
+ ),
15109
+ tool(
15110
+ "list_models",
15111
+ "List the models a runner may use right now, with labels and thinking-effort variants.",
15112
+ listModelsInput,
15113
+ (h) => h.listModels?.bind(h),
15114
+ PLANNER_READ_ONLY
15115
+ ),
15116
+ tool(
15117
+ "submit_plan",
15118
+ "Submit the whole plan. It is checked against the live runners and models; an error names the task, the field and what would be accepted.",
15119
+ submitPlanInput,
15120
+ (h) => h.submitPlan?.bind(h),
15121
+ PLANNER_READ_ONLY
15122
+ ),
15123
+ tool(
15124
+ "edit_plan",
15125
+ "Change the current plan with task operations: update, add, remove, reorder, merge, split or rearm.",
15126
+ editPlanInput,
15127
+ (h) => h.editPlan?.bind(h),
15128
+ PLANNER_READ_ONLY
15129
+ ),
15130
+ tool(
15131
+ "task_query",
15132
+ "Read the long fields of plan tasks that the plan summary leaves out (prompt, user steps, verdict, output summary), and optionally the live runner catalog. Read a task before you rewrite it.",
15133
+ taskQueryInput,
15134
+ (h) => h.taskQuery?.bind(h),
15135
+ PLANNER_READ_ONLY
15136
+ ),
15137
+ tool(
15138
+ "task_output",
15139
+ "Read the recent output of a running task, to check what its runner is doing; paged by offset. A task that is not running answers with its verdict, output summary and a digest of its last attempt.",
15140
+ taskOutputInput,
15141
+ (h) => h.taskOutput?.bind(h),
15142
+ PLANNER_READ_ONLY
15143
+ )
15144
+ ];
15145
+
15146
+ // src/services/mcp/OrdewellMcpServer.ts
15147
+ var ORDEWELL_MCP_PATH = "/mcp";
15148
+ var DEFAULT_HEARTBEAT_MS = 3e4;
15149
+ var OrdewellMcpServer = class {
15150
+ grants = /* @__PURE__ */ new Map();
15151
+ http;
15152
+ listening;
15153
+ boundUrl;
15154
+ boundHost;
15155
+ heartbeatMs;
15156
+ constructor(options = {}) {
15157
+ this.heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
15158
+ }
15159
+ /** The listener's URL, once the first token has started it. */
15160
+ get url() {
15161
+ return this.boundUrl;
15162
+ }
15163
+ issueTaskToken(scope, handler = {}) {
15164
+ return this.issue({ role: "task", scope: { ...scope }, handler, revoked: new AbortController() });
15165
+ }
15166
+ issuePlannerToken(scope, handler = {}) {
15167
+ return this.issue({ role: "planner", scope: { ...scope }, handler, revoked: new AbortController() });
15168
+ }
15169
+ /** Refuse every later request carrying `token`, and abort the calls it has in flight. */
15170
+ revoke(token) {
15171
+ const grant = this.grants.get(token);
15172
+ if (!grant) return;
15173
+ this.grants.delete(token);
15174
+ grant.revoked.abort();
15175
+ }
15176
+ async dispose() {
15177
+ for (const token of [...this.grants.keys()]) this.revoke(token);
15178
+ const http = this.http;
15179
+ this.http = void 0;
15180
+ this.listening = void 0;
15181
+ this.boundUrl = void 0;
15182
+ this.boundHost = void 0;
15183
+ if (!http) return;
15184
+ http.closeAllConnections();
15185
+ await new Promise((resolve7) => http.close(() => resolve7()));
15186
+ }
15187
+ async issue(grant) {
15188
+ const url = await this.start();
15189
+ const token = (0, import_crypto4.randomBytes)(32).toString("base64url");
15190
+ this.grants.set(token, grant);
15191
+ return { url, token };
15192
+ }
15193
+ start() {
15194
+ this.listening ??= new Promise((resolve7, reject) => {
15195
+ const http = (0, import_http.createServer)((req, res) => {
15196
+ this.handle(req, res).catch(() => {
15197
+ if (!res.headersSent) reply(res, 500, "Internal error");
15198
+ else res.destroy();
15199
+ });
15200
+ });
15201
+ http.once("error", (err) => {
15202
+ this.listening = void 0;
15203
+ reject(err);
15204
+ });
15205
+ http.listen(0, "127.0.0.1", () => {
15206
+ const address = http.address();
15207
+ if (address === null || typeof address === "string") {
15208
+ reject(new Error("Ordewell MCP server bound to no TCP port"));
15209
+ return;
15210
+ }
15211
+ this.boundHost = `127.0.0.1:${address.port}`;
15212
+ this.boundUrl = `http://${this.boundHost}${ORDEWELL_MCP_PATH}`;
15213
+ resolve7(this.boundUrl);
15214
+ });
15215
+ http.unref();
15216
+ this.http = http;
15217
+ });
15218
+ return this.listening;
15219
+ }
15220
+ async handle(req, res) {
15221
+ if (req.headers.host !== this.boundHost || req.headers.origin !== void 0) {
15222
+ reply(res, 403, "Forbidden");
15223
+ return;
15224
+ }
15225
+ const token = extractPresentedToken({ authorization: req.headers.authorization });
15226
+ const grant = token === void 0 ? void 0 : this.grants.get(token);
15227
+ if (!grant) {
15228
+ reply(res, 401, "Unauthorized");
15229
+ return;
15230
+ }
15231
+ if (new URL(req.url ?? "/", `http://${this.boundHost}`).pathname !== ORDEWELL_MCP_PATH) {
15232
+ reply(res, 404, "Not found");
15233
+ return;
15234
+ }
15235
+ const mcp = new import_server.Server({ name: "ordewell", version: "1" }, { capabilities: { tools: {} } });
15236
+ if (grant.role === "task") serveTools(mcp, TASK_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
15237
+ else serveTools(mcp, PLANNER_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
15238
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
15239
+ res.on("close", () => {
15240
+ void transport.close();
15241
+ void mcp.close();
15242
+ });
15243
+ await mcp.connect(transport);
15244
+ await transport.handleRequest(req, res);
15245
+ }
15246
+ };
15247
+ function serveTools(mcp, tools, handler, revoked, heartbeatMs) {
15248
+ mcp.setRequestHandler(import_types.ListToolsRequestSchema, () => ({
15249
+ tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, ...annotations ? { annotations } : {} }))
15250
+ }));
15251
+ mcp.setRequestHandler(import_types.CallToolRequestSchema, async (request, extra) => {
15252
+ const tool2 = tools.find((t) => t.name === request.params.name);
15253
+ if (!tool2) throw new import_types.McpError(import_types.ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
15254
+ const progressToken = request.params._meta?.progressToken;
15255
+ let beats = 0;
15256
+ const heartbeat = progressToken === void 0 ? void 0 : setInterval(() => {
15257
+ beats += 1;
15258
+ extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress: beats } }).catch(() => void 0);
15259
+ }, heartbeatMs);
15260
+ try {
15261
+ const result = await tool2.call(handler, request.params.arguments, { signal: AbortSignal.any([revoked, extra.signal]) });
15262
+ return { content: [{ type: "text", text: result.text }], isError: result.isError ?? false };
15263
+ } catch (err) {
15264
+ return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
15265
+ } finally {
15266
+ clearInterval(heartbeat);
15267
+ }
15268
+ });
15269
+ }
15270
+ function reply(res, status, message) {
15271
+ res.writeHead(status, { "content-type": "text/plain" }).end(message);
15272
+ }
15273
+ var shared;
15274
+ function sharedMcpServer() {
15275
+ shared ??= new OrdewellMcpServer();
15276
+ return shared;
15277
+ }
15278
+
15279
+ // src/services/mcp/clientConfig.ts
15280
+ var import_fs4 = require("fs");
15281
+ var import_os = require("os");
15282
+ var import_path3 = require("path");
15283
+ var ORDEWELL_MCP_SERVER_NAME = "ordewell";
15284
+ function mcpClientConfig(credential) {
15285
+ return {
15286
+ name: ORDEWELL_MCP_SERVER_NAME,
15287
+ url: credential.url,
15288
+ headers: { Authorization: bearerHeaderValue(credential.token) }
15289
+ };
15290
+ }
15291
+ function ownerOnlyConfigFile(fileName, contents) {
15292
+ const dir = (0, import_fs4.mkdtempSync)((0, import_path3.join)((0, import_os.tmpdir)(), "ordewell-mcp-"));
15293
+ const path27 = (0, import_path3.join)(dir, fileName);
15294
+ (0, import_fs4.writeFileSync)(path27, contents, { mode: 384 });
15295
+ return { path: path27, remove: () => (0, import_fs4.rmSync)(dir, { recursive: true, force: true }) };
15296
+ }
15297
+
15298
+ // src/services/harness/ClaudeCodeAdapter.ts
15299
+ var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
15300
+ var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
15301
+ var PROTOCOL_ARGS = [
15302
+ "-p",
15303
+ "--input-format",
15304
+ "stream-json",
15305
+ "--output-format",
15306
+ "stream-json",
15307
+ "--verbose",
15308
+ "--include-partial-messages"
15309
+ ];
15310
+ var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
15311
+ var FOLLOW_ON_TURN_GRACE_MS = 5e3;
15312
+ var MCP_ATTACH_TIMEOUT_MS = 1e4;
15313
+ var MCP_STATUS_POLL_MS = 100;
15314
+ var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
15315
+ var ORDEWELL_TASK_TOOLS = ["task_complete", "checkpoint"];
15316
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
15317
+ function usageRecord(usage, model, subagentId) {
15318
+ const record = {
15319
+ source: "claude-code",
15320
+ ...partedPromptUsage({ uncached: usage.input_tokens, cacheRead: usage.cache_read_input_tokens, cacheWrite: usage.cache_creation_input_tokens })
15321
+ };
15322
+ if (model) record.model = model;
15323
+ if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
15324
+ if (subagentId) record.subagentId = subagentId;
15325
+ return record;
15326
+ }
15327
+ function subagentFinalCall(result) {
15328
+ if (typeof result !== "object" || result === null) return null;
15329
+ const { usage, resolvedModel } = result;
15330
+ if (typeof usage !== "object" || usage === null) return null;
15331
+ return { usage, model: typeof resolvedModel === "string" ? resolvedModel : void 0 };
15332
+ }
15333
+ function notificationOutcome(status) {
15334
+ if (status === "completed") return "done";
15335
+ if (status === "killed" || status === "stopped") return "stopped";
15336
+ return "failed";
15337
+ }
15338
+ function blocksOf(msg) {
15339
+ return Array.isArray(msg.message?.content) ? msg.message.content : [];
15340
+ }
15341
+ function flattenContent(content) {
15342
+ if (typeof content === "string") return content;
15343
+ if (Array.isArray(content)) {
15344
+ return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").filter(Boolean).join("\n");
15345
+ }
15346
+ return "";
15347
+ }
15348
+ function editDiff(result) {
15349
+ if (!result || typeof result !== "object") return "";
15350
+ const { type, content, structuredPatch } = result;
15351
+ if (type === "create" && typeof content === "string") return markedLines(content, "+");
15352
+ return structuredPatchText(structuredPatch);
15353
+ }
15354
+ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
15355
+ agentId = "claude-code";
15356
+ controlCount = 0;
15357
+ /** Control requests sent and not yet answered, by request id. */
15358
+ pendingControl = /* @__PURE__ */ new Map();
15359
+ /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
15360
+ interruptRequested = false;
15361
+ /** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
15362
+ openPermissions = /* @__PURE__ */ new Map();
15363
+ /** Whether this turn has already emitted reply text — see {@link handleLine}. */
15364
+ turnHasText = false;
15365
+ /** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
15366
+ pendingBreak = false;
15367
+ /** The model answering the planner's current message, as its `message_start` named it. */
15368
+ plannerModel;
15369
+ /**
15370
+ * The session's `total_cost_usd` as last reported. Undefined after a resume:
15371
+ * the CLI restores the resumed session's running total, and what Ordewell
15372
+ * already counted of it is not ours to know here.
15373
+ */
15374
+ reportedCostUsd = 0;
15375
+ /**
15376
+ * Subagents started and not yet finished, keyed by the `Agent` call's id.
15377
+ * Kept across turns: a backgrounded one reports after its turn has ended.
15378
+ */
15379
+ openSubagents = /* @__PURE__ */ new Map();
15380
+ /** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
15381
+ countedSubagentMessages = /* @__PURE__ */ new Set();
15382
+ /** Background tasks (shells, agents) the CLI reports as still running, as of its last `background_tasks_changed`. */
15383
+ backgroundTaskCount = 0;
15384
+ /**
15385
+ * A task's turn whose `result` arrived while background work was still open.
15386
+ * The CLI reports that result when the model stops talking, then opens a turn
15387
+ * of its own when the work finishes; a turn ended at the first result would
15388
+ * lose everything said after it, the completion marker included.
15389
+ */
15390
+ resultHeld = false;
15391
+ followOnTimer = null;
15392
+ /** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
15393
+ requestedMode = null;
15394
+ /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
15395
+ pendingResume = null;
15396
+ /** The `--mcp-config` file of the running process, which holds its token; removed with the process. */
15397
+ mcpConfig = null;
15398
+ /** `mcp__ordewell__`, once this process was given the server. */
15399
+ ordewellToolPrefix = null;
15400
+ spawnSpec(opts) {
15401
+ if (opts.kind === "task") return this.taskSpawnSpec(opts);
15402
+ const args = [
15403
+ ...PROTOCOL_ARGS,
15404
+ // The read-only guarantee, enforced at spawn rather than by prompt.
15405
+ "--permission-mode",
15406
+ "plan",
15407
+ "--disallowedTools",
15408
+ DISALLOWED_TOOLS.join(","),
15409
+ "--append-system-prompt",
15410
+ opts.systemPrompt
15411
+ ];
15412
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, PLANNER_TOOLS.map((t) => t.name)));
15413
+ if (opts.model) args.push("--model", opts.model);
15414
+ if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
15415
+ if (opts.resumeSessionId) {
15416
+ args.push("--resume", opts.resumeSessionId);
15417
+ this.reportedCostUsd = void 0;
15418
+ this.pendingResume = opts.resumeSessionId;
15419
+ }
15420
+ return { command: "claude", args };
15421
+ }
15422
+ /**
15423
+ * A task's run: the manifest decides what its mode and effort mean
15424
+ * (ADR-0001), and this adds only the protocol around them. No tool list and
15425
+ * no system prompt — the task's prompt is its first turn, as on the terminal
15426
+ * transport. `--permission-prompt-tool stdio` routes the questions the mode
15427
+ * leaves open to the control channel, where the adapter must answer them;
15428
+ * without it `-p` refuses them silently and nothing can ever surface one.
15429
+ */
15430
+ taskSpawnSpec(opts) {
15431
+ this.requestedMode = opts.flags.permissionMode;
15432
+ const args = [
15433
+ ...PROTOCOL_ARGS,
15434
+ "--permission-prompt-tool",
15435
+ "stdio",
15436
+ "--permission-mode",
15437
+ opts.flags.permissionMode,
15438
+ "--disallowedTools",
15439
+ TASK_DISALLOWED_TOOLS.join(","),
15440
+ ...opts.flags.effort ? claudeThinkingArgs(opts.flags.effort) : []
15441
+ ];
15442
+ if (opts.model) args.push("--model", opts.model);
15443
+ if (opts.resumeSessionId) {
15444
+ args.push("--resume", opts.resumeSessionId);
15445
+ this.reportedCostUsd = void 0;
15446
+ this.pendingResume = opts.resumeSessionId;
15447
+ }
15448
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, ORDEWELL_TASK_TOOLS));
15449
+ return { command: "claude", args };
15450
+ }
15451
+ /**
15452
+ * `--mcp-config` takes a path as well as inline JSON; the path keeps the
15453
+ * token out of the argv every local user can list (ADR-0022, A5). The file
15454
+ * lives exactly as long as the process that reads it. `alwaysLoad` because
15455
+ * the CLI otherwise defers MCP tools behind its tool search, and a model
15456
+ * that has to look an Ordewell tool up first tends to go on without it.
15457
+ */
15458
+ ordewellServerArgs(mcp, tools) {
15459
+ this.removeMcpConfig();
15460
+ const config = { mcpServers: { [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } } };
15461
+ const file = ownerOnlyConfigFile("mcp.json", JSON.stringify(config));
15462
+ this.mcpConfig = file;
15463
+ void this.processEnded.then(() => file.remove());
15464
+ const prefix = `mcp__${mcp.name}__`;
15465
+ this.ordewellToolPrefix = prefix;
15466
+ return ["--mcp-config", file.path, "--allowedTools", tools.map((tool2) => `${prefix}${tool2}`).join(",")];
15467
+ }
15468
+ removeMcpConfig() {
15469
+ this.mcpConfig?.remove();
15470
+ this.mcpConfig = null;
15471
+ }
15472
+ async start(opts) {
15473
+ try {
15474
+ await super.start(opts);
15475
+ } catch (err) {
15476
+ this.removeMcpConfig();
15477
+ throw err;
15478
+ }
15479
+ }
15480
+ /** Asks the CLI itself, which reports `pending` until its connection attempt settles. */
15481
+ async mcpAttached() {
15482
+ if (!this.mcpConfig) return false;
15483
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS;
15484
+ while (Date.now() < deadline) {
15485
+ const answer2 = await this.control({ subtype: "mcp_status" }, deadline - Date.now());
15486
+ const status = answer2?.response?.mcpServers?.find((s) => s.name === ORDEWELL_MCP_SERVER_NAME)?.status;
15487
+ if (status === "connected") return true;
15488
+ if (status !== "pending") return false;
15489
+ await new Promise((resolve7) => setTimeout(resolve7, MCP_STATUS_POLL_MS));
15490
+ }
15491
+ return false;
15492
+ }
15493
+ /**
15494
+ * Claude Code's soft interrupt: the turn stops, the process and its session
15495
+ * stay. The CLI acknowledges on the control channel, then closes the turn
15496
+ * with an `error_during_execution` result, which {@link handleLine} reports
15497
+ * as an interrupted `turn_end`.
15498
+ */
15499
+ async interrupt(timeoutMs) {
15500
+ if (!this.process) return false;
15501
+ this.interruptRequested = true;
15502
+ return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
15503
+ }
15504
+ /** Send a control request and wait for its answer; null when none came in time or the process ended. */
15505
+ control(request, timeoutMs) {
15506
+ if (!this.process) return Promise.resolve(null);
15507
+ this.controlCount += 1;
15508
+ const requestId = `ordewell-${request.subtype}-${this.controlCount}`;
15509
+ return new Promise((resolve7) => {
15510
+ const settle = (response) => {
14741
15511
  if (!this.pendingControl.delete(requestId)) return;
14742
15512
  clearTimeout(timer);
14743
- resolve7(ok);
15513
+ resolve7(response);
14744
15514
  };
14745
- const timer = setTimeout(() => settle(false), timeoutMs);
15515
+ const timer = setTimeout(() => settle(null), timeoutMs);
14746
15516
  timer.unref?.();
14747
15517
  this.pendingControl.set(requestId, settle);
14748
- void this.processEnded.then(() => settle(false));
14749
- this.writeLine({ type: "control_request", request_id: requestId, request: { subtype: "interrupt" } });
15518
+ void this.processEnded.then(() => settle(null));
15519
+ this.writeLine({ type: "control_request", request_id: requestId, request });
14750
15520
  });
14751
15521
  }
14752
15522
  /**
@@ -14781,6 +15551,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14781
15551
  }
14782
15552
  dispose() {
14783
15553
  this.clearFollowOnTimer();
15554
+ this.removeMcpConfig();
14784
15555
  super.dispose();
14785
15556
  }
14786
15557
  clearFollowOnTimer() {
@@ -14831,12 +15602,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14831
15602
  emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
14832
15603
  return;
14833
15604
  }
15605
+ const name = msg.request.tool_name ?? "unknown";
15606
+ if (this.ordewellToolPrefix && name.startsWith(this.ordewellToolPrefix)) {
15607
+ this.writeLine({ type: "control_response", response: { subtype: "success", request_id: id, response: { behavior: "allow", updatedInput: input } } });
15608
+ emit({ type: "permission_request", id, name, detail: JSON.stringify(input), input, decided: { decision: "allow" } });
15609
+ return;
15610
+ }
14834
15611
  const suggestions = msg.request.permission_suggestions ?? [];
14835
15612
  this.openPermissions.set(id, { input, suggestions });
14836
15613
  emit({
14837
15614
  type: "permission_request",
14838
15615
  id,
14839
- name: msg.request.tool_name ?? "unknown",
15616
+ name,
14840
15617
  detail: JSON.stringify(input),
14841
15618
  input,
14842
15619
  suggestions,
@@ -14852,7 +15629,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14852
15629
  }
14853
15630
  case "control_response": {
14854
15631
  const requestId = msg.response?.request_id;
14855
- if (requestId) this.pendingControl.get(requestId)?.(msg.response?.subtype === "success");
15632
+ if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
14856
15633
  return;
14857
15634
  }
14858
15635
  case "assistant":
@@ -14880,6 +15657,7 @@ ${block.text}` : block.text });
14880
15657
  if (block.type !== "tool_result") continue;
14881
15658
  const id = block.tool_use_id ?? "";
14882
15659
  const output = flattenContent(block.content);
15660
+ const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
14883
15661
  const subagent = subagentId ? void 0 : this.openSubagents.get(id);
14884
15662
  if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
14885
15663
  emit({ type: "background_agent", id });
@@ -14887,7 +15665,7 @@ ${block.text}` : block.text });
14887
15665
  } else if (subagent) {
14888
15666
  this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
14889
15667
  }
14890
- emit({ type: "tool_result", id, name: "", output, success: block.is_error !== true, subagentId });
15668
+ emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
14891
15669
  }
14892
15670
  return;
14893
15671
  case "system":
@@ -15023,6 +15801,9 @@ ${block.text}` : block.text });
15023
15801
  }
15024
15802
  };
15025
15803
 
15804
+ // src/services/harness/CodexAdapter.ts
15805
+ var import_path4 = require("path");
15806
+
15026
15807
  // src/services/harness/codexSandbox.ts
15027
15808
  var PROBE_TIMEOUT_MS = 1e4;
15028
15809
  var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
@@ -15082,6 +15863,17 @@ function runProbe(deps, cwd, env, flags) {
15082
15863
 
15083
15864
  // src/services/harness/CodexAdapter.ts
15084
15865
  var HANDSHAKE_TIMEOUT_MS = 3e4;
15866
+ var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
15867
+ var CHANGE_NAMES = { add: "Add", update: "Update", delete: "Delete" };
15868
+ function changeName(change) {
15869
+ return CHANGE_NAMES[change.kind?.type ?? ""] ?? "file_change";
15870
+ }
15871
+ function changeDiff(change) {
15872
+ const diff = change.diff ?? "";
15873
+ if (change.kind?.type === "add") return markedLines(diff, "+");
15874
+ if (change.kind?.type === "delete") return markedLines(diff, "-");
15875
+ return diff;
15876
+ }
15085
15877
  function subagentOutcome(status) {
15086
15878
  switch (status) {
15087
15879
  case "completed":
@@ -15115,6 +15907,28 @@ var DECLINE_RESULTS = {
15115
15907
  execCommandApproval: { decision: "denied" },
15116
15908
  applyPatchApproval: { decision: "denied" }
15117
15909
  };
15910
+ function shownToolName(item) {
15911
+ const tool2 = item.tool ?? "mcp_tool";
15912
+ return item.server === ORDEWELL_MCP_SERVER_NAME ? `mcp__${ORDEWELL_MCP_SERVER_NAME}__${tool2}` : tool2;
15913
+ }
15914
+ function mcpHeaderEnv(mcp) {
15915
+ return Object.fromEntries(Object.keys(mcp.headers).map((header, i) => [header, `ORDEWELL_MCP_TOKEN_${i}`]));
15916
+ }
15917
+ function ordewellServerConfig(mcp) {
15918
+ return {
15919
+ url: mcp.url,
15920
+ env_http_headers: mcpHeaderEnv(mcp),
15921
+ default_tools_approval_mode: "approve"
15922
+ };
15923
+ }
15924
+ var TASK_TOOLS_NOTE = [
15925
+ "This task has two tools from the `ordewell` MCP server: `task_complete` and `checkpoint`.",
15926
+ "They are not in your tool list up front. Find them with the tool discovery you have (the `exec` tool's `ALL_TOOLS` list) and call them by their full names, `mcp__ordewell__task_complete` and `mcp__ordewell__checkpoint`.",
15927
+ "Look for them before you finish; the task is reported complete through `task_complete`."
15928
+ ].join(" ");
15929
+ function isOrdewellElicitation(method, params) {
15930
+ return method === "mcpServer/elicitation/request" && params.serverName === ORDEWELL_MCP_SERVER_NAME;
15931
+ }
15118
15932
  var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
15119
15933
  var TASK_APPROVALS = {
15120
15934
  "item/commandExecution/requestApproval": {
@@ -15189,11 +16003,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
15189
16003
  * names only the item, and a person deciding needs to see the files.
15190
16004
  */
15191
16005
  fileChangePaths = /* @__PURE__ */ new Map();
16006
+ /** The row each announced file of an in-flight `fileChange` item is shown under, by the file's shown path. */
16007
+ fileChangeRows = /* @__PURE__ */ new Map();
15192
16008
  /** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
15193
16009
  pendingRequests = /* @__PURE__ */ new Map();
15194
16010
  resumeAttempted = false;
15195
16011
  resumeFallbackSent = false;
15196
16012
  sandbox = "default";
16013
+ /** The Ordewell server's startup state, as Codex last reported it for this thread. */
16014
+ mcpStartup = null;
16015
+ mcpStartupSettled = null;
15197
16016
  /**
15198
16017
  * Subagent threads this session has spawned, keyed by the child thread id.
15199
16018
  * Codex runs a subagent in its own thread and replays both threads' events on
@@ -15202,7 +16021,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
15202
16021
  subagents = /* @__PURE__ */ new Map();
15203
16022
  spawnSpec(opts) {
15204
16023
  this.startOpts = opts;
15205
- return { command: "codex", args: ["app-server"] };
16024
+ const headers = opts.mcp ? mcpHeaderEnv(opts.mcp) : {};
16025
+ const env = Object.fromEntries(Object.entries(headers).map(([header, name]) => [name, opts.mcp.headers[header]]));
16026
+ return { command: "codex", args: ["app-server"], ...opts.mcp ? { env } : {} };
15206
16027
  }
15207
16028
  /**
15208
16029
  * `initialize`, then `thread/start`, both before the first user message. A
@@ -15269,7 +16090,11 @@ ${this.exitMessage()}`);
15269
16090
  cwd: opts?.cwd,
15270
16091
  ...opts?.model ? { model: opts.model } : {}
15271
16092
  };
15272
- const legacyLandlock = this.sandbox === "legacy-landlock" ? { config: { features: { use_legacy_landlock: true } } } : {};
16093
+ const config = {
16094
+ ...this.sandbox === "legacy-landlock" ? { features: { use_legacy_landlock: true } } : {},
16095
+ ...opts?.mcp ? { mcp_servers: { [opts.mcp.name]: ordewellServerConfig(opts.mcp) } } : {}
16096
+ };
16097
+ const threadConfig = Object.keys(config).length ? { config } : {};
15273
16098
  if (opts?.kind === "task") {
15274
16099
  const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
15275
16100
  return {
@@ -15277,14 +16102,15 @@ ${this.exitMessage()}`);
15277
16102
  sandbox: opts.flags.permissionMode,
15278
16103
  ...approvalPolicy ? { approvalPolicy } : {},
15279
16104
  ...approvalsReviewer ? { approvalsReviewer } : {},
15280
- ...legacyLandlock
16105
+ ...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
16106
+ ...threadConfig
15281
16107
  };
15282
16108
  }
15283
16109
  return {
15284
16110
  ...common,
15285
16111
  sandbox: "read-only",
15286
16112
  approvalPolicy: "never",
15287
- ...legacyLandlock,
16113
+ ...threadConfig,
15288
16114
  // `developerInstructions` layers on top of Codex's own base prompt, the
15289
16115
  // way Claude Code's `--append-system-prompt` does. `baseInstructions`
15290
16116
  // replaces it — which takes Codex's description of its own tools with
@@ -15293,6 +16119,32 @@ ${this.exitMessage()}`);
15293
16119
  developerInstructions: opts?.systemPrompt
15294
16120
  };
15295
16121
  }
16122
+ /**
16123
+ * Codex's own startup report for the server, which it sends once the thread
16124
+ * has tried to connect (ADR-0022, S4). A report that came before this was
16125
+ * asked is kept, so asking late still gets the answer.
16126
+ */
16127
+ async mcpAttached() {
16128
+ if (!this.startOpts?.mcp || !this.process) return false;
16129
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS2;
16130
+ while (this.mcpStartup === null || this.mcpStartup === "starting") {
16131
+ const left = deadline - Date.now();
16132
+ if (left <= 0) return false;
16133
+ const ended = await Promise.race([
16134
+ new Promise((resolve7) => {
16135
+ this.mcpStartupSettled = () => resolve7(false);
16136
+ }),
16137
+ this.processEnded.then(() => true),
16138
+ new Promise((resolve7) => {
16139
+ const t = setTimeout(() => resolve7(false), left);
16140
+ t.unref?.();
16141
+ })
16142
+ ]);
16143
+ this.mcpStartupSettled = null;
16144
+ if (ended) return false;
16145
+ }
16146
+ return this.mcpStartup === "ready";
16147
+ }
15296
16148
  effort() {
15297
16149
  const opts = this.startOpts;
15298
16150
  return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
@@ -15464,6 +16316,13 @@ ${this.exitMessage()}`);
15464
16316
  if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
15465
16317
  return;
15466
16318
  }
16319
+ case "mcpServer/startupStatus/updated": {
16320
+ const startup = msg.params;
16321
+ if (startup?.name !== ORDEWELL_MCP_SERVER_NAME || !startup.status) return;
16322
+ this.mcpStartup = startup.status;
16323
+ this.mcpStartupSettled?.();
16324
+ return;
16325
+ }
15467
16326
  case "thread/tokenUsage/updated":
15468
16327
  this.emitUsage(msg.params, emit);
15469
16328
  return;
@@ -15602,6 +16461,10 @@ ${this.exitMessage()}`);
15602
16461
  const method = msg.method;
15603
16462
  const params = msg.params ?? {};
15604
16463
  const approval = TASK_APPROVALS[method];
16464
+ if (isOrdewellElicitation(method, params)) {
16465
+ this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
16466
+ return;
16467
+ }
15605
16468
  if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
15606
16469
  const id = String(msg.id);
15607
16470
  const input = this.approvalInput(method, params);
@@ -15642,6 +16505,10 @@ ${this.exitMessage()}`);
15642
16505
  */
15643
16506
  answerServerRequest(msg, emit) {
15644
16507
  const method = msg.method;
16508
+ if (isOrdewellElicitation(method, msg.params ?? {})) {
16509
+ this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
16510
+ return;
16511
+ }
15645
16512
  const result = DECLINE_RESULTS[method];
15646
16513
  if (result) {
15647
16514
  this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
@@ -15668,7 +16535,7 @@ ${this.exitMessage()}`);
15668
16535
  emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
15669
16536
  return;
15670
16537
  case "mcpToolCall":
15671
- emit({ type: "tool_call", id: item.id, name: item.tool ?? "mcp_tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
16538
+ emit({ type: "tool_call", id: item.id, name: shownToolName(item), args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
15672
16539
  return;
15673
16540
  case "dynamicToolCall":
15674
16541
  emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
@@ -15680,7 +16547,7 @@ ${this.exitMessage()}`);
15680
16547
  const paths = (item.changes ?? []).map((change) => change.path).filter((path27) => !!path27);
15681
16548
  this.fileChangePaths.set(item.id, paths);
15682
16549
  if (this.startOpts?.kind === "task") {
15683
- emit({ type: "tool_call", id: item.id, name: "file_change", args: { path: paths.join(", ") }, ...subagentId ? { subagentId } : {} });
16550
+ for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
15684
16551
  }
15685
16552
  return;
15686
16553
  }
@@ -15737,7 +16604,7 @@ ${item.text}` : item.text });
15737
16604
  emit({
15738
16605
  type: "tool_result",
15739
16606
  id,
15740
- name: item.tool ?? "tool",
16607
+ name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
15741
16608
  output: item.error ?? flattenText(item.result) ?? "",
15742
16609
  success: item.status !== "error" && item.success !== false && !item.error,
15743
16610
  ...subagentId ? { subagentId } : {}
@@ -15751,8 +16618,11 @@ ${item.text}` : item.text });
15751
16618
  case "fileChange":
15752
16619
  this.fileChangePaths.delete(id);
15753
16620
  if (this.startOpts?.kind === "task") {
15754
- const diff = (item.changes ?? []).map((change) => change.diff ?? "").join("\n");
15755
- emit({ type: "tool_result", id, name: "file_change", output: diff, success: item.status === "completed", ...subagentId ? { subagentId } : {} });
16621
+ for (const change of item.changes ?? []) {
16622
+ const row = this.announceFileChange(id, change, emit, subagentId);
16623
+ emit({ type: "tool_result", id: row, name: changeName(change), output: changeDiff(change), success: item.status === "completed", ...subagentId ? { subagentId } : {} });
16624
+ }
16625
+ this.fileChangeRows.delete(id);
15756
16626
  return;
15757
16627
  }
15758
16628
  emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
@@ -15761,6 +16631,34 @@ ${item.text}` : item.text });
15761
16631
  return;
15762
16632
  }
15763
16633
  }
16634
+ /**
16635
+ * One row per changed file: a patch is several files, and one row naming
16636
+ * them all reads as a single cut-off path. The first file keeps the item's
16637
+ * id, which an approval for the patch points at. Announcing a file twice
16638
+ * returns the row it already has, so a change first seen completed still
16639
+ * gets its call.
16640
+ */
16641
+ announceFileChange(itemId, change, emit, subagentId) {
16642
+ const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
16643
+ this.fileChangeRows.set(itemId, rows);
16644
+ const path27 = this.shownChangePath(change);
16645
+ const known = rows.get(path27);
16646
+ if (known) return known;
16647
+ const row = rows.size === 0 ? itemId : `${itemId}:${path27}`;
16648
+ rows.set(path27, row);
16649
+ emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path27 }, ...subagentId ? { subagentId } : {} });
16650
+ return row;
16651
+ }
16652
+ /** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
16653
+ shownChangePath(change) {
16654
+ const shown = (path27) => {
16655
+ const cwd = this.startOpts?.cwd;
16656
+ const rel = cwd ? (0, import_path4.relative)(cwd, path27) : path27;
16657
+ return rel && rel !== ".." && !rel.startsWith(`..${import_path4.sep}`) && !(0, import_path4.isAbsolute)(rel) ? rel : path27;
16658
+ };
16659
+ const from = shown(change.path ?? "");
16660
+ return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
16661
+ }
15764
16662
  /**
15765
16663
  * A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
15766
16664
  * subagent. The call itself is planner-level tool activity; the lifecycle it
@@ -15800,7 +16698,45 @@ ${item.text}` : item.text });
15800
16698
  };
15801
16699
 
15802
16700
  // src/services/harness/OpenCodeAdapter.ts
15803
- var import_crypto2 = require("crypto");
16701
+ var import_crypto5 = require("crypto");
16702
+
16703
+ // src/services/harness/openCodeOrdewell.ts
16704
+ function ordewellToolPrefix(mcp) {
16705
+ return `${mcp.name}_`;
16706
+ }
16707
+ function isOrdewellTool(mcp, permission) {
16708
+ return !!mcp && !!permission && permission.startsWith(ordewellToolPrefix(mcp));
16709
+ }
16710
+ function isRecord2(value) {
16711
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16712
+ }
16713
+ function deepMerge(base, over) {
16714
+ const merged = { ...base };
16715
+ for (const [key, value] of Object.entries(over)) {
16716
+ const current = merged[key];
16717
+ merged[key] = isRecord2(current) && isRecord2(value) ? deepMerge(current, value) : value;
16718
+ }
16719
+ return merged;
16720
+ }
16721
+ function mergeOrdewellConfig(existing, mcp) {
16722
+ let base = {};
16723
+ if (existing?.trim()) {
16724
+ let parsed;
16725
+ try {
16726
+ parsed = JSON.parse(existing);
16727
+ } catch {
16728
+ return null;
16729
+ }
16730
+ if (!isRecord2(parsed)) return null;
16731
+ base = parsed;
16732
+ }
16733
+ const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
16734
+ const ours = {
16735
+ mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } },
16736
+ permission: { [`${ordewellToolPrefix(mcp)}*`]: "allow" }
16737
+ };
16738
+ return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
16739
+ }
15804
16740
 
15805
16741
  // src/services/harness/OpenCodeV2.ts
15806
16742
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
@@ -15883,12 +16819,16 @@ var OpenCodeV2 = class {
15883
16819
  const created = await this.host.json("POST", "/api/session", {
15884
16820
  agent: this.agent,
15885
16821
  ...this.model ? { model: this.model } : {},
15886
- permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
16822
+ permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
15887
16823
  });
15888
16824
  if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
15889
16825
  this.sessionId = created.data.id;
15890
16826
  await this.applyInstructions();
15891
16827
  }
16828
+ /** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
16829
+ ordewellRules() {
16830
+ return this.opts.mcp ? [{ action: `${ordewellToolPrefix(this.opts.mcp)}*`, resource: "*", effect: "allow" }] : [];
16831
+ }
15892
16832
  /** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
15893
16833
  async applyInstructions() {
15894
16834
  if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
@@ -16026,8 +16966,8 @@ var OpenCodeV2 = class {
16026
16966
  });
16027
16967
  return true;
16028
16968
  }
16029
- async replyPermission(id, sessionId, reply) {
16030
- await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
16969
+ async replyPermission(id, sessionId, reply2) {
16970
+ await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply2);
16031
16971
  }
16032
16972
  async openStream(state, turn, onEvent, onActivity) {
16033
16973
  const streamAbort = new AbortController();
@@ -16267,6 +17207,11 @@ var OpenCodeV2 = class {
16267
17207
  const request = this.permissionRequest(ask);
16268
17208
  if (!request || state.seen.has(`perm:${request.id}`)) return;
16269
17209
  state.seen.add(`perm:${request.id}`);
17210
+ if (isOrdewellTool(this.opts.mcp, request.name)) {
17211
+ void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
17212
+ });
17213
+ return;
17214
+ }
16270
17215
  onEvent(request);
16271
17216
  void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
16272
17217
  });
@@ -16282,7 +17227,7 @@ var OpenCodeV2 = class {
16282
17227
  const request = this.permissionRequest(ask);
16283
17228
  if (!request || state.seen.has(`perm:${request.id}`)) return;
16284
17229
  state.seen.add(`perm:${request.id}`);
16285
- if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
17230
+ if (isOrdewellTool(this.opts.mcp, request.name) || this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
16286
17231
  onEvent({ ...request, decided: { decision: "allow" } });
16287
17232
  void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
16288
17233
  });
@@ -16301,6 +17246,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
16301
17246
  var RECOVERY_TIMEOUT_MS = 9e5;
16302
17247
  var STATUS_POLL_INTERVAL_MS = 1e3;
16303
17248
  var SERVER_USERNAME = "opencode";
17249
+ var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
17250
+ var MCP_STATUS_POLL_MS2 = 200;
16304
17251
  var DISABLED_TOOLS = {
16305
17252
  question: false,
16306
17253
  edit: false,
@@ -16317,20 +17264,20 @@ function splitModelId2(id) {
16317
17264
  }
16318
17265
  function describeError(err) {
16319
17266
  const visited = /* @__PURE__ */ new Set();
16320
- const lines = [];
17267
+ const lines2 = [];
16321
17268
  let current = err;
16322
17269
  while (current && !visited.has(current)) {
16323
17270
  visited.add(current);
16324
17271
  if (!(current instanceof Error)) {
16325
- lines.push(String(current));
17272
+ lines2.push(String(current));
16326
17273
  break;
16327
17274
  }
16328
17275
  const code = current.code;
16329
17276
  const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
16330
- lines.push(`${current.message}${suffix}`);
17277
+ lines2.push(`${current.message}${suffix}`);
16331
17278
  current = current.cause;
16332
17279
  }
16333
- return lines.join(": ");
17280
+ return lines2.join(": ");
16334
17281
  }
16335
17282
  function permissionName(ask) {
16336
17283
  return ask.permission ?? ask.action ?? "permission";
@@ -16366,6 +17313,12 @@ function unwrapFileToolOutput(output) {
16366
17313
  const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
16367
17314
  return inner ? inner[1] : output;
16368
17315
  }
17316
+ function editDiff2(tool2, state) {
17317
+ const { diff, exists } = state?.metadata ?? {};
17318
+ if (typeof diff === "string" && diff) return hunksOf(diff);
17319
+ const content = state?.input?.content;
17320
+ return tool2 === "write" && exists === false && typeof content === "string" ? markedLines(content, "+") : "";
17321
+ }
16369
17322
  function delay2(ms, signal) {
16370
17323
  return new Promise((resolve7) => {
16371
17324
  const timer = setTimeout(resolve7, ms);
@@ -16418,6 +17371,8 @@ var OpenCodeAdapter = class {
16418
17371
  openPermissions = /* @__PURE__ */ new Map();
16419
17372
  /** Set once the server turns out to speak the 2.x API, which then owns the session. */
16420
17373
  v2 = null;
17374
+ /** The Ordewell server this process was configured with; null when none was given or its config could not be merged. */
17375
+ ordewell = null;
16421
17376
  async start(opts) {
16422
17377
  if (opts.kind === "task") this.task = opts;
16423
17378
  else this.planner = opts;
@@ -16432,9 +17387,19 @@ var OpenCodeAdapter = class {
16432
17387
  throw new ExecutableNotFoundError("opencode", PATH);
16433
17388
  }
16434
17389
  const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
16435
- const password = (0, import_crypto2.randomBytes)(24).toString("base64url");
17390
+ const password = (0, import_crypto5.randomBytes)(24).toString("base64url");
17391
+ const ordewellConfig = opts.mcp ? mergeOrdewellConfig(workspaceEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT, opts.mcp) : null;
17392
+ if (opts.mcp && ordewellConfig === null) {
17393
+ console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
17394
+ }
17395
+ this.ordewell = ordewellConfig === null ? null : opts.mcp ?? null;
16436
17396
  this.process = this.deps.spawn(launch.file, launch.args, {
16437
- env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
17397
+ env: runnerEnv(PATH, {
17398
+ ...workspaceEnv,
17399
+ ...ordewellConfig === null ? {} : { OPENCODE_CONFIG_CONTENT: ordewellConfig },
17400
+ OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
17401
+ OPENCODE_SERVER_PASSWORD: password
17402
+ }),
16438
17403
  stdio: ["pipe", "pipe", "pipe"],
16439
17404
  cwd: opts.cwd,
16440
17405
  windowsVerbatimArguments: launch.verbatim
@@ -16484,7 +17449,7 @@ ${this.stderrTail.trim()}` : ""}`);
16484
17449
  processEnded: this.processEnded,
16485
17450
  isExited: () => this.exited,
16486
17451
  exitMessage: () => this.exitMessage()
16487
- }, opts, this.role());
17452
+ }, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
16488
17453
  try {
16489
17454
  await this.v2.start();
16490
17455
  } catch (err) {
@@ -16537,12 +17502,12 @@ ${this.stderrTail.trim()}` : ""}`);
16537
17502
  ...this.planner?.effort ? { variant: this.planner.effort } : {},
16538
17503
  ...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
16539
17504
  };
16540
- const reply = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
17505
+ const reply2 = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
16541
17506
  if (signal?.aborted) {
16542
17507
  this.dispose();
16543
17508
  return;
16544
17509
  }
16545
- this.settle(reply, turn, onEvent);
17510
+ this.settle(reply2, turn, onEvent);
16546
17511
  } catch (err) {
16547
17512
  if (signal?.aborted) {
16548
17513
  this.dispose();
@@ -16648,13 +17613,13 @@ ${this.stderrTail.trim()}` : ""}`);
16648
17613
  break;
16649
17614
  }
16650
17615
  }
16651
- for (const reply of messages.slice(start)) {
16652
- const id = reply.info?.id;
16653
- if (reply.info?.role !== "assistant" || !id) continue;
16654
- for (const part of reply.parts ?? []) {
17616
+ for (const reply2 of messages.slice(start)) {
17617
+ const id = reply2.info?.id;
17618
+ if (reply2.info?.role !== "assistant" || !id) continue;
17619
+ for (const part of reply2.parts ?? []) {
16655
17620
  if (part.messageID === id) this.emitPart(part, turn, onEvent);
16656
17621
  }
16657
- this.countUsage(reply.info, turn, onEvent);
17622
+ this.countUsage(reply2.info, turn, onEvent);
16658
17623
  }
16659
17624
  }
16660
17625
  /**
@@ -16764,18 +17729,18 @@ ${this.stderrTail.trim()}` : ""}`);
16764
17729
  * their text, reasoning and usage — reach Ordewell over the stream or not at
16765
17730
  * all.
16766
17731
  */
16767
- settle(reply, turn, onEvent) {
16768
- const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
17732
+ settle(reply2, turn, onEvent) {
17733
+ const failure = typeof reply2?.error === "string" ? reply2.error : reply2?.error?.message ?? reply2?.info?.error?.data?.message;
16769
17734
  if (failure) {
16770
17735
  onEvent({ type: "error", message: failure });
16771
17736
  return;
16772
17737
  }
16773
- const assistantId = reply?.info?.id;
16774
- for (const part of reply?.parts ?? []) {
17738
+ const assistantId = reply2?.info?.id;
17739
+ for (const part of reply2?.parts ?? []) {
16775
17740
  if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
16776
17741
  this.emitPart(part, turn, onEvent);
16777
17742
  }
16778
- if (reply?.info) this.countUsage(reply.info, turn, onEvent);
17743
+ if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
16779
17744
  if (assistantId) this.lastAssistantId = assistantId;
16780
17745
  onEvent({ type: "turn_end" });
16781
17746
  }
@@ -16852,7 +17817,7 @@ ${this.stderrTail.trim()}` : ""}`);
16852
17817
  type: "tool_result",
16853
17818
  id: callId,
16854
17819
  name,
16855
- output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
17820
+ output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
16856
17821
  success: status === "completed",
16857
17822
  subagentId
16858
17823
  });
@@ -16986,6 +17951,11 @@ ${this.stderrTail.trim()}` : ""}`);
16986
17951
  const id = ask.id;
16987
17952
  if (!id || seen.has(`perm:${id}`)) return;
16988
17953
  seen.add(`perm:${id}`);
17954
+ if (isOrdewellTool(this.ordewell, permissionName(ask))) {
17955
+ void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "once" }).catch(() => {
17956
+ });
17957
+ return;
17958
+ }
16989
17959
  onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
16990
17960
  void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
16991
17961
  });
@@ -17012,7 +17982,7 @@ ${this.stderrTail.trim()}` : ""}`);
17012
17982
  ...ask.always?.length ? { suggestions: ask.always } : {},
17013
17983
  ...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
17014
17984
  };
17015
- if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
17985
+ if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
17016
17986
  onEvent({ ...request, decided: { decision: "allow" } });
17017
17987
  void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
17018
17988
  });
@@ -17025,10 +17995,10 @@ ${this.stderrTail.trim()}` : ""}`);
17025
17995
  * `POST /permission/:id/reply` is the current answer. The per-session path
17026
17996
  * it replaced is tried only when a server too old to know the new one 404s.
17027
17997
  */
17028
- async replyPermission(id, sessionId, reply) {
17029
- const response = await this.request("POST", `/permission/${id}/reply`, reply);
17998
+ async replyPermission(id, sessionId, reply2) {
17999
+ const response = await this.request("POST", `/permission/${id}/reply`, reply2);
17030
18000
  if (response.status !== 404) return;
17031
- await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply.reply });
18001
+ await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
17032
18002
  }
17033
18003
  /**
17034
18004
  * Open `/event` for one turn and wait until it is connected. The stream
@@ -17107,6 +18077,19 @@ ${this.stderrTail.trim()}` : ""}`);
17107
18077
  nativeSessionId() {
17108
18078
  return this.sessionId;
17109
18079
  }
18080
+ /** Asks the server, which lists every MCP server it has with the state of its connection. */
18081
+ async mcpAttached() {
18082
+ if (!this.ordewell || !this.baseUrl) return false;
18083
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS3;
18084
+ while (Date.now() < deadline && !this.exited) {
18085
+ const servers = await this.json("GET", "/mcp").catch(() => null);
18086
+ const status = servers?.[this.ordewell.name]?.status;
18087
+ if (status === "connected") return true;
18088
+ if (status !== void 0 && status !== "pending") return false;
18089
+ await delay2(MCP_STATUS_POLL_MS2);
18090
+ }
18091
+ return false;
18092
+ }
17110
18093
  dispose() {
17111
18094
  if (this.disposed) return;
17112
18095
  this.disposed = true;
@@ -17129,9 +18112,13 @@ var TASK_MODE_ADAPTERS = {
17129
18112
  codex: (deps) => new CodexAdapter(deps),
17130
18113
  opencode: (deps) => new OpenCodeAdapter(deps)
17131
18114
  };
18115
+ var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
17132
18116
  function supportsTaskMode(runner) {
17133
18117
  return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
17134
18118
  }
18119
+ function takesOrdewellTools(runner) {
18120
+ return ORDEWELL_TOOL_RUNNERS.has(runner);
18121
+ }
17135
18122
  function createTaskAdapter(runner, deps) {
17136
18123
  if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
17137
18124
  return TASK_MODE_ADAPTERS[runner](deps);
@@ -17144,6 +18131,9 @@ function routeTransport(requested, runner, registry) {
17144
18131
  const name = registry?.get(runner)?.manifest.displayName ?? runner;
17145
18132
  return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
17146
18133
  }
18134
+ function givesCompletionTool(requested, runner, registry) {
18135
+ return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
18136
+ }
17147
18137
  var TransportRouter = class {
17148
18138
  constructor(runners) {
17149
18139
  this.runners = runners;
@@ -17282,6 +18272,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
17282
18272
  this.emit("onTaskSettled", { taskId });
17283
18273
  this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
17284
18274
  });
18275
+ this.verifier.onCheckpointWithdrawn((taskId) => {
18276
+ if (!this.atCheckpoint(taskId)) return;
18277
+ this.store.markInProgress(taskId);
18278
+ this.emit("onTaskChanged");
18279
+ });
17285
18280
  this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
17286
18281
  }
17287
18282
  /**
@@ -17368,7 +18363,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
17368
18363
  this.notifications.warn(message);
17369
18364
  };
17370
18365
  if (resolved.blockedEnvrc) {
17371
- warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path14.dirname(resolved.blockedEnvrc)} to use them.`);
18366
+ warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path15.dirname(resolved.blockedEnvrc)} to use them.`);
17372
18367
  }
17373
18368
  if (resolved.trackedEnvFile) {
17374
18369
  warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
@@ -18199,17 +19194,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
18199
19194
  this.abandonSpawn(task, attempt);
18200
19195
  return false;
18201
19196
  }
18202
- const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message, { ops: attempt.ops }) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
19197
+ const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
19198
+ const completionTool = givesCompletionTool(transport, attempt.runner, this.registry);
19199
+ const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message, { ops: attempt.ops, completionTool }) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
18203
19200
  planMapEnabled: this.config.planMapEnabled,
18204
19201
  // A merge to resolve is not new behaviour to drive test-first.
18205
19202
  tddEnabled: !attempt.repair && this.tddEnabled(),
18206
19203
  // An ops task's effects outlive a failed attempt and are never rolled
18207
19204
  // back, so the next one is told what the last one did (ADR-0020).
18208
- previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
19205
+ previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
19206
+ completionTool
18209
19207
  });
18210
19208
  this.lingering.close(task.id);
18211
19209
  const env = await this.envForTask(cwd);
18212
- const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
18213
19210
  const session = await this.terminalRunner.spawn({
18214
19211
  taskId: task.id,
18215
19212
  runner: attempt.runner,
@@ -18224,7 +19221,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
18224
19221
  title: task.title,
18225
19222
  env,
18226
19223
  transport,
18227
- resumeSessionId: attempt.continuation?.resumeSessionId
19224
+ resumeSessionId: attempt.continuation?.resumeSessionId,
19225
+ attempt: attempt.attempt
18228
19226
  });
18229
19227
  if (this.attempts.get(task.id) !== attempt) {
18230
19228
  this.abandonSpawn(task, attempt, session);
@@ -18512,62 +19510,16 @@ var Planner = class {
18512
19510
  ${errors.map((e) => `- ${e}`).join("\n")}`
18513
19511
  );
18514
19512
  }
18515
- });
18516
- }
18517
- };
18518
-
18519
- // src/services/ModelDiscovery.ts
18520
- var path16 = __toESM(require("path"));
18521
- var import_child_process5 = require("child_process");
18522
- var import_util3 = require("util");
18523
- var fsSync3 = __toESM(require("fs"));
18524
- var osMod = __toESM(require("os"));
18525
-
18526
- // src/utils/globalDataDir.ts
18527
- var fs7 = __toESM(require("fs"));
18528
- var path15 = __toESM(require("path"));
18529
- var os5 = __toESM(require("os"));
18530
- function globalDataDir() {
18531
- return path15.join(os5.homedir(), ".ordewell");
18532
- }
18533
- function copyDirSync(src, dest) {
18534
- fs7.mkdirSync(dest, { recursive: true });
18535
- const entries = fs7.readdirSync(src, { withFileTypes: true });
18536
- for (const entry of entries) {
18537
- const srcPath = path15.join(src, entry.name);
18538
- const destPath = path15.join(dest, entry.name);
18539
- if (entry.isDirectory()) {
18540
- copyDirSync(srcPath, destPath);
18541
- } else {
18542
- fs7.copyFileSync(srcPath, destPath);
18543
- }
18544
- }
18545
- }
18546
- var migrated = false;
18547
- function migrateOldConfigDir() {
18548
- if (migrated) return;
18549
- migrated = true;
18550
- const oldDir = path15.join(os5.homedir(), ".config", "ordewell");
18551
- if (!fs7.existsSync(oldDir)) return;
18552
- const newDir = globalDataDir();
18553
- const liftFile = (name) => {
18554
- const oldFile = path15.join(oldDir, name);
18555
- const newFile = path15.join(newDir, name);
18556
- if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
18557
- fs7.mkdirSync(newDir, { recursive: true });
18558
- fs7.copyFileSync(oldFile, newFile);
18559
- }
18560
- };
18561
- liftFile("settings.json");
18562
- liftFile(".env");
18563
- const oldPlugins = path15.join(oldDir, "plugins");
18564
- const newPlugins = path15.join(newDir, "plugins");
18565
- if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
18566
- copyDirSync(oldPlugins, newPlugins);
19513
+ });
18567
19514
  }
18568
- }
19515
+ };
18569
19516
 
18570
19517
  // src/services/ModelDiscovery.ts
19518
+ var path16 = __toESM(require("path"));
19519
+ var import_child_process5 = require("child_process");
19520
+ var import_util3 = require("util");
19521
+ var fsSync3 = __toESM(require("fs"));
19522
+ var osMod = __toESM(require("os"));
18571
19523
  var execAsync3 = (0, import_util3.promisify)(import_child_process5.exec);
18572
19524
  var defaultExec2 = async (command, options) => {
18573
19525
  const PATH = await augmentedPath();
@@ -18943,8 +19895,8 @@ function parseClaudeHelp(stdout) {
18943
19895
  return models;
18944
19896
  }
18945
19897
  function parseOpencodeModels(stdout, manifest) {
18946
- const lines = stdout.split("\n").map((l) => l.trim()).filter((l) => /^[A-Za-z0-9._-]+\/[^\s"{}]+$/.test(l));
18947
- if (lines.length === 0) return [];
19898
+ const lines2 = stdout.split("\n").map((l) => l.trim()).filter((l) => /^[A-Za-z0-9._-]+\/[^\s"{}]+$/.test(l));
19899
+ if (lines2.length === 0) return [];
18948
19900
  const preferred = manifest.modelDiscovery.preferredPatterns;
18949
19901
  const prefLabels = /* @__PURE__ */ new Map();
18950
19902
  if (preferred) {
@@ -18954,14 +19906,14 @@ function parseOpencodeModels(stdout, manifest) {
18954
19906
  }
18955
19907
  const seen = /* @__PURE__ */ new Set();
18956
19908
  const models = [];
18957
- for (const id of lines) {
19909
+ for (const id of lines2) {
18958
19910
  if (prefLabels.has(id) && !seen.has(id)) {
18959
19911
  seen.add(id);
18960
19912
  const provider = id.split("/")[0];
18961
19913
  models.push({ modelId: id, modelLabel: prefLabels.get(id), runnerProvider: provider, variants: [] });
18962
19914
  }
18963
19915
  }
18964
- for (const id of lines) {
19916
+ for (const id of lines2) {
18965
19917
  if (!seen.has(id)) {
18966
19918
  seen.add(id);
18967
19919
  const provider = id.split("/")[0];
@@ -19774,6 +20726,7 @@ var CliAgentAiService = class {
19774
20726
  };
19775
20727
  this.makeAdapter = deps.createAdapter ?? defaultAdapter;
19776
20728
  this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
20729
+ this.mcpServer = deps.mcpServer;
19777
20730
  }
19778
20731
  config;
19779
20732
  runner;
@@ -19788,6 +20741,11 @@ var CliAgentAiService = class {
19788
20741
  */
19789
20742
  lastNativeSessionId = null;
19790
20743
  conversation = null;
20744
+ mcpServer;
20745
+ /** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
20746
+ plannerToken = null;
20747
+ /** Whether the running planner was spawned with its tools and the CLI reported them connected. */
20748
+ toolsAttached = false;
19791
20749
  activeAbort = null;
19792
20750
  /**
19793
20751
  * Every subagent this conversation has reported starting, and finishing.
@@ -19810,6 +20768,10 @@ var CliAgentAiService = class {
19810
20768
  if (!this.conversation) return true;
19811
20769
  return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
19812
20770
  }
20771
+ /** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
20772
+ plannerToolsAttached() {
20773
+ return this.toolsAttached;
20774
+ }
19813
20775
  /** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
19814
20776
  nativeSessionId() {
19815
20777
  return this.adapter?.nativeSessionId() ?? null;
@@ -19817,8 +20779,8 @@ var CliAgentAiService = class {
19817
20779
  reset() {
19818
20780
  this.activeAbort?.abort();
19819
20781
  this.activeAbort = null;
19820
- this.adapter?.dispose();
19821
- this.adapter = null;
20782
+ this.disposeAdapter();
20783
+ this.toolsAttached = false;
19822
20784
  this.lastNativeSessionId = null;
19823
20785
  this.conversation = null;
19824
20786
  this.subagents.started.clear();
@@ -19845,9 +20807,23 @@ var CliAgentAiService = class {
19845
20807
  model: this.plannerModel(),
19846
20808
  effort: this.config.plannerThinkingEffort
19847
20809
  };
19848
- await this.startAdapter(startOptions);
20810
+ const tools = req.plannerTools && this.mcpServer ? {
20811
+ offer: req.plannerTools,
20812
+ systemPrompt: buildConversationSystemPrompt(
20813
+ req.goal,
20814
+ contextStr,
20815
+ req.modelsByRunner,
20816
+ req.runners,
20817
+ req.runnerModes,
20818
+ req.autonomousDefault ?? true,
20819
+ req.verificationEnabled ?? false,
20820
+ { harness: true, isolatedExecution: req.isolatedExecution, plannerTools: true }
20821
+ )
20822
+ } : void 0;
20823
+ await this.startAdapter(startOptions, tools);
19849
20824
  this.conversation = {
19850
20825
  startOptions,
20826
+ tools,
19851
20827
  runners: req.runners,
19852
20828
  runnerModes: req.runnerModes,
19853
20829
  autonomousDefault: req.autonomousDefault
@@ -20052,10 +21028,7 @@ var CliAgentAiService = class {
20052
21028
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
20053
21029
  }
20054
21030
  this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
20055
- if (error || signal?.aborted) {
20056
- adapter.dispose();
20057
- this.adapter = null;
20058
- }
21031
+ if (error || signal?.aborted) this.disposeAdapter();
20059
21032
  return { text: text2, researchLog, error, aborted: signal?.aborted, backgroundAgents };
20060
21033
  }
20061
21034
  // --- Process lifecycle ---
@@ -20064,13 +21037,50 @@ var CliAgentAiService = class {
20064
21037
  * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
20065
21038
  * mode from here without changing this signature.
20066
21039
  */
20067
- async startAdapter(opts) {
21040
+ async startAdapter(opts, tools) {
20068
21041
  const adapter = this.makeAdapter(this.runner, this.processDeps);
20069
21042
  if (!adapter) throw new Error(`No planner adapter is available for "${this.runner}".`);
21043
+ this.toolsAttached = false;
21044
+ if (tools && this.mcpServer && adapter.mcpAttached) {
21045
+ if (await this.startWithTools(adapter, opts, tools, this.mcpServer)) return adapter;
21046
+ return this.startAdapter(opts);
21047
+ }
20070
21048
  await adapter.start(opts);
20071
21049
  this.adapter = adapter;
20072
21050
  return adapter;
20073
21051
  }
21052
+ /** Spawn with the Ordewell server injected. False, with nothing left running, when it did not connect. */
21053
+ async startWithTools(adapter, opts, tools, server) {
21054
+ let credential;
21055
+ try {
21056
+ credential = await server.issuePlannerToken({ sessionId: tools.offer.sessionId }, tools.offer.handler);
21057
+ } catch {
21058
+ return false;
21059
+ }
21060
+ try {
21061
+ await adapter.start({ ...opts, systemPrompt: tools.systemPrompt, mcp: mcpClientConfig(credential) });
21062
+ if (await adapter.mcpAttached()) {
21063
+ this.adapter = adapter;
21064
+ this.plannerToken = credential.token;
21065
+ this.toolsAttached = true;
21066
+ return true;
21067
+ }
21068
+ } catch (err) {
21069
+ adapter.dispose();
21070
+ server.revoke(credential.token);
21071
+ throw err;
21072
+ }
21073
+ adapter.dispose();
21074
+ server.revoke(credential.token);
21075
+ return false;
21076
+ }
21077
+ /** Kill the planner process, and with it the token it was spawned with. */
21078
+ disposeAdapter() {
21079
+ this.adapter?.dispose();
21080
+ this.adapter = null;
21081
+ if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
21082
+ this.plannerToken = null;
21083
+ }
20074
21084
  /**
20075
21085
  * The live agent process, restarted from its own session id if it died
20076
21086
  * between turns. Resume is a hint: when it fails, the caller's next
@@ -20081,7 +21091,7 @@ var CliAgentAiService = class {
20081
21091
  if (this.adapter) return this.adapter;
20082
21092
  const conversation = this.conversation;
20083
21093
  if (!conversation) throw new Error("No active planner conversation.");
20084
- await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
21094
+ await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
20085
21095
  return this.adapter;
20086
21096
  }
20087
21097
  plannerModel() {
@@ -20100,7 +21110,9 @@ var CliAgentAiService = class {
20100
21110
  const previous = this.adapter;
20101
21111
  const previousConversation = this.conversation;
20102
21112
  const previousSessionId = this.lastNativeSessionId;
21113
+ const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
20103
21114
  this.adapter = null;
21115
+ this.plannerToken = null;
20104
21116
  const startOptions = {
20105
21117
  kind: "planner",
20106
21118
  cwd: this.workspaceRoot(),
@@ -20130,6 +21142,8 @@ var CliAgentAiService = class {
20130
21142
  } finally {
20131
21143
  oneShotAdapter?.dispose();
20132
21144
  this.adapter = previous;
21145
+ this.plannerToken = previousTools.token;
21146
+ this.toolsAttached = previousTools.attached;
20133
21147
  this.conversation = previousConversation;
20134
21148
  this.lastNativeSessionId = previousSessionId;
20135
21149
  }
@@ -20225,6 +21239,173 @@ function createAiService(config, deps) {
20225
21239
  return new OpenAiService(config);
20226
21240
  }
20227
21241
 
21242
+ // src/services/plannerTools.ts
21243
+ function runnersOf(tasks) {
21244
+ return [...new Set(flattenTasks(tasks).filter((t) => t.type !== "user").map((t) => t.assignedRunner))];
21245
+ }
21246
+ function plannerToolHandler(host) {
21247
+ return {
21248
+ async listRunners() {
21249
+ const catalog = await host.liveCatalog();
21250
+ return answer({
21251
+ autonomy: autonomyLevelLabel(catalog.autonomousDefault),
21252
+ runners: catalog.runners.map((id) => {
21253
+ const modes = catalog.modes[id] ?? [];
21254
+ const offered = [...filteredBuildModes(modes, catalog.autonomousDefault), ...modes.filter((m) => m.id === "plan")];
21255
+ return {
21256
+ id,
21257
+ defaultMode: resolveDefaultMode(modes, catalog.autonomousDefault) ?? null,
21258
+ modes: offered.map(({ id: modeId2, label, description }) => ({ id: modeId2, label, description }))
21259
+ };
21260
+ })
21261
+ });
21262
+ },
21263
+ async listModels({ runner }) {
21264
+ const catalog = await host.liveCatalog();
21265
+ if (!catalog.runners.includes(runner)) return notEnabled(runner, catalog.runners);
21266
+ return answer({
21267
+ runner,
21268
+ models: (catalog.models[runner] ?? []).map((m) => ({
21269
+ modelId: m.modelId,
21270
+ modelLabel: m.modelLabel,
21271
+ variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
21272
+ }))
21273
+ });
21274
+ },
21275
+ async submitPlan({ tasks }) {
21276
+ const catalog = await host.liveCatalog();
21277
+ const result = validatePlanTasks({ tasks }, catalog.runners, catalog.modes, catalog.autonomousDefault);
21278
+ if (!result.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: result.errors, enabledRunners: catalog.runners }) };
21279
+ const runners = runnersOf(result.tasks);
21280
+ const coerced = coercions(result.tasks, host.coerce(result.tasks, runners));
21281
+ if (!host.submitPlan(result.tasks, runners)) {
21282
+ return { isError: true, text: JSON.stringify({ ok: false, errors: [{ field: "tasks", message: "No planning turn is open to take the plan." }] }) };
21283
+ }
21284
+ return answer({
21285
+ ok: true,
21286
+ tasks: result.tasks.length,
21287
+ coerced,
21288
+ next: "The plan is committed when this reply ends. Tell the user briefly what you submitted; do not repeat the plan as JSON."
21289
+ });
21290
+ },
21291
+ async taskQuery({ tasks, fields, catalog }) {
21292
+ const query = { tasks: (tasks ?? []).map(String), fields: fields ?? [...TASK_QUERY_TASK_FIELDS], catalog: catalog === true };
21293
+ return read(host, taskQuerySignature(query), async () => ({
21294
+ tasks: query.tasks.map((ref) => taskDetail(host.tasks(), ref, query.fields ?? [])),
21295
+ ...query.catalog ? { catalog: catalogDetail(await host.liveCatalog()) } : {}
21296
+ }));
21297
+ },
21298
+ async taskOutput({ task: ref, lines: lines2, since }) {
21299
+ const task = findTask(host.tasks(), String(ref));
21300
+ if (!task) return { isError: true, text: JSON.stringify({ ok: false, error: `No task matches "${ref}" in the current plan.` }) };
21301
+ const maxLines = Math.min(lines2 ?? OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX);
21302
+ return read(host, JSON.stringify(["output", task.id, maxLines, since ?? null]), async () => {
21303
+ const head = { task: { id: task.id, order: task.order, title: task.title, status: task.status } };
21304
+ const tail = host.liveOutput(task.id, { maxLines, sinceOffset: since });
21305
+ if (tail?.running) {
21306
+ const fitted = fitTail(tail.text, OUTPUT_TAIL_MAX_CHARS);
21307
+ return { ...head, running: true, output: fitted.text, nextOffset: tail.nextOffset, ...fitted.trimmed ? { trimmedToFit: true } : {} };
21308
+ }
21309
+ return {
21310
+ ...head,
21311
+ running: false,
21312
+ reason: "This task is not running, so there is no live output: its outcome is below.",
21313
+ verdict: verdictDetail(task.verdict),
21314
+ outputSummary: outputSummaryDetail(task),
21315
+ lastAttempt: host.lastAttempt(task.id)
21316
+ };
21317
+ });
21318
+ },
21319
+ async editPlan({ ops }) {
21320
+ const outcome = await host.editPlan(ops);
21321
+ if (!outcome.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: outcome.errors.map(opError) }) };
21322
+ return answer({
21323
+ ok: true,
21324
+ summary: outcome.summary,
21325
+ queued: outcome.queued,
21326
+ next: outcome.queued ? "A task you named is running, so the edit is parked and nothing was checked: it is applied between task batches, when the user's message comes back to you. Tell the user it is queued." : "The edit is applied when this reply ends, and the user is shown what changed. Tell them briefly; do not repeat it as JSON."
21327
+ });
21328
+ }
21329
+ };
21330
+ }
21331
+ function opError(message) {
21332
+ const match = /^op (\d+) \((\w+)\): ([\s\S]*)$/.exec(message);
21333
+ return match ? { op: Number(match[1]), kind: match[2], message: match[3] } : { message };
21334
+ }
21335
+ async function read(host, signature, answerRead) {
21336
+ const result = await host.read(signature, answerRead);
21337
+ if (result.status === "refused") {
21338
+ return { isError: true, text: JSON.stringify({ ok: false, error: "You have used every read this message allows. Nothing was read: answer the user in prose, or call edit_plan for the change you came to make." }) };
21339
+ }
21340
+ return answer({ ...result.value, ...result.landNow ? { note: TASK_READ_ANSWER_OR_EDIT } : {} });
21341
+ }
21342
+ function verdictDetail(verdict) {
21343
+ if (!verdict) return null;
21344
+ return {
21345
+ outcome: verdict.outcome,
21346
+ reason: verdict.reason,
21347
+ checks: verdict.checks.map((c) => ({ name: c.name, result: c.skipped ? "skipped" : c.passed ? "pass" : "fail" }))
21348
+ };
21349
+ }
21350
+ function outputSummaryDetail({ outputSummary }) {
21351
+ return outputSummary ? { reviewReason: outputSummary.reviewReason, ...outputSummary.logTail ? { logTail: outputSummary.logTail } : {} } : null;
21352
+ }
21353
+ function taskDetail(plan, ref, fields) {
21354
+ const task = findTask(plan, ref);
21355
+ if (!task) return { ref, error: "no task matches this reference in the current plan." };
21356
+ const wants = (field) => fields.includes(field);
21357
+ return {
21358
+ id: task.id,
21359
+ order: task.order,
21360
+ title: task.title,
21361
+ status: task.status,
21362
+ type: task.type === "user" ? "user" : "ai",
21363
+ ...wants("description") ? { description: task.description || null } : {},
21364
+ ...wants("prompt") ? { prompt: task.prompt || null } : {},
21365
+ ...wants("userSteps") ? { userSteps: task.userSteps?.length ? task.userSteps.map((s) => ({ order: s.order, instruction: s.instruction, completed: s.completed === true })) : null } : {},
21366
+ ...wants("verdict") ? { verdict: verdictDetail(task.verdict) } : {},
21367
+ ...wants("outputSummary") ? { outputSummary: outputSummaryDetail(task) } : {},
21368
+ ...wants("userStoriesCovered") ? { userStoriesCovered: task.userStoriesCovered?.length ? task.userStoriesCovered : null } : {}
21369
+ };
21370
+ }
21371
+ function catalogDetail(catalog) {
21372
+ return {
21373
+ runners: catalog.runners.map((id) => {
21374
+ const modes = catalog.modes[id] ?? [];
21375
+ const defaultMode = resolveDefaultMode(modes, catalog.autonomousDefault);
21376
+ return {
21377
+ id,
21378
+ models: (catalog.models[id] ?? []).map((m) => ({
21379
+ modelId: m.modelId,
21380
+ modelLabel: m.modelLabel,
21381
+ variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
21382
+ })),
21383
+ modes: modes.map((m) => ({ id: m.id, label: m.label, description: m.description, default: m.id === defaultMode }))
21384
+ };
21385
+ })
21386
+ };
21387
+ }
21388
+ function answer(body) {
21389
+ return { text: JSON.stringify(body) };
21390
+ }
21391
+ function notEnabled(runner, enabled) {
21392
+ return { isError: true, text: `Runner "${runner}" is not enabled. Enabled runners: ${enabled.join(", ") || "(none)"}.` };
21393
+ }
21394
+ function coercions(submitted, landed) {
21395
+ return submitted.flatMap((task, i) => {
21396
+ const after = landed[i];
21397
+ const out = [];
21398
+ if (after.assignedRunner !== task.assignedRunner) {
21399
+ out.push({ taskId: task.id, field: "assignedRunner", from: task.assignedRunner, to: after.assignedRunner });
21400
+ }
21401
+ const [fromModel, toModel] = [task.assignedModel?.modelId ?? null, after.assignedModel?.modelId ?? null];
21402
+ if (fromModel !== toModel) out.push({ taskId: task.id, field: "assignedModel", from: fromModel, to: toModel });
21403
+ const [fromEffort, toEffort] = [task.assignedModel?.thinkingEffort ?? null, after.assignedModel?.thinkingEffort ?? null];
21404
+ if (fromEffort !== toEffort) out.push({ taskId: task.id, field: "thinkingEffort", from: fromEffort, to: toEffort });
21405
+ return out;
21406
+ });
21407
+ }
21408
+
20228
21409
  // src/services/PlannerConversation.ts
20229
21410
  var import_uuid7 = require("uuid");
20230
21411
 
@@ -20241,8 +21422,8 @@ function summaryRequest(planLines) {
20241
21422
  ...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
20242
21423
  ].join("\n");
20243
21424
  }
20244
- function extractSummary(reply) {
20245
- const matches = [...reply.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
21425
+ function extractSummary(reply2) {
21426
+ const matches = [...reply2.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
20246
21427
  const summary = matches.at(-1)?.[1].trim();
20247
21428
  return summary || null;
20248
21429
  }
@@ -20261,6 +21442,7 @@ function keptTail(transcript) {
20261
21442
 
20262
21443
  // src/services/PlannerConversation.ts
20263
21444
  var CATALOG_MODEL_CAP = 100;
21445
+ var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
20264
21446
  var MAX_TASK_QUERIES = 3;
20265
21447
  var MAX_TASK_QUERIES_HARD = 6;
20266
21448
  function freshReadBudget() {
@@ -20290,7 +21472,7 @@ var PlannerConversation = class {
20290
21472
  savedInBackground = 0;
20291
21473
  turnsInFlight = 0;
20292
21474
  compacting = false;
20293
- openTurnId = null;
21475
+ openTurn = null;
20294
21476
  get transcript() {
20295
21477
  return this.host.plan()?.conversationHistory ?? [];
20296
21478
  }
@@ -20300,7 +21482,48 @@ var PlannerConversation = class {
20300
21482
  }
20301
21483
  /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
20302
21484
  get currentTurnId() {
20303
- return this.openTurnId ?? void 0;
21485
+ return this.openTurn?.stream.turnId;
21486
+ }
21487
+ /**
21488
+ * Hand the open turn a plan (already validated) or a task edit. The turn
21489
+ * settles with it exactly as with the same plan or ops parsed out of its
21490
+ * reply, and it wins over any envelope that reply carries. A later
21491
+ * submission replaces an earlier one. False when no user turn is open.
21492
+ */
21493
+ submit(submission) {
21494
+ if (!this.openTurn) return false;
21495
+ this.openTurn.submitted = submission;
21496
+ return true;
21497
+ }
21498
+ /**
21499
+ * The edit the open turn already holds, for a further edit in the same reply
21500
+ * to join rather than replace. Empty when it holds nothing; null when it holds
21501
+ * a whole plan, which an edit cannot be layered on.
21502
+ */
21503
+ pendingOps() {
21504
+ const held = this.openTurn?.submitted;
21505
+ if (!held) return [];
21506
+ return held.kind === "task_ops" ? held.ops : null;
21507
+ }
21508
+ /** Whether the turn would park this edit until a batch boundary instead of applying it as it settles. */
21509
+ editWouldQueue(ops) {
21510
+ return this.editTouchesLiveWork({ kind: "task_ops", ops, text: "", researchLog: [] });
21511
+ }
21512
+ /**
21513
+ * A read the planner makes through a tool. It spends the same per-turn budget
21514
+ * as an envelope read, so a planner cannot read more by switching channels:
21515
+ * past the soft limit, or on a repeated question, the answer still comes but
21516
+ * is told to land the turn; at the hard limit it is refused. `signature`
21517
+ * names the question, for the repeat check.
21518
+ */
21519
+ async read(signature, answer2) {
21520
+ const reads = this.openTurn?.reads;
21521
+ if (!reads) return { status: "answered", value: await answer2(), landNow: false };
21522
+ if (reads.answered >= MAX_TASK_QUERIES_HARD) return { status: "refused" };
21523
+ const landNow = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
21524
+ reads.seen.add(signature);
21525
+ reads.answered++;
21526
+ return { status: "answered", value: await answer2(), landNow };
20304
21527
  }
20305
21528
  /** Whether the model still holds this conversation in memory. */
20306
21529
  get isActive() {
@@ -20588,15 +21811,16 @@ ${message}` : message;
20588
21811
  async userTurn(prompt, signal, run) {
20589
21812
  const turnId = (0, import_uuid7.v4)();
20590
21813
  const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
20591
- this.openTurnId = turnId;
21814
+ const turn = { stream, signal, reads: freshReadBudget() };
21815
+ this.openTurn = turn;
20592
21816
  this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
20593
21817
  let outcome = "error";
20594
21818
  try {
20595
- const settled = await this.inTurn(() => run({ stream, signal, reads: freshReadBudget() }));
21819
+ const settled = await this.inTurn(() => run(turn));
20596
21820
  outcome = settled.outcome;
20597
21821
  return settled.plan;
20598
21822
  } finally {
20599
- if (this.openTurnId === turnId) this.openTurnId = null;
21823
+ if (this.openTurn === turn) this.openTurn = null;
20600
21824
  this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
20601
21825
  }
20602
21826
  }
@@ -20647,10 +21871,11 @@ ${message}` : message;
20647
21871
  * ops JSON still gets its two corrective retries; charging it for the read
20648
21872
  * would cost it the chance to fix the edit.
20649
21873
  */
20650
- async drainTaskQueries(turn, { reads, signal, stream }) {
21874
+ async drainTaskQueries(turn, userTurn) {
21875
+ const { reads, signal, stream } = userTurn;
20651
21876
  const ai = this.host.aiService();
20652
21877
  const carried = [];
20653
- let current = turn;
21878
+ let current = this.claimSubmission(turn, userTurn);
20654
21879
  while (current.kind === "task_query") {
20655
21880
  carried.push(...current.researchLog);
20656
21881
  if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
@@ -20664,17 +21889,30 @@ ${message}` : message;
20664
21889
  const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
20665
21890
  reads.seen.add(signature);
20666
21891
  reads.answered++;
20667
- const answer = this.taskQueryAnswer(current.query);
20668
- current = await ai.continueConversation(
20669
- insist ? `${answer}
21892
+ const answer2 = this.taskQueryAnswer(current.query);
21893
+ current = this.claimSubmission(await ai.continueConversation(
21894
+ insist ? `${answer2}
20670
21895
 
20671
- ${TASK_QUERY_ANSWER_OR_OPS}` : answer,
21896
+ ${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
20672
21897
  stream.sink(),
20673
21898
  signal
20674
- );
21899
+ ), userTurn);
20675
21900
  }
20676
21901
  return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
20677
21902
  }
21903
+ /**
21904
+ * The backend's answer, with what was handed in while it ran standing in
21905
+ * for whatever its reply carried. Taken once: a corrective re-send after a
21906
+ * rejected edit must not settle on the same submission again. A stopped
21907
+ * turn commits nothing, the same as a stopped reply with a plan in it.
21908
+ */
21909
+ claimSubmission(turn, userTurn) {
21910
+ const submitted = userTurn.submitted;
21911
+ userTurn.submitted = void 0;
21912
+ if (!submitted || userTurn.signal?.aborted) return turn;
21913
+ const { text: text2, researchLog } = turn;
21914
+ return submitted.kind === "plan" ? { kind: "plan", tasks: submitted.tasks, text: text2, researchLog } : { kind: "task_ops", ops: submitted.ops, text: text2, researchLog };
21915
+ }
20678
21916
  /**
20679
21917
  * Render one read out of live state. Never persisted to the transcript: the
20680
21918
  * detail is context for the planner's next reply, and re-sending it on every
@@ -20718,7 +21956,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20718
21956
  return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
20719
21957
  },
20720
21958
  maxRepairs: 2,
20721
- onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
21959
+ onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
20722
21960
  });
20723
21961
  if ("plan" in settled) {
20724
21962
  await this.host.afterEdit();
@@ -20781,10 +22019,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20781
22019
  *
20782
22020
  * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
20783
22021
  * hard bound on every turn, and reads the plan's runners live, so a runner
20784
- * admitted mid-session by a retarget is shown like every other.
22022
+ * admitted mid-session by a retarget is shown like every other. A planner
22023
+ * with Ordewell's tools gets a reminder to read the catalog instead.
20785
22024
  */
20786
22025
  catalogBlock() {
20787
22026
  if (!this.host.plan()) return null;
22027
+ if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
20788
22028
  const { runners, models, modes, autonomousDefault } = this.host.catalog();
20789
22029
  const modelLines = runners.map((runner) => {
20790
22030
  const list = models[runner] ?? [];
@@ -20817,19 +22057,20 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20817
22057
  */
20818
22058
  planContextBlock() {
20819
22059
  const tasks = this.host.tasks();
20820
- const lines = this.currentPlanLines();
20821
- if (!lines) return null;
22060
+ const lines2 = this.currentPlanLines();
22061
+ if (!lines2) return null;
22062
+ const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
20822
22063
  const locked = tasks.filter((t) => t.status === "in_progress");
20823
22064
  const execNote = this.host.hasLiveWork() ? `
20824
22065
  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.` : "";
20825
22066
  return [
20826
22067
  "<current_plan>",
20827
- ...lines,
22068
+ ...lines2,
20828
22069
  "</current_plan>",
20829
22070
  "The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
20830
22071
  "- To answer a question or discuss, reply in plain prose (no JSON).",
20831
- TASK_QUERY_REMINDER,
20832
- ...taskOpsProtocol(execNote)
22072
+ tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
22073
+ ...taskOpsProtocol(execNote, tools)
20833
22074
  ].filter(Boolean).join("\n");
20834
22075
  }
20835
22076
  /** One line per task with stable references — null until the plan has tasks. */
@@ -21105,6 +22346,7 @@ function retargetTaskRunner(task, runner, catalog) {
21105
22346
  }
21106
22347
 
21107
22348
  // src/services/SkillsService.ts
22349
+ var import_crypto6 = require("crypto");
21108
22350
  var fs9 = __toESM(require("fs"));
21109
22351
  var path19 = __toESM(require("path"));
21110
22352
 
@@ -21122,6 +22364,11 @@ function builtinSkillsDir() {
21122
22364
  // src/services/SkillsService.ts
21123
22365
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
21124
22366
  var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
22367
+ var PRIOR_BUILTIN_HASHES = {
22368
+ grilling: ["d5d4cb8589fbb4a0f3296bae15033f2297ef682aaf5615a3dee258451f96e5c3"],
22369
+ "improve-codebase-architecture": ["b5154b5e3baca9a5f244634453e83706d742011c31163648c2b8395a2524dd9c"]
22370
+ };
22371
+ var SEED_MANIFEST = ".seeded.json";
21125
22372
  function parseSkillFile(filePath, name, source) {
21126
22373
  const raw = fs9.readFileSync(filePath, "utf8");
21127
22374
  const frontmatter = {};
@@ -21161,6 +22408,13 @@ function copyDirSync2(src, dest) {
21161
22408
  else fs9.copyFileSync(srcPath, destPath);
21162
22409
  }
21163
22410
  }
22411
+ function fileHash(file) {
22412
+ try {
22413
+ return (0, import_crypto6.createHash)("sha256").update(fs9.readFileSync(file)).digest("hex");
22414
+ } catch {
22415
+ return void 0;
22416
+ }
22417
+ }
21164
22418
  function readDir(dir, source) {
21165
22419
  if (!fs9.existsSync(dir)) return [];
21166
22420
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
@@ -21185,21 +22439,59 @@ var SkillsService = class {
21185
22439
  globalDir() {
21186
22440
  return path19.join(globalDataDir(), "skills");
21187
22441
  }
22442
+ readSeedManifest() {
22443
+ try {
22444
+ const parsed = JSON.parse(fs9.readFileSync(path19.join(this.globalDir(), SEED_MANIFEST), "utf8"));
22445
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
22446
+ return Object.fromEntries(
22447
+ Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
22448
+ );
22449
+ }
22450
+ } catch {
22451
+ }
22452
+ return {};
22453
+ }
22454
+ recordSeed(name, hash) {
22455
+ const manifest = this.readSeedManifest();
22456
+ if (manifest[name] === hash) return;
22457
+ manifest[name] = hash;
22458
+ try {
22459
+ fs9.mkdirSync(this.globalDir(), { recursive: true });
22460
+ fs9.writeFileSync(path19.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
22461
+ } catch (err) {
22462
+ console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
22463
+ }
22464
+ }
22465
+ /** A seed the user never edited: safe to replace with a newer built-in. */
22466
+ isUntouchedSeed(name, currentHash) {
22467
+ if (currentHash === void 0) return false;
22468
+ return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
22469
+ }
21188
22470
  seed(name) {
21189
22471
  const dest = path19.join(this.globalDir(), name);
21190
- if (fs9.existsSync(dest)) return false;
22472
+ const exists = fs9.existsSync(dest);
21191
22473
  let srcDir;
21192
22474
  try {
21193
22475
  srcDir = path19.join(builtinSkillsDir(), name);
21194
22476
  } catch (err) {
21195
- console.warn(`Could not locate built-in skills dir: ${String(err)}`);
22477
+ if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
21196
22478
  return false;
21197
22479
  }
21198
22480
  if (!fs9.existsSync(srcDir)) {
21199
- console.warn(`Built-in skill not found in package: ${name}`);
22481
+ if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
21200
22482
  return false;
21201
22483
  }
22484
+ const srcHash = fileHash(path19.join(srcDir, "SKILL.md"));
22485
+ if (exists) {
22486
+ const currentHash = fileHash(path19.join(dest, "SKILL.md"));
22487
+ if (currentHash !== void 0 && currentHash === srcHash) {
22488
+ this.recordSeed(name, currentHash);
22489
+ return false;
22490
+ }
22491
+ if (!this.isUntouchedSeed(name, currentHash)) return false;
22492
+ }
21202
22493
  copyDirSync2(srcDir, dest);
22494
+ if (srcHash) this.recordSeed(name, srcHash);
21203
22495
  return true;
21204
22496
  }
21205
22497
  seedBuiltinSkill(name) {
@@ -21555,9 +22847,9 @@ var fs10 = __toESM(require("fs"));
21555
22847
  var path20 = __toESM(require("path"));
21556
22848
 
21557
22849
  // src/utils/sessionId.ts
21558
- var import_crypto3 = require("crypto");
22850
+ var import_crypto7 = require("crypto");
21559
22851
  function mintSessionId() {
21560
- return `session-${(0, import_crypto3.randomBytes)(16).toString("hex")}`;
22852
+ return `session-${(0, import_crypto7.randomBytes)(16).toString("hex")}`;
21561
22853
  }
21562
22854
 
21563
22855
  // src/utils/sessionStore.ts
@@ -21894,7 +23186,12 @@ function lastAttemptDigest(location, taskId) {
21894
23186
  return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
21895
23187
  }
21896
23188
  function sessionRuntimeSettings(settings) {
21897
- return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
23189
+ return {
23190
+ ...plannerRuntimeToggles(settings),
23191
+ modelAllowlist: settings.modelAllowlist,
23192
+ runnerTransport: settings.runnerTransport,
23193
+ enabledRunners: settings.enabledRunners
23194
+ };
21898
23195
  }
21899
23196
  var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
21900
23197
  function resolveSkillInvocation(text2, skillsService) {
@@ -21918,14 +23215,14 @@ function resolveSkillInvocation(text2, skillsService) {
21918
23215
  return `${lead}${skill.content}${punct}`;
21919
23216
  });
21920
23217
  }
21921
- function liveAiService(config, workspaceRoot) {
23218
+ function liveAiService(config, workspaceRoot, mcpServer) {
21922
23219
  let live = null;
21923
23220
  let liveProvider = null;
21924
23221
  return () => {
21925
23222
  const provider = config.aiProvider;
21926
23223
  if (live && liveProvider === provider) return live;
21927
23224
  live?.reset();
21928
- live = createAiService(config, { workspaceRoot });
23225
+ live = createAiService(config, { workspaceRoot, mcpServer });
21929
23226
  liveProvider = provider;
21930
23227
  return live;
21931
23228
  };
@@ -21935,7 +23232,7 @@ function isPlannerApproval(request) {
21935
23232
  }
21936
23233
  function createSession(deps) {
21937
23234
  const pinnedAiService = deps.aiService;
21938
- const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
23235
+ const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
21939
23236
  const store = new PlanStore();
21940
23237
  const taskLogs = new TaskLogRecorder({
21941
23238
  broadcast: deps.broadcast,
@@ -22065,6 +23362,16 @@ var Session = class {
22065
23362
  currentSessionId;
22066
23363
  skillsService;
22067
23364
  conversation;
23365
+ plannerTools = plannerToolHandler({
23366
+ liveCatalog: () => this.liveCatalog(),
23367
+ coerce: (tasks, runners) => coerceAssignments(tasks, this.allowlist(), runners, this.models()),
23368
+ submitPlan: (tasks, runners) => this.submitPlanFromTool(tasks, runners),
23369
+ editPlan: (ops) => this.editPlanFromTool(ops),
23370
+ tasks: () => this.store.planTasks,
23371
+ read: (signature, answer2) => this.conversation.read(signature, answer2),
23372
+ liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
23373
+ lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId)
23374
+ });
22068
23375
  constructor(parts) {
22069
23376
  this.config = parts.config;
22070
23377
  this.registry = parts.registry;
@@ -22093,17 +23400,7 @@ var Session = class {
22093
23400
  aiService: () => this.aiService(),
22094
23401
  onProgress: (p) => this.events.progress(p),
22095
23402
  opening: (runners) => this.conversationOpening(runners),
22096
- catalog: () => {
22097
- const runners = this.plan?.runners ?? [];
22098
- return {
22099
- runners,
22100
- // Allowlist-filtered: neither the per-turn block nor a read may offer
22101
- // a model the planner is forbidden to assign.
22102
- models: filterModelsForPrompt(this.models(), this.allowlist()),
22103
- modes: this.runnerModesFor(runners),
22104
- autonomousDefault: this.config.autonomousMode
22105
- };
22106
- },
23403
+ catalog: () => this.catalogOf(this.plan?.runners ?? []),
22107
23404
  tasks: () => this.store.planTasks,
22108
23405
  liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
22109
23406
  hasLiveWork: () => this.hasLiveWork,
@@ -22127,8 +23424,8 @@ var Session = class {
22127
23424
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
22128
23425
  * path is identical regardless of who is looking.
22129
23426
  */
22130
- resolveApproval(id, answer) {
22131
- return this.approvals.resolve(id, answer);
23427
+ resolveApproval(id, answer2) {
23428
+ return this.approvals.resolve(id, answer2);
22132
23429
  }
22133
23430
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
22134
23431
  outstandingApprovals() {
@@ -22248,6 +23545,27 @@ var Session = class {
22248
23545
  this.modelsCache = {};
22249
23546
  this.remintSessionId();
22250
23547
  }
23548
+ /**
23549
+ * What a planner may assign right now: every enabled runner, its modes, and
23550
+ * its allowlisted models — read as of this call, so a runner enabled or an
23551
+ * allowlist edited since planning started is in it (#69). Discovers a
23552
+ * runner's models the first time it is asked for.
23553
+ */
23554
+ async liveCatalog() {
23555
+ const runners = this.enabledRunners();
23556
+ this.modelsCache = { ...this.modelsCache, ...await this.modelResolver.modelsForRunners(runners) };
23557
+ return this.catalogOf(runners);
23558
+ }
23559
+ catalogOf(runners) {
23560
+ return {
23561
+ runners,
23562
+ // Allowlist-filtered: neither the per-turn block nor a read may offer
23563
+ // a model the planner is forbidden to assign.
23564
+ models: filterModelsForPrompt(this.models(), this.allowlist()),
23565
+ modes: this.runnerModesFor(runners),
23566
+ autonomousDefault: this.config.autonomousMode
23567
+ };
23568
+ }
22251
23569
  runnerModesFor(runners) {
22252
23570
  return runnerModesFrom(this.registry, runners);
22253
23571
  }
@@ -22259,13 +23577,17 @@ var Session = class {
22259
23577
  * allowlist here — {@link checkModelAndModeValidity} narrows by
22260
23578
  * allowlist itself, the same way `coerceAssignments` does.
22261
23579
  */
22262
- editCatalog() {
23580
+ editCatalog(runners = this.plan?.runners ?? []) {
22263
23581
  return {
22264
23582
  modelsByRunner: this.models(),
22265
- runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
23583
+ runnerModes: this.runnerModesFor(runners),
22266
23584
  perRunnerAllowlist: this.allowlist()
22267
23585
  };
22268
23586
  }
23587
+ /** The runners enabled right now — a toggle made since the session was built counts (#69). */
23588
+ enabledRunners() {
23589
+ return this.settingsFn().enabledRunners ?? this.config.enabledRunners;
23590
+ }
22269
23591
  /**
22270
23592
  * The allowlist in force right now. Unset means no restriction — falling
22271
23593
  * back to the one planning started under would keep a restriction the user
@@ -22367,7 +23689,7 @@ var Session = class {
22367
23689
  this.goal = goal;
22368
23690
  this.remintSessionId();
22369
23691
  this.beginFreshPlan();
22370
- const enabled = this.config.enabledRunners;
23692
+ const enabled = this.enabledRunners();
22371
23693
  const chosenRunners = runners.filter((r) => enabled.includes(r));
22372
23694
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
22373
23695
  const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
@@ -22411,7 +23733,7 @@ var Session = class {
22411
23733
  this.goal = this.resolveSkillInvocation(goal);
22412
23734
  this.remintSessionId();
22413
23735
  this.beginFreshPlan();
22414
- const enabled = this.config.enabledRunners;
23736
+ const enabled = this.enabledRunners();
22415
23737
  const chosenRunners = runners.filter((r) => enabled.includes(r));
22416
23738
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
22417
23739
  const opening = await this.conversationOpening(chosenRunners);
@@ -22489,6 +23811,49 @@ var Session = class {
22489
23811
  rewindTargets() {
22490
23812
  return this.conversation.rewindTargets();
22491
23813
  }
23814
+ /**
23815
+ * Hand the planner turn in flight a validated plan or a task edit; the turn
23816
+ * commits it as it would the same JSON in the planner's reply. False when
23817
+ * no turn is open.
23818
+ */
23819
+ submitToTurn(submission) {
23820
+ return this.conversation.submit(submission);
23821
+ }
23822
+ /**
23823
+ * A plan the planner submitted through its tool, already checked against the
23824
+ * live catalog. Its runners join `plan.runners` here, so a runner enabled
23825
+ * since planning started is not snapped back by the commit (#69).
23826
+ */
23827
+ submitPlanFromTool(tasks, runners) {
23828
+ if (!this.plan || !this.conversation.submit({ kind: "plan", tasks })) return false;
23829
+ for (const runner of runners) this.admitRunner(runner, this.modelsCache[runner] ?? []);
23830
+ return true;
23831
+ }
23832
+ /**
23833
+ * A task edit the planner made through its tool. Checked against the live
23834
+ * catalog, then handed to the open turn, which commits it as it would the
23835
+ * same ops in a taskOps reply — including parking it behind a running batch,
23836
+ * which the envelope does before any check, so a queued edit is not checked
23837
+ * here either. Edits made in one reply join one batch.
23838
+ */
23839
+ async editPlanFromTool(ops) {
23840
+ const refuse = (message) => ({ ok: false, errors: [message] });
23841
+ const catalog = await this.liveCatalog();
23842
+ if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
23843
+ const held = this.conversation.pendingOps();
23844
+ if (!held) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
23845
+ const batch = [...held, ...ops];
23846
+ const queued = this.conversation.editWouldQueue(batch);
23847
+ let summary = [];
23848
+ if (!queued) {
23849
+ const result = applyTaskOps(this.store.planTasks, batch, catalog.runners, this.editCatalog(catalog.runners));
23850
+ if (!result.ok) return { ok: false, errors: result.errors };
23851
+ summary = result.summary;
23852
+ for (const runner of runnersOf(result.tasks)) this.admitRunner(runner, this.modelsCache[runner] ?? []);
23853
+ }
23854
+ if (!this.conversation.submit({ kind: "task_ops", ops: batch })) return refuse("No planning turn is open to take the edit.");
23855
+ return { ok: true, summary, queued };
23856
+ }
22492
23857
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
22493
23858
  get isConversationActive() {
22494
23859
  return this.conversation.isActive;
@@ -22534,7 +23899,8 @@ var Session = class {
22534
23899
  // usage line can show context fill (#49). Unknown stays absent.
22535
23900
  contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
22536
23901
  fs: this.fsAdapter,
22537
- fetcher: this.fetcher
23902
+ fetcher: this.fetcher,
23903
+ plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
22538
23904
  };
22539
23905
  }
22540
23906
  /**
@@ -22652,7 +24018,7 @@ var Session = class {
22652
24018
  ])
22653
24019
  );
22654
24020
  try {
22655
- const modelsByRunner = await this.modelResolver.modelsForRunners(this.config.enabledRunners);
24021
+ const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
22656
24022
  const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
22657
24023
  const { modelAllowlist } = this.settingsFn();
22658
24024
  const result = await this.planner.modifyDuringExecution({
@@ -22999,7 +24365,7 @@ var Session = class {
22999
24365
  if (!this.plan) throw new Error("No active plan state");
23000
24366
  const check = canMergeTasks(this.store.planTasks, taskIds);
23001
24367
  if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
23002
- const prompt = buildMergePrompt(taskIds, this.store.planTasks);
24368
+ const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
23003
24369
  return this.continueConversation(prompt, options);
23004
24370
  }
23005
24371
  /**
@@ -23011,7 +24377,7 @@ var Session = class {
23011
24377
  if (!this.plan) throw new Error("No active plan state");
23012
24378
  const check = canSplitTask(this.store.planTasks, taskId);
23013
24379
  if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
23014
- const prompt = buildSplitPrompt(taskId, this.store.planTasks);
24380
+ const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
23015
24381
  return this.continueConversation(prompt, options);
23016
24382
  }
23017
24383
  loadPlan(plan, goal, workspace, opts) {
@@ -23051,41 +24417,6 @@ var Session = class {
23051
24417
  }
23052
24418
  };
23053
24419
 
23054
- // src/utils/privateFile.ts
23055
- var import_fs2 = require("fs");
23056
- var import_path = require("path");
23057
- var import_crypto4 = require("crypto");
23058
- var DIR_MODE = 448;
23059
- var FILE_MODE = 384;
23060
- var POSIX_MODES = process.platform !== "win32";
23061
- function ensurePrivateDir(dir) {
23062
- if (POSIX_MODES) {
23063
- (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
23064
- return;
23065
- }
23066
- (0, import_fs2.mkdirSync)(dir, { recursive: true });
23067
- }
23068
- function writePrivateFile(filePath, content) {
23069
- const dir = (0, import_path.dirname)(filePath);
23070
- ensurePrivateDir(dir);
23071
- const tempPath = (0, import_path.join)(dir, `.${(0, import_path.basename)(filePath)}.${(0, import_crypto4.randomBytes)(8).toString("hex")}.tmp`);
23072
- const fd = (0, import_fs2.openSync)(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
23073
- try {
23074
- (0, import_fs2.writeSync)(fd, content);
23075
- } finally {
23076
- (0, import_fs2.closeSync)(fd);
23077
- }
23078
- try {
23079
- (0, import_fs2.renameSync)(tempPath, filePath);
23080
- } catch (err) {
23081
- try {
23082
- (0, import_fs2.unlinkSync)(tempPath);
23083
- } catch {
23084
- }
23085
- throw err;
23086
- }
23087
- }
23088
-
23089
24420
  // src/conversation/reduce.ts
23090
24421
  var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
23091
24422
  function isUnsettledSegment(block, turnId) {
@@ -23767,9 +25098,9 @@ var HeadlessRunner = class extends AbstractRunner {
23767
25098
  // src/services/TmuxRunner.ts
23768
25099
  var import_child_process11 = require("child_process");
23769
25100
  var import_util5 = require("util");
23770
- var import_os = require("os");
23771
- var import_path2 = require("path");
23772
- var import_fs3 = require("fs");
25101
+ var import_os2 = require("os");
25102
+ var import_path5 = require("path");
25103
+ var import_fs5 = require("fs");
23773
25104
 
23774
25105
  // src/utils/tmux.ts
23775
25106
  var import_child_process10 = require("child_process");
@@ -23867,7 +25198,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23867
25198
  * lets the user keep poking at a finished task's terminal.
23868
25199
  */
23869
25200
  async start(command, args, cwd, env) {
23870
- (0, import_fs3.writeFileSync)(this.logPath, "");
25201
+ (0, import_fs5.writeFileSync)(this.logPath, "");
23871
25202
  const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
23872
25203
  const inner = [command, ...args].map(posixShellQuote).join(" ");
23873
25204
  const prefix = envAssignments ? `env ${envAssignments} ` : "";
@@ -23890,7 +25221,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23890
25221
  readLog() {
23891
25222
  let content;
23892
25223
  try {
23893
- content = (0, import_fs3.existsSync)(this.logPath) ? (0, import_fs3.readFileSync)(this.logPath, "utf8") : "";
25224
+ content = (0, import_fs5.existsSync)(this.logPath) ? (0, import_fs5.readFileSync)(this.logPath, "utf8") : "";
23894
25225
  } catch {
23895
25226
  return false;
23896
25227
  }
@@ -23946,7 +25277,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23946
25277
  this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
23947
25278
  });
23948
25279
  try {
23949
- (0, import_fs3.unlinkSync)(this.logPath);
25280
+ (0, import_fs5.unlinkSync)(this.logPath);
23950
25281
  } catch {
23951
25282
  }
23952
25283
  this.baseHandleExit(code);
@@ -23983,7 +25314,7 @@ var TmuxRunner = class extends AbstractRunner {
23983
25314
  this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
23984
25315
  this.resolvePath = deps.resolvePath ?? augmentedPath;
23985
25316
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
23986
- this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
25317
+ this.logDir = deps.logDir ?? (0, import_os2.tmpdir)();
23987
25318
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
23988
25319
  this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
23989
25320
  }
@@ -24103,7 +25434,7 @@ var TmuxRunner = class extends AbstractRunner {
24103
25434
  windowName,
24104
25435
  this.execFileImpl,
24105
25436
  this.pollIntervalMs,
24106
- (0, import_path2.join)(this.logDir, `${id}.log`),
25437
+ (0, import_path5.join)(this.logDir, `${id}.log`),
24107
25438
  this.socket
24108
25439
  );
24109
25440
  invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
@@ -24131,9 +25462,9 @@ var KEY_ARGS = {
24131
25462
  };
24132
25463
  var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
24133
25464
  function toolLine(name, args) {
24134
- const { tool } = mapAgentTool(name);
24135
- const normalized = normalizeAgentArgs(tool, args);
24136
- const preferred = KEY_ARGS[tool];
25465
+ const { tool: tool2 } = mapAgentTool(name);
25466
+ const normalized = normalizeAgentArgs(tool2, args);
25467
+ const preferred = KEY_ARGS[tool2];
24137
25468
  for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
24138
25469
  const value = normalized[key];
24139
25470
  if (typeof value !== "string") continue;
@@ -24196,6 +25527,7 @@ var StructuredSession = class extends AbstractTerminalSession {
24196
25527
  constructor(id, taskId, launch) {
24197
25528
  super(id, taskId);
24198
25529
  this.launch = launch;
25530
+ this.startOptions = launch.startOptions;
24199
25531
  this.text = new PlainTextChannel((text2) => {
24200
25532
  this.output += text2;
24201
25533
  this.outputEmitter.emit("output", text2);
@@ -24224,9 +25556,20 @@ var StructuredSession = class extends AbstractTerminalSession {
24224
25556
  * requests — while an approval must be answerable by id across the session.
24225
25557
  */
24226
25558
  permissions = /* @__PURE__ */ new Map();
25559
+ /** The launch's start options, with this attempt's server once its token is issued. */
25560
+ startOptions;
25561
+ /** This attempt's MCP token, until the session ends (ADR-0022, A2). */
25562
+ toolToken = null;
25563
+ checkpointHandler = null;
24227
25564
  /** Start the runner and send the task's prompt as its first turn. */
24228
25565
  async start(prompt) {
24229
- await this.startAdapter(this.launch.startOptions);
25566
+ await this.issueTools();
25567
+ try {
25568
+ await this.startAdapter(this.startOptions);
25569
+ } catch (err) {
25570
+ this.revokeTools();
25571
+ throw err;
25572
+ }
24230
25573
  this.state = "working";
24231
25574
  setImmediate(() => {
24232
25575
  if (!this.exited) this.deliver(prompt);
@@ -24235,6 +25578,49 @@ var StructuredSession = class extends AbstractTerminalSession {
24235
25578
  getOutput() {
24236
25579
  return this.output;
24237
25580
  }
25581
+ onTaskComplete(listener) {
25582
+ this.structuredEmitter.on("taskComplete", listener);
25583
+ }
25584
+ onToolCheckpoint(handler) {
25585
+ this.checkpointHandler = handler;
25586
+ }
25587
+ /**
25588
+ * A server that cannot start costs the task its tools, not its run: the
25589
+ * prompt still teaches the marker (ADR-0022, S2).
25590
+ */
25591
+ async issueTools() {
25592
+ const tools = this.launch.tools;
25593
+ if (!tools) return;
25594
+ try {
25595
+ const credential = await tools.server.issueTaskToken(tools.scope, {
25596
+ taskComplete: async (report) => {
25597
+ this.structuredEmitter.emit("taskComplete", report);
25598
+ return { text: "Recorded. End your turn now." };
25599
+ },
25600
+ checkpoint: async ({ question }, { signal }) => {
25601
+ if (!this.checkpointHandler) return { text: "checkpoint is not available in this session.", isError: true };
25602
+ return checkpointReply(await this.checkpointHandler(question, signal));
25603
+ }
25604
+ });
25605
+ if (this.exited) {
25606
+ tools.server.revoke(credential.token);
25607
+ return;
25608
+ }
25609
+ this.toolToken = credential.token;
25610
+ this.startOptions = { ...this.startOptions, mcp: mcpClientConfig(credential) };
25611
+ } catch (err) {
25612
+ console.error(`[structured] No Ordewell tools for task ${this.taskId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`);
25613
+ }
25614
+ }
25615
+ revokeTools() {
25616
+ if (this.toolToken === null) return;
25617
+ this.launch.tools?.server.revoke(this.toolToken);
25618
+ this.toolToken = null;
25619
+ }
25620
+ baseHandleExit(code) {
25621
+ this.revokeTools();
25622
+ super.baseHandleExit(code);
25623
+ }
24238
25624
  /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
24239
25625
  write(text2) {
24240
25626
  const message = text2.trim();
@@ -24332,12 +25718,12 @@ var StructuredSession = class extends AbstractTerminalSession {
24332
25718
  * session id after an abort.
24333
25719
  */
24334
25720
  async restartInterrupted(turn) {
24335
- const resumeSessionId = this.nativeSessionId() ?? this.launch.startOptions.resumeSessionId;
25721
+ const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
24336
25722
  this.generation += 1;
24337
25723
  turn.abort.abort();
24338
25724
  if (this.adapter) this.withdrawPermissions(this.adapter);
24339
25725
  try {
24340
- await this.startAdapter({ ...this.launch.startOptions, resumeSessionId });
25726
+ await this.startAdapter({ ...this.startOptions, resumeSessionId });
24341
25727
  } catch (err) {
24342
25728
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
24343
25729
  this.endTurn(turn, "interrupted");
@@ -24475,11 +25861,13 @@ var StructuredRunner = class extends AbstractRunner {
24475
25861
  processDeps;
24476
25862
  createAdapter;
24477
25863
  interruptGraceMs;
25864
+ mcp;
24478
25865
  constructor(deps = {}) {
24479
25866
  super();
24480
25867
  this.processDeps = deps.process ?? {};
24481
25868
  this.createAdapter = deps.createAdapter ?? createTaskAdapter;
24482
25869
  this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
25870
+ this.mcp = deps.mcp ?? sharedMcpServer();
24483
25871
  }
24484
25872
  async spawn(opts) {
24485
25873
  const manifest = opts.registry?.get(opts.runner)?.manifest;
@@ -24506,7 +25894,8 @@ var StructuredRunner = class extends AbstractRunner {
24506
25894
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
24507
25895
  resumeSessionId: opts.resumeSessionId
24508
25896
  },
24509
- interruptGraceMs: this.interruptGraceMs
25897
+ interruptGraceMs: this.interruptGraceMs,
25898
+ ...takesOrdewellTools(opts.runner) ? { tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } } } : {}
24510
25899
  });
24511
25900
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
24512
25901
  this.registerSession(id, session);
@@ -24520,73 +25909,12 @@ var StructuredRunner = class extends AbstractRunner {
24520
25909
  }
24521
25910
  };
24522
25911
 
24523
- // src/utils/daemonToken.ts
24524
- var import_crypto5 = require("crypto");
24525
- var import_fs4 = require("fs");
24526
- var import_path3 = require("path");
24527
- var BEARER_PREFIX = "bearer ";
24528
- var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
24529
- var DAEMON_SUBPROTOCOL = "ordewell.v1";
24530
- function configDir() {
24531
- return globalDataDir();
24532
- }
24533
- function daemonTokenPath(port) {
24534
- return (0, import_path3.join)(configDir(), `server-${port}.token`);
24535
- }
24536
- function mintDaemonToken(port) {
24537
- const token = (0, import_crypto5.randomBytes)(32).toString("base64url");
24538
- const file = daemonTokenPath(port);
24539
- writePrivateFile(file, token);
24540
- return { token, file };
24541
- }
24542
- function readDaemonToken(port) {
24543
- try {
24544
- const token = (0, import_fs4.readFileSync)(daemonTokenPath(port), "utf8").trim();
24545
- return token === "" ? void 0 : token;
24546
- } catch {
24547
- return void 0;
24548
- }
24549
- }
24550
- function clearDaemonToken(port) {
24551
- try {
24552
- (0, import_fs4.unlinkSync)(daemonTokenPath(port));
24553
- } catch {
24554
- }
24555
- }
24556
- function bearerHeaderValue(token) {
24557
- return `Bearer ${token}`;
24558
- }
24559
- function tokenSubprotocols(token) {
24560
- return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
24561
- }
24562
- function extractPresentedToken(carriers) {
24563
- const authorization = carriers.authorization;
24564
- if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
24565
- const value = authorization.slice(BEARER_PREFIX.length).trim();
24566
- if (value !== "") return value;
24567
- }
24568
- for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
24569
- const value = offered.trim();
24570
- if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
24571
- const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
24572
- if (token !== "") return token;
24573
- }
24574
- return void 0;
24575
- }
24576
- function tokensMatch(presented, expected) {
24577
- if (presented === void 0) return false;
24578
- const a = Buffer.from(presented, "utf8");
24579
- const b = Buffer.from(expected, "utf8");
24580
- if (a.length !== b.length) return false;
24581
- return (0, import_crypto5.timingSafeEqual)(a, b);
24582
- }
24583
-
24584
25912
  // src/plugins/RunnerRegistry.ts
24585
25913
  var fs13 = __toESM(require("fs"));
24586
25914
  var path24 = __toESM(require("path"));
24587
25915
  var os6 = __toESM(require("os"));
24588
25916
  var import_child_process13 = require("child_process");
24589
- var import_crypto6 = require("crypto");
25917
+ var import_crypto8 = require("crypto");
24590
25918
 
24591
25919
  // src/plugins/FsPluginStore.ts
24592
25920
  var fs12 = __toESM(require("fs"));
@@ -24610,8 +25938,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
24610
25938
  assertPlainPluginName(name);
24611
25939
  const root = path22.resolve(pluginsDir);
24612
25940
  const dest = path22.resolve(root, name);
24613
- const relative5 = path22.relative(root, dest);
24614
- if (relative5 !== name || relative5 === "" || path22.isAbsolute(relative5)) {
25941
+ const relative6 = path22.relative(root, dest);
25942
+ if (relative6 !== name || relative6 === "" || path22.isAbsolute(relative6)) {
24615
25943
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
24616
25944
  }
24617
25945
  return dest;
@@ -25225,7 +26553,7 @@ var RunnerRegistry = class {
25225
26553
  }
25226
26554
  installFromGit(url) {
25227
26555
  const validated = assertInstallablePluginUrl(url);
25228
- const tmpDir = path24.join(os6.tmpdir(), `ordewell-plugin-${(0, import_crypto6.randomBytes)(12).toString("hex")}`);
26556
+ const tmpDir = path24.join(os6.tmpdir(), `ordewell-plugin-${(0, import_crypto8.randomBytes)(12).toString("hex")}`);
25229
26557
  this.store.ensureDir(tmpDir);
25230
26558
  try {
25231
26559
  try {
@@ -25645,11 +26973,15 @@ var PlannerModelMemory = class {
25645
26973
  NO_TURN,
25646
26974
  OPENCODE_MANIFEST,
25647
26975
  ORCHESTRATOR_SHORTCUTS,
26976
+ ORDEWELL_MCP_PATH,
26977
+ ORDEWELL_MCP_SERVER_NAME,
25648
26978
  ORDEWELL_SETTABLE_ENV,
25649
26979
  OUTPUT_LINES_DEFAULT,
25650
26980
  OUTPUT_LINES_MAX,
25651
26981
  OpenAiService,
25652
26982
  OpenCodeAdapter,
26983
+ OrdewellMcpServer,
26984
+ PLANNER_TOOLS,
25653
26985
  PLAN_ENVELOPE_KEY,
25654
26986
  PLUGIN_NAME_PATTERN,
25655
26987
  PROVIDER_CREDENTIAL_ENV,
@@ -25686,6 +27018,7 @@ var PlannerModelMemory = class {
25686
27018
  TASK_QUERY_FIELDS,
25687
27019
  TASK_QUERY_PROTOCOL,
25688
27020
  TASK_QUERY_REMINDER,
27021
+ TASK_TOOLS,
25689
27022
  TRUNCATED_PLAN_REPAIR_INSTRUCTION,
25690
27023
  TaskControlError,
25691
27024
  TaskLogRecorder,
@@ -25769,10 +27102,14 @@ var PlannerModelMemory = class {
25769
27102
  daemonTokenPath,
25770
27103
  defaultLogger,
25771
27104
  definitionPattern,
27105
+ defuseMarkers,
25772
27106
  deleteSession,
25773
27107
  dependencyCandidates,
25774
27108
  dependentsOf,
25775
27109
  describeMergeResult,
27110
+ diffRows,
27111
+ diffStat,
27112
+ diffSummary,
25776
27113
  digestTaskLog,
25777
27114
  discoverGeminiModels,
25778
27115
  drainNext,
@@ -25835,6 +27172,7 @@ var PlannerModelMemory = class {
25835
27172
  loadState,
25836
27173
  looksLikePlanAttempt,
25837
27174
  mapAgentTool,
27175
+ mcpClientConfig,
25838
27176
  migrateLegacyPlan,
25839
27177
  migrateOldConfigDir,
25840
27178
  migratePlanIsolation,
@@ -25850,6 +27188,7 @@ var PlannerModelMemory = class {
25850
27188
  opensWithJsonObject,
25851
27189
  outputLines,
25852
27190
  outputPreview,
27191
+ ownerOnlyConfigFile,
25853
27192
  parseAutonomyLevel,
25854
27193
  parseMaxParallel,
25855
27194
  parsePartialPlan,
@@ -25908,6 +27247,7 @@ var PlannerModelMemory = class {
25908
27247
  serializeTaskStatus,
25909
27248
  sessionDataDir,
25910
27249
  sessionRuntimeSettings,
27250
+ sharedMcpServer,
25911
27251
  stateExists,
25912
27252
  stopTurn,
25913
27253
  stripAnsi,
@@ -25941,6 +27281,7 @@ var PlannerModelMemory = class {
25941
27281
  usageLine,
25942
27282
  validateModifiedPlan,
25943
27283
  validatePlanModification,
27284
+ validatePlanTasks,
25944
27285
  warningsText,
25945
27286
  wellKnownBinDirs,
25946
27287
  windowsCommandLine,