@ordewell/core 0.6.4 → 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 +2002 -670
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +1642 -366
  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";
@@ -4456,12 +4469,12 @@ var ApprovalPolicy = class {
4456
4469
  if (this.generation === gen) this.inFlight.delete(openKey);
4457
4470
  });
4458
4471
  this.inFlight.set(openKey, pending);
4459
- const answer = await pending;
4472
+ const answer2 = await pending;
4460
4473
  if (this.generation === gen) {
4461
- if (answer) open.forEach((s) => this.granted.add(s));
4474
+ if (answer2) open.forEach((s) => this.granted.add(s));
4462
4475
  else this.refused.add(key);
4463
4476
  }
4464
- return decide(answer, "asked");
4477
+ return decide(answer2, "asked");
4465
4478
  }
4466
4479
  /** Scopes the user has granted this session — for display and for persistence. */
4467
4480
  grantedScopes() {
@@ -4522,10 +4535,10 @@ var PendingApprovals = class {
4522
4535
  * settled. "Allow for this task" on a request that did not offer it is a
4523
4536
  * plain allow: no grant is made that the requester never proposed.
4524
4537
  */
4525
- resolve(id, answer) {
4538
+ resolve(id, answer2) {
4526
4539
  const entry = this.entries.get(id);
4527
4540
  if (!entry) return false;
4528
- const decision = toApprovalDecision(answer);
4541
+ const decision = toApprovalDecision(answer2);
4529
4542
  entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
4530
4543
  return true;
4531
4544
  }
@@ -4570,6 +4583,10 @@ var KNOWN_TOOLS = {
4570
4583
  list: "list_dir",
4571
4584
  fetch: "fetch"
4572
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
+ }
4573
4590
  function mapAgentTool(name) {
4574
4591
  const normalized = name.trim().toLowerCase().replace(/[_-]/g, "");
4575
4592
  const direct = KNOWN_TOOLS[normalized] ?? KNOWN_TOOLS[name.trim().toLowerCase()];
@@ -4578,25 +4595,25 @@ function mapAgentTool(name) {
4578
4595
  }
4579
4596
  return { tool: "agent_tool", toolLabel: name };
4580
4597
  }
4581
- function normalizeAgentArgs(tool, args) {
4598
+ function normalizeAgentArgs(tool2, args) {
4582
4599
  const out = { ...args };
4583
4600
  const alias = (from, to) => {
4584
4601
  if (out[to] === void 0 && out[from] !== void 0) out[to] = out[from];
4585
4602
  };
4586
- if (tool === "read_file" || tool === "list_dir") {
4603
+ if (tool2 === "read_file" || tool2 === "list_dir") {
4587
4604
  alias("filePath", "path");
4588
4605
  alias("file_path", "path");
4589
4606
  alias("target", "path");
4590
4607
  }
4591
- if (tool === "bash") {
4608
+ if (tool2 === "bash") {
4592
4609
  alias("cmd", "command");
4593
4610
  alias("script", "command");
4594
4611
  if (typeof out.command !== "string" && Array.isArray(out.command)) {
4595
4612
  out.command = out.command.join(" ");
4596
4613
  }
4597
4614
  }
4598
- if (tool === "fetch") alias("uri", "url");
4599
- if (tool === "web_search") alias("q", "query");
4615
+ if (tool2 === "fetch") alias("uri", "url");
4616
+ if (tool2 === "web_search") alias("q", "query");
4600
4617
  return out;
4601
4618
  }
4602
4619
 
@@ -4765,13 +4782,13 @@ function renderTerminalOutput(raw) {
4765
4782
  function renderCleanCapture(raw, doneToken) {
4766
4783
  const rendered = renderTerminalOutput(raw).replace(/[\s─-▟]+$/gm, "");
4767
4784
  if (!doneToken) return rendered.trim();
4768
- const lines = rendered.split("\n");
4785
+ const lines2 = rendered.split("\n");
4769
4786
  const flat = (s) => flattenTerminalOutput(s);
4770
- for (let i = 0; i < lines.length; i++) {
4771
- const own = flat(lines[i]).includes(doneToken);
4772
- if (own) return lines.slice(0, i).join("\n").trim();
4773
- const window = flat(lines[i]) + (i + 1 < lines.length ? flat(lines[i + 1]) : "");
4774
- 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();
4775
4792
  }
4776
4793
  return rendered.trim();
4777
4794
  }
@@ -4802,9 +4819,9 @@ function oneLine(text2) {
4802
4819
  return text2.replace(/\s+/g, " ").trim();
4803
4820
  }
4804
4821
  function firstLineOf(command) {
4805
- const lines = command.trim().split(/\r?\n/);
4806
- const more = lines.length - 1;
4807
- 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];
4808
4825
  }
4809
4826
  function text(value) {
4810
4827
  return typeof value === "string" ? value : "";
@@ -4820,8 +4837,8 @@ function hintArg(args) {
4820
4837
  const firstText = Object.values(args).find((v) => typeof v === "string" && v.trim() !== "");
4821
4838
  return firstText ? oneLine(firstText) : "";
4822
4839
  }
4823
- function keyArgOf(tool, args) {
4824
- switch (tool) {
4840
+ function keyArgOf(tool2, args) {
4841
+ switch (tool2) {
4825
4842
  case "bash":
4826
4843
  return firstLineOf(text(args.command));
4827
4844
  case "read_file":
@@ -4845,20 +4862,62 @@ function keyArgOf(tool, args) {
4845
4862
  return hintArg(args);
4846
4863
  }
4847
4864
  }
4848
- function toolHeadline(tool, args, toolLabel) {
4849
- const name = toolLabel?.trim() || DISPLAY_NAMES[tool] || tool;
4865
+ function toolHeadline(tool2, args, toolLabel) {
4866
+ const name = toolLabel?.trim() || DISPLAY_NAMES[tool2] || tool2;
4850
4867
  const parsed = parseArgs(args);
4851
- return { name, keyArg: parsed ? keyArgOf(tool, parsed) : oneLine(args) };
4868
+ return { name, keyArg: parsed ? keyArgOf(tool2, parsed) : oneLine(args) };
4852
4869
  }
4853
4870
  function outputLines(output) {
4854
- const lines = output.replace(/\r\n?/g, "\n").replace(ANSI_OR_CTRL_RE, "").split("\n");
4855
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
4856
- 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;
4857
4874
  }
4858
4875
  function outputPreview(output, maxLines) {
4859
- const lines = outputLines(output);
4860
- const shown = lines.slice(0, Math.max(0, maxLines));
4861
- 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)}`;
4862
4921
  }
4863
4922
 
4864
4923
  // src/conversation/records.ts
@@ -4887,17 +4946,18 @@ var STATUS_OF_OUTCOME = {
4887
4946
  not_executed: "interrupted"
4888
4947
  };
4889
4948
  function finishedTool(call, output, outcome) {
4890
- 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 } : {} };
4891
4951
  }
4892
4952
  function settledTool(call, step) {
4893
4953
  return finishedTool(call, step.result, step.outcome);
4894
4954
  }
4895
- function pendingTool(id, { tool, toolLabel, args, toolCallId, turnId }) {
4955
+ function pendingTool(id, { tool: tool2, toolLabel, args, toolCallId, turnId }) {
4896
4956
  return {
4897
4957
  type: "tool",
4898
4958
  id,
4899
- tool,
4900
- headline: toolHeadline(tool, args, toolLabel),
4959
+ tool: tool2,
4960
+ headline: toolHeadline(tool2, args, toolLabel),
4901
4961
  args,
4902
4962
  status: "pending",
4903
4963
  output: "",
@@ -5087,12 +5147,12 @@ function thinkingWhole(view, { text: text2, subagentId }) {
5087
5147
  return streamWhole(placed, lane, thinkingStream(subagentId), text2, (id) => thinkingBlock(id, blockText(text2), false, subagentId));
5088
5148
  }
5089
5149
  function announceTool(view, { id: toolCallId, name, args, subagentId }) {
5090
- const { tool, toolLabel } = mapAgentTool(name);
5150
+ const { tool: tool2, toolLabel } = mapAgentTool(name);
5091
5151
  const [placed, lane] = laneOf(view, subagentId);
5092
5152
  return appendOutput(placed, lane, (id) => {
5093
- const call = pendingTool(id, { tool, toolLabel, args, toolCallId });
5153
+ const call = pendingTool(id, { tool: tool2, toolLabel, args, toolCallId });
5094
5154
  const parsed = parseArgs2(args);
5095
- 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;
5096
5156
  });
5097
5157
  }
5098
5158
  function parseArgs2(args) {
@@ -5132,21 +5192,21 @@ function reportUsage(view, { record }) {
5132
5192
  const next = { ...view, usage };
5133
5193
  return isMeasured(usage.totals) ? setUsageLine(next, usageLine(usage)) : next;
5134
5194
  }
5135
- function runnerToolSubject(tool, args) {
5136
- const { tool: mapped, toolLabel } = mapAgentTool(tool);
5195
+ function runnerToolSubject(tool2, args) {
5196
+ const { tool: mapped, toolLabel } = mapAgentTool(tool2);
5137
5197
  const parsed = parseArgs2(args);
5138
5198
  const headline = toolHeadline(mapped, parsed ? JSON.stringify(normalizeAgentArgs(mapped, parsed)) : args, toolLabel);
5139
5199
  return headline.keyArg ? `${headline.name}(${headline.keyArg})` : headline.name;
5140
5200
  }
5141
- function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId }) {
5201
+ function requestApproval(view, { approvalId, tool: tool2, args, allowForTask, toolCallId }) {
5142
5202
  if (view.blocks.some((b) => b.type === "approval" && b.approvalId === approvalId)) return view;
5143
5203
  return append(view, (id) => ({
5144
5204
  type: "approval",
5145
5205
  id,
5146
5206
  approvalId,
5147
5207
  kind: "runner_tool",
5148
- subject: runnerToolSubject(tool, args),
5149
- scope: tool,
5208
+ subject: runnerToolSubject(tool2, args),
5209
+ scope: tool2,
5150
5210
  status: "pending",
5151
5211
  ...allowForTask ? { allowForTask } : {},
5152
5212
  ...toolCallId ? { toolCallId } : {}
@@ -5440,9 +5500,9 @@ function applyHeadLimit(stdout, headLimit) {
5440
5500
  }
5441
5501
  function formatSearchOutput(capped, root, opts) {
5442
5502
  if (capped.rows.length === 0) return opts.emptyMessage;
5443
- const sep2 = opts.sep ?? import_node_path.default.sep;
5444
- const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
5445
- 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");
5446
5506
  if (!capped.truncated) return body;
5447
5507
  const hint = opts.hint ? ` ${opts.hint}` : "";
5448
5508
  return `${body}
@@ -6492,28 +6552,28 @@ function posixBinDirs() {
6492
6552
  ];
6493
6553
  }
6494
6554
  function windowsBinDirs(env, home) {
6495
- const join26 = (...parts) => path5.win32.join(...parts);
6496
- 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)] : [];
6497
6557
  const appData = env.APPDATA;
6498
6558
  const localAppData = env.LOCALAPPDATA;
6499
6559
  const programData = env.ProgramData;
6500
6560
  return [
6501
6561
  // Native per-user installers, PowerShell-driven and otherwise.
6502
- join26(home, ".local", "bin"),
6503
- join26(home, ".opencode", "bin"),
6504
- join26(home, ".claude", "bin"),
6505
- join26(home, ".codex", "bin"),
6562
+ join27(home, ".local", "bin"),
6563
+ join27(home, ".opencode", "bin"),
6564
+ join27(home, ".claude", "bin"),
6565
+ join27(home, ".codex", "bin"),
6506
6566
  ...under(localAppData, "Programs", "claude"),
6507
6567
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
6508
6568
  // literal spelling is kept as a fallback for a host that does not export
6509
6569
  // APPDATA (a service account, a stripped CI container).
6510
6570
  ...under(appData, "npm"),
6511
- join26(home, "AppData", "Roaming", "npm"),
6571
+ join27(home, "AppData", "Roaming", "npm"),
6512
6572
  ...under(localAppData, "pnpm"),
6513
6573
  ...under(localAppData, "Yarn", "bin"),
6514
- join26(home, ".bun", "bin"),
6574
+ join27(home, ".bun", "bin"),
6515
6575
  // Windows package managers.
6516
- join26(home, "scoop", "shims"),
6576
+ join27(home, "scoop", "shims"),
6517
6577
  ...under(env.SCOOP, "shims"),
6518
6578
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
6519
6579
  ...under(programData, "chocolatey", "bin"),
@@ -6523,7 +6583,7 @@ function windowsBinDirs(env, home) {
6523
6583
  ...under(localAppData, "Microsoft", "WindowsApps"),
6524
6584
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
6525
6585
  ...under(localAppData, "Volta", "bin"),
6526
- join26(home, ".volta", "bin")
6586
+ join27(home, ".volta", "bin")
6527
6587
  ];
6528
6588
  }
6529
6589
  function wellKnownBinDirs(deps = {}) {
@@ -7011,18 +7071,18 @@ var GitWorktreeIsolation = class {
7011
7071
  */
7012
7072
  sharedPaths(workspaceRoot, isolated) {
7013
7073
  if (isolated.includes(SELF_REPO)) return [];
7014
- const shared = [];
7074
+ const shared2 = [];
7015
7075
  const walk = (rel) => {
7016
7076
  for (const name of listDir(path9.join(workspaceRoot, rel))) {
7017
7077
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
7018
7078
  const child = rel ? `${rel}/${name}` : name;
7019
7079
  if (isolated.includes(child)) continue;
7020
7080
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
7021
- else if (resolves(path9.join(workspaceRoot, child))) shared.push(child);
7081
+ else if (resolves(path9.join(workspaceRoot, child))) shared2.push(child);
7022
7082
  }
7023
7083
  };
7024
7084
  walk("");
7025
- return shared;
7085
+ return shared2;
7026
7086
  }
7027
7087
  prepare(task, run) {
7028
7088
  return this.admin(run.workspaceRoot, async () => {
@@ -7836,13 +7896,14 @@ function filteredBuildModes(modes, autonomousDefault) {
7836
7896
  return buildModes;
7837
7897
  }
7838
7898
  function autonomyLevelLabel(autonomous) {
7839
- return autonomous ? "Full auto" : "Auto";
7899
+ return autonomous ? "Full" : "Guarded";
7840
7900
  }
7841
7901
  function parseAutonomyLevel(arg) {
7842
7902
  switch (arg?.trim().toLowerCase()) {
7843
7903
  case "full":
7844
7904
  case "on":
7845
7905
  return true;
7906
+ case "guarded":
7846
7907
  case "auto":
7847
7908
  case "off":
7848
7909
  return false;
@@ -7851,7 +7912,7 @@ function parseAutonomyLevel(arg) {
7851
7912
  }
7852
7913
  }
7853
7914
  function buildModeGuide(runnerModes, autonomousDefault = true) {
7854
- const lines = [];
7915
+ const lines2 = [];
7855
7916
  for (const [runnerId, modes] of Object.entries(runnerModes)) {
7856
7917
  if (!modes || modes.length === 0) continue;
7857
7918
  const filtered = filteredBuildModes(modes, autonomousDefault);
@@ -7860,13 +7921,13 @@ function buildModeGuide(runnerModes, autonomousDefault = true) {
7860
7921
  const tag = isDefaultMode(m, autonomousDefault) ? " (DEFAULT)" : "";
7861
7922
  return `${m.id}${tag} (${m.description})`;
7862
7923
  }).join(", ");
7863
- lines.push(`- ${runnerId}: ${modeStr}`);
7924
+ lines2.push(`- ${runnerId}: ${modeStr}`);
7864
7925
  }
7865
- if (lines.length === 0) return "";
7926
+ if (lines2.length === 0) return "";
7866
7927
  return [
7867
7928
  `Autonomy level: ${autonomyLevelLabel(autonomousDefault)}.`,
7868
7929
  "AVAILABLE MODES PER RUNNER:",
7869
- ...lines,
7930
+ ...lines2,
7870
7931
  "",
7871
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.'
7872
7933
  ].join("\n");
@@ -8107,10 +8168,12 @@ var TASK_QUERY_FIELDS = [
8107
8168
  "userStoriesCovered",
8108
8169
  "output"
8109
8170
  ];
8171
+ var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
8110
8172
  var OUTPUT_LINES_DEFAULT = 80;
8111
8173
  var OUTPUT_LINES_MAX = 400;
8112
8174
  var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
8113
8175
  var ANSWER_TAIL_RESERVE = 4e3;
8176
+ var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
8114
8177
  function textHasTaskQuery(text2) {
8115
8178
  return text2.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
8116
8179
  }
@@ -8186,6 +8249,7 @@ function taskQuerySignature(query) {
8186
8249
  return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
8187
8250
  }
8188
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.";
8189
8253
  var TASK_QUERY_PROTOCOL = [
8190
8254
  "READING A TASK BEFORE YOU EDIT IT:",
8191
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:",
@@ -8197,7 +8261,16 @@ var TASK_QUERY_PROTOCOL = [
8197
8261
  "Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
8198
8262
  "Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
8199
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
+ ];
8200
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.";
8201
8274
  function findTask(tasks, ref) {
8202
8275
  const byId = tasks.find((t) => t.id === ref);
8203
8276
  if (byId) return byId;
@@ -8211,13 +8284,13 @@ function findTask(tasks, ref) {
8211
8284
  function fitTail(text2, budget) {
8212
8285
  if (text2.length <= budget) return { text: text2, trimmed: false };
8213
8286
  if (budget <= 0) return { text: "", trimmed: true };
8214
- const lines = text2.split("\n");
8287
+ const lines2 = text2.split("\n");
8215
8288
  const kept = [];
8216
8289
  let total = 0;
8217
- for (let i = lines.length - 1; i >= 0; i--) {
8218
- 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);
8219
8292
  if (total + cost > budget) break;
8220
- kept.unshift(lines[i]);
8293
+ kept.unshift(lines2[i]);
8221
8294
  total += cost;
8222
8295
  }
8223
8296
  if (kept.length === 0) return { text: text2.slice(text2.length - budget), trimmed: true };
@@ -8268,34 +8341,34 @@ function renderField(task, field, ctx) {
8268
8341
  }
8269
8342
  }
8270
8343
  function renderTask(task, fields, ctx) {
8271
- const lines = [
8344
+ const lines2 = [
8272
8345
  `#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
8273
8346
  ...fields.filter((f) => f !== "output").flatMap((f) => renderField(task, f, ctx)),
8274
8347
  ...fields.includes("output") ? renderLiveOutput(task.id, ctx) : [],
8275
8348
  ""
8276
8349
  ];
8277
- ctx.used += lines.reduce((n, l) => n + l.length + 1, 0);
8278
- return lines;
8350
+ ctx.used += lines2.reduce((n, l) => n + l.length + 1, 0);
8351
+ return lines2;
8279
8352
  }
8280
8353
  function renderCatalog(catalog) {
8281
- const lines = [];
8354
+ const lines2 = [];
8282
8355
  for (const runner of catalog.runners) {
8283
8356
  const models = catalog.models[runner] ?? [];
8284
- lines.push(`${runner} models:`);
8285
- if (models.length === 0) lines.push(" (none discovered)");
8357
+ lines2.push(`${runner} models:`);
8358
+ if (models.length === 0) lines2.push(" (none discovered)");
8286
8359
  for (const m of models) {
8287
8360
  const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
8288
- lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
8361
+ lines2.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
8289
8362
  }
8290
8363
  const modes = catalog.modes[runner] ?? [];
8291
- lines.push(`${runner} task modes:`);
8292
- if (modes.length === 0) lines.push(" (none declared)");
8364
+ lines2.push(`${runner} task modes:`);
8365
+ if (modes.length === 0) lines2.push(" (none declared)");
8293
8366
  const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
8294
8367
  for (const mode of modes) {
8295
- 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)" : ""}`);
8296
8369
  }
8297
8370
  }
8298
- return lines;
8371
+ return lines2;
8299
8372
  }
8300
8373
  function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
8301
8374
  const fields = query.fields ?? TASK_QUERY_FIELDS;
@@ -8329,7 +8402,7 @@ function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
8329
8402
 
8330
8403
  // src/services/PlanPrompts.ts
8331
8404
  function buildResearchToolsPrompt() {
8332
- const lines = [
8405
+ const lines2 = [
8333
8406
  "You have access to the following tools to explore the workspace:",
8334
8407
  "",
8335
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.",
@@ -8349,7 +8422,7 @@ function buildResearchToolsPrompt() {
8349
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.',
8350
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."
8351
8424
  ];
8352
- return lines.join("\n");
8425
+ return lines2.join("\n");
8353
8426
  }
8354
8427
  function buildSubagentSystemPrompt() {
8355
8428
  return [
@@ -8366,18 +8439,18 @@ function buildSubagentSystemPrompt() {
8366
8439
  ].join("\n");
8367
8440
  }
8368
8441
  function buildModeGuideForRunners(runners, _autonomousDefault) {
8369
- 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:'];
8370
8443
  for (const r of runners) {
8371
8444
  if (r === "claude-code") {
8372
- 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).');
8373
8446
  } else if (r === "opencode") {
8374
- 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).');
8375
8448
  } else {
8376
- 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.`);
8377
8450
  }
8378
8451
  }
8379
- lines.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
8380
- return lines.join("\n");
8452
+ lines2.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
8453
+ return lines2.join("\n");
8381
8454
  }
8382
8455
  function buildModeExamplesForRunners(runners) {
8383
8456
  return runners.map((r) => {
@@ -8406,7 +8479,7 @@ function verificationModeBlock() {
8406
8479
  ].join("\n");
8407
8480
  }
8408
8481
  function researchPhaseBlock(harnessMode) {
8409
- const shared = [
8482
+ const shared2 = [
8410
8483
  "RESEARCH PHASE:",
8411
8484
  "- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
8412
8485
  ];
@@ -8420,7 +8493,7 @@ function researchPhaseBlock(harnessMode) {
8420
8493
  "- Read key source files to understand architecture"
8421
8494
  ];
8422
8495
  return [
8423
- ...shared,
8496
+ ...shared2,
8424
8497
  ...toolSpecific,
8425
8498
  "- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
8426
8499
  "- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
@@ -8482,28 +8555,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
8482
8555
  verificationBlock
8483
8556
  );
8484
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
+ ];
8485
8567
  function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
8568
+ const tools = variant.plannerTools ?? false;
8486
8569
  return [
8487
- "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.`,
8488
8571
  "",
8489
8572
  "WORKFLOW:",
8490
8573
  "1. Explore the workspace with tools to understand the codebase.",
8491
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.",
8492
8575
  "3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
8493
- '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.',
8494
8577
  "",
8495
8578
  researchPhaseBlock(variant.harness ?? false),
8496
8579
  verificationBlock,
8497
8580
  "",
8498
8581
  "OUTLINE PHASE:",
8499
- "- 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"}.`,
8500
8583
  "- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
8501
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"',
8502
8585
  "- The user may reply with adjustments. Revise the outline and re-present it.",
8503
- "- 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.",
8504
8587
  "",
8505
8588
  "PLAN FORMAT:",
8506
- "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:",
8507
8590
  "",
8508
8591
  "{",
8509
8592
  ' "tasks": [',
@@ -8517,8 +8600,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8517
8600
  ' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
8518
8601
  ' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
8519
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" },`,
8520
- ' "assignedRunner": "' + runners.join("|") + '",',
8521
- ' "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) + '",',
8522
8605
  ' "autonomy": "AFK|HITL",',
8523
8606
  ' "sliceType": "AFK|HITL",',
8524
8607
  ' "ops": true,',
@@ -8550,8 +8633,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8550
8633
  ...opsTaskSection(true),
8551
8634
  "",
8552
8635
  "RULES:",
8553
- "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
8554
- "- 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
+ ],
8555
8643
  "- Reference specific files and patterns from your research in task prompts.",
8556
8644
  ...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
8557
8645
  "",
@@ -8559,23 +8647,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
8559
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.",
8560
8648
  "- Follow the existing code style, naming conventions, and architectural patterns of the project.",
8561
8649
  "",
8562
- "MODEL ASSIGNMENT:",
8563
- "Available models:",
8564
- modelsJson,
8565
- "",
8566
- runnerInstruction(runners),
8650
+ ...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
8567
8651
  "MODEL SELECTION GUIDELINES:",
8568
8652
  "- Use stronger models for complex refactoring, architecture changes, security-critical code.",
8569
8653
  "- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
8570
- `- 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.`,
8571
8655
  "",
8572
8656
  "TASK MODE:",
8573
- 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,
8574
8658
  "",
8575
- // Shared by both variants on purpose: the read channel is a text envelope
8576
- // exactly so a harness planner, which Ordewell cannot hand tools to, speaks
8577
- // the same protocol as an API-backed one (ADR-0009).
8578
- ...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,
8579
8663
  "",
8580
8664
  context ? `PROJECT CONTEXT:
8581
8665
  ${context}
@@ -8817,25 +8901,25 @@ function executionLogBlock(executionLog) {
8817
8901
  if (executionLog.length === 0) return "";
8818
8902
  const completed = executionLog.filter((t) => t.status === "completed");
8819
8903
  const failed = executionLog.filter((t) => t.status === "failed");
8820
- const lines = ["EXECUTION LOG SUMMARY:"];
8821
- 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)`);
8822
8906
  if (completed.length > 0) {
8823
- lines.push("");
8824
- lines.push("COMPLETED TASKS:");
8907
+ lines2.push("");
8908
+ lines2.push("COMPLETED TASKS:");
8825
8909
  for (const t of completed) {
8826
8910
  const verdict = t.verdict ? ` \u2014 ${t.verdict.outcome.toUpperCase()}: ${t.verdict.reason}` : "";
8827
- lines.push(`- [${t.id}] ${t.title}${verdict}`);
8911
+ lines2.push(`- [${t.id}] ${t.title}${verdict}`);
8828
8912
  }
8829
8913
  }
8830
8914
  if (failed.length > 0) {
8831
- lines.push("");
8832
- lines.push("FAILED TASKS (eligible for retry):");
8915
+ lines2.push("");
8916
+ lines2.push("FAILED TASKS (eligible for retry):");
8833
8917
  for (const t of failed) {
8834
8918
  const verdict = t.verdict ? ` \u2014 ${t.verdict.reason}` : "";
8835
- lines.push(`- [${t.id}] ${t.title} (retry #${t.retryCount})${verdict}`);
8919
+ lines2.push(`- [${t.id}] ${t.title} (retry #${t.retryCount})${verdict}`);
8836
8920
  }
8837
8921
  }
8838
- return lines.join("\n");
8922
+ return lines2.join("\n");
8839
8923
  }
8840
8924
  function pendingEditRulesBlock() {
8841
8925
  return [
@@ -8878,7 +8962,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
8878
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.');
8879
8963
  return sections.join("\n");
8880
8964
  }
8881
- function buildMergePrompt(taskIds, tasks) {
8965
+ function buildMergePrompt(taskIds, tasks, tools = false) {
8882
8966
  const idSet = new Set(taskIds);
8883
8967
  const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
8884
8968
  const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
@@ -8886,22 +8970,22 @@ function buildMergePrompt(taskIds, tasks) {
8886
8970
  `Merge these tasks into ONE combined task: ${refs}.`,
8887
8971
  "Write a clear combined title, description, and prompt that cover all of their work.",
8888
8972
  "The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
8889
- '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.',
8890
- 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
8891
- ` {"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":"..."}}}]}`,
8892
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.`
8893
8977
  ].join("\n");
8894
8978
  }
8895
- function buildSplitPrompt(taskId, tasks) {
8979
+ function buildSplitPrompt(taskId, tasks, tools = false) {
8896
8980
  const task = tasks.find((t) => t.id === taskId);
8897
8981
  if (!task) return `Split task ${taskId} into smaller tasks.`;
8898
8982
  return [
8899
8983
  `Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
8900
8984
  "Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
8901
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.",
8902
- '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).',
8903
- 'Reply with ONLY a taskOps JSON object using a single "split" op:',
8904
- ` {"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":"..."}},...]}]}`,
8905
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.`
8906
8990
  ].join("\n");
8907
8991
  }
@@ -8984,25 +9068,37 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = tr
8984
9068
  const detail = err instanceof Error ? err.message : String(err);
8985
9069
  throw new PlanParseError(`Plan response was not valid JSON: ${detail}`, raw);
8986
9070
  }
8987
- if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.tasks)) {
8988
- 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" }] };
8989
9080
  }
8990
- const rawTasks = parsed.tasks;
8991
9081
  if (rawTasks.length === 0) {
8992
- throw new PlanParseError("Plan contained no tasks", raw);
9082
+ return { ok: false, errors: [{ field: "tasks", message: "Plan contained no tasks" }] };
8993
9083
  }
8994
- const tasks = rawTasks.map((t) => parseTask(t, runners, runnerModes, autonomousDefault));
8995
- validateVerticalSliceShape(tasks, raw);
9084
+ const tasks = rawTasks.map((t) => parseTask(asRecord(t), runners, runnerModes, autonomousDefault));
9085
+ const errors = verticalSliceErrors(tasks);
8996
9086
  for (const t of tasks) {
8997
9087
  if (!runners.includes(t.assignedRunner)) {
8998
- throw new PlanParseError(
8999
- `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`,
9000
- raw,
9001
- { semantic: true }
9002
- );
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
+ });
9003
9093
  }
9004
9094
  }
9005
- 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 : {};
9006
9102
  }
9007
9103
  function looksLikePlanAttempt(raw) {
9008
9104
  const { matches, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
@@ -9027,12 +9123,12 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
9027
9123
  // description/title — same fallback TaskOps.applyTaskOps uses for `add`, so
9028
9124
  // a task never silently ends up unschedulable (getReadyTasks requires prompt).
9029
9125
  prompt: raw.prompt ? String(raw.prompt) : description || title || void 0,
9030
- userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map((s) => ({
9126
+ userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map(asRecord).map((s) => ({
9031
9127
  order: Number(s.order ?? 0),
9032
9128
  instruction: String(s.instruction ?? ""),
9033
9129
  completed: false
9034
9130
  })) : void 0,
9035
- 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 })) : [],
9036
9132
  assignedModel: raw.assignedModel ? {
9037
9133
  modelId: String(raw.assignedModel.modelId ?? ""),
9038
9134
  modelLabel: String(raw.assignedModel.modelLabel ?? ""),
@@ -9053,42 +9149,28 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
9053
9149
  ops: !inherited && taskType === "ai" && raw.ops === true
9054
9150
  });
9055
9151
  }
9056
- function validateVerticalSliceShape(tasks, raw, parentTitle) {
9152
+ function verticalSliceErrors(tasks, parentTitle) {
9057
9153
  const where = (t) => parentTitle ? `subtask "${t.title}" of "${parentTitle}"` : `Task "${t.title}"`;
9058
- const semantic = { semantic: true };
9154
+ const errors = [];
9059
9155
  for (const t of tasks) {
9156
+ const fail = (field, message) => errors.push({ taskId: t.id, field, message });
9060
9157
  if (!t.sliceType) {
9061
- throw new PlanParseError(
9062
- `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`,
9063
- raw,
9064
- semantic
9065
- );
9158
+ fail("sliceType", `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`);
9066
9159
  }
9067
9160
  if (t.type === "ai" && !t.autonomy) {
9068
- throw new PlanParseError(
9069
- `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`,
9070
- raw,
9071
- semantic
9072
- );
9161
+ fail("autonomy", `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`);
9073
9162
  }
9074
9163
  if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
9075
- throw new PlanParseError(
9076
- `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`,
9077
- raw,
9078
- semantic
9079
- );
9164
+ fail("userSteps", `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
9080
9165
  }
9081
- if (t.type === "user" && t.sliceType !== "HITL") {
9082
- throw new PlanParseError(
9083
- `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`,
9084
- raw,
9085
- semantic
9086
- );
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".`);
9087
9168
  }
9088
9169
  if (t.subtasks.length > 0) {
9089
- validateVerticalSliceShape(t.subtasks, raw, t.title);
9170
+ errors.push(...verticalSliceErrors(t.subtasks, t.title));
9090
9171
  }
9091
9172
  }
9173
+ return errors;
9092
9174
  }
9093
9175
  function checkUniqueIds(ctx) {
9094
9176
  const seen = /* @__PURE__ */ new Set();
@@ -9391,11 +9473,11 @@ var UPDATABLE_FIELDS = [
9391
9473
  "sliceType",
9392
9474
  "ops"
9393
9475
  ];
9394
- function taskOpsProtocol(executionNote = "") {
9476
+ function taskOpsProtocol(executionNote = "", tools = false) {
9395
9477
  const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
9396
9478
  return [
9397
- "- To modify specific tasks, reply with ONLY this JSON object:",
9398
- ' {"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": [',
9399
9481
  ` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
9400
9482
  ' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"ops"?},"handle"?:"<name>"},',
9401
9483
  ' {"op":"remove","taskId":"<id or #order>"},',
@@ -9404,11 +9486,12 @@ function taskOpsProtocol(executionNote = "") {
9404
9486
  ' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
9405
9487
  ` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
9406
9488
  " ]}",
9407
- '- 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.',
9408
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.',
9409
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.',
9410
- '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>"}.`,
9411
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."] : [],
9412
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,
9413
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.',
9414
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.'
@@ -9640,21 +9723,21 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9640
9723
  const handleOwner = /* @__PURE__ */ new Map();
9641
9724
  const handleId = /* @__PURE__ */ new Map();
9642
9725
  for (const [i, op] of ops.entries()) {
9643
- const handle = op.handle;
9644
- if (handle === void 0) continue;
9645
- if (typeof handle !== "string" || !handle.trim()) {
9726
+ const handle2 = op.handle;
9727
+ if (handle2 === void 0) continue;
9728
+ if (typeof handle2 !== "string" || !handle2.trim()) {
9646
9729
  errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
9647
9730
  continue;
9648
9731
  }
9649
- if (handleOwner.has(handle)) {
9650
- 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`);
9651
9734
  continue;
9652
9735
  }
9653
- if (refCollidesWithExisting(handle, originalFlatAll)) {
9654
- 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`);
9655
9738
  continue;
9656
9739
  }
9657
- handleOwner.set(handle, i);
9740
+ handleOwner.set(handle2, i);
9658
9741
  }
9659
9742
  if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
9660
9743
  const resolveDeps = (deps, opIndex) => {
@@ -10021,15 +10104,15 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
10021
10104
 
10022
10105
  // src/services/PlanRepair.ts
10023
10106
  async function repairLoop(opts) {
10024
- let reply = await opts.first();
10107
+ let reply2 = await opts.first();
10025
10108
  for (let repairs = 0; ; repairs++) {
10026
- const verdict = await opts.interpret(reply);
10109
+ const verdict = await opts.interpret(reply2);
10027
10110
  if ("done" in verdict) return verdict.done;
10028
10111
  if (repairs >= opts.maxRepairs) {
10029
- 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 });
10030
10113
  }
10031
10114
  const { corrective } = verdict.retry;
10032
- reply = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
10115
+ reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
10033
10116
  }
10034
10117
  }
10035
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.";
@@ -10145,34 +10228,34 @@ async function settleReply(opts) {
10145
10228
  }
10146
10229
  if (attempt.failure !== void 0) return { done: message(attempt.failure) };
10147
10230
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
10148
- const reply = classifyPlannerReply(attempt.text, opts.classify);
10149
- switch (reply.kind) {
10231
+ const reply2 = classifyPlannerReply(attempt.text, opts.classify);
10232
+ switch (reply2.kind) {
10150
10233
  case "plan":
10151
- return { done: { kind: "plan", tasks: reply.tasks, text: said(attempt), researchLog } };
10234
+ return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
10152
10235
  case "task_ops":
10153
- 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 } };
10154
10237
  // A read is answered by the conversation, which owns the plan and the
10155
10238
  // catalog, so it leaves here the way a plan or an edit does.
10156
10239
  case "task_query":
10157
- 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 } };
10158
10241
  case "prose":
10159
10242
  return { done: asProse(attempt) };
10160
10243
  }
10161
10244
  if (opts.signal?.aborted) return { done: asProse(attempt) };
10162
- const errors = [reply.error.message];
10163
- switch (reply.kind) {
10245
+ const errors = [reply2.error.message];
10246
+ switch (reply2.kind) {
10164
10247
  case "broken_task_ops":
10165
- return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply.error.message), cause: reply.error } };
10248
+ return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
10166
10249
  case "broken_task_query":
10167
- return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply.error.message), cause: reply.error } };
10250
+ return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
10168
10251
  case "broken_plan": {
10169
- const corrective = reply.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply.error.message);
10170
- 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 } };
10171
10254
  }
10172
10255
  }
10173
10256
  },
10174
10257
  maxRepairs: MAX_JSON_REPAIRS,
10175
- onExhausted: ({ reply }) => asProse(reply)
10258
+ onExhausted: ({ reply: reply2 }) => asProse(reply2)
10176
10259
  });
10177
10260
  }
10178
10261
  var said = (attempt) => attempt.fullText ?? attempt.text;
@@ -10399,8 +10482,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
10399
10482
  const blocks = [{ tool: null, lines: [] }];
10400
10483
  for (const line of resultsText.split("\n")) {
10401
10484
  const m = headerRe.exec(line);
10402
- const tool = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
10403
- 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] });
10404
10487
  else blocks[blocks.length - 1].lines.push(line);
10405
10488
  }
10406
10489
  const texts = blocks.map((b) => b.lines.join("\n"));
@@ -10494,9 +10577,9 @@ function ellipsize(text2, max) {
10494
10577
  const oneLine2 = text2.replace(/\s+/g, " ").trim();
10495
10578
  return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
10496
10579
  }
10497
- function summarizeToolCall(tool, argsJson, toolLabel) {
10498
- const display = toolLabel?.trim() || tool;
10499
- if (tool === "agent_tool") {
10580
+ function summarizeToolCall(tool2, argsJson, toolLabel) {
10581
+ const display = toolLabel?.trim() || tool2;
10582
+ if (tool2 === "agent_tool") {
10500
10583
  try {
10501
10584
  const args = JSON.parse(argsJson);
10502
10585
  const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
@@ -10505,7 +10588,7 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
10505
10588
  }
10506
10589
  return display;
10507
10590
  }
10508
- if (tool === "spawn_research_agent") {
10591
+ if (tool2 === "spawn_research_agent") {
10509
10592
  try {
10510
10593
  const args = JSON.parse(argsJson);
10511
10594
  const prompt = typeof args.prompt === "string" ? args.prompt : "";
@@ -10517,19 +10600,19 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
10517
10600
  }
10518
10601
  try {
10519
10602
  const args = JSON.parse(argsJson);
10520
- if (tool === "find_symbol" && args.symbol) {
10603
+ if (tool2 === "find_symbol" && args.symbol) {
10521
10604
  return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
10522
10605
  }
10523
- if (tool === "web_search" && args.query) {
10606
+ if (tool2 === "web_search" && args.query) {
10524
10607
  return `web_search "${ellipsize(String(args.query), 50)}"`;
10525
10608
  }
10526
- if (tool === "fetch" && args.url) {
10609
+ if (tool2 === "fetch" && args.url) {
10527
10610
  return `fetch ${ellipsize(String(args.url), 60)}`;
10528
10611
  }
10529
- if (tool === "glob" && args.pattern) {
10612
+ if (tool2 === "glob" && args.pattern) {
10530
10613
  return `glob ${ellipsize(String(args.pattern), 50)}`;
10531
10614
  }
10532
- if (tool === "grep" && args.pattern) {
10615
+ if (tool2 === "grep" && args.pattern) {
10533
10616
  const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
10534
10617
  const scope = args.include ? ` in ${args.include}` : "";
10535
10618
  return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
@@ -10962,9 +11045,9 @@ ${llmOutput}
10962
11045
  return { tasks: null, researchLog, researchResults };
10963
11046
  }
10964
11047
  const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
10965
- const reply = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
10966
- if (reply.kind === "plan") {
10967
- 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 };
10968
11051
  }
10969
11052
  if (!turn.hasToolCalls) break;
10970
11053
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
@@ -11681,7 +11764,7 @@ ${collected.aiflowContext}
11681
11764
  };
11682
11765
 
11683
11766
  // src/services/TaskOrchestrator.ts
11684
- var path14 = __toESM(require("path"));
11767
+ var path15 = __toESM(require("path"));
11685
11768
 
11686
11769
  // src/services/promptAugment.ts
11687
11770
  var MAX_TAIL_CHARS = 500;
@@ -11768,7 +11851,7 @@ function renderPlanMap(planTasks, currentTaskId, opts) {
11768
11851
  const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
11769
11852
  const { window, omitted } = pickWindow(sorted, currentIdx, max);
11770
11853
  const currentTask = sorted.find((r) => r.task.id === currentTaskId)?.task;
11771
- const lines = window.map((row) => {
11854
+ const lines2 = window.map((row) => {
11772
11855
  const isCurrent = row.task.id === currentTaskId;
11773
11856
  const tag = planMapStatus(row.task, isCurrent);
11774
11857
  const order = row.parent ? row.label : row.label.padStart(2, " ");
@@ -11785,7 +11868,7 @@ You are running as: ${currentTask.assignedRunner}` : "";
11785
11868
  "",
11786
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.",
11787
11870
  "",
11788
- lines.join("\n") + footer + runnerNote
11871
+ lines2.join("\n") + footer + runnerNote
11789
11872
  ].join("\n");
11790
11873
  }
11791
11874
  function renderPreviousAttempt(output) {
@@ -11799,10 +11882,14 @@ function renderPreviousAttempt(output) {
11799
11882
  tail
11800
11883
  ].join("\n");
11801
11884
  }
11802
- 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}`;
11803
11890
  return `
11804
11891
 
11805
- 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}`;
11806
11893
  }
11807
11894
  function renderTddInstruction() {
11808
11895
  return [
@@ -11823,7 +11910,21 @@ function renderTddInstruction() {
11823
11910
  ].join("\n");
11824
11911
  }
11825
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 `>>>`";
11826
- 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
+ }
11827
11928
  return [
11828
11929
  "## Human-in-the-loop checkpoints",
11829
11930
  "",
@@ -11855,9 +11956,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
11855
11956
  blocks.push(renderTddInstruction());
11856
11957
  }
11857
11958
  if (isHitlTask(task)) {
11858
- blocks.push(renderCheckpointInstruction());
11959
+ blocks.push(renderCheckpointInstruction(opts?.completionTool));
11859
11960
  }
11860
- const marker2 = renderCompletionMarker(task);
11961
+ const marker2 = renderCompletionMarker(task, opts?.completionTool);
11861
11962
  if (blocks.length === 0) return basePrompt + marker2;
11862
11963
  return `${blocks.join("\n\n")}
11863
11964
 
@@ -11868,12 +11969,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
11868
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."
11869
11970
  ];
11870
11971
  if (isHitlTask(task)) {
11871
- 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}.`);
11872
11973
  }
11873
11974
  return `${message.trim()}
11874
11975
 
11875
11976
  ---
11876
- ${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
11977
+ ${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
11877
11978
  }
11878
11979
 
11879
11980
  // src/services/VerdictEngine.ts
@@ -11891,6 +11992,13 @@ var VerdictEngine = class {
11891
11992
  pausedSessions = /* @__PURE__ */ new Map();
11892
11993
  listeners = [];
11893
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();
11894
12002
  idleListeners = [];
11895
12003
  /**
11896
12004
  * Per-task generation. Replaced on every watch(), clear() and verdict.
@@ -11921,6 +12029,9 @@ var VerdictEngine = class {
11921
12029
  onCheckpoint(listener) {
11922
12030
  this.checkpointListeners.push(listener);
11923
12031
  }
12032
+ onCheckpointWithdrawn(listener) {
12033
+ this.withdrawnListeners.push(listener);
12034
+ }
11924
12035
  onIdleChange(listener) {
11925
12036
  this.idleListeners.push(listener);
11926
12037
  }
@@ -11982,6 +12093,11 @@ ${line}
11982
12093
  }
11983
12094
  approveCheckpoint(taskId) {
11984
12095
  this.resumeIdle(taskId);
12096
+ const toolCall = this.toolCheckpoints.get(taskId);
12097
+ if (toolCall) {
12098
+ toolCall({ kind: "continue" });
12099
+ return;
12100
+ }
11985
12101
  const session = this.pausedSessions.get(taskId);
11986
12102
  if (session) {
11987
12103
  session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
@@ -11990,6 +12106,11 @@ ${line}
11990
12106
  }
11991
12107
  rejectCheckpoint(taskId, reason) {
11992
12108
  this.resumeIdle(taskId);
12109
+ const toolCall = this.toolCheckpoints.get(taskId);
12110
+ if (toolCall) {
12111
+ toolCall({ kind: "rejected", reason });
12112
+ return;
12113
+ }
11993
12114
  const session = this.pausedSessions.get(taskId);
11994
12115
  if (session) {
11995
12116
  session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
@@ -12000,7 +12121,10 @@ ${line}
12000
12121
  * Attach to a spawned session: scan the output tail for the task's completion
12001
12122
  * marker (delivering a verdict immediately while leaving interactive sessions
12002
12123
  * open), scan for checkpoint markers, and on exit produce a failed verdict
12003
- * 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.
12004
12128
  */
12005
12129
  watch(task, session) {
12006
12130
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
@@ -12030,6 +12154,8 @@ ${line}
12030
12154
  else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
12031
12155
  else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
12032
12156
  });
12157
+ session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
12158
+ session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
12033
12159
  }
12034
12160
  session.onExit((exitCode) => {
12035
12161
  if (this.generations.get(task.id) !== gen) return;
@@ -12038,6 +12164,52 @@ ${line}
12038
12164
  const verdict = this.decide(task, exitCode);
12039
12165
  for (const l of this.listeners) l(task.id, verdict);
12040
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." });
12041
12213
  }
12042
12214
  approvalOpened(taskId, approvalId) {
12043
12215
  const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
@@ -12071,6 +12243,7 @@ ${line}
12071
12243
  this.markerTails.delete(taskId);
12072
12244
  this.checkpointCarry.delete(taskId);
12073
12245
  this.pausedSessions.delete(taskId);
12246
+ this.withdrawToolCheckpoint(taskId);
12074
12247
  this.idlePaused.delete(taskId);
12075
12248
  this.openApprovals.delete(taskId);
12076
12249
  this.clearIdle(taskId);
@@ -12106,6 +12279,7 @@ ${line}
12106
12279
  this.markerTails.clear();
12107
12280
  this.checkpointCarry.clear();
12108
12281
  this.pausedSessions.clear();
12282
+ for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
12109
12283
  for (const timer of this.idleTimers.values()) clearTimeout(timer);
12110
12284
  this.idleTimers.clear();
12111
12285
  this.idleSince.clear();
@@ -12159,6 +12333,28 @@ ${line}
12159
12333
  };
12160
12334
  }
12161
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
+ }
12162
12358
 
12163
12359
  // src/services/transcriptCapture.ts
12164
12360
  var import_fs = require("fs");
@@ -12322,10 +12518,10 @@ async function fromDirenv(cwd, deps) {
12322
12518
  }
12323
12519
  return { env, blocked: null };
12324
12520
  }
12325
- function findUp(start, relative5, deps) {
12521
+ function findUp(start, relative6, deps) {
12326
12522
  let dir = path10.resolve(start);
12327
12523
  for (; ; ) {
12328
- const candidate = path10.join(dir, relative5);
12524
+ const candidate = path10.join(dir, relative6);
12329
12525
  if (deps.readFile(candidate) !== null) return candidate;
12330
12526
  const parent = path10.dirname(dir);
12331
12527
  if (parent === dir) return null;
@@ -12531,10 +12727,10 @@ function codexFinal(home, query, maxChars) {
12531
12727
  function codexLastAssistant(file, query) {
12532
12728
  const raw = (0, import_fs.readFileSync)(file, "utf8");
12533
12729
  if (!raw.includes(query.marker)) return null;
12534
- const lines = raw.split("\n").filter((l) => l.trim());
12730
+ const lines2 = raw.split("\n").filter((l) => l.trim());
12535
12731
  let sawCwd = false;
12536
12732
  let last = null;
12537
- for (const line of lines) {
12733
+ for (const line of lines2) {
12538
12734
  let rec;
12539
12735
  try {
12540
12736
  rec = JSON.parse(line);
@@ -12574,6 +12770,11 @@ var BufferedTaskOutputSource = class {
12574
12770
  session.onExit(() => {
12575
12771
  capture.exited = true;
12576
12772
  });
12773
+ if (isStructuredSession(session)) {
12774
+ session.onTaskComplete(({ summary }) => {
12775
+ if (capture.attached && summary.trim()) capture.reported = summary.trim();
12776
+ });
12777
+ }
12577
12778
  }
12578
12779
  detach(taskId) {
12579
12780
  const capture = this.captures.get(taskId);
@@ -12584,6 +12785,8 @@ var BufferedTaskOutputSource = class {
12584
12785
  this.captures.clear();
12585
12786
  }
12586
12787
  async finalText(attempt, doneToken) {
12788
+ const reported = this.captures.get(attempt.taskId)?.reported;
12789
+ if (reported) return defuseMarkers(reported);
12587
12790
  if (attempt.cwd && attempt.completionMarker) {
12588
12791
  const transcript = await this.transcripts.finalAssistantText({
12589
12792
  runner: attempt.runner,
@@ -12601,8 +12804,8 @@ var BufferedTaskOutputSource = class {
12601
12804
  if (!capture) return null;
12602
12805
  const total = capture.dropped + capture.raw.length;
12603
12806
  const from = Math.min(Math.max(0, (opts.sinceOffset ?? 0) - capture.dropped), capture.raw.length);
12604
- const lines = renderCleanCapture(capture.raw.slice(from)).split("\n");
12605
- 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) : [];
12606
12809
  return {
12607
12810
  text: kept.join("\n"),
12608
12811
  nextOffset: total,
@@ -13423,8 +13626,8 @@ var IsolationRunController = class {
13423
13626
  this.resolvers = {};
13424
13627
  this.reportedCopies.clear();
13425
13628
  this.listener.changed();
13426
- const shared = sharedPathsNotice(this.run);
13427
- if (shared) this.tell("info", shared);
13629
+ const shared2 = sharedPathsNotice(this.run);
13630
+ if (shared2) this.tell("info", shared2);
13428
13631
  }
13429
13632
  reportCopies(copied) {
13430
13633
  const fresh = copied.filter((p) => !this.reportedCopies.has(p));
@@ -14596,166 +14799,724 @@ ${tail}` : ""}`;
14596
14799
  }
14597
14800
  };
14598
14801
 
14599
- // src/services/harness/ClaudeCodeAdapter.ts
14600
- var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
14601
- var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
14602
- var PROTOCOL_ARGS = [
14603
- "-p",
14604
- "--input-format",
14605
- "stream-json",
14606
- "--output-format",
14607
- "stream-json",
14608
- "--verbose",
14609
- "--include-partial-messages"
14610
- ];
14611
- var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
14612
- var FOLLOW_ON_TURN_GRACE_MS = 5e3;
14613
- var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
14614
- var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
14615
- function usageRecord(usage, model, subagentId) {
14616
- const record = {
14617
- source: "claude-code",
14618
- ...partedPromptUsage({ uncached: usage.input_tokens, cacheRead: usage.cache_read_input_tokens, cacheWrite: usage.cache_creation_input_tokens })
14619
- };
14620
- if (model) record.model = model;
14621
- if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
14622
- if (subagentId) record.subagentId = subagentId;
14623
- return record;
14624
- }
14625
- function subagentFinalCall(result) {
14626
- if (typeof result !== "object" || result === null) return null;
14627
- const { usage, resolvedModel } = result;
14628
- if (typeof usage !== "object" || usage === null) return null;
14629
- 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"}`;
14630
14807
  }
14631
- function notificationOutcome(status) {
14632
- if (status === "completed") return "done";
14633
- if (status === "killed" || status === "stopped") return "stopped";
14634
- 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;
14635
14828
  }
14636
- function blocksOf(msg) {
14637
- return Array.isArray(msg.message?.content) ? msg.message.content : [];
14829
+ function range(start, lines2) {
14830
+ return lines2 === 1 ? `${start}` : `${start},${lines2}`;
14638
14831
  }
14639
- function flattenContent(content) {
14640
- if (typeof content === "string") return content;
14641
- if (Array.isArray(content)) {
14642
- 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("")}`;
14643
14843
  }
14644
- return "";
14844
+ return text2;
14645
14845
  }
14646
- var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14647
- agentId = "claude-code";
14648
- interruptCount = 0;
14649
- /** Interrupts sent and not yet acknowledged, by request id. */
14650
- pendingControl = /* @__PURE__ */ new Map();
14651
- /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
14652
- interruptRequested = false;
14653
- /** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
14654
- openPermissions = /* @__PURE__ */ new Map();
14655
- /** Whether this turn has already emitted reply text — see {@link handleLine}. */
14656
- turnHasText = false;
14657
- /** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
14658
- pendingBreak = false;
14659
- /** The model answering the planner's current message, as its `message_start` named it. */
14660
- plannerModel;
14661
- /**
14662
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
14663
- * the CLI restores the resumed session's running total, and what Ordewell
14664
- * already counted of it is not ours to know here.
14665
- */
14666
- reportedCostUsd = 0;
14667
- /**
14668
- * Subagents started and not yet finished, keyed by the `Agent` call's id.
14669
- * Kept across turns: a backgrounded one reports after its turn has ended.
14670
- */
14671
- openSubagents = /* @__PURE__ */ new Map();
14672
- /** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
14673
- countedSubagentMessages = /* @__PURE__ */ new Set();
14674
- /** Background tasks (shells, agents) the CLI reports as still running, as of its last `background_tasks_changed`. */
14675
- backgroundTaskCount = 0;
14676
- /**
14677
- * A task's turn whose `result` arrived while background work was still open.
14678
- * The CLI reports that result when the model stops talking, then opens a turn
14679
- * of its own when the work finishes; a turn ended at the first result would
14680
- * lose everything said after it, the completion marker included.
14681
- */
14682
- resultHeld = false;
14683
- followOnTimer = null;
14684
- /** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
14685
- requestedMode = null;
14686
- /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
14687
- pendingResume = null;
14688
- spawnSpec(opts) {
14689
- if (opts.kind === "task") return this.taskSpawnSpec(opts);
14690
- const args = [
14691
- ...PROTOCOL_ARGS,
14692
- // The read-only guarantee, enforced at spawn rather than by prompt.
14693
- "--permission-mode",
14694
- "plan",
14695
- "--disallowedTools",
14696
- DISALLOWED_TOOLS.join(","),
14697
- "--append-system-prompt",
14698
- opts.systemPrompt
14699
- ];
14700
- if (opts.model) args.push("--model", opts.model);
14701
- if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
14702
- if (opts.resumeSessionId) {
14703
- args.push("--resume", opts.resumeSessionId);
14704
- this.reportedCostUsd = void 0;
14705
- 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);
14706
14876
  }
14707
- return { command: "claude", args };
14708
14877
  }
14709
- /**
14710
- * A task's run: the manifest decides what its mode and effort mean
14711
- * (ADR-0001), and this adds only the protocol around them. No tool list and
14712
- * no system prompt — the task's prompt is its first turn, as on the terminal
14713
- * transport. `--permission-prompt-tool stdio` routes the questions the mode
14714
- * leaves open to the control channel, where the adapter must answer them;
14715
- * without it `-p` refuses them silently and nothing can ever surface one.
14716
- */
14717
- taskSpawnSpec(opts) {
14718
- this.requestedMode = opts.flags.permissionMode;
14719
- const args = [
14720
- ...PROTOCOL_ARGS,
14721
- "--permission-prompt-tool",
14722
- "stdio",
14723
- "--permission-mode",
14724
- opts.flags.permissionMode,
14725
- "--disallowedTools",
14726
- TASK_DISALLOWED_TOOLS.join(","),
14727
- ...opts.flags.effort ? claudeThinkingArgs(opts.flags.effort) : []
14728
- ];
14729
- if (opts.model) args.push("--model", opts.model);
14730
- if (opts.resumeSessionId) {
14731
- args.push("--resume", opts.resumeSessionId);
14732
- this.reportedCostUsd = void 0;
14733
- 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);
14734
14892
  }
14735
- 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);
14736
14900
  }
14737
- /**
14738
- * Claude Code's soft interrupt: the turn stops, the process and its session
14739
- * stay. The CLI acknowledges on the control channel, then closes the turn
14740
- * with an `error_during_execution` result, which {@link handleLine} reports
14741
- * as an interrupted `turn_end`.
14742
- */
14743
- interrupt(timeoutMs) {
14744
- if (!this.process) return Promise.resolve(false);
14745
- this.interruptCount += 1;
14746
- const requestId = `ordewell-interrupt-${this.interruptCount}`;
14747
- this.interruptRequested = true;
14748
- return new Promise((resolve7) => {
14749
- const settle = (ok) => {
14750
- if (!this.pendingControl.delete(requestId)) return;
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) => {
15511
+ if (!this.pendingControl.delete(requestId)) return;
14751
15512
  clearTimeout(timer);
14752
- resolve7(ok);
15513
+ resolve7(response);
14753
15514
  };
14754
- const timer = setTimeout(() => settle(false), timeoutMs);
15515
+ const timer = setTimeout(() => settle(null), timeoutMs);
14755
15516
  timer.unref?.();
14756
15517
  this.pendingControl.set(requestId, settle);
14757
- void this.processEnded.then(() => settle(false));
14758
- 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 });
14759
15520
  });
14760
15521
  }
14761
15522
  /**
@@ -14790,6 +15551,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14790
15551
  }
14791
15552
  dispose() {
14792
15553
  this.clearFollowOnTimer();
15554
+ this.removeMcpConfig();
14793
15555
  super.dispose();
14794
15556
  }
14795
15557
  clearFollowOnTimer() {
@@ -14840,12 +15602,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14840
15602
  emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
14841
15603
  return;
14842
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
+ }
14843
15611
  const suggestions = msg.request.permission_suggestions ?? [];
14844
15612
  this.openPermissions.set(id, { input, suggestions });
14845
15613
  emit({
14846
15614
  type: "permission_request",
14847
15615
  id,
14848
- name: msg.request.tool_name ?? "unknown",
15616
+ name,
14849
15617
  detail: JSON.stringify(input),
14850
15618
  input,
14851
15619
  suggestions,
@@ -14861,7 +15629,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
14861
15629
  }
14862
15630
  case "control_response": {
14863
15631
  const requestId = msg.response?.request_id;
14864
- if (requestId) this.pendingControl.get(requestId)?.(msg.response?.subtype === "success");
15632
+ if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
14865
15633
  return;
14866
15634
  }
14867
15635
  case "assistant":
@@ -14889,6 +15657,7 @@ ${block.text}` : block.text });
14889
15657
  if (block.type !== "tool_result") continue;
14890
15658
  const id = block.tool_use_id ?? "";
14891
15659
  const output = flattenContent(block.content);
15660
+ const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
14892
15661
  const subagent = subagentId ? void 0 : this.openSubagents.get(id);
14893
15662
  if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
14894
15663
  emit({ type: "background_agent", id });
@@ -14896,7 +15665,7 @@ ${block.text}` : block.text });
14896
15665
  } else if (subagent) {
14897
15666
  this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
14898
15667
  }
14899
- 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 });
14900
15669
  }
14901
15670
  return;
14902
15671
  case "system":
@@ -15032,6 +15801,9 @@ ${block.text}` : block.text });
15032
15801
  }
15033
15802
  };
15034
15803
 
15804
+ // src/services/harness/CodexAdapter.ts
15805
+ var import_path4 = require("path");
15806
+
15035
15807
  // src/services/harness/codexSandbox.ts
15036
15808
  var PROBE_TIMEOUT_MS = 1e4;
15037
15809
  var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
@@ -15091,6 +15863,17 @@ function runProbe(deps, cwd, env, flags) {
15091
15863
 
15092
15864
  // src/services/harness/CodexAdapter.ts
15093
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
+ }
15094
15877
  function subagentOutcome(status) {
15095
15878
  switch (status) {
15096
15879
  case "completed":
@@ -15124,6 +15907,28 @@ var DECLINE_RESULTS = {
15124
15907
  execCommandApproval: { decision: "denied" },
15125
15908
  applyPatchApproval: { decision: "denied" }
15126
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
+ }
15127
15932
  var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
15128
15933
  var TASK_APPROVALS = {
15129
15934
  "item/commandExecution/requestApproval": {
@@ -15198,11 +16003,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
15198
16003
  * names only the item, and a person deciding needs to see the files.
15199
16004
  */
15200
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();
15201
16008
  /** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
15202
16009
  pendingRequests = /* @__PURE__ */ new Map();
15203
16010
  resumeAttempted = false;
15204
16011
  resumeFallbackSent = false;
15205
16012
  sandbox = "default";
16013
+ /** The Ordewell server's startup state, as Codex last reported it for this thread. */
16014
+ mcpStartup = null;
16015
+ mcpStartupSettled = null;
15206
16016
  /**
15207
16017
  * Subagent threads this session has spawned, keyed by the child thread id.
15208
16018
  * Codex runs a subagent in its own thread and replays both threads' events on
@@ -15211,7 +16021,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
15211
16021
  subagents = /* @__PURE__ */ new Map();
15212
16022
  spawnSpec(opts) {
15213
16023
  this.startOpts = opts;
15214
- 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 } : {} };
15215
16027
  }
15216
16028
  /**
15217
16029
  * `initialize`, then `thread/start`, both before the first user message. A
@@ -15278,7 +16090,11 @@ ${this.exitMessage()}`);
15278
16090
  cwd: opts?.cwd,
15279
16091
  ...opts?.model ? { model: opts.model } : {}
15280
16092
  };
15281
- 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 } : {};
15282
16098
  if (opts?.kind === "task") {
15283
16099
  const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
15284
16100
  return {
@@ -15286,14 +16102,15 @@ ${this.exitMessage()}`);
15286
16102
  sandbox: opts.flags.permissionMode,
15287
16103
  ...approvalPolicy ? { approvalPolicy } : {},
15288
16104
  ...approvalsReviewer ? { approvalsReviewer } : {},
15289
- ...legacyLandlock
16105
+ ...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
16106
+ ...threadConfig
15290
16107
  };
15291
16108
  }
15292
16109
  return {
15293
16110
  ...common,
15294
16111
  sandbox: "read-only",
15295
16112
  approvalPolicy: "never",
15296
- ...legacyLandlock,
16113
+ ...threadConfig,
15297
16114
  // `developerInstructions` layers on top of Codex's own base prompt, the
15298
16115
  // way Claude Code's `--append-system-prompt` does. `baseInstructions`
15299
16116
  // replaces it — which takes Codex's description of its own tools with
@@ -15302,6 +16119,32 @@ ${this.exitMessage()}`);
15302
16119
  developerInstructions: opts?.systemPrompt
15303
16120
  };
15304
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
+ }
15305
16148
  effort() {
15306
16149
  const opts = this.startOpts;
15307
16150
  return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
@@ -15473,6 +16316,13 @@ ${this.exitMessage()}`);
15473
16316
  if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
15474
16317
  return;
15475
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
+ }
15476
16326
  case "thread/tokenUsage/updated":
15477
16327
  this.emitUsage(msg.params, emit);
15478
16328
  return;
@@ -15611,6 +16461,10 @@ ${this.exitMessage()}`);
15611
16461
  const method = msg.method;
15612
16462
  const params = msg.params ?? {};
15613
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
+ }
15614
16468
  if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
15615
16469
  const id = String(msg.id);
15616
16470
  const input = this.approvalInput(method, params);
@@ -15651,6 +16505,10 @@ ${this.exitMessage()}`);
15651
16505
  */
15652
16506
  answerServerRequest(msg, emit) {
15653
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
+ }
15654
16512
  const result = DECLINE_RESULTS[method];
15655
16513
  if (result) {
15656
16514
  this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
@@ -15677,7 +16535,7 @@ ${this.exitMessage()}`);
15677
16535
  emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
15678
16536
  return;
15679
16537
  case "mcpToolCall":
15680
- 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 } : {} });
15681
16539
  return;
15682
16540
  case "dynamicToolCall":
15683
16541
  emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
@@ -15689,7 +16547,7 @@ ${this.exitMessage()}`);
15689
16547
  const paths = (item.changes ?? []).map((change) => change.path).filter((path27) => !!path27);
15690
16548
  this.fileChangePaths.set(item.id, paths);
15691
16549
  if (this.startOpts?.kind === "task") {
15692
- 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);
15693
16551
  }
15694
16552
  return;
15695
16553
  }
@@ -15746,7 +16604,7 @@ ${item.text}` : item.text });
15746
16604
  emit({
15747
16605
  type: "tool_result",
15748
16606
  id,
15749
- name: item.tool ?? "tool",
16607
+ name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
15750
16608
  output: item.error ?? flattenText(item.result) ?? "",
15751
16609
  success: item.status !== "error" && item.success !== false && !item.error,
15752
16610
  ...subagentId ? { subagentId } : {}
@@ -15760,8 +16618,11 @@ ${item.text}` : item.text });
15760
16618
  case "fileChange":
15761
16619
  this.fileChangePaths.delete(id);
15762
16620
  if (this.startOpts?.kind === "task") {
15763
- const diff = (item.changes ?? []).map((change) => change.diff ?? "").join("\n");
15764
- 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);
15765
16626
  return;
15766
16627
  }
15767
16628
  emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
@@ -15770,6 +16631,34 @@ ${item.text}` : item.text });
15770
16631
  return;
15771
16632
  }
15772
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
+ }
15773
16662
  /**
15774
16663
  * A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
15775
16664
  * subagent. The call itself is planner-level tool activity; the lifecycle it
@@ -15809,7 +16698,45 @@ ${item.text}` : item.text });
15809
16698
  };
15810
16699
 
15811
16700
  // src/services/harness/OpenCodeAdapter.ts
15812
- 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
+ }
15813
16740
 
15814
16741
  // src/services/harness/OpenCodeV2.ts
15815
16742
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
@@ -15892,12 +16819,16 @@ var OpenCodeV2 = class {
15892
16819
  const created = await this.host.json("POST", "/api/session", {
15893
16820
  agent: this.agent,
15894
16821
  ...this.model ? { model: this.model } : {},
15895
- permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
16822
+ permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
15896
16823
  });
15897
16824
  if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
15898
16825
  this.sessionId = created.data.id;
15899
16826
  await this.applyInstructions();
15900
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
+ }
15901
16832
  /** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
15902
16833
  async applyInstructions() {
15903
16834
  if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
@@ -16035,8 +16966,8 @@ var OpenCodeV2 = class {
16035
16966
  });
16036
16967
  return true;
16037
16968
  }
16038
- async replyPermission(id, sessionId, reply) {
16039
- 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);
16040
16971
  }
16041
16972
  async openStream(state, turn, onEvent, onActivity) {
16042
16973
  const streamAbort = new AbortController();
@@ -16276,6 +17207,11 @@ var OpenCodeV2 = class {
16276
17207
  const request = this.permissionRequest(ask);
16277
17208
  if (!request || state.seen.has(`perm:${request.id}`)) return;
16278
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
+ }
16279
17215
  onEvent(request);
16280
17216
  void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
16281
17217
  });
@@ -16291,7 +17227,7 @@ var OpenCodeV2 = class {
16291
17227
  const request = this.permissionRequest(ask);
16292
17228
  if (!request || state.seen.has(`perm:${request.id}`)) return;
16293
17229
  state.seen.add(`perm:${request.id}`);
16294
- 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) {
16295
17231
  onEvent({ ...request, decided: { decision: "allow" } });
16296
17232
  void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
16297
17233
  });
@@ -16310,6 +17246,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
16310
17246
  var RECOVERY_TIMEOUT_MS = 9e5;
16311
17247
  var STATUS_POLL_INTERVAL_MS = 1e3;
16312
17248
  var SERVER_USERNAME = "opencode";
17249
+ var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
17250
+ var MCP_STATUS_POLL_MS2 = 200;
16313
17251
  var DISABLED_TOOLS = {
16314
17252
  question: false,
16315
17253
  edit: false,
@@ -16326,20 +17264,20 @@ function splitModelId2(id) {
16326
17264
  }
16327
17265
  function describeError(err) {
16328
17266
  const visited = /* @__PURE__ */ new Set();
16329
- const lines = [];
17267
+ const lines2 = [];
16330
17268
  let current = err;
16331
17269
  while (current && !visited.has(current)) {
16332
17270
  visited.add(current);
16333
17271
  if (!(current instanceof Error)) {
16334
- lines.push(String(current));
17272
+ lines2.push(String(current));
16335
17273
  break;
16336
17274
  }
16337
17275
  const code = current.code;
16338
17276
  const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
16339
- lines.push(`${current.message}${suffix}`);
17277
+ lines2.push(`${current.message}${suffix}`);
16340
17278
  current = current.cause;
16341
17279
  }
16342
- return lines.join(": ");
17280
+ return lines2.join(": ");
16343
17281
  }
16344
17282
  function permissionName(ask) {
16345
17283
  return ask.permission ?? ask.action ?? "permission";
@@ -16375,6 +17313,12 @@ function unwrapFileToolOutput(output) {
16375
17313
  const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
16376
17314
  return inner ? inner[1] : output;
16377
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
+ }
16378
17322
  function delay2(ms, signal) {
16379
17323
  return new Promise((resolve7) => {
16380
17324
  const timer = setTimeout(resolve7, ms);
@@ -16427,6 +17371,8 @@ var OpenCodeAdapter = class {
16427
17371
  openPermissions = /* @__PURE__ */ new Map();
16428
17372
  /** Set once the server turns out to speak the 2.x API, which then owns the session. */
16429
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;
16430
17376
  async start(opts) {
16431
17377
  if (opts.kind === "task") this.task = opts;
16432
17378
  else this.planner = opts;
@@ -16441,9 +17387,19 @@ var OpenCodeAdapter = class {
16441
17387
  throw new ExecutableNotFoundError("opencode", PATH);
16442
17388
  }
16443
17389
  const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
16444
- 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;
16445
17396
  this.process = this.deps.spawn(launch.file, launch.args, {
16446
- 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
+ }),
16447
17403
  stdio: ["pipe", "pipe", "pipe"],
16448
17404
  cwd: opts.cwd,
16449
17405
  windowsVerbatimArguments: launch.verbatim
@@ -16493,7 +17449,7 @@ ${this.stderrTail.trim()}` : ""}`);
16493
17449
  processEnded: this.processEnded,
16494
17450
  isExited: () => this.exited,
16495
17451
  exitMessage: () => this.exitMessage()
16496
- }, opts, this.role());
17452
+ }, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
16497
17453
  try {
16498
17454
  await this.v2.start();
16499
17455
  } catch (err) {
@@ -16546,12 +17502,12 @@ ${this.stderrTail.trim()}` : ""}`);
16546
17502
  ...this.planner?.effort ? { variant: this.planner.effort } : {},
16547
17503
  ...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
16548
17504
  };
16549
- 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);
16550
17506
  if (signal?.aborted) {
16551
17507
  this.dispose();
16552
17508
  return;
16553
17509
  }
16554
- this.settle(reply, turn, onEvent);
17510
+ this.settle(reply2, turn, onEvent);
16555
17511
  } catch (err) {
16556
17512
  if (signal?.aborted) {
16557
17513
  this.dispose();
@@ -16657,13 +17613,13 @@ ${this.stderrTail.trim()}` : ""}`);
16657
17613
  break;
16658
17614
  }
16659
17615
  }
16660
- for (const reply of messages.slice(start)) {
16661
- const id = reply.info?.id;
16662
- if (reply.info?.role !== "assistant" || !id) continue;
16663
- 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 ?? []) {
16664
17620
  if (part.messageID === id) this.emitPart(part, turn, onEvent);
16665
17621
  }
16666
- this.countUsage(reply.info, turn, onEvent);
17622
+ this.countUsage(reply2.info, turn, onEvent);
16667
17623
  }
16668
17624
  }
16669
17625
  /**
@@ -16773,18 +17729,18 @@ ${this.stderrTail.trim()}` : ""}`);
16773
17729
  * their text, reasoning and usage — reach Ordewell over the stream or not at
16774
17730
  * all.
16775
17731
  */
16776
- settle(reply, turn, onEvent) {
16777
- 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;
16778
17734
  if (failure) {
16779
17735
  onEvent({ type: "error", message: failure });
16780
17736
  return;
16781
17737
  }
16782
- const assistantId = reply?.info?.id;
16783
- for (const part of reply?.parts ?? []) {
17738
+ const assistantId = reply2?.info?.id;
17739
+ for (const part of reply2?.parts ?? []) {
16784
17740
  if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
16785
17741
  this.emitPart(part, turn, onEvent);
16786
17742
  }
16787
- if (reply?.info) this.countUsage(reply.info, turn, onEvent);
17743
+ if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
16788
17744
  if (assistantId) this.lastAssistantId = assistantId;
16789
17745
  onEvent({ type: "turn_end" });
16790
17746
  }
@@ -16861,7 +17817,7 @@ ${this.stderrTail.trim()}` : ""}`);
16861
17817
  type: "tool_result",
16862
17818
  id: callId,
16863
17819
  name,
16864
- output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
17820
+ output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
16865
17821
  success: status === "completed",
16866
17822
  subagentId
16867
17823
  });
@@ -16995,6 +17951,11 @@ ${this.stderrTail.trim()}` : ""}`);
16995
17951
  const id = ask.id;
16996
17952
  if (!id || seen.has(`perm:${id}`)) return;
16997
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
+ }
16998
17959
  onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
16999
17960
  void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
17000
17961
  });
@@ -17021,7 +17982,7 @@ ${this.stderrTail.trim()}` : ""}`);
17021
17982
  ...ask.always?.length ? { suggestions: ask.always } : {},
17022
17983
  ...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
17023
17984
  };
17024
- if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
17985
+ if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
17025
17986
  onEvent({ ...request, decided: { decision: "allow" } });
17026
17987
  void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
17027
17988
  });
@@ -17034,10 +17995,10 @@ ${this.stderrTail.trim()}` : ""}`);
17034
17995
  * `POST /permission/:id/reply` is the current answer. The per-session path
17035
17996
  * it replaced is tried only when a server too old to know the new one 404s.
17036
17997
  */
17037
- async replyPermission(id, sessionId, reply) {
17038
- 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);
17039
18000
  if (response.status !== 404) return;
17040
- await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply.reply });
18001
+ await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
17041
18002
  }
17042
18003
  /**
17043
18004
  * Open `/event` for one turn and wait until it is connected. The stream
@@ -17116,6 +18077,19 @@ ${this.stderrTail.trim()}` : ""}`);
17116
18077
  nativeSessionId() {
17117
18078
  return this.sessionId;
17118
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
+ }
17119
18093
  dispose() {
17120
18094
  if (this.disposed) return;
17121
18095
  this.disposed = true;
@@ -17138,9 +18112,13 @@ var TASK_MODE_ADAPTERS = {
17138
18112
  codex: (deps) => new CodexAdapter(deps),
17139
18113
  opencode: (deps) => new OpenCodeAdapter(deps)
17140
18114
  };
18115
+ var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
17141
18116
  function supportsTaskMode(runner) {
17142
18117
  return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
17143
18118
  }
18119
+ function takesOrdewellTools(runner) {
18120
+ return ORDEWELL_TOOL_RUNNERS.has(runner);
18121
+ }
17144
18122
  function createTaskAdapter(runner, deps) {
17145
18123
  if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
17146
18124
  return TASK_MODE_ADAPTERS[runner](deps);
@@ -17153,6 +18131,9 @@ function routeTransport(requested, runner, registry) {
17153
18131
  const name = registry?.get(runner)?.manifest.displayName ?? runner;
17154
18132
  return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
17155
18133
  }
18134
+ function givesCompletionTool(requested, runner, registry) {
18135
+ return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
18136
+ }
17156
18137
  var TransportRouter = class {
17157
18138
  constructor(runners) {
17158
18139
  this.runners = runners;
@@ -17291,6 +18272,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
17291
18272
  this.emit("onTaskSettled", { taskId });
17292
18273
  this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
17293
18274
  });
18275
+ this.verifier.onCheckpointWithdrawn((taskId) => {
18276
+ if (!this.atCheckpoint(taskId)) return;
18277
+ this.store.markInProgress(taskId);
18278
+ this.emit("onTaskChanged");
18279
+ });
17294
18280
  this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
17295
18281
  }
17296
18282
  /**
@@ -17377,7 +18363,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
17377
18363
  this.notifications.warn(message);
17378
18364
  };
17379
18365
  if (resolved.blockedEnvrc) {
17380
- 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.`);
17381
18367
  }
17382
18368
  if (resolved.trackedEnvFile) {
17383
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.`);
@@ -18208,17 +19194,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
18208
19194
  this.abandonSpawn(task, attempt);
18209
19195
  return false;
18210
19196
  }
18211
- 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, {
18212
19200
  planMapEnabled: this.config.planMapEnabled,
18213
19201
  // A merge to resolve is not new behaviour to drive test-first.
18214
19202
  tddEnabled: !attempt.repair && this.tddEnabled(),
18215
19203
  // An ops task's effects outlive a failed attempt and are never rolled
18216
19204
  // back, so the next one is told what the last one did (ADR-0020).
18217
- previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
19205
+ previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
19206
+ completionTool
18218
19207
  });
18219
19208
  this.lingering.close(task.id);
18220
19209
  const env = await this.envForTask(cwd);
18221
- const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
18222
19210
  const session = await this.terminalRunner.spawn({
18223
19211
  taskId: task.id,
18224
19212
  runner: attempt.runner,
@@ -18233,7 +19221,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
18233
19221
  title: task.title,
18234
19222
  env,
18235
19223
  transport,
18236
- resumeSessionId: attempt.continuation?.resumeSessionId
19224
+ resumeSessionId: attempt.continuation?.resumeSessionId,
19225
+ attempt: attempt.attempt
18237
19226
  });
18238
19227
  if (this.attempts.get(task.id) !== attempt) {
18239
19228
  this.abandonSpawn(task, attempt, session);
@@ -18521,62 +19510,16 @@ var Planner = class {
18521
19510
  ${errors.map((e) => `- ${e}`).join("\n")}`
18522
19511
  );
18523
19512
  }
18524
- });
18525
- }
18526
- };
18527
-
18528
- // src/services/ModelDiscovery.ts
18529
- var path16 = __toESM(require("path"));
18530
- var import_child_process5 = require("child_process");
18531
- var import_util3 = require("util");
18532
- var fsSync3 = __toESM(require("fs"));
18533
- var osMod = __toESM(require("os"));
18534
-
18535
- // src/utils/globalDataDir.ts
18536
- var fs7 = __toESM(require("fs"));
18537
- var path15 = __toESM(require("path"));
18538
- var os5 = __toESM(require("os"));
18539
- function globalDataDir() {
18540
- return path15.join(os5.homedir(), ".ordewell");
18541
- }
18542
- function copyDirSync(src, dest) {
18543
- fs7.mkdirSync(dest, { recursive: true });
18544
- const entries = fs7.readdirSync(src, { withFileTypes: true });
18545
- for (const entry of entries) {
18546
- const srcPath = path15.join(src, entry.name);
18547
- const destPath = path15.join(dest, entry.name);
18548
- if (entry.isDirectory()) {
18549
- copyDirSync(srcPath, destPath);
18550
- } else {
18551
- fs7.copyFileSync(srcPath, destPath);
18552
- }
18553
- }
18554
- }
18555
- var migrated = false;
18556
- function migrateOldConfigDir() {
18557
- if (migrated) return;
18558
- migrated = true;
18559
- const oldDir = path15.join(os5.homedir(), ".config", "ordewell");
18560
- if (!fs7.existsSync(oldDir)) return;
18561
- const newDir = globalDataDir();
18562
- const liftFile = (name) => {
18563
- const oldFile = path15.join(oldDir, name);
18564
- const newFile = path15.join(newDir, name);
18565
- if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
18566
- fs7.mkdirSync(newDir, { recursive: true });
18567
- fs7.copyFileSync(oldFile, newFile);
18568
- }
18569
- };
18570
- liftFile("settings.json");
18571
- liftFile(".env");
18572
- const oldPlugins = path15.join(oldDir, "plugins");
18573
- const newPlugins = path15.join(newDir, "plugins");
18574
- if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
18575
- copyDirSync(oldPlugins, newPlugins);
19513
+ });
18576
19514
  }
18577
- }
19515
+ };
18578
19516
 
18579
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"));
18580
19523
  var execAsync3 = (0, import_util3.promisify)(import_child_process5.exec);
18581
19524
  var defaultExec2 = async (command, options) => {
18582
19525
  const PATH = await augmentedPath();
@@ -18952,8 +19895,8 @@ function parseClaudeHelp(stdout) {
18952
19895
  return models;
18953
19896
  }
18954
19897
  function parseOpencodeModels(stdout, manifest) {
18955
- const lines = stdout.split("\n").map((l) => l.trim()).filter((l) => /^[A-Za-z0-9._-]+\/[^\s"{}]+$/.test(l));
18956
- 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 [];
18957
19900
  const preferred = manifest.modelDiscovery.preferredPatterns;
18958
19901
  const prefLabels = /* @__PURE__ */ new Map();
18959
19902
  if (preferred) {
@@ -18963,14 +19906,14 @@ function parseOpencodeModels(stdout, manifest) {
18963
19906
  }
18964
19907
  const seen = /* @__PURE__ */ new Set();
18965
19908
  const models = [];
18966
- for (const id of lines) {
19909
+ for (const id of lines2) {
18967
19910
  if (prefLabels.has(id) && !seen.has(id)) {
18968
19911
  seen.add(id);
18969
19912
  const provider = id.split("/")[0];
18970
19913
  models.push({ modelId: id, modelLabel: prefLabels.get(id), runnerProvider: provider, variants: [] });
18971
19914
  }
18972
19915
  }
18973
- for (const id of lines) {
19916
+ for (const id of lines2) {
18974
19917
  if (!seen.has(id)) {
18975
19918
  seen.add(id);
18976
19919
  const provider = id.split("/")[0];
@@ -19783,6 +20726,7 @@ var CliAgentAiService = class {
19783
20726
  };
19784
20727
  this.makeAdapter = deps.createAdapter ?? defaultAdapter;
19785
20728
  this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
20729
+ this.mcpServer = deps.mcpServer;
19786
20730
  }
19787
20731
  config;
19788
20732
  runner;
@@ -19797,6 +20741,11 @@ var CliAgentAiService = class {
19797
20741
  */
19798
20742
  lastNativeSessionId = null;
19799
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;
19800
20749
  activeAbort = null;
19801
20750
  /**
19802
20751
  * Every subagent this conversation has reported starting, and finishing.
@@ -19819,6 +20768,10 @@ var CliAgentAiService = class {
19819
20768
  if (!this.conversation) return true;
19820
20769
  return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
19821
20770
  }
20771
+ /** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
20772
+ plannerToolsAttached() {
20773
+ return this.toolsAttached;
20774
+ }
19822
20775
  /** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
19823
20776
  nativeSessionId() {
19824
20777
  return this.adapter?.nativeSessionId() ?? null;
@@ -19826,8 +20779,8 @@ var CliAgentAiService = class {
19826
20779
  reset() {
19827
20780
  this.activeAbort?.abort();
19828
20781
  this.activeAbort = null;
19829
- this.adapter?.dispose();
19830
- this.adapter = null;
20782
+ this.disposeAdapter();
20783
+ this.toolsAttached = false;
19831
20784
  this.lastNativeSessionId = null;
19832
20785
  this.conversation = null;
19833
20786
  this.subagents.started.clear();
@@ -19854,9 +20807,23 @@ var CliAgentAiService = class {
19854
20807
  model: this.plannerModel(),
19855
20808
  effort: this.config.plannerThinkingEffort
19856
20809
  };
19857
- 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);
19858
20824
  this.conversation = {
19859
20825
  startOptions,
20826
+ tools,
19860
20827
  runners: req.runners,
19861
20828
  runnerModes: req.runnerModes,
19862
20829
  autonomousDefault: req.autonomousDefault
@@ -20061,10 +21028,7 @@ var CliAgentAiService = class {
20061
21028
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
20062
21029
  }
20063
21030
  this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
20064
- if (error || signal?.aborted) {
20065
- adapter.dispose();
20066
- this.adapter = null;
20067
- }
21031
+ if (error || signal?.aborted) this.disposeAdapter();
20068
21032
  return { text: text2, researchLog, error, aborted: signal?.aborted, backgroundAgents };
20069
21033
  }
20070
21034
  // --- Process lifecycle ---
@@ -20073,13 +21037,50 @@ var CliAgentAiService = class {
20073
21037
  * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
20074
21038
  * mode from here without changing this signature.
20075
21039
  */
20076
- async startAdapter(opts) {
21040
+ async startAdapter(opts, tools) {
20077
21041
  const adapter = this.makeAdapter(this.runner, this.processDeps);
20078
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
+ }
20079
21048
  await adapter.start(opts);
20080
21049
  this.adapter = adapter;
20081
21050
  return adapter;
20082
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
+ }
20083
21084
  /**
20084
21085
  * The live agent process, restarted from its own session id if it died
20085
21086
  * between turns. Resume is a hint: when it fails, the caller's next
@@ -20090,7 +21091,7 @@ var CliAgentAiService = class {
20090
21091
  if (this.adapter) return this.adapter;
20091
21092
  const conversation = this.conversation;
20092
21093
  if (!conversation) throw new Error("No active planner conversation.");
20093
- await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
21094
+ await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
20094
21095
  return this.adapter;
20095
21096
  }
20096
21097
  plannerModel() {
@@ -20109,7 +21110,9 @@ var CliAgentAiService = class {
20109
21110
  const previous = this.adapter;
20110
21111
  const previousConversation = this.conversation;
20111
21112
  const previousSessionId = this.lastNativeSessionId;
21113
+ const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
20112
21114
  this.adapter = null;
21115
+ this.plannerToken = null;
20113
21116
  const startOptions = {
20114
21117
  kind: "planner",
20115
21118
  cwd: this.workspaceRoot(),
@@ -20139,6 +21142,8 @@ var CliAgentAiService = class {
20139
21142
  } finally {
20140
21143
  oneShotAdapter?.dispose();
20141
21144
  this.adapter = previous;
21145
+ this.plannerToken = previousTools.token;
21146
+ this.toolsAttached = previousTools.attached;
20142
21147
  this.conversation = previousConversation;
20143
21148
  this.lastNativeSessionId = previousSessionId;
20144
21149
  }
@@ -20234,6 +21239,173 @@ function createAiService(config, deps) {
20234
21239
  return new OpenAiService(config);
20235
21240
  }
20236
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
+
20237
21409
  // src/services/PlannerConversation.ts
20238
21410
  var import_uuid7 = require("uuid");
20239
21411
 
@@ -20250,8 +21422,8 @@ function summaryRequest(planLines) {
20250
21422
  ...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
20251
21423
  ].join("\n");
20252
21424
  }
20253
- function extractSummary(reply) {
20254
- 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"))];
20255
21427
  const summary = matches.at(-1)?.[1].trim();
20256
21428
  return summary || null;
20257
21429
  }
@@ -20270,6 +21442,7 @@ function keptTail(transcript) {
20270
21442
 
20271
21443
  // src/services/PlannerConversation.ts
20272
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.";
20273
21446
  var MAX_TASK_QUERIES = 3;
20274
21447
  var MAX_TASK_QUERIES_HARD = 6;
20275
21448
  function freshReadBudget() {
@@ -20299,7 +21472,7 @@ var PlannerConversation = class {
20299
21472
  savedInBackground = 0;
20300
21473
  turnsInFlight = 0;
20301
21474
  compacting = false;
20302
- openTurnId = null;
21475
+ openTurn = null;
20303
21476
  get transcript() {
20304
21477
  return this.host.plan()?.conversationHistory ?? [];
20305
21478
  }
@@ -20309,7 +21482,48 @@ var PlannerConversation = class {
20309
21482
  }
20310
21483
  /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
20311
21484
  get currentTurnId() {
20312
- 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 };
20313
21527
  }
20314
21528
  /** Whether the model still holds this conversation in memory. */
20315
21529
  get isActive() {
@@ -20597,15 +21811,16 @@ ${message}` : message;
20597
21811
  async userTurn(prompt, signal, run) {
20598
21812
  const turnId = (0, import_uuid7.v4)();
20599
21813
  const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
20600
- this.openTurnId = turnId;
21814
+ const turn = { stream, signal, reads: freshReadBudget() };
21815
+ this.openTurn = turn;
20601
21816
  this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
20602
21817
  let outcome = "error";
20603
21818
  try {
20604
- const settled = await this.inTurn(() => run({ stream, signal, reads: freshReadBudget() }));
21819
+ const settled = await this.inTurn(() => run(turn));
20605
21820
  outcome = settled.outcome;
20606
21821
  return settled.plan;
20607
21822
  } finally {
20608
- if (this.openTurnId === turnId) this.openTurnId = null;
21823
+ if (this.openTurn === turn) this.openTurn = null;
20609
21824
  this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
20610
21825
  }
20611
21826
  }
@@ -20656,10 +21871,11 @@ ${message}` : message;
20656
21871
  * ops JSON still gets its two corrective retries; charging it for the read
20657
21872
  * would cost it the chance to fix the edit.
20658
21873
  */
20659
- async drainTaskQueries(turn, { reads, signal, stream }) {
21874
+ async drainTaskQueries(turn, userTurn) {
21875
+ const { reads, signal, stream } = userTurn;
20660
21876
  const ai = this.host.aiService();
20661
21877
  const carried = [];
20662
- let current = turn;
21878
+ let current = this.claimSubmission(turn, userTurn);
20663
21879
  while (current.kind === "task_query") {
20664
21880
  carried.push(...current.researchLog);
20665
21881
  if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
@@ -20673,17 +21889,30 @@ ${message}` : message;
20673
21889
  const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
20674
21890
  reads.seen.add(signature);
20675
21891
  reads.answered++;
20676
- const answer = this.taskQueryAnswer(current.query);
20677
- current = await ai.continueConversation(
20678
- insist ? `${answer}
21892
+ const answer2 = this.taskQueryAnswer(current.query);
21893
+ current = this.claimSubmission(await ai.continueConversation(
21894
+ insist ? `${answer2}
20679
21895
 
20680
- ${TASK_QUERY_ANSWER_OR_OPS}` : answer,
21896
+ ${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
20681
21897
  stream.sink(),
20682
21898
  signal
20683
- );
21899
+ ), userTurn);
20684
21900
  }
20685
21901
  return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
20686
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
+ }
20687
21916
  /**
20688
21917
  * Render one read out of live state. Never persisted to the transcript: the
20689
21918
  * detail is context for the planner's next reply, and re-sending it on every
@@ -20727,7 +21956,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20727
21956
  return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
20728
21957
  },
20729
21958
  maxRepairs: 2,
20730
- onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
21959
+ onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
20731
21960
  });
20732
21961
  if ("plan" in settled) {
20733
21962
  await this.host.afterEdit();
@@ -20790,10 +22019,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20790
22019
  *
20791
22020
  * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
20792
22021
  * hard bound on every turn, and reads the plan's runners live, so a runner
20793
- * 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.
20794
22024
  */
20795
22025
  catalogBlock() {
20796
22026
  if (!this.host.plan()) return null;
22027
+ if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
20797
22028
  const { runners, models, modes, autonomousDefault } = this.host.catalog();
20798
22029
  const modelLines = runners.map((runner) => {
20799
22030
  const list = models[runner] ?? [];
@@ -20826,19 +22057,20 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20826
22057
  */
20827
22058
  planContextBlock() {
20828
22059
  const tasks = this.host.tasks();
20829
- const lines = this.currentPlanLines();
20830
- if (!lines) return null;
22060
+ const lines2 = this.currentPlanLines();
22061
+ if (!lines2) return null;
22062
+ const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
20831
22063
  const locked = tasks.filter((t) => t.status === "in_progress");
20832
22064
  const execNote = this.host.hasLiveWork() ? `
20833
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.` : "";
20834
22066
  return [
20835
22067
  "<current_plan>",
20836
- ...lines,
22068
+ ...lines2,
20837
22069
  "</current_plan>",
20838
22070
  "The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
20839
22071
  "- To answer a question or discuss, reply in plain prose (no JSON).",
20840
- TASK_QUERY_REMINDER,
20841
- ...taskOpsProtocol(execNote)
22072
+ tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
22073
+ ...taskOpsProtocol(execNote, tools)
20842
22074
  ].filter(Boolean).join("\n");
20843
22075
  }
20844
22076
  /** One line per task with stable references — null until the plan has tasks. */
@@ -21114,6 +22346,7 @@ function retargetTaskRunner(task, runner, catalog) {
21114
22346
  }
21115
22347
 
21116
22348
  // src/services/SkillsService.ts
22349
+ var import_crypto6 = require("crypto");
21117
22350
  var fs9 = __toESM(require("fs"));
21118
22351
  var path19 = __toESM(require("path"));
21119
22352
 
@@ -21131,6 +22364,11 @@ function builtinSkillsDir() {
21131
22364
  // src/services/SkillsService.ts
21132
22365
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
21133
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";
21134
22372
  function parseSkillFile(filePath, name, source) {
21135
22373
  const raw = fs9.readFileSync(filePath, "utf8");
21136
22374
  const frontmatter = {};
@@ -21170,6 +22408,13 @@ function copyDirSync2(src, dest) {
21170
22408
  else fs9.copyFileSync(srcPath, destPath);
21171
22409
  }
21172
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
+ }
21173
22418
  function readDir(dir, source) {
21174
22419
  if (!fs9.existsSync(dir)) return [];
21175
22420
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
@@ -21194,21 +22439,59 @@ var SkillsService = class {
21194
22439
  globalDir() {
21195
22440
  return path19.join(globalDataDir(), "skills");
21196
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
+ }
21197
22470
  seed(name) {
21198
22471
  const dest = path19.join(this.globalDir(), name);
21199
- if (fs9.existsSync(dest)) return false;
22472
+ const exists = fs9.existsSync(dest);
21200
22473
  let srcDir;
21201
22474
  try {
21202
22475
  srcDir = path19.join(builtinSkillsDir(), name);
21203
22476
  } catch (err) {
21204
- 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)}`);
21205
22478
  return false;
21206
22479
  }
21207
22480
  if (!fs9.existsSync(srcDir)) {
21208
- console.warn(`Built-in skill not found in package: ${name}`);
22481
+ if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
21209
22482
  return false;
21210
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
+ }
21211
22493
  copyDirSync2(srcDir, dest);
22494
+ if (srcHash) this.recordSeed(name, srcHash);
21212
22495
  return true;
21213
22496
  }
21214
22497
  seedBuiltinSkill(name) {
@@ -21564,9 +22847,9 @@ var fs10 = __toESM(require("fs"));
21564
22847
  var path20 = __toESM(require("path"));
21565
22848
 
21566
22849
  // src/utils/sessionId.ts
21567
- var import_crypto3 = require("crypto");
22850
+ var import_crypto7 = require("crypto");
21568
22851
  function mintSessionId() {
21569
- return `session-${(0, import_crypto3.randomBytes)(16).toString("hex")}`;
22852
+ return `session-${(0, import_crypto7.randomBytes)(16).toString("hex")}`;
21570
22853
  }
21571
22854
 
21572
22855
  // src/utils/sessionStore.ts
@@ -21903,7 +23186,12 @@ function lastAttemptDigest(location, taskId) {
21903
23186
  return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
21904
23187
  }
21905
23188
  function sessionRuntimeSettings(settings) {
21906
- 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
+ };
21907
23195
  }
21908
23196
  var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
21909
23197
  function resolveSkillInvocation(text2, skillsService) {
@@ -21927,14 +23215,14 @@ function resolveSkillInvocation(text2, skillsService) {
21927
23215
  return `${lead}${skill.content}${punct}`;
21928
23216
  });
21929
23217
  }
21930
- function liveAiService(config, workspaceRoot) {
23218
+ function liveAiService(config, workspaceRoot, mcpServer) {
21931
23219
  let live = null;
21932
23220
  let liveProvider = null;
21933
23221
  return () => {
21934
23222
  const provider = config.aiProvider;
21935
23223
  if (live && liveProvider === provider) return live;
21936
23224
  live?.reset();
21937
- live = createAiService(config, { workspaceRoot });
23225
+ live = createAiService(config, { workspaceRoot, mcpServer });
21938
23226
  liveProvider = provider;
21939
23227
  return live;
21940
23228
  };
@@ -21944,7 +23232,7 @@ function isPlannerApproval(request) {
21944
23232
  }
21945
23233
  function createSession(deps) {
21946
23234
  const pinnedAiService = deps.aiService;
21947
- const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
23235
+ const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
21948
23236
  const store = new PlanStore();
21949
23237
  const taskLogs = new TaskLogRecorder({
21950
23238
  broadcast: deps.broadcast,
@@ -22074,6 +23362,16 @@ var Session = class {
22074
23362
  currentSessionId;
22075
23363
  skillsService;
22076
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
+ });
22077
23375
  constructor(parts) {
22078
23376
  this.config = parts.config;
22079
23377
  this.registry = parts.registry;
@@ -22102,17 +23400,7 @@ var Session = class {
22102
23400
  aiService: () => this.aiService(),
22103
23401
  onProgress: (p) => this.events.progress(p),
22104
23402
  opening: (runners) => this.conversationOpening(runners),
22105
- catalog: () => {
22106
- const runners = this.plan?.runners ?? [];
22107
- return {
22108
- runners,
22109
- // Allowlist-filtered: neither the per-turn block nor a read may offer
22110
- // a model the planner is forbidden to assign.
22111
- models: filterModelsForPrompt(this.models(), this.allowlist()),
22112
- modes: this.runnerModesFor(runners),
22113
- autonomousDefault: this.config.autonomousMode
22114
- };
22115
- },
23403
+ catalog: () => this.catalogOf(this.plan?.runners ?? []),
22116
23404
  tasks: () => this.store.planTasks,
22117
23405
  liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
22118
23406
  hasLiveWork: () => this.hasLiveWork,
@@ -22136,8 +23424,8 @@ var Session = class {
22136
23424
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
22137
23425
  * path is identical regardless of who is looking.
22138
23426
  */
22139
- resolveApproval(id, answer) {
22140
- return this.approvals.resolve(id, answer);
23427
+ resolveApproval(id, answer2) {
23428
+ return this.approvals.resolve(id, answer2);
22141
23429
  }
22142
23430
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
22143
23431
  outstandingApprovals() {
@@ -22257,6 +23545,27 @@ var Session = class {
22257
23545
  this.modelsCache = {};
22258
23546
  this.remintSessionId();
22259
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
+ }
22260
23569
  runnerModesFor(runners) {
22261
23570
  return runnerModesFrom(this.registry, runners);
22262
23571
  }
@@ -22268,13 +23577,17 @@ var Session = class {
22268
23577
  * allowlist here — {@link checkModelAndModeValidity} narrows by
22269
23578
  * allowlist itself, the same way `coerceAssignments` does.
22270
23579
  */
22271
- editCatalog() {
23580
+ editCatalog(runners = this.plan?.runners ?? []) {
22272
23581
  return {
22273
23582
  modelsByRunner: this.models(),
22274
- runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
23583
+ runnerModes: this.runnerModesFor(runners),
22275
23584
  perRunnerAllowlist: this.allowlist()
22276
23585
  };
22277
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
+ }
22278
23591
  /**
22279
23592
  * The allowlist in force right now. Unset means no restriction — falling
22280
23593
  * back to the one planning started under would keep a restriction the user
@@ -22376,7 +23689,7 @@ var Session = class {
22376
23689
  this.goal = goal;
22377
23690
  this.remintSessionId();
22378
23691
  this.beginFreshPlan();
22379
- const enabled = this.config.enabledRunners;
23692
+ const enabled = this.enabledRunners();
22380
23693
  const chosenRunners = runners.filter((r) => enabled.includes(r));
22381
23694
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
22382
23695
  const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
@@ -22420,7 +23733,7 @@ var Session = class {
22420
23733
  this.goal = this.resolveSkillInvocation(goal);
22421
23734
  this.remintSessionId();
22422
23735
  this.beginFreshPlan();
22423
- const enabled = this.config.enabledRunners;
23736
+ const enabled = this.enabledRunners();
22424
23737
  const chosenRunners = runners.filter((r) => enabled.includes(r));
22425
23738
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
22426
23739
  const opening = await this.conversationOpening(chosenRunners);
@@ -22498,6 +23811,49 @@ var Session = class {
22498
23811
  rewindTargets() {
22499
23812
  return this.conversation.rewindTargets();
22500
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
+ }
22501
23857
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
22502
23858
  get isConversationActive() {
22503
23859
  return this.conversation.isActive;
@@ -22543,7 +23899,8 @@ var Session = class {
22543
23899
  // usage line can show context fill (#49). Unknown stays absent.
22544
23900
  contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
22545
23901
  fs: this.fsAdapter,
22546
- fetcher: this.fetcher
23902
+ fetcher: this.fetcher,
23903
+ plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
22547
23904
  };
22548
23905
  }
22549
23906
  /**
@@ -22661,7 +24018,7 @@ var Session = class {
22661
24018
  ])
22662
24019
  );
22663
24020
  try {
22664
- const modelsByRunner = await this.modelResolver.modelsForRunners(this.config.enabledRunners);
24021
+ const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
22665
24022
  const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
22666
24023
  const { modelAllowlist } = this.settingsFn();
22667
24024
  const result = await this.planner.modifyDuringExecution({
@@ -23008,7 +24365,7 @@ var Session = class {
23008
24365
  if (!this.plan) throw new Error("No active plan state");
23009
24366
  const check = canMergeTasks(this.store.planTasks, taskIds);
23010
24367
  if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
23011
- const prompt = buildMergePrompt(taskIds, this.store.planTasks);
24368
+ const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
23012
24369
  return this.continueConversation(prompt, options);
23013
24370
  }
23014
24371
  /**
@@ -23020,7 +24377,7 @@ var Session = class {
23020
24377
  if (!this.plan) throw new Error("No active plan state");
23021
24378
  const check = canSplitTask(this.store.planTasks, taskId);
23022
24379
  if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
23023
- const prompt = buildSplitPrompt(taskId, this.store.planTasks);
24380
+ const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
23024
24381
  return this.continueConversation(prompt, options);
23025
24382
  }
23026
24383
  loadPlan(plan, goal, workspace, opts) {
@@ -23060,41 +24417,6 @@ var Session = class {
23060
24417
  }
23061
24418
  };
23062
24419
 
23063
- // src/utils/privateFile.ts
23064
- var import_fs2 = require("fs");
23065
- var import_path = require("path");
23066
- var import_crypto4 = require("crypto");
23067
- var DIR_MODE = 448;
23068
- var FILE_MODE = 384;
23069
- var POSIX_MODES = process.platform !== "win32";
23070
- function ensurePrivateDir(dir) {
23071
- if (POSIX_MODES) {
23072
- (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
23073
- return;
23074
- }
23075
- (0, import_fs2.mkdirSync)(dir, { recursive: true });
23076
- }
23077
- function writePrivateFile(filePath, content) {
23078
- const dir = (0, import_path.dirname)(filePath);
23079
- ensurePrivateDir(dir);
23080
- const tempPath = (0, import_path.join)(dir, `.${(0, import_path.basename)(filePath)}.${(0, import_crypto4.randomBytes)(8).toString("hex")}.tmp`);
23081
- const fd = (0, import_fs2.openSync)(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
23082
- try {
23083
- (0, import_fs2.writeSync)(fd, content);
23084
- } finally {
23085
- (0, import_fs2.closeSync)(fd);
23086
- }
23087
- try {
23088
- (0, import_fs2.renameSync)(tempPath, filePath);
23089
- } catch (err) {
23090
- try {
23091
- (0, import_fs2.unlinkSync)(tempPath);
23092
- } catch {
23093
- }
23094
- throw err;
23095
- }
23096
- }
23097
-
23098
24420
  // src/conversation/reduce.ts
23099
24421
  var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
23100
24422
  function isUnsettledSegment(block, turnId) {
@@ -23776,9 +25098,9 @@ var HeadlessRunner = class extends AbstractRunner {
23776
25098
  // src/services/TmuxRunner.ts
23777
25099
  var import_child_process11 = require("child_process");
23778
25100
  var import_util5 = require("util");
23779
- var import_os = require("os");
23780
- var import_path2 = require("path");
23781
- var import_fs3 = require("fs");
25101
+ var import_os2 = require("os");
25102
+ var import_path5 = require("path");
25103
+ var import_fs5 = require("fs");
23782
25104
 
23783
25105
  // src/utils/tmux.ts
23784
25106
  var import_child_process10 = require("child_process");
@@ -23876,7 +25198,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23876
25198
  * lets the user keep poking at a finished task's terminal.
23877
25199
  */
23878
25200
  async start(command, args, cwd, env) {
23879
- (0, import_fs3.writeFileSync)(this.logPath, "");
25201
+ (0, import_fs5.writeFileSync)(this.logPath, "");
23880
25202
  const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
23881
25203
  const inner = [command, ...args].map(posixShellQuote).join(" ");
23882
25204
  const prefix = envAssignments ? `env ${envAssignments} ` : "";
@@ -23899,7 +25221,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23899
25221
  readLog() {
23900
25222
  let content;
23901
25223
  try {
23902
- 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") : "";
23903
25225
  } catch {
23904
25226
  return false;
23905
25227
  }
@@ -23955,7 +25277,7 @@ var TmuxSession = class extends AbstractTerminalSession {
23955
25277
  this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
23956
25278
  });
23957
25279
  try {
23958
- (0, import_fs3.unlinkSync)(this.logPath);
25280
+ (0, import_fs5.unlinkSync)(this.logPath);
23959
25281
  } catch {
23960
25282
  }
23961
25283
  this.baseHandleExit(code);
@@ -23992,7 +25314,7 @@ var TmuxRunner = class extends AbstractRunner {
23992
25314
  this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
23993
25315
  this.resolvePath = deps.resolvePath ?? augmentedPath;
23994
25316
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
23995
- this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
25317
+ this.logDir = deps.logDir ?? (0, import_os2.tmpdir)();
23996
25318
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
23997
25319
  this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
23998
25320
  }
@@ -24112,7 +25434,7 @@ var TmuxRunner = class extends AbstractRunner {
24112
25434
  windowName,
24113
25435
  this.execFileImpl,
24114
25436
  this.pollIntervalMs,
24115
- (0, import_path2.join)(this.logDir, `${id}.log`),
25437
+ (0, import_path5.join)(this.logDir, `${id}.log`),
24116
25438
  this.socket
24117
25439
  );
24118
25440
  invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
@@ -24140,9 +25462,9 @@ var KEY_ARGS = {
24140
25462
  };
24141
25463
  var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
24142
25464
  function toolLine(name, args) {
24143
- const { tool } = mapAgentTool(name);
24144
- const normalized = normalizeAgentArgs(tool, args);
24145
- const preferred = KEY_ARGS[tool];
25465
+ const { tool: tool2 } = mapAgentTool(name);
25466
+ const normalized = normalizeAgentArgs(tool2, args);
25467
+ const preferred = KEY_ARGS[tool2];
24146
25468
  for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
24147
25469
  const value = normalized[key];
24148
25470
  if (typeof value !== "string") continue;
@@ -24205,6 +25527,7 @@ var StructuredSession = class extends AbstractTerminalSession {
24205
25527
  constructor(id, taskId, launch) {
24206
25528
  super(id, taskId);
24207
25529
  this.launch = launch;
25530
+ this.startOptions = launch.startOptions;
24208
25531
  this.text = new PlainTextChannel((text2) => {
24209
25532
  this.output += text2;
24210
25533
  this.outputEmitter.emit("output", text2);
@@ -24233,9 +25556,20 @@ var StructuredSession = class extends AbstractTerminalSession {
24233
25556
  * requests — while an approval must be answerable by id across the session.
24234
25557
  */
24235
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;
24236
25564
  /** Start the runner and send the task's prompt as its first turn. */
24237
25565
  async start(prompt) {
24238
- 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
+ }
24239
25573
  this.state = "working";
24240
25574
  setImmediate(() => {
24241
25575
  if (!this.exited) this.deliver(prompt);
@@ -24244,6 +25578,49 @@ var StructuredSession = class extends AbstractTerminalSession {
24244
25578
  getOutput() {
24245
25579
  return this.output;
24246
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
+ }
24247
25624
  /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
24248
25625
  write(text2) {
24249
25626
  const message = text2.trim();
@@ -24341,12 +25718,12 @@ var StructuredSession = class extends AbstractTerminalSession {
24341
25718
  * session id after an abort.
24342
25719
  */
24343
25720
  async restartInterrupted(turn) {
24344
- const resumeSessionId = this.nativeSessionId() ?? this.launch.startOptions.resumeSessionId;
25721
+ const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
24345
25722
  this.generation += 1;
24346
25723
  turn.abort.abort();
24347
25724
  if (this.adapter) this.withdrawPermissions(this.adapter);
24348
25725
  try {
24349
- await this.startAdapter({ ...this.launch.startOptions, resumeSessionId });
25726
+ await this.startAdapter({ ...this.startOptions, resumeSessionId });
24350
25727
  } catch (err) {
24351
25728
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
24352
25729
  this.endTurn(turn, "interrupted");
@@ -24484,11 +25861,13 @@ var StructuredRunner = class extends AbstractRunner {
24484
25861
  processDeps;
24485
25862
  createAdapter;
24486
25863
  interruptGraceMs;
25864
+ mcp;
24487
25865
  constructor(deps = {}) {
24488
25866
  super();
24489
25867
  this.processDeps = deps.process ?? {};
24490
25868
  this.createAdapter = deps.createAdapter ?? createTaskAdapter;
24491
25869
  this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
25870
+ this.mcp = deps.mcp ?? sharedMcpServer();
24492
25871
  }
24493
25872
  async spawn(opts) {
24494
25873
  const manifest = opts.registry?.get(opts.runner)?.manifest;
@@ -24515,7 +25894,8 @@ var StructuredRunner = class extends AbstractRunner {
24515
25894
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
24516
25895
  resumeSessionId: opts.resumeSessionId
24517
25896
  },
24518
- 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 } } } : {}
24519
25899
  });
24520
25900
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
24521
25901
  this.registerSession(id, session);
@@ -24529,73 +25909,12 @@ var StructuredRunner = class extends AbstractRunner {
24529
25909
  }
24530
25910
  };
24531
25911
 
24532
- // src/utils/daemonToken.ts
24533
- var import_crypto5 = require("crypto");
24534
- var import_fs4 = require("fs");
24535
- var import_path3 = require("path");
24536
- var BEARER_PREFIX = "bearer ";
24537
- var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
24538
- var DAEMON_SUBPROTOCOL = "ordewell.v1";
24539
- function configDir() {
24540
- return globalDataDir();
24541
- }
24542
- function daemonTokenPath(port) {
24543
- return (0, import_path3.join)(configDir(), `server-${port}.token`);
24544
- }
24545
- function mintDaemonToken(port) {
24546
- const token = (0, import_crypto5.randomBytes)(32).toString("base64url");
24547
- const file = daemonTokenPath(port);
24548
- writePrivateFile(file, token);
24549
- return { token, file };
24550
- }
24551
- function readDaemonToken(port) {
24552
- try {
24553
- const token = (0, import_fs4.readFileSync)(daemonTokenPath(port), "utf8").trim();
24554
- return token === "" ? void 0 : token;
24555
- } catch {
24556
- return void 0;
24557
- }
24558
- }
24559
- function clearDaemonToken(port) {
24560
- try {
24561
- (0, import_fs4.unlinkSync)(daemonTokenPath(port));
24562
- } catch {
24563
- }
24564
- }
24565
- function bearerHeaderValue(token) {
24566
- return `Bearer ${token}`;
24567
- }
24568
- function tokenSubprotocols(token) {
24569
- return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
24570
- }
24571
- function extractPresentedToken(carriers) {
24572
- const authorization = carriers.authorization;
24573
- if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
24574
- const value = authorization.slice(BEARER_PREFIX.length).trim();
24575
- if (value !== "") return value;
24576
- }
24577
- for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
24578
- const value = offered.trim();
24579
- if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
24580
- const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
24581
- if (token !== "") return token;
24582
- }
24583
- return void 0;
24584
- }
24585
- function tokensMatch(presented, expected) {
24586
- if (presented === void 0) return false;
24587
- const a = Buffer.from(presented, "utf8");
24588
- const b = Buffer.from(expected, "utf8");
24589
- if (a.length !== b.length) return false;
24590
- return (0, import_crypto5.timingSafeEqual)(a, b);
24591
- }
24592
-
24593
25912
  // src/plugins/RunnerRegistry.ts
24594
25913
  var fs13 = __toESM(require("fs"));
24595
25914
  var path24 = __toESM(require("path"));
24596
25915
  var os6 = __toESM(require("os"));
24597
25916
  var import_child_process13 = require("child_process");
24598
- var import_crypto6 = require("crypto");
25917
+ var import_crypto8 = require("crypto");
24599
25918
 
24600
25919
  // src/plugins/FsPluginStore.ts
24601
25920
  var fs12 = __toESM(require("fs"));
@@ -24619,8 +25938,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
24619
25938
  assertPlainPluginName(name);
24620
25939
  const root = path22.resolve(pluginsDir);
24621
25940
  const dest = path22.resolve(root, name);
24622
- const relative5 = path22.relative(root, dest);
24623
- if (relative5 !== name || relative5 === "" || path22.isAbsolute(relative5)) {
25941
+ const relative6 = path22.relative(root, dest);
25942
+ if (relative6 !== name || relative6 === "" || path22.isAbsolute(relative6)) {
24624
25943
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
24625
25944
  }
24626
25945
  return dest;
@@ -25234,7 +26553,7 @@ var RunnerRegistry = class {
25234
26553
  }
25235
26554
  installFromGit(url) {
25236
26555
  const validated = assertInstallablePluginUrl(url);
25237
- 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")}`);
25238
26557
  this.store.ensureDir(tmpDir);
25239
26558
  try {
25240
26559
  try {
@@ -25654,11 +26973,15 @@ var PlannerModelMemory = class {
25654
26973
  NO_TURN,
25655
26974
  OPENCODE_MANIFEST,
25656
26975
  ORCHESTRATOR_SHORTCUTS,
26976
+ ORDEWELL_MCP_PATH,
26977
+ ORDEWELL_MCP_SERVER_NAME,
25657
26978
  ORDEWELL_SETTABLE_ENV,
25658
26979
  OUTPUT_LINES_DEFAULT,
25659
26980
  OUTPUT_LINES_MAX,
25660
26981
  OpenAiService,
25661
26982
  OpenCodeAdapter,
26983
+ OrdewellMcpServer,
26984
+ PLANNER_TOOLS,
25662
26985
  PLAN_ENVELOPE_KEY,
25663
26986
  PLUGIN_NAME_PATTERN,
25664
26987
  PROVIDER_CREDENTIAL_ENV,
@@ -25695,6 +27018,7 @@ var PlannerModelMemory = class {
25695
27018
  TASK_QUERY_FIELDS,
25696
27019
  TASK_QUERY_PROTOCOL,
25697
27020
  TASK_QUERY_REMINDER,
27021
+ TASK_TOOLS,
25698
27022
  TRUNCATED_PLAN_REPAIR_INSTRUCTION,
25699
27023
  TaskControlError,
25700
27024
  TaskLogRecorder,
@@ -25778,10 +27102,14 @@ var PlannerModelMemory = class {
25778
27102
  daemonTokenPath,
25779
27103
  defaultLogger,
25780
27104
  definitionPattern,
27105
+ defuseMarkers,
25781
27106
  deleteSession,
25782
27107
  dependencyCandidates,
25783
27108
  dependentsOf,
25784
27109
  describeMergeResult,
27110
+ diffRows,
27111
+ diffStat,
27112
+ diffSummary,
25785
27113
  digestTaskLog,
25786
27114
  discoverGeminiModels,
25787
27115
  drainNext,
@@ -25844,6 +27172,7 @@ var PlannerModelMemory = class {
25844
27172
  loadState,
25845
27173
  looksLikePlanAttempt,
25846
27174
  mapAgentTool,
27175
+ mcpClientConfig,
25847
27176
  migrateLegacyPlan,
25848
27177
  migrateOldConfigDir,
25849
27178
  migratePlanIsolation,
@@ -25859,6 +27188,7 @@ var PlannerModelMemory = class {
25859
27188
  opensWithJsonObject,
25860
27189
  outputLines,
25861
27190
  outputPreview,
27191
+ ownerOnlyConfigFile,
25862
27192
  parseAutonomyLevel,
25863
27193
  parseMaxParallel,
25864
27194
  parsePartialPlan,
@@ -25917,6 +27247,7 @@ var PlannerModelMemory = class {
25917
27247
  serializeTaskStatus,
25918
27248
  sessionDataDir,
25919
27249
  sessionRuntimeSettings,
27250
+ sharedMcpServer,
25920
27251
  stateExists,
25921
27252
  stopTurn,
25922
27253
  stripAnsi,
@@ -25950,6 +27281,7 @@ var PlannerModelMemory = class {
25950
27281
  usageLine,
25951
27282
  validateModifiedPlan,
25952
27283
  validatePlanModification,
27284
+ validatePlanTasks,
25953
27285
  warningsText,
25954
27286
  wellKnownBinDirs,
25955
27287
  windowsCommandLine,