@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.mjs CHANGED
@@ -14,8 +14,9 @@ import {
14
14
  resolveDefaultMode,
15
15
  resolveTaskMode,
16
16
  runnerModesFrom,
17
- validatePlanModification
18
- } from "./chunk-M4KXBEFJ.mjs";
17
+ validatePlanModification,
18
+ validatePlanTasks
19
+ } from "./chunk-J3YSTPCE.mjs";
19
20
  import {
20
21
  CHECKPOINT_TRUNCATE_LENGTH,
21
22
  EMPTY_CONVERSATION,
@@ -37,6 +38,9 @@ import {
37
38
  continuability,
38
39
  dependencyCandidates,
39
40
  dependentsOf,
41
+ diffRows,
42
+ diffStat,
43
+ diffSummary,
40
44
  drainNext,
41
45
  effectiveAllowlist,
42
46
  executionSummary,
@@ -76,7 +80,7 @@ import {
76
80
  unsendLatest,
77
81
  usageLine,
78
82
  validateTaskEdit
79
- } from "./chunk-I6U3PVHL.mjs";
83
+ } from "./chunk-LSGY7AIZ.mjs";
80
84
  import {
81
85
  PLAN_ENVELOPE_KEY,
82
86
  PlanParseError,
@@ -105,7 +109,7 @@ import {
105
109
  updateTaskInPlan,
106
110
  validateModifiedPlan,
107
111
  warningsText
108
- } from "./chunk-JRZPSNRO.mjs";
112
+ } from "./chunk-YU4UKKKI.mjs";
109
113
  import {
110
114
  resolveOrderLabel,
111
115
  taskOrderLabel
@@ -266,9 +270,9 @@ var SEARCH_EXCLUSIONS = [
266
270
  ];
267
271
 
268
272
  // src/interfaces/IApproval.ts
269
- function toApprovalDecision(answer) {
270
- if (typeof answer === "boolean") return { decision: answer ? "allow" : "deny" };
271
- return answer;
273
+ function toApprovalDecision(answer2) {
274
+ if (typeof answer2 === "boolean") return { decision: answer2 ? "allow" : "deny" };
275
+ return answer2;
272
276
  }
273
277
  function isGranted(decision) {
274
278
  return decision.decision !== "deny";
@@ -3814,12 +3818,12 @@ var ApprovalPolicy = class {
3814
3818
  if (this.generation === gen) this.inFlight.delete(openKey);
3815
3819
  });
3816
3820
  this.inFlight.set(openKey, pending);
3817
- const answer = await pending;
3821
+ const answer2 = await pending;
3818
3822
  if (this.generation === gen) {
3819
- if (answer) open.forEach((s) => this.granted.add(s));
3823
+ if (answer2) open.forEach((s) => this.granted.add(s));
3820
3824
  else this.refused.add(key);
3821
3825
  }
3822
- return decide(answer, "asked");
3826
+ return decide(answer2, "asked");
3823
3827
  }
3824
3828
  /** Scopes the user has granted this session — for display and for persistence. */
3825
3829
  grantedScopes() {
@@ -3880,10 +3884,10 @@ var PendingApprovals = class {
3880
3884
  * settled. "Allow for this task" on a request that did not offer it is a
3881
3885
  * plain allow: no grant is made that the requester never proposed.
3882
3886
  */
3883
- resolve(id, answer) {
3887
+ resolve(id, answer2) {
3884
3888
  const entry = this.entries.get(id);
3885
3889
  if (!entry) return false;
3886
- const decision = toApprovalDecision(answer);
3890
+ const decision = toApprovalDecision(answer2);
3887
3891
  entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
3888
3892
  return true;
3889
3893
  }
@@ -4121,9 +4125,9 @@ function applyHeadLimit(stdout, headLimit) {
4121
4125
  }
4122
4126
  function formatSearchOutput(capped, root, opts) {
4123
4127
  if (capped.rows.length === 0) return opts.emptyMessage;
4124
- const sep2 = opts.sep ?? path3.sep;
4125
- const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
4126
- const body = prefix === sep2 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
4128
+ const sep3 = opts.sep ?? path3.sep;
4129
+ const prefix = root === sep3 || root.endsWith(sep3) ? root : `${root}${sep3}`;
4130
+ const body = prefix === sep3 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
4127
4131
  if (!capped.truncated) return body;
4128
4132
  const hint = opts.hint ? ` ${opts.hint}` : "";
4129
4133
  return `${body}
@@ -5078,28 +5082,28 @@ function posixBinDirs() {
5078
5082
  ];
5079
5083
  }
5080
5084
  function windowsBinDirs(env, home) {
5081
- const join25 = (...parts) => path4.win32.join(...parts);
5082
- const under = (base, ...parts) => base ? [join25(base, ...parts)] : [];
5085
+ const join26 = (...parts) => path4.win32.join(...parts);
5086
+ const under = (base, ...parts) => base ? [join26(base, ...parts)] : [];
5083
5087
  const appData = env.APPDATA;
5084
5088
  const localAppData = env.LOCALAPPDATA;
5085
5089
  const programData = env.ProgramData;
5086
5090
  return [
5087
5091
  // Native per-user installers, PowerShell-driven and otherwise.
5088
- join25(home, ".local", "bin"),
5089
- join25(home, ".opencode", "bin"),
5090
- join25(home, ".claude", "bin"),
5091
- join25(home, ".codex", "bin"),
5092
+ join26(home, ".local", "bin"),
5093
+ join26(home, ".opencode", "bin"),
5094
+ join26(home, ".claude", "bin"),
5095
+ join26(home, ".codex", "bin"),
5092
5096
  ...under(localAppData, "Programs", "claude"),
5093
5097
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
5094
5098
  // literal spelling is kept as a fallback for a host that does not export
5095
5099
  // APPDATA (a service account, a stripped CI container).
5096
5100
  ...under(appData, "npm"),
5097
- join25(home, "AppData", "Roaming", "npm"),
5101
+ join26(home, "AppData", "Roaming", "npm"),
5098
5102
  ...under(localAppData, "pnpm"),
5099
5103
  ...under(localAppData, "Yarn", "bin"),
5100
- join25(home, ".bun", "bin"),
5104
+ join26(home, ".bun", "bin"),
5101
5105
  // Windows package managers.
5102
- join25(home, "scoop", "shims"),
5106
+ join26(home, "scoop", "shims"),
5103
5107
  ...under(env.SCOOP, "shims"),
5104
5108
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
5105
5109
  ...under(programData, "chocolatey", "bin"),
@@ -5109,7 +5113,7 @@ function windowsBinDirs(env, home) {
5109
5113
  ...under(localAppData, "Microsoft", "WindowsApps"),
5110
5114
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
5111
5115
  ...under(localAppData, "Volta", "bin"),
5112
- join25(home, ".volta", "bin")
5116
+ join26(home, ".volta", "bin")
5113
5117
  ];
5114
5118
  }
5115
5119
  function wellKnownBinDirs(deps = {}) {
@@ -5597,18 +5601,18 @@ var GitWorktreeIsolation = class {
5597
5601
  */
5598
5602
  sharedPaths(workspaceRoot, isolated) {
5599
5603
  if (isolated.includes(SELF_REPO)) return [];
5600
- const shared = [];
5604
+ const shared2 = [];
5601
5605
  const walk = (rel) => {
5602
5606
  for (const name of listDir(path8.join(workspaceRoot, rel))) {
5603
5607
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
5604
5608
  const child = rel ? `${rel}/${name}` : name;
5605
5609
  if (isolated.includes(child)) continue;
5606
5610
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
5607
- else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
5611
+ else if (resolves(path8.join(workspaceRoot, child))) shared2.push(child);
5608
5612
  }
5609
5613
  };
5610
5614
  walk("");
5611
- return shared;
5615
+ return shared2;
5612
5616
  }
5613
5617
  prepare(task, run) {
5614
5618
  return this.admin(run.workspaceRoot, async () => {
@@ -6436,10 +6440,12 @@ var TASK_QUERY_FIELDS = [
6436
6440
  "userStoriesCovered",
6437
6441
  "output"
6438
6442
  ];
6443
+ var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
6439
6444
  var OUTPUT_LINES_DEFAULT = 80;
6440
6445
  var OUTPUT_LINES_MAX = 400;
6441
6446
  var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
6442
6447
  var ANSWER_TAIL_RESERVE = 4e3;
6448
+ var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
6443
6449
  function textHasTaskQuery(text) {
6444
6450
  return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
6445
6451
  }
@@ -6515,6 +6521,7 @@ function taskQuerySignature(query) {
6515
6521
  return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
6516
6522
  }
6517
6523
  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.";
6524
+ 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.";
6518
6525
  var TASK_QUERY_PROTOCOL = [
6519
6526
  "READING A TASK BEFORE YOU EDIT IT:",
6520
6527
  "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:",
@@ -6526,7 +6533,16 @@ var TASK_QUERY_PROTOCOL = [
6526
6533
  "Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
6527
6534
  "Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
6528
6535
  ];
6536
+ var TASK_READ_TOOLS_PROTOCOL = [
6537
+ "READING A TASK BEFORE YOU EDIT IT:",
6538
+ "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:",
6539
+ '- 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.',
6540
+ `- 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.`,
6541
+ "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.",
6542
+ "Once a plan exists, change it with edit_plan \u2014 every turn shows you its operations. Never reply with taskOps or taskQuery JSON."
6543
+ ];
6529
6544
  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.`;
6545
+ 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.";
6530
6546
  function findTask(tasks, ref) {
6531
6547
  const byId = tasks.find((t) => t.id === ref);
6532
6548
  if (byId) return byId;
@@ -6735,7 +6751,7 @@ function verificationModeBlock() {
6735
6751
  ].join("\n");
6736
6752
  }
6737
6753
  function researchPhaseBlock(harnessMode) {
6738
- const shared = [
6754
+ const shared2 = [
6739
6755
  "RESEARCH PHASE:",
6740
6756
  "- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
6741
6757
  ];
@@ -6749,7 +6765,7 @@ function researchPhaseBlock(harnessMode) {
6749
6765
  "- Read key source files to understand architecture"
6750
6766
  ];
6751
6767
  return [
6752
- ...shared,
6768
+ ...shared2,
6753
6769
  ...toolSpecific,
6754
6770
  "- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
6755
6771
  "- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
@@ -6811,28 +6827,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
6811
6827
  verificationBlock
6812
6828
  );
6813
6829
  }
6830
+ var PLANNER_TOOLS_CATALOG = [
6831
+ "RUNNERS, MODELS AND MODES:",
6832
+ "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:",
6833
+ "1. Call list_runners: the enabled runners, each with its task modes and default mode.",
6834
+ "2. Call list_models for each runner you will assign: its allowed models, with their thinking-effort variants.",
6835
+ `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.`,
6836
+ '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.',
6837
+ ""
6838
+ ];
6814
6839
  function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
6840
+ const tools = variant.plannerTools ?? false;
6815
6841
  return [
6816
- "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.",
6842
+ `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.`,
6817
6843
  "",
6818
6844
  "WORKFLOW:",
6819
6845
  "1. Explore the workspace with tools to understand the codebase.",
6820
6846
  "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.",
6821
6847
  "3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
6822
- '4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
6848
+ 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.',
6823
6849
  "",
6824
6850
  researchPhaseBlock(variant.harness ?? false),
6825
6851
  verificationBlock,
6826
6852
  "",
6827
6853
  "OUTLINE PHASE:",
6828
- "- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to JSON.",
6854
+ `- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to ${tools ? "submit_plan" : "JSON"}.`,
6829
6855
  "- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
6830
6856
  '- 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"',
6831
6857
  "- The user may reply with adjustments. Revise the outline and re-present it.",
6832
- "- When the user confirms the outline, emit the task plan JSON.",
6858
+ tools ? "- When the user confirms the outline, submit the plan with submit_plan." : "- When the user confirms the outline, emit the task plan JSON.",
6833
6859
  "",
6834
6860
  "PLAN FORMAT:",
6835
- "Generate a task plan using this JSON format:",
6861
+ tools ? 'submit_plan takes {"tasks": [...]}, every task in this shape:' : "Generate a task plan using this JSON format:",
6836
6862
  "",
6837
6863
  "{",
6838
6864
  ' "tasks": [',
@@ -6846,8 +6872,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6846
6872
  ' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
6847
6873
  ' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
6848
6874
  ` "assignedModel": { "modelId": "model-id", "modelLabel": "Model Label", "thinkingEffort": "a variant id from that model's variants list \u2014 omit if the model has none" },`,
6849
- ' "assignedRunner": "' + runners.join("|") + '",',
6850
- ' "taskMode": "' + modeExamples + '",',
6875
+ ' "assignedRunner": "' + (tools ? "a runner id from list_runners" : runners.join("|")) + '",',
6876
+ ' "taskMode": "' + (tools ? "one of that runner's modes from list_runners" : modeExamples) + '",',
6851
6877
  ' "autonomy": "AFK|HITL",',
6852
6878
  ' "sliceType": "AFK|HITL",',
6853
6879
  ' "ops": true,',
@@ -6879,8 +6905,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6879
6905
  ...opsTaskSection(true),
6880
6906
  "",
6881
6907
  "RULES:",
6882
- "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
6883
- "- Emit the outline BEFORE the JSON. Do not skip the outline.",
6908
+ ...tools ? [
6909
+ "- Submit the plan ONLY through submit_plan. Never write it as JSON in your reply.",
6910
+ "- Show the outline BEFORE submitting. Do not skip the outline."
6911
+ ] : [
6912
+ "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
6913
+ "- Emit the outline BEFORE the JSON. Do not skip the outline."
6914
+ ],
6884
6915
  "- Reference specific files and patterns from your research in task prompts.",
6885
6916
  ...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
6886
6917
  "",
@@ -6888,23 +6919,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6888
6919
  "- 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.",
6889
6920
  "- Follow the existing code style, naming conventions, and architectural patterns of the project.",
6890
6921
  "",
6891
- "MODEL ASSIGNMENT:",
6892
- "Available models:",
6893
- modelsJson,
6894
- "",
6895
- runnerInstruction(runners),
6922
+ ...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
6896
6923
  "MODEL SELECTION GUIDELINES:",
6897
6924
  "- Use stronger models for complex refactoring, architecture changes, security-critical code.",
6898
6925
  "- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
6899
- `- 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.`,
6926
+ `- 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.`,
6900
6927
  "",
6901
6928
  "TASK MODE:",
6902
- modeGuide,
6929
+ 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,
6903
6930
  "",
6904
- // Shared by both variants on purpose: the read channel is a text envelope
6905
- // exactly so a harness planner, which Ordewell cannot hand tools to, speaks
6906
- // the same protocol as an API-backed one (ADR-0009).
6907
- ...TASK_QUERY_PROTOCOL,
6931
+ // The envelope is shared by both variants on purpose: a harness planner
6932
+ // Ordewell could not hand tools to speaks the same protocol as an
6933
+ // API-backed one (ADR-0009). One it did hand them to reads through them.
6934
+ ...tools ? TASK_READ_TOOLS_PROTOCOL : TASK_QUERY_PROTOCOL,
6908
6935
  "",
6909
6936
  context ? `PROJECT CONTEXT:
6910
6937
  ${context}
@@ -7207,7 +7234,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
7207
7234
  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.');
7208
7235
  return sections.join("\n");
7209
7236
  }
7210
- function buildMergePrompt(taskIds, tasks) {
7237
+ function buildMergePrompt(taskIds, tasks, tools = false) {
7211
7238
  const idSet = new Set(taskIds);
7212
7239
  const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
7213
7240
  const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
@@ -7215,22 +7242,22 @@ function buildMergePrompt(taskIds, tasks) {
7215
7242
  `Merge these tasks into ONE combined task: ${refs}.`,
7216
7243
  "Write a clear combined title, description, and prompt that cover all of their work.",
7217
7244
  "The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
7218
- '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.',
7219
- 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
7220
- ` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
7245
+ `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.`,
7246
+ tools ? 'Call edit_plan with a single "merge" op:' : 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
7247
+ ` {"${tools ? "ops" : "taskOps"}":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
7221
7248
  `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.`
7222
7249
  ].join("\n");
7223
7250
  }
7224
- function buildSplitPrompt(taskId, tasks) {
7251
+ function buildSplitPrompt(taskId, tasks, tools = false) {
7225
7252
  const task = tasks.find((t) => t.id === taskId);
7226
7253
  if (!task) return `Split task ${taskId} into smaller tasks.`;
7227
7254
  return [
7228
7255
  `Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
7229
7256
  "Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
7230
7257
  "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.",
7231
- '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).',
7232
- 'Reply with ONLY a taskOps JSON object using a single "split" op:',
7233
- ` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
7258
+ `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).`,
7259
+ tools ? 'Call edit_plan with a single "split" op:' : 'Reply with ONLY a taskOps JSON object using a single "split" op:',
7260
+ ` {"${tools ? "ops" : "taskOps"}":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
7234
7261
  `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.`
7235
7262
  ].join("\n");
7236
7263
  }
@@ -7284,15 +7311,15 @@ ${task.prompt ?? task.description}`;
7284
7311
 
7285
7312
  // src/services/PlanRepair.ts
7286
7313
  async function repairLoop(opts) {
7287
- let reply = await opts.first();
7314
+ let reply2 = await opts.first();
7288
7315
  for (let repairs = 0; ; repairs++) {
7289
- const verdict = await opts.interpret(reply);
7316
+ const verdict = await opts.interpret(reply2);
7290
7317
  if ("done" in verdict) return verdict.done;
7291
7318
  if (repairs >= opts.maxRepairs) {
7292
- return opts.onExhausted({ reply, errors: verdict.retry.errors, cause: verdict.retry.cause });
7319
+ return opts.onExhausted({ reply: reply2, errors: verdict.retry.errors, cause: verdict.retry.cause });
7293
7320
  }
7294
7321
  const { corrective } = verdict.retry;
7295
- reply = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
7322
+ reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
7296
7323
  }
7297
7324
  }
7298
7325
  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.";
@@ -7408,34 +7435,34 @@ async function settleReply(opts) {
7408
7435
  }
7409
7436
  if (attempt.failure !== void 0) return { done: message(attempt.failure) };
7410
7437
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
7411
- const reply = classifyPlannerReply(attempt.text, opts.classify);
7412
- switch (reply.kind) {
7438
+ const reply2 = classifyPlannerReply(attempt.text, opts.classify);
7439
+ switch (reply2.kind) {
7413
7440
  case "plan":
7414
- return { done: { kind: "plan", tasks: reply.tasks, text: said(attempt), researchLog } };
7441
+ return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
7415
7442
  case "task_ops":
7416
- return { done: { kind: "task_ops", ops: reply.ops, text: said(attempt), researchLog } };
7443
+ return { done: { kind: "task_ops", ops: reply2.ops, text: said(attempt), researchLog } };
7417
7444
  // A read is answered by the conversation, which owns the plan and the
7418
7445
  // catalog, so it leaves here the way a plan or an edit does.
7419
7446
  case "task_query":
7420
- return { done: { kind: "task_query", query: reply.query, text: said(attempt), researchLog } };
7447
+ return { done: { kind: "task_query", query: reply2.query, text: said(attempt), researchLog } };
7421
7448
  case "prose":
7422
7449
  return { done: asProse(attempt) };
7423
7450
  }
7424
7451
  if (opts.signal?.aborted) return { done: asProse(attempt) };
7425
- const errors = [reply.error.message];
7426
- switch (reply.kind) {
7452
+ const errors = [reply2.error.message];
7453
+ switch (reply2.kind) {
7427
7454
  case "broken_task_ops":
7428
- return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply.error.message), cause: reply.error } };
7455
+ return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
7429
7456
  case "broken_task_query":
7430
- return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply.error.message), cause: reply.error } };
7457
+ return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
7431
7458
  case "broken_plan": {
7432
- const corrective = reply.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply.error.message);
7433
- return { retry: { errors, corrective, cause: reply.error } };
7459
+ const corrective = reply2.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply2.error.message);
7460
+ return { retry: { errors, corrective, cause: reply2.error } };
7434
7461
  }
7435
7462
  }
7436
7463
  },
7437
7464
  maxRepairs: MAX_JSON_REPAIRS,
7438
- onExhausted: ({ reply }) => asProse(reply)
7465
+ onExhausted: ({ reply: reply2 }) => asProse(reply2)
7439
7466
  });
7440
7467
  }
7441
7468
  var said = (attempt) => attempt.fullText ?? attempt.text;
@@ -7662,8 +7689,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
7662
7689
  const blocks = [{ tool: null, lines: [] }];
7663
7690
  for (const line of resultsText.split("\n")) {
7664
7691
  const m = headerRe.exec(line);
7665
- const tool = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
7666
- if (tool) blocks.push({ tool, lines: [line] });
7692
+ const tool2 = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
7693
+ if (tool2) blocks.push({ tool: tool2, lines: [line] });
7667
7694
  else blocks[blocks.length - 1].lines.push(line);
7668
7695
  }
7669
7696
  const texts = blocks.map((b) => b.lines.join("\n"));
@@ -8156,9 +8183,9 @@ ${llmOutput}
8156
8183
  return { tasks: null, researchLog, researchResults };
8157
8184
  }
8158
8185
  const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
8159
- const reply = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
8160
- if (reply.kind === "plan") {
8161
- return { tasks: reply.tasks, researchLog, researchResults };
8186
+ const reply2 = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
8187
+ if (reply2.kind === "plan") {
8188
+ return { tasks: reply2.tasks, researchLog, researchResults };
8162
8189
  }
8163
8190
  if (!turn.hasToolCalls) break;
8164
8191
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
@@ -8877,7 +8904,7 @@ ${collected.aiflowContext}
8877
8904
  };
8878
8905
 
8879
8906
  // src/services/TaskOrchestrator.ts
8880
- import * as path13 from "path";
8907
+ import * as path14 from "path";
8881
8908
 
8882
8909
  // src/services/promptAugment.ts
8883
8910
  var MAX_TAIL_CHARS = 500;
@@ -8995,10 +9022,14 @@ function renderPreviousAttempt(output) {
8995
9022
  tail
8996
9023
  ].join("\n");
8997
9024
  }
8998
- function renderCompletionMarker(task) {
9025
+ function renderCompletionMarker(task, completionTool = false) {
9026
+ 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.`;
9027
+ if (!completionTool) return `
9028
+
9029
+ When you have fully completed this task, print one final line containing only the completion marker. ${howto}`;
8999
9030
  return `
9000
9031
 
9001
- 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.`;
9032
+ 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}`;
9002
9033
  }
9003
9034
  function renderTddInstruction() {
9004
9035
  return [
@@ -9019,7 +9050,21 @@ function renderTddInstruction() {
9019
9050
  ].join("\n");
9020
9051
  }
9021
9052
  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 `>>>`";
9022
- function renderCheckpointInstruction() {
9053
+ function renderCheckpointInstruction(checkpointTool = false) {
9054
+ if (checkpointTool) {
9055
+ return [
9056
+ "## Human-in-the-loop checkpoints",
9057
+ "",
9058
+ "When you reach a decision point that requires human judgment \u2014 before destructive",
9059
+ "operations, after major design decisions, or when multiple viable paths exist \u2014 pause",
9060
+ "and request input:",
9061
+ "",
9062
+ "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",
9063
+ "2. If the result is `continue`: proceed with the action you described",
9064
+ "3. If the result starts with `rejected:`: the rest is why. Adjust your approach and call `checkpoint` again if needed",
9065
+ `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}`
9066
+ ].join("\n");
9067
+ }
9023
9068
  return [
9024
9069
  "## Human-in-the-loop checkpoints",
9025
9070
  "",
@@ -9051,9 +9096,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
9051
9096
  blocks.push(renderTddInstruction());
9052
9097
  }
9053
9098
  if (isHitlTask(task)) {
9054
- blocks.push(renderCheckpointInstruction());
9099
+ blocks.push(renderCheckpointInstruction(opts?.completionTool));
9055
9100
  }
9056
- const marker2 = renderCompletionMarker(task);
9101
+ const marker2 = renderCompletionMarker(task, opts?.completionTool);
9057
9102
  if (blocks.length === 0) return basePrompt + marker2;
9058
9103
  return `${blocks.join("\n\n")}
9059
9104
 
@@ -9064,12 +9109,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
9064
9109
  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."
9065
9110
  ];
9066
9111
  if (isHitlTask(task)) {
9067
- 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}.`);
9112
+ 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}.`);
9068
9113
  }
9069
9114
  return `${message.trim()}
9070
9115
 
9071
9116
  ---
9072
- ${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
9117
+ ${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
9073
9118
  }
9074
9119
 
9075
9120
  // src/services/VerdictEngine.ts
@@ -9087,6 +9132,13 @@ var VerdictEngine = class {
9087
9132
  pausedSessions = /* @__PURE__ */ new Map();
9088
9133
  listeners = [];
9089
9134
  checkpointListeners = [];
9135
+ withdrawnListeners = [];
9136
+ /**
9137
+ * The open `checkpoint` tool call per task (ADR-0022, V5): settling it is how
9138
+ * an answer reaches a runner that asked through the tool, where the marker's
9139
+ * answer is typed into the session instead.
9140
+ */
9141
+ toolCheckpoints = /* @__PURE__ */ new Map();
9090
9142
  idleListeners = [];
9091
9143
  /**
9092
9144
  * Per-task generation. Replaced on every watch(), clear() and verdict.
@@ -9117,6 +9169,9 @@ var VerdictEngine = class {
9117
9169
  onCheckpoint(listener) {
9118
9170
  this.checkpointListeners.push(listener);
9119
9171
  }
9172
+ onCheckpointWithdrawn(listener) {
9173
+ this.withdrawnListeners.push(listener);
9174
+ }
9120
9175
  onIdleChange(listener) {
9121
9176
  this.idleListeners.push(listener);
9122
9177
  }
@@ -9178,6 +9233,11 @@ ${line}
9178
9233
  }
9179
9234
  approveCheckpoint(taskId) {
9180
9235
  this.resumeIdle(taskId);
9236
+ const toolCall = this.toolCheckpoints.get(taskId);
9237
+ if (toolCall) {
9238
+ toolCall({ kind: "continue" });
9239
+ return;
9240
+ }
9181
9241
  const session = this.pausedSessions.get(taskId);
9182
9242
  if (session) {
9183
9243
  session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
@@ -9186,6 +9246,11 @@ ${line}
9186
9246
  }
9187
9247
  rejectCheckpoint(taskId, reason) {
9188
9248
  this.resumeIdle(taskId);
9249
+ const toolCall = this.toolCheckpoints.get(taskId);
9250
+ if (toolCall) {
9251
+ toolCall({ kind: "rejected", reason });
9252
+ return;
9253
+ }
9189
9254
  const session = this.pausedSessions.get(taskId);
9190
9255
  if (session) {
9191
9256
  session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
@@ -9196,7 +9261,10 @@ ${line}
9196
9261
  * Attach to a spawned session: scan the output tail for the task's completion
9197
9262
  * marker (delivering a verdict immediately while leaving interactive sessions
9198
9263
  * open), scan for checkpoint markers, and on exit produce a failed verdict
9199
- * when the marker was never observed.
9264
+ * when the marker was never observed. A structured session's `task_complete`
9265
+ * call is evidence too (ADR-0022, V2): whichever signal comes first decides.
9266
+ *
9267
+ * Returns the attempt's generation, what {@link signalComplete} is checked against.
9200
9268
  */
9201
9269
  watch(task, session) {
9202
9270
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
@@ -9226,6 +9294,8 @@ ${line}
9226
9294
  else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
9227
9295
  else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
9228
9296
  });
9297
+ session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
9298
+ session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
9229
9299
  }
9230
9300
  session.onExit((exitCode) => {
9231
9301
  if (this.generations.get(task.id) !== gen) return;
@@ -9234,6 +9304,52 @@ ${line}
9234
9304
  const verdict = this.decide(task, exitCode);
9235
9305
  for (const l of this.listeners) l(task.id, verdict);
9236
9306
  });
9307
+ return gen;
9308
+ }
9309
+ /**
9310
+ * The runner's own `task_complete` call (ADR-0022, V1/V3): settles the
9311
+ * attempt exactly as the marker does, unless that attempt is no longer the
9312
+ * task's current one or another signal already settled it.
9313
+ */
9314
+ signalComplete(taskId, generation, report) {
9315
+ if (this.generations.get(taskId) !== generation) return;
9316
+ this.forget(taskId);
9317
+ this.bumpGeneration(taskId);
9318
+ const verdict = reportedVerdict(report);
9319
+ for (const l of this.listeners) l(taskId, verdict);
9320
+ }
9321
+ /**
9322
+ * The runner's `checkpoint` call (ADR-0022, V5): raised through the same
9323
+ * listeners as the marker, so the task waits on the user the same way, and
9324
+ * settled by {@link approveCheckpoint} or {@link rejectCheckpoint}. It is
9325
+ * withdrawn, never left hanging, once the attempt is over or the call goes.
9326
+ */
9327
+ raiseCheckpoint(taskId, generation, question, signal) {
9328
+ if (this.generations.get(taskId) !== generation || signal.aborted) {
9329
+ return Promise.resolve({ kind: "withdrawn", why: "this attempt has ended." });
9330
+ }
9331
+ if (this.toolCheckpoints.has(taskId)) {
9332
+ return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
9333
+ }
9334
+ return new Promise((resolve7) => {
9335
+ const settle = (answer2) => {
9336
+ signal.removeEventListener("abort", onAbort);
9337
+ if (this.toolCheckpoints.get(taskId) === settle) this.toolCheckpoints.delete(taskId);
9338
+ resolve7(answer2);
9339
+ };
9340
+ const onAbort = () => {
9341
+ settle({ kind: "withdrawn", why: "the call was cancelled." });
9342
+ if (this.generations.get(taskId) !== generation) return;
9343
+ this.resumeIdle(taskId);
9344
+ for (const l of this.withdrawnListeners) l(taskId);
9345
+ };
9346
+ signal.addEventListener("abort", onAbort, { once: true });
9347
+ this.toolCheckpoints.set(taskId, settle);
9348
+ for (const l of this.checkpointListeners) l(taskId, question.trim());
9349
+ });
9350
+ }
9351
+ withdrawToolCheckpoint(taskId) {
9352
+ this.toolCheckpoints.get(taskId)?.({ kind: "withdrawn", why: "this attempt has ended." });
9237
9353
  }
9238
9354
  approvalOpened(taskId, approvalId) {
9239
9355
  const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
@@ -9267,6 +9383,7 @@ ${line}
9267
9383
  this.markerTails.delete(taskId);
9268
9384
  this.checkpointCarry.delete(taskId);
9269
9385
  this.pausedSessions.delete(taskId);
9386
+ this.withdrawToolCheckpoint(taskId);
9270
9387
  this.idlePaused.delete(taskId);
9271
9388
  this.openApprovals.delete(taskId);
9272
9389
  this.clearIdle(taskId);
@@ -9302,6 +9419,7 @@ ${line}
9302
9419
  this.markerTails.clear();
9303
9420
  this.checkpointCarry.clear();
9304
9421
  this.pausedSessions.clear();
9422
+ for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
9305
9423
  for (const timer of this.idleTimers.values()) clearTimeout(timer);
9306
9424
  this.idleTimers.clear();
9307
9425
  this.idleSince.clear();
@@ -9355,6 +9473,28 @@ ${line}
9355
9473
  };
9356
9474
  }
9357
9475
  };
9476
+ function reportedVerdict(report) {
9477
+ const checks = [
9478
+ {
9479
+ name: "task_complete",
9480
+ passed: report.status === "done",
9481
+ skipped: false,
9482
+ detail: `the runner called task_complete with status "${report.status}"`
9483
+ },
9484
+ {
9485
+ name: "completion_marker",
9486
+ passed: report.status === "done",
9487
+ skipped: true,
9488
+ detail: "bypassed \u2014 the runner reported through task_complete"
9489
+ }
9490
+ ];
9491
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
9492
+ if (report.status === "done") {
9493
+ return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt };
9494
+ }
9495
+ const why = report.reason?.trim() || "no reason given";
9496
+ return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt };
9497
+ }
9358
9498
 
9359
9499
  // src/services/transcriptCapture.ts
9360
9500
  import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync4 } from "fs";
@@ -9518,10 +9658,10 @@ async function fromDirenv(cwd, deps) {
9518
9658
  }
9519
9659
  return { env, blocked: null };
9520
9660
  }
9521
- function findUp(start, relative5, deps) {
9661
+ function findUp(start, relative6, deps) {
9522
9662
  let dir = path9.resolve(start);
9523
9663
  for (; ; ) {
9524
- const candidate = path9.join(dir, relative5);
9664
+ const candidate = path9.join(dir, relative6);
9525
9665
  if (deps.readFile(candidate) !== null) return candidate;
9526
9666
  const parent = path9.dirname(dir);
9527
9667
  if (parent === dir) return null;
@@ -9770,6 +9910,11 @@ var BufferedTaskOutputSource = class {
9770
9910
  session.onExit(() => {
9771
9911
  capture.exited = true;
9772
9912
  });
9913
+ if (isStructuredSession(session)) {
9914
+ session.onTaskComplete(({ summary }) => {
9915
+ if (capture.attached && summary.trim()) capture.reported = summary.trim();
9916
+ });
9917
+ }
9773
9918
  }
9774
9919
  detach(taskId) {
9775
9920
  const capture = this.captures.get(taskId);
@@ -9780,6 +9925,8 @@ var BufferedTaskOutputSource = class {
9780
9925
  this.captures.clear();
9781
9926
  }
9782
9927
  async finalText(attempt, doneToken) {
9928
+ const reported = this.captures.get(attempt.taskId)?.reported;
9929
+ if (reported) return defuseMarkers(reported);
9783
9930
  if (attempt.cwd && attempt.completionMarker) {
9784
9931
  const transcript = await this.transcripts.finalAssistantText({
9785
9932
  runner: attempt.runner,
@@ -10619,8 +10766,8 @@ var IsolationRunController = class {
10619
10766
  this.resolvers = {};
10620
10767
  this.reportedCopies.clear();
10621
10768
  this.listener.changed();
10622
- const shared = sharedPathsNotice(this.run);
10623
- if (shared) this.tell("info", shared);
10769
+ const shared2 = sharedPathsNotice(this.run);
10770
+ if (shared2) this.tell("info", shared2);
10624
10771
  }
10625
10772
  reportCopies(copied) {
10626
10773
  const fresh = copied.filter((p) => !this.reportedCopies.has(p));
@@ -11792,6 +11939,502 @@ ${tail}` : ""}`;
11792
11939
  }
11793
11940
  };
11794
11941
 
11942
+ // src/services/harness/fileDiff.ts
11943
+ function markedLines(text, mark) {
11944
+ if (!text) return text;
11945
+ const body = text.endsWith("\n") ? text.slice(0, -1) : text;
11946
+ return `${body.split("\n").map((line) => `${mark}${line}`).join("\n")}${body === text ? "" : "\n"}`;
11947
+ }
11948
+ var HUNK_HEADER = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/;
11949
+ function hunksOf(diff) {
11950
+ const kept = [];
11951
+ let oldLeft = 0;
11952
+ let newLeft = 0;
11953
+ for (const line of diff.split("\n")) {
11954
+ if (oldLeft > 0 || newLeft > 0 || line.startsWith("\\")) {
11955
+ kept.push(line);
11956
+ if (line.startsWith("\\")) continue;
11957
+ if (!line.startsWith("+")) oldLeft -= 1;
11958
+ if (!line.startsWith("-")) newLeft -= 1;
11959
+ continue;
11960
+ }
11961
+ const header = HUNK_HEADER.exec(line);
11962
+ if (!header) continue;
11963
+ kept.push(line);
11964
+ oldLeft = header[1] === void 0 ? 1 : Number(header[1]);
11965
+ newLeft = header[2] === void 0 ? 1 : Number(header[2]);
11966
+ }
11967
+ return kept.length ? `${kept.join("\n")}${diff.endsWith("\n") ? "\n" : ""}` : diff;
11968
+ }
11969
+ function range(start, lines) {
11970
+ return lines === 1 ? `${start}` : `${start},${lines}`;
11971
+ }
11972
+ function structuredPatchText(patch) {
11973
+ if (!Array.isArray(patch)) return "";
11974
+ let text = "";
11975
+ for (const hunk of patch) {
11976
+ if (!hunk || typeof hunk !== "object") return "";
11977
+ const { oldStart, oldLines, newStart, newLines, lines } = hunk;
11978
+ if (typeof oldStart !== "number" || typeof oldLines !== "number" || typeof newStart !== "number" || typeof newLines !== "number") return "";
11979
+ if (!Array.isArray(lines) || !lines.every((line) => typeof line === "string")) return "";
11980
+ text += `@@ -${range(oldStart, oldLines)} +${range(newStart, newLines)} @@
11981
+ ${lines.map((line) => `${line}
11982
+ `).join("")}`;
11983
+ }
11984
+ return text;
11985
+ }
11986
+
11987
+ // src/services/mcp/OrdewellMcpServer.ts
11988
+ import { createServer } from "http";
11989
+ import { randomBytes as randomBytes4 } from "crypto";
11990
+ import { Server as McpProtocolServer } from "@modelcontextprotocol/sdk/server/index.js";
11991
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11992
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
11993
+
11994
+ // src/utils/daemonToken.ts
11995
+ import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
11996
+ import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
11997
+ import { join as join13 } from "path";
11998
+
11999
+ // src/utils/globalDataDir.ts
12000
+ import * as fs7 from "fs";
12001
+ import * as path13 from "path";
12002
+ import * as os5 from "os";
12003
+ function globalDataDir() {
12004
+ return path13.join(os5.homedir(), ".ordewell");
12005
+ }
12006
+ function copyDirSync(src, dest) {
12007
+ fs7.mkdirSync(dest, { recursive: true });
12008
+ const entries = fs7.readdirSync(src, { withFileTypes: true });
12009
+ for (const entry of entries) {
12010
+ const srcPath = path13.join(src, entry.name);
12011
+ const destPath = path13.join(dest, entry.name);
12012
+ if (entry.isDirectory()) {
12013
+ copyDirSync(srcPath, destPath);
12014
+ } else {
12015
+ fs7.copyFileSync(srcPath, destPath);
12016
+ }
12017
+ }
12018
+ }
12019
+ var migrated = false;
12020
+ function migrateOldConfigDir() {
12021
+ if (migrated) return;
12022
+ migrated = true;
12023
+ const oldDir = path13.join(os5.homedir(), ".config", "ordewell");
12024
+ if (!fs7.existsSync(oldDir)) return;
12025
+ const newDir = globalDataDir();
12026
+ const liftFile = (name) => {
12027
+ const oldFile = path13.join(oldDir, name);
12028
+ const newFile = path13.join(newDir, name);
12029
+ if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
12030
+ fs7.mkdirSync(newDir, { recursive: true });
12031
+ fs7.copyFileSync(oldFile, newFile);
12032
+ }
12033
+ };
12034
+ liftFile("settings.json");
12035
+ liftFile(".env");
12036
+ const oldPlugins = path13.join(oldDir, "plugins");
12037
+ const newPlugins = path13.join(newDir, "plugins");
12038
+ if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
12039
+ copyDirSync(oldPlugins, newPlugins);
12040
+ }
12041
+ }
12042
+
12043
+ // src/utils/privateFile.ts
12044
+ import { closeSync, mkdirSync as mkdirSync6, openSync, renameSync, unlinkSync, writeSync } from "fs";
12045
+ import { basename as basename2, dirname as dirname5, join as join12 } from "path";
12046
+ import { randomBytes as randomBytes2 } from "crypto";
12047
+ var DIR_MODE = 448;
12048
+ var FILE_MODE = 384;
12049
+ var POSIX_MODES = process.platform !== "win32";
12050
+ function ensurePrivateDir(dir) {
12051
+ if (POSIX_MODES) {
12052
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
12053
+ return;
12054
+ }
12055
+ mkdirSync6(dir, { recursive: true });
12056
+ }
12057
+ function writePrivateFile(filePath, content) {
12058
+ const dir = dirname5(filePath);
12059
+ ensurePrivateDir(dir);
12060
+ const tempPath = join12(dir, `.${basename2(filePath)}.${randomBytes2(8).toString("hex")}.tmp`);
12061
+ const fd = openSync(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
12062
+ try {
12063
+ writeSync(fd, content);
12064
+ } finally {
12065
+ closeSync(fd);
12066
+ }
12067
+ try {
12068
+ renameSync(tempPath, filePath);
12069
+ } catch (err) {
12070
+ try {
12071
+ unlinkSync(tempPath);
12072
+ } catch {
12073
+ }
12074
+ throw err;
12075
+ }
12076
+ }
12077
+
12078
+ // src/utils/daemonToken.ts
12079
+ var BEARER_PREFIX = "bearer ";
12080
+ var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
12081
+ var DAEMON_SUBPROTOCOL = "ordewell.v1";
12082
+ function configDir() {
12083
+ return globalDataDir();
12084
+ }
12085
+ function daemonTokenPath(port) {
12086
+ return join13(configDir(), `server-${port}.token`);
12087
+ }
12088
+ function mintDaemonToken(port) {
12089
+ const token = randomBytes3(32).toString("base64url");
12090
+ const file = daemonTokenPath(port);
12091
+ writePrivateFile(file, token);
12092
+ return { token, file };
12093
+ }
12094
+ function readDaemonToken(port) {
12095
+ try {
12096
+ const token = readFileSync4(daemonTokenPath(port), "utf8").trim();
12097
+ return token === "" ? void 0 : token;
12098
+ } catch {
12099
+ return void 0;
12100
+ }
12101
+ }
12102
+ function clearDaemonToken(port) {
12103
+ try {
12104
+ unlinkSync2(daemonTokenPath(port));
12105
+ } catch {
12106
+ }
12107
+ }
12108
+ function bearerHeaderValue(token) {
12109
+ return `Bearer ${token}`;
12110
+ }
12111
+ function tokenSubprotocols(token) {
12112
+ return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
12113
+ }
12114
+ function extractPresentedToken(carriers) {
12115
+ const authorization = carriers.authorization;
12116
+ if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
12117
+ const value = authorization.slice(BEARER_PREFIX.length).trim();
12118
+ if (value !== "") return value;
12119
+ }
12120
+ for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
12121
+ const value = offered.trim();
12122
+ if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
12123
+ const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
12124
+ if (token !== "") return token;
12125
+ }
12126
+ return void 0;
12127
+ }
12128
+ function tokensMatch(presented, expected) {
12129
+ if (presented === void 0) return false;
12130
+ const a = Buffer.from(presented, "utf8");
12131
+ const b = Buffer.from(expected, "utf8");
12132
+ if (a.length !== b.length) return false;
12133
+ return timingSafeEqual(a, b);
12134
+ }
12135
+
12136
+ // src/services/mcp/tools.ts
12137
+ import { z } from "zod/v4";
12138
+ var taskCompleteInput = z.object({
12139
+ status: z.enum(["done", "blocked", "failed"]).describe("'done' only when the task is fully complete. 'blocked' or 'failed' end the attempt without passing."),
12140
+ summary: z.string().describe("What was done, handed to the tasks that depend on this one."),
12141
+ reason: z.string().optional().describe("Why the task is blocked or failed. Expected for anything but 'done'.")
12142
+ });
12143
+ var checkpointInput = z.object({
12144
+ question: z.string().min(1).describe("The question for the user. The call returns their answer.")
12145
+ });
12146
+ var listRunnersInput = z.object({});
12147
+ var listModelsInput = z.object({
12148
+ runner: z.string().min(1).describe("A runner id from list_runners.")
12149
+ });
12150
+ var taskRef = z.union([z.string().min(1), z.number().int().min(0)]).describe('A task id, "#order", a bare order, or a title.');
12151
+ var modelAssignment = z.looseObject({
12152
+ modelId: z.string(),
12153
+ modelLabel: z.string().optional(),
12154
+ thinkingEffort: z.string().optional().describe("A variant id from that model's variants list.")
12155
+ });
12156
+ var planTask = z.looseObject({
12157
+ id: z.string().optional(),
12158
+ order: z.number().optional(),
12159
+ title: z.string(),
12160
+ description: z.string().optional(),
12161
+ type: z.enum(["ai", "user"]).optional(),
12162
+ dependencies: z.array(z.string()).optional(),
12163
+ prompt: z.string().optional(),
12164
+ userSteps: z.array(z.looseObject({ order: z.number().optional(), instruction: z.string() })).optional(),
12165
+ assignedRunner: z.string().optional(),
12166
+ assignedModel: modelAssignment.optional(),
12167
+ taskMode: z.string().optional(),
12168
+ autonomy: z.enum(["AFK", "HITL"]).optional(),
12169
+ sliceType: z.enum(["AFK", "HITL"]).optional(),
12170
+ ops: z.boolean().optional(),
12171
+ userStoriesCovered: z.array(z.string()).optional(),
12172
+ subtasks: z.array(z.record(z.string(), z.unknown())).optional()
12173
+ });
12174
+ var submitPlanInput = z.object({
12175
+ tasks: z.array(planTask).min(1).describe("The whole plan, in order.")
12176
+ });
12177
+ var taskFields = z.record(z.string(), z.unknown());
12178
+ var handle = z.string().optional();
12179
+ var taskOp = z.discriminatedUnion("op", [
12180
+ z.object({ op: z.literal("update"), taskId: taskRef, changes: taskFields }),
12181
+ z.object({ op: z.literal("add"), task: taskFields, handle }),
12182
+ z.object({ op: z.literal("remove"), taskId: taskRef }),
12183
+ z.object({ op: z.literal("reorder"), taskIds: z.array(taskRef) }),
12184
+ z.object({ op: z.literal("merge"), taskIds: z.array(taskRef), merged: taskFields, handle }),
12185
+ z.object({ op: z.literal("split"), taskId: taskRef, parts: z.array(taskFields), handle }),
12186
+ z.object({ op: z.literal("rearm"), taskId: taskRef, changes: taskFields.optional() })
12187
+ ]);
12188
+ var editPlanInput = z.object({
12189
+ ops: z.array(taskOp).min(1).describe("Applied as one batch; every ref resolves against the plan before any op runs.")
12190
+ });
12191
+ var taskQueryInput = z.object({
12192
+ tasks: z.array(taskRef).optional(),
12193
+ fields: z.array(z.enum(TASK_QUERY_TASK_FIELDS)).optional().describe("Omit to read every field. A task's output is read with task_output."),
12194
+ catalog: z.boolean().optional().describe("Also return every runner with its models, thinking-effort variants and modes.")
12195
+ }).refine((q) => (q.tasks?.length ?? 0) > 0 || q.catalog === true, { message: "Name at least one task, or set catalog: true." });
12196
+ var taskOutputInput = z.object({
12197
+ task: taskRef,
12198
+ lines: 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}.`),
12199
+ since: z.number().int().min(0).optional().describe("A previous answer's nextOffset: return only what came after it.")
12200
+ });
12201
+ function checkpointReply(answer2) {
12202
+ switch (answer2.kind) {
12203
+ case "continue":
12204
+ return { text: "continue" };
12205
+ case "rejected":
12206
+ return { text: `rejected: ${answer2.reason}` };
12207
+ case "withdrawn":
12208
+ return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
12209
+ }
12210
+ }
12211
+ function tool(name, description, input, pick, annotations) {
12212
+ return {
12213
+ name,
12214
+ description,
12215
+ inputSchema: { ...z.toJSONSchema(input, { io: "input" }), type: "object" },
12216
+ ...annotations ? { annotations } : {},
12217
+ async call(handler, args, context) {
12218
+ const run = pick(handler);
12219
+ if (!run) return { text: `${name} is not available in this session.`, isError: true };
12220
+ const parsed = input.safeParse(args ?? {});
12221
+ if (!parsed.success) return { text: `Invalid ${name} arguments: ${z.prettifyError(parsed.error)}`, isError: true };
12222
+ return run(parsed.data, context);
12223
+ }
12224
+ };
12225
+ }
12226
+ var TASK_TOOLS = [
12227
+ tool(
12228
+ "task_complete",
12229
+ "Report that this task has ended, and how. Call it once, as your last action.",
12230
+ taskCompleteInput,
12231
+ (h) => h.taskComplete?.bind(h)
12232
+ ),
12233
+ tool(
12234
+ "checkpoint",
12235
+ "Ask the user a question and wait for the answer, which is this call's result.",
12236
+ checkpointInput,
12237
+ (h) => h.checkpoint?.bind(h)
12238
+ )
12239
+ ];
12240
+ var PLANNER_READ_ONLY = { readOnlyHint: true };
12241
+ var PLANNER_TOOLS = [
12242
+ tool(
12243
+ "list_runners",
12244
+ "List the runners enabled right now, each with its modes and default mode. Call it just before submit_plan.",
12245
+ listRunnersInput,
12246
+ (h) => h.listRunners?.bind(h),
12247
+ PLANNER_READ_ONLY
12248
+ ),
12249
+ tool(
12250
+ "list_models",
12251
+ "List the models a runner may use right now, with labels and thinking-effort variants.",
12252
+ listModelsInput,
12253
+ (h) => h.listModels?.bind(h),
12254
+ PLANNER_READ_ONLY
12255
+ ),
12256
+ tool(
12257
+ "submit_plan",
12258
+ "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.",
12259
+ submitPlanInput,
12260
+ (h) => h.submitPlan?.bind(h),
12261
+ PLANNER_READ_ONLY
12262
+ ),
12263
+ tool(
12264
+ "edit_plan",
12265
+ "Change the current plan with task operations: update, add, remove, reorder, merge, split or rearm.",
12266
+ editPlanInput,
12267
+ (h) => h.editPlan?.bind(h),
12268
+ PLANNER_READ_ONLY
12269
+ ),
12270
+ tool(
12271
+ "task_query",
12272
+ "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.",
12273
+ taskQueryInput,
12274
+ (h) => h.taskQuery?.bind(h),
12275
+ PLANNER_READ_ONLY
12276
+ ),
12277
+ tool(
12278
+ "task_output",
12279
+ "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.",
12280
+ taskOutputInput,
12281
+ (h) => h.taskOutput?.bind(h),
12282
+ PLANNER_READ_ONLY
12283
+ )
12284
+ ];
12285
+
12286
+ // src/services/mcp/OrdewellMcpServer.ts
12287
+ var ORDEWELL_MCP_PATH = "/mcp";
12288
+ var DEFAULT_HEARTBEAT_MS = 3e4;
12289
+ var OrdewellMcpServer = class {
12290
+ grants = /* @__PURE__ */ new Map();
12291
+ http;
12292
+ listening;
12293
+ boundUrl;
12294
+ boundHost;
12295
+ heartbeatMs;
12296
+ constructor(options = {}) {
12297
+ this.heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
12298
+ }
12299
+ /** The listener's URL, once the first token has started it. */
12300
+ get url() {
12301
+ return this.boundUrl;
12302
+ }
12303
+ issueTaskToken(scope, handler = {}) {
12304
+ return this.issue({ role: "task", scope: { ...scope }, handler, revoked: new AbortController() });
12305
+ }
12306
+ issuePlannerToken(scope, handler = {}) {
12307
+ return this.issue({ role: "planner", scope: { ...scope }, handler, revoked: new AbortController() });
12308
+ }
12309
+ /** Refuse every later request carrying `token`, and abort the calls it has in flight. */
12310
+ revoke(token) {
12311
+ const grant = this.grants.get(token);
12312
+ if (!grant) return;
12313
+ this.grants.delete(token);
12314
+ grant.revoked.abort();
12315
+ }
12316
+ async dispose() {
12317
+ for (const token of [...this.grants.keys()]) this.revoke(token);
12318
+ const http = this.http;
12319
+ this.http = void 0;
12320
+ this.listening = void 0;
12321
+ this.boundUrl = void 0;
12322
+ this.boundHost = void 0;
12323
+ if (!http) return;
12324
+ http.closeAllConnections();
12325
+ await new Promise((resolve7) => http.close(() => resolve7()));
12326
+ }
12327
+ async issue(grant) {
12328
+ const url = await this.start();
12329
+ const token = randomBytes4(32).toString("base64url");
12330
+ this.grants.set(token, grant);
12331
+ return { url, token };
12332
+ }
12333
+ start() {
12334
+ this.listening ??= new Promise((resolve7, reject) => {
12335
+ const http = createServer((req, res) => {
12336
+ this.handle(req, res).catch(() => {
12337
+ if (!res.headersSent) reply(res, 500, "Internal error");
12338
+ else res.destroy();
12339
+ });
12340
+ });
12341
+ http.once("error", (err) => {
12342
+ this.listening = void 0;
12343
+ reject(err);
12344
+ });
12345
+ http.listen(0, "127.0.0.1", () => {
12346
+ const address = http.address();
12347
+ if (address === null || typeof address === "string") {
12348
+ reject(new Error("Ordewell MCP server bound to no TCP port"));
12349
+ return;
12350
+ }
12351
+ this.boundHost = `127.0.0.1:${address.port}`;
12352
+ this.boundUrl = `http://${this.boundHost}${ORDEWELL_MCP_PATH}`;
12353
+ resolve7(this.boundUrl);
12354
+ });
12355
+ http.unref();
12356
+ this.http = http;
12357
+ });
12358
+ return this.listening;
12359
+ }
12360
+ async handle(req, res) {
12361
+ if (req.headers.host !== this.boundHost || req.headers.origin !== void 0) {
12362
+ reply(res, 403, "Forbidden");
12363
+ return;
12364
+ }
12365
+ const token = extractPresentedToken({ authorization: req.headers.authorization });
12366
+ const grant = token === void 0 ? void 0 : this.grants.get(token);
12367
+ if (!grant) {
12368
+ reply(res, 401, "Unauthorized");
12369
+ return;
12370
+ }
12371
+ if (new URL(req.url ?? "/", `http://${this.boundHost}`).pathname !== ORDEWELL_MCP_PATH) {
12372
+ reply(res, 404, "Not found");
12373
+ return;
12374
+ }
12375
+ const mcp = new McpProtocolServer({ name: "ordewell", version: "1" }, { capabilities: { tools: {} } });
12376
+ if (grant.role === "task") serveTools(mcp, TASK_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
12377
+ else serveTools(mcp, PLANNER_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
12378
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
12379
+ res.on("close", () => {
12380
+ void transport.close();
12381
+ void mcp.close();
12382
+ });
12383
+ await mcp.connect(transport);
12384
+ await transport.handleRequest(req, res);
12385
+ }
12386
+ };
12387
+ function serveTools(mcp, tools, handler, revoked, heartbeatMs) {
12388
+ mcp.setRequestHandler(ListToolsRequestSchema, () => ({
12389
+ tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, ...annotations ? { annotations } : {} }))
12390
+ }));
12391
+ mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
12392
+ const tool2 = tools.find((t) => t.name === request.params.name);
12393
+ if (!tool2) throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
12394
+ const progressToken = request.params._meta?.progressToken;
12395
+ let beats = 0;
12396
+ const heartbeat = progressToken === void 0 ? void 0 : setInterval(() => {
12397
+ beats += 1;
12398
+ extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress: beats } }).catch(() => void 0);
12399
+ }, heartbeatMs);
12400
+ try {
12401
+ const result = await tool2.call(handler, request.params.arguments, { signal: AbortSignal.any([revoked, extra.signal]) });
12402
+ return { content: [{ type: "text", text: result.text }], isError: result.isError ?? false };
12403
+ } catch (err) {
12404
+ return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
12405
+ } finally {
12406
+ clearInterval(heartbeat);
12407
+ }
12408
+ });
12409
+ }
12410
+ function reply(res, status, message) {
12411
+ res.writeHead(status, { "content-type": "text/plain" }).end(message);
12412
+ }
12413
+ var shared;
12414
+ function sharedMcpServer() {
12415
+ shared ??= new OrdewellMcpServer();
12416
+ return shared;
12417
+ }
12418
+
12419
+ // src/services/mcp/clientConfig.ts
12420
+ import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
12421
+ import { tmpdir } from "os";
12422
+ import { join as join14 } from "path";
12423
+ var ORDEWELL_MCP_SERVER_NAME = "ordewell";
12424
+ function mcpClientConfig(credential) {
12425
+ return {
12426
+ name: ORDEWELL_MCP_SERVER_NAME,
12427
+ url: credential.url,
12428
+ headers: { Authorization: bearerHeaderValue(credential.token) }
12429
+ };
12430
+ }
12431
+ function ownerOnlyConfigFile(fileName, contents) {
12432
+ const dir = mkdtempSync(join14(tmpdir(), "ordewell-mcp-"));
12433
+ const path26 = join14(dir, fileName);
12434
+ writeFileSync3(path26, contents, { mode: 384 });
12435
+ return { path: path26, remove: () => rmSync2(dir, { recursive: true, force: true }) };
12436
+ }
12437
+
11795
12438
  // src/services/harness/ClaudeCodeAdapter.ts
11796
12439
  var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
11797
12440
  var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
@@ -11806,7 +12449,10 @@ var PROTOCOL_ARGS = [
11806
12449
  ];
11807
12450
  var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
11808
12451
  var FOLLOW_ON_TURN_GRACE_MS = 5e3;
12452
+ var MCP_ATTACH_TIMEOUT_MS = 1e4;
12453
+ var MCP_STATUS_POLL_MS = 100;
11809
12454
  var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
12455
+ var ORDEWELL_TASK_TOOLS = ["task_complete", "checkpoint"];
11810
12456
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
11811
12457
  function usageRecord(usage, model, subagentId) {
11812
12458
  const record = {
@@ -11839,10 +12485,16 @@ function flattenContent(content) {
11839
12485
  }
11840
12486
  return "";
11841
12487
  }
12488
+ function editDiff(result) {
12489
+ if (!result || typeof result !== "object") return "";
12490
+ const { type, content, structuredPatch } = result;
12491
+ if (type === "create" && typeof content === "string") return markedLines(content, "+");
12492
+ return structuredPatchText(structuredPatch);
12493
+ }
11842
12494
  var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11843
12495
  agentId = "claude-code";
11844
- interruptCount = 0;
11845
- /** Interrupts sent and not yet acknowledged, by request id. */
12496
+ controlCount = 0;
12497
+ /** Control requests sent and not yet answered, by request id. */
11846
12498
  pendingControl = /* @__PURE__ */ new Map();
11847
12499
  /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
11848
12500
  interruptRequested = false;
@@ -11881,6 +12533,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11881
12533
  requestedMode = null;
11882
12534
  /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
11883
12535
  pendingResume = null;
12536
+ /** The `--mcp-config` file of the running process, which holds its token; removed with the process. */
12537
+ mcpConfig = null;
12538
+ /** `mcp__ordewell__`, once this process was given the server. */
12539
+ ordewellToolPrefix = null;
11884
12540
  spawnSpec(opts) {
11885
12541
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
11886
12542
  const args = [
@@ -11893,6 +12549,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11893
12549
  "--append-system-prompt",
11894
12550
  opts.systemPrompt
11895
12551
  ];
12552
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, PLANNER_TOOLS.map((t) => t.name)));
11896
12553
  if (opts.model) args.push("--model", opts.model);
11897
12554
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
11898
12555
  if (opts.resumeSessionId) {
@@ -11928,30 +12585,78 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11928
12585
  this.reportedCostUsd = void 0;
11929
12586
  this.pendingResume = opts.resumeSessionId;
11930
12587
  }
12588
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, ORDEWELL_TASK_TOOLS));
11931
12589
  return { command: "claude", args };
11932
12590
  }
12591
+ /**
12592
+ * `--mcp-config` takes a path as well as inline JSON; the path keeps the
12593
+ * token out of the argv every local user can list (ADR-0022, A5). The file
12594
+ * lives exactly as long as the process that reads it. `alwaysLoad` because
12595
+ * the CLI otherwise defers MCP tools behind its tool search, and a model
12596
+ * that has to look an Ordewell tool up first tends to go on without it.
12597
+ */
12598
+ ordewellServerArgs(mcp, tools) {
12599
+ this.removeMcpConfig();
12600
+ const config = { mcpServers: { [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } } };
12601
+ const file = ownerOnlyConfigFile("mcp.json", JSON.stringify(config));
12602
+ this.mcpConfig = file;
12603
+ void this.processEnded.then(() => file.remove());
12604
+ const prefix = `mcp__${mcp.name}__`;
12605
+ this.ordewellToolPrefix = prefix;
12606
+ return ["--mcp-config", file.path, "--allowedTools", tools.map((tool2) => `${prefix}${tool2}`).join(",")];
12607
+ }
12608
+ removeMcpConfig() {
12609
+ this.mcpConfig?.remove();
12610
+ this.mcpConfig = null;
12611
+ }
12612
+ async start(opts) {
12613
+ try {
12614
+ await super.start(opts);
12615
+ } catch (err) {
12616
+ this.removeMcpConfig();
12617
+ throw err;
12618
+ }
12619
+ }
12620
+ /** Asks the CLI itself, which reports `pending` until its connection attempt settles. */
12621
+ async mcpAttached() {
12622
+ if (!this.mcpConfig) return false;
12623
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS;
12624
+ while (Date.now() < deadline) {
12625
+ const answer2 = await this.control({ subtype: "mcp_status" }, deadline - Date.now());
12626
+ const status = answer2?.response?.mcpServers?.find((s) => s.name === ORDEWELL_MCP_SERVER_NAME)?.status;
12627
+ if (status === "connected") return true;
12628
+ if (status !== "pending") return false;
12629
+ await new Promise((resolve7) => setTimeout(resolve7, MCP_STATUS_POLL_MS));
12630
+ }
12631
+ return false;
12632
+ }
11933
12633
  /**
11934
12634
  * Claude Code's soft interrupt: the turn stops, the process and its session
11935
12635
  * stay. The CLI acknowledges on the control channel, then closes the turn
11936
12636
  * with an `error_during_execution` result, which {@link handleLine} reports
11937
12637
  * as an interrupted `turn_end`.
11938
12638
  */
11939
- interrupt(timeoutMs) {
11940
- if (!this.process) return Promise.resolve(false);
11941
- this.interruptCount += 1;
11942
- const requestId = `ordewell-interrupt-${this.interruptCount}`;
12639
+ async interrupt(timeoutMs) {
12640
+ if (!this.process) return false;
11943
12641
  this.interruptRequested = true;
12642
+ return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
12643
+ }
12644
+ /** Send a control request and wait for its answer; null when none came in time or the process ended. */
12645
+ control(request, timeoutMs) {
12646
+ if (!this.process) return Promise.resolve(null);
12647
+ this.controlCount += 1;
12648
+ const requestId = `ordewell-${request.subtype}-${this.controlCount}`;
11944
12649
  return new Promise((resolve7) => {
11945
- const settle = (ok) => {
12650
+ const settle = (response) => {
11946
12651
  if (!this.pendingControl.delete(requestId)) return;
11947
12652
  clearTimeout(timer);
11948
- resolve7(ok);
12653
+ resolve7(response);
11949
12654
  };
11950
- const timer = setTimeout(() => settle(false), timeoutMs);
12655
+ const timer = setTimeout(() => settle(null), timeoutMs);
11951
12656
  timer.unref?.();
11952
12657
  this.pendingControl.set(requestId, settle);
11953
- void this.processEnded.then(() => settle(false));
11954
- this.writeLine({ type: "control_request", request_id: requestId, request: { subtype: "interrupt" } });
12658
+ void this.processEnded.then(() => settle(null));
12659
+ this.writeLine({ type: "control_request", request_id: requestId, request });
11955
12660
  });
11956
12661
  }
11957
12662
  /**
@@ -11986,6 +12691,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11986
12691
  }
11987
12692
  dispose() {
11988
12693
  this.clearFollowOnTimer();
12694
+ this.removeMcpConfig();
11989
12695
  super.dispose();
11990
12696
  }
11991
12697
  clearFollowOnTimer() {
@@ -12036,12 +12742,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12036
12742
  emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
12037
12743
  return;
12038
12744
  }
12745
+ const name = msg.request.tool_name ?? "unknown";
12746
+ if (this.ordewellToolPrefix && name.startsWith(this.ordewellToolPrefix)) {
12747
+ this.writeLine({ type: "control_response", response: { subtype: "success", request_id: id, response: { behavior: "allow", updatedInput: input } } });
12748
+ emit({ type: "permission_request", id, name, detail: JSON.stringify(input), input, decided: { decision: "allow" } });
12749
+ return;
12750
+ }
12039
12751
  const suggestions = msg.request.permission_suggestions ?? [];
12040
12752
  this.openPermissions.set(id, { input, suggestions });
12041
12753
  emit({
12042
12754
  type: "permission_request",
12043
12755
  id,
12044
- name: msg.request.tool_name ?? "unknown",
12756
+ name,
12045
12757
  detail: JSON.stringify(input),
12046
12758
  input,
12047
12759
  suggestions,
@@ -12057,7 +12769,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12057
12769
  }
12058
12770
  case "control_response": {
12059
12771
  const requestId = msg.response?.request_id;
12060
- if (requestId) this.pendingControl.get(requestId)?.(msg.response?.subtype === "success");
12772
+ if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
12061
12773
  return;
12062
12774
  }
12063
12775
  case "assistant":
@@ -12085,6 +12797,7 @@ ${block.text}` : block.text });
12085
12797
  if (block.type !== "tool_result") continue;
12086
12798
  const id = block.tool_use_id ?? "";
12087
12799
  const output = flattenContent(block.content);
12800
+ const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
12088
12801
  const subagent = subagentId ? void 0 : this.openSubagents.get(id);
12089
12802
  if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
12090
12803
  emit({ type: "background_agent", id });
@@ -12092,7 +12805,7 @@ ${block.text}` : block.text });
12092
12805
  } else if (subagent) {
12093
12806
  this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
12094
12807
  }
12095
- emit({ type: "tool_result", id, name: "", output, success: block.is_error !== true, subagentId });
12808
+ emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
12096
12809
  }
12097
12810
  return;
12098
12811
  case "system":
@@ -12228,6 +12941,9 @@ ${block.text}` : block.text });
12228
12941
  }
12229
12942
  };
12230
12943
 
12944
+ // src/services/harness/CodexAdapter.ts
12945
+ import { isAbsolute as isAbsolute4, relative as relative4, sep as sep2 } from "path";
12946
+
12231
12947
  // src/services/harness/codexSandbox.ts
12232
12948
  var PROBE_TIMEOUT_MS = 1e4;
12233
12949
  var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
@@ -12287,6 +13003,17 @@ function runProbe(deps, cwd, env, flags) {
12287
13003
 
12288
13004
  // src/services/harness/CodexAdapter.ts
12289
13005
  var HANDSHAKE_TIMEOUT_MS = 3e4;
13006
+ var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
13007
+ var CHANGE_NAMES = { add: "Add", update: "Update", delete: "Delete" };
13008
+ function changeName(change) {
13009
+ return CHANGE_NAMES[change.kind?.type ?? ""] ?? "file_change";
13010
+ }
13011
+ function changeDiff(change) {
13012
+ const diff = change.diff ?? "";
13013
+ if (change.kind?.type === "add") return markedLines(diff, "+");
13014
+ if (change.kind?.type === "delete") return markedLines(diff, "-");
13015
+ return diff;
13016
+ }
12290
13017
  function subagentOutcome(status) {
12291
13018
  switch (status) {
12292
13019
  case "completed":
@@ -12320,9 +13047,31 @@ var DECLINE_RESULTS = {
12320
13047
  execCommandApproval: { decision: "denied" },
12321
13048
  applyPatchApproval: { decision: "denied" }
12322
13049
  };
12323
- var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
12324
- var TASK_APPROVALS = {
12325
- "item/commandExecution/requestApproval": {
13050
+ function shownToolName(item) {
13051
+ const tool2 = item.tool ?? "mcp_tool";
13052
+ return item.server === ORDEWELL_MCP_SERVER_NAME ? `mcp__${ORDEWELL_MCP_SERVER_NAME}__${tool2}` : tool2;
13053
+ }
13054
+ function mcpHeaderEnv(mcp) {
13055
+ return Object.fromEntries(Object.keys(mcp.headers).map((header, i) => [header, `ORDEWELL_MCP_TOKEN_${i}`]));
13056
+ }
13057
+ function ordewellServerConfig(mcp) {
13058
+ return {
13059
+ url: mcp.url,
13060
+ env_http_headers: mcpHeaderEnv(mcp),
13061
+ default_tools_approval_mode: "approve"
13062
+ };
13063
+ }
13064
+ var TASK_TOOLS_NOTE = [
13065
+ "This task has two tools from the `ordewell` MCP server: `task_complete` and `checkpoint`.",
13066
+ "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`.",
13067
+ "Look for them before you finish; the task is reported complete through `task_complete`."
13068
+ ].join(" ");
13069
+ function isOrdewellElicitation(method, params) {
13070
+ return method === "mcpServer/elicitation/request" && params.serverName === ORDEWELL_MCP_SERVER_NAME;
13071
+ }
13072
+ var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
13073
+ var TASK_APPROVALS = {
13074
+ "item/commandExecution/requestApproval": {
12326
13075
  name: "shell",
12327
13076
  forSession: { decision: "acceptForSession" },
12328
13077
  answer: (decision) => ({ decision: REVIEW_DECISIONS[decision] })
@@ -12394,11 +13143,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
12394
13143
  * names only the item, and a person deciding needs to see the files.
12395
13144
  */
12396
13145
  fileChangePaths = /* @__PURE__ */ new Map();
13146
+ /** The row each announced file of an in-flight `fileChange` item is shown under, by the file's shown path. */
13147
+ fileChangeRows = /* @__PURE__ */ new Map();
12397
13148
  /** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
12398
13149
  pendingRequests = /* @__PURE__ */ new Map();
12399
13150
  resumeAttempted = false;
12400
13151
  resumeFallbackSent = false;
12401
13152
  sandbox = "default";
13153
+ /** The Ordewell server's startup state, as Codex last reported it for this thread. */
13154
+ mcpStartup = null;
13155
+ mcpStartupSettled = null;
12402
13156
  /**
12403
13157
  * Subagent threads this session has spawned, keyed by the child thread id.
12404
13158
  * Codex runs a subagent in its own thread and replays both threads' events on
@@ -12407,7 +13161,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
12407
13161
  subagents = /* @__PURE__ */ new Map();
12408
13162
  spawnSpec(opts) {
12409
13163
  this.startOpts = opts;
12410
- return { command: "codex", args: ["app-server"] };
13164
+ const headers = opts.mcp ? mcpHeaderEnv(opts.mcp) : {};
13165
+ const env = Object.fromEntries(Object.entries(headers).map(([header, name]) => [name, opts.mcp.headers[header]]));
13166
+ return { command: "codex", args: ["app-server"], ...opts.mcp ? { env } : {} };
12411
13167
  }
12412
13168
  /**
12413
13169
  * `initialize`, then `thread/start`, both before the first user message. A
@@ -12474,7 +13230,11 @@ ${this.exitMessage()}`);
12474
13230
  cwd: opts?.cwd,
12475
13231
  ...opts?.model ? { model: opts.model } : {}
12476
13232
  };
12477
- const legacyLandlock = this.sandbox === "legacy-landlock" ? { config: { features: { use_legacy_landlock: true } } } : {};
13233
+ const config = {
13234
+ ...this.sandbox === "legacy-landlock" ? { features: { use_legacy_landlock: true } } : {},
13235
+ ...opts?.mcp ? { mcp_servers: { [opts.mcp.name]: ordewellServerConfig(opts.mcp) } } : {}
13236
+ };
13237
+ const threadConfig = Object.keys(config).length ? { config } : {};
12478
13238
  if (opts?.kind === "task") {
12479
13239
  const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
12480
13240
  return {
@@ -12482,14 +13242,15 @@ ${this.exitMessage()}`);
12482
13242
  sandbox: opts.flags.permissionMode,
12483
13243
  ...approvalPolicy ? { approvalPolicy } : {},
12484
13244
  ...approvalsReviewer ? { approvalsReviewer } : {},
12485
- ...legacyLandlock
13245
+ ...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
13246
+ ...threadConfig
12486
13247
  };
12487
13248
  }
12488
13249
  return {
12489
13250
  ...common,
12490
13251
  sandbox: "read-only",
12491
13252
  approvalPolicy: "never",
12492
- ...legacyLandlock,
13253
+ ...threadConfig,
12493
13254
  // `developerInstructions` layers on top of Codex's own base prompt, the
12494
13255
  // way Claude Code's `--append-system-prompt` does. `baseInstructions`
12495
13256
  // replaces it — which takes Codex's description of its own tools with
@@ -12498,6 +13259,32 @@ ${this.exitMessage()}`);
12498
13259
  developerInstructions: opts?.systemPrompt
12499
13260
  };
12500
13261
  }
13262
+ /**
13263
+ * Codex's own startup report for the server, which it sends once the thread
13264
+ * has tried to connect (ADR-0022, S4). A report that came before this was
13265
+ * asked is kept, so asking late still gets the answer.
13266
+ */
13267
+ async mcpAttached() {
13268
+ if (!this.startOpts?.mcp || !this.process) return false;
13269
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS2;
13270
+ while (this.mcpStartup === null || this.mcpStartup === "starting") {
13271
+ const left = deadline - Date.now();
13272
+ if (left <= 0) return false;
13273
+ const ended = await Promise.race([
13274
+ new Promise((resolve7) => {
13275
+ this.mcpStartupSettled = () => resolve7(false);
13276
+ }),
13277
+ this.processEnded.then(() => true),
13278
+ new Promise((resolve7) => {
13279
+ const t = setTimeout(() => resolve7(false), left);
13280
+ t.unref?.();
13281
+ })
13282
+ ]);
13283
+ this.mcpStartupSettled = null;
13284
+ if (ended) return false;
13285
+ }
13286
+ return this.mcpStartup === "ready";
13287
+ }
12501
13288
  effort() {
12502
13289
  const opts = this.startOpts;
12503
13290
  return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
@@ -12669,6 +13456,13 @@ ${this.exitMessage()}`);
12669
13456
  if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
12670
13457
  return;
12671
13458
  }
13459
+ case "mcpServer/startupStatus/updated": {
13460
+ const startup = msg.params;
13461
+ if (startup?.name !== ORDEWELL_MCP_SERVER_NAME || !startup.status) return;
13462
+ this.mcpStartup = startup.status;
13463
+ this.mcpStartupSettled?.();
13464
+ return;
13465
+ }
12672
13466
  case "thread/tokenUsage/updated":
12673
13467
  this.emitUsage(msg.params, emit);
12674
13468
  return;
@@ -12807,6 +13601,10 @@ ${this.exitMessage()}`);
12807
13601
  const method = msg.method;
12808
13602
  const params = msg.params ?? {};
12809
13603
  const approval = TASK_APPROVALS[method];
13604
+ if (isOrdewellElicitation(method, params)) {
13605
+ this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
13606
+ return;
13607
+ }
12810
13608
  if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
12811
13609
  const id = String(msg.id);
12812
13610
  const input = this.approvalInput(method, params);
@@ -12847,6 +13645,10 @@ ${this.exitMessage()}`);
12847
13645
  */
12848
13646
  answerServerRequest(msg, emit) {
12849
13647
  const method = msg.method;
13648
+ if (isOrdewellElicitation(method, msg.params ?? {})) {
13649
+ this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
13650
+ return;
13651
+ }
12850
13652
  const result = DECLINE_RESULTS[method];
12851
13653
  if (result) {
12852
13654
  this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
@@ -12873,7 +13675,7 @@ ${this.exitMessage()}`);
12873
13675
  emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
12874
13676
  return;
12875
13677
  case "mcpToolCall":
12876
- emit({ type: "tool_call", id: item.id, name: item.tool ?? "mcp_tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
13678
+ emit({ type: "tool_call", id: item.id, name: shownToolName(item), args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
12877
13679
  return;
12878
13680
  case "dynamicToolCall":
12879
13681
  emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
@@ -12885,7 +13687,7 @@ ${this.exitMessage()}`);
12885
13687
  const paths = (item.changes ?? []).map((change) => change.path).filter((path26) => !!path26);
12886
13688
  this.fileChangePaths.set(item.id, paths);
12887
13689
  if (this.startOpts?.kind === "task") {
12888
- emit({ type: "tool_call", id: item.id, name: "file_change", args: { path: paths.join(", ") }, ...subagentId ? { subagentId } : {} });
13690
+ for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
12889
13691
  }
12890
13692
  return;
12891
13693
  }
@@ -12942,7 +13744,7 @@ ${item.text}` : item.text });
12942
13744
  emit({
12943
13745
  type: "tool_result",
12944
13746
  id,
12945
- name: item.tool ?? "tool",
13747
+ name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
12946
13748
  output: item.error ?? flattenText(item.result) ?? "",
12947
13749
  success: item.status !== "error" && item.success !== false && !item.error,
12948
13750
  ...subagentId ? { subagentId } : {}
@@ -12956,8 +13758,11 @@ ${item.text}` : item.text });
12956
13758
  case "fileChange":
12957
13759
  this.fileChangePaths.delete(id);
12958
13760
  if (this.startOpts?.kind === "task") {
12959
- const diff = (item.changes ?? []).map((change) => change.diff ?? "").join("\n");
12960
- emit({ type: "tool_result", id, name: "file_change", output: diff, success: item.status === "completed", ...subagentId ? { subagentId } : {} });
13761
+ for (const change of item.changes ?? []) {
13762
+ const row = this.announceFileChange(id, change, emit, subagentId);
13763
+ emit({ type: "tool_result", id: row, name: changeName(change), output: changeDiff(change), success: item.status === "completed", ...subagentId ? { subagentId } : {} });
13764
+ }
13765
+ this.fileChangeRows.delete(id);
12961
13766
  return;
12962
13767
  }
12963
13768
  emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
@@ -12966,6 +13771,34 @@ ${item.text}` : item.text });
12966
13771
  return;
12967
13772
  }
12968
13773
  }
13774
+ /**
13775
+ * One row per changed file: a patch is several files, and one row naming
13776
+ * them all reads as a single cut-off path. The first file keeps the item's
13777
+ * id, which an approval for the patch points at. Announcing a file twice
13778
+ * returns the row it already has, so a change first seen completed still
13779
+ * gets its call.
13780
+ */
13781
+ announceFileChange(itemId, change, emit, subagentId) {
13782
+ const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
13783
+ this.fileChangeRows.set(itemId, rows);
13784
+ const path26 = this.shownChangePath(change);
13785
+ const known = rows.get(path26);
13786
+ if (known) return known;
13787
+ const row = rows.size === 0 ? itemId : `${itemId}:${path26}`;
13788
+ rows.set(path26, row);
13789
+ emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path26 }, ...subagentId ? { subagentId } : {} });
13790
+ return row;
13791
+ }
13792
+ /** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
13793
+ shownChangePath(change) {
13794
+ const shown = (path26) => {
13795
+ const cwd = this.startOpts?.cwd;
13796
+ const rel = cwd ? relative4(cwd, path26) : path26;
13797
+ return rel && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute4(rel) ? rel : path26;
13798
+ };
13799
+ const from = shown(change.path ?? "");
13800
+ return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
13801
+ }
12969
13802
  /**
12970
13803
  * A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
12971
13804
  * subagent. The call itself is planner-level tool activity; the lifecycle it
@@ -13005,7 +13838,45 @@ ${item.text}` : item.text });
13005
13838
  };
13006
13839
 
13007
13840
  // src/services/harness/OpenCodeAdapter.ts
13008
- import { randomBytes as randomBytes2 } from "crypto";
13841
+ import { randomBytes as randomBytes5 } from "crypto";
13842
+
13843
+ // src/services/harness/openCodeOrdewell.ts
13844
+ function ordewellToolPrefix(mcp) {
13845
+ return `${mcp.name}_`;
13846
+ }
13847
+ function isOrdewellTool(mcp, permission) {
13848
+ return !!mcp && !!permission && permission.startsWith(ordewellToolPrefix(mcp));
13849
+ }
13850
+ function isRecord(value) {
13851
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13852
+ }
13853
+ function deepMerge(base, over) {
13854
+ const merged = { ...base };
13855
+ for (const [key, value] of Object.entries(over)) {
13856
+ const current = merged[key];
13857
+ merged[key] = isRecord(current) && isRecord(value) ? deepMerge(current, value) : value;
13858
+ }
13859
+ return merged;
13860
+ }
13861
+ function mergeOrdewellConfig(existing, mcp) {
13862
+ let base = {};
13863
+ if (existing?.trim()) {
13864
+ let parsed;
13865
+ try {
13866
+ parsed = JSON.parse(existing);
13867
+ } catch {
13868
+ return null;
13869
+ }
13870
+ if (!isRecord(parsed)) return null;
13871
+ base = parsed;
13872
+ }
13873
+ const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
13874
+ const ours = {
13875
+ mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } },
13876
+ permission: { [`${ordewellToolPrefix(mcp)}*`]: "allow" }
13877
+ };
13878
+ return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
13879
+ }
13009
13880
 
13010
13881
  // src/services/harness/OpenCodeV2.ts
13011
13882
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
@@ -13088,12 +13959,16 @@ var OpenCodeV2 = class {
13088
13959
  const created = await this.host.json("POST", "/api/session", {
13089
13960
  agent: this.agent,
13090
13961
  ...this.model ? { model: this.model } : {},
13091
- permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
13962
+ permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
13092
13963
  });
13093
13964
  if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
13094
13965
  this.sessionId = created.data.id;
13095
13966
  await this.applyInstructions();
13096
13967
  }
13968
+ /** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
13969
+ ordewellRules() {
13970
+ return this.opts.mcp ? [{ action: `${ordewellToolPrefix(this.opts.mcp)}*`, resource: "*", effect: "allow" }] : [];
13971
+ }
13097
13972
  /** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
13098
13973
  async applyInstructions() {
13099
13974
  if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
@@ -13231,8 +14106,8 @@ var OpenCodeV2 = class {
13231
14106
  });
13232
14107
  return true;
13233
14108
  }
13234
- async replyPermission(id, sessionId, reply) {
13235
- await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
14109
+ async replyPermission(id, sessionId, reply2) {
14110
+ await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply2);
13236
14111
  }
13237
14112
  async openStream(state, turn, onEvent, onActivity) {
13238
14113
  const streamAbort = new AbortController();
@@ -13472,6 +14347,11 @@ var OpenCodeV2 = class {
13472
14347
  const request = this.permissionRequest(ask);
13473
14348
  if (!request || state.seen.has(`perm:${request.id}`)) return;
13474
14349
  state.seen.add(`perm:${request.id}`);
14350
+ if (isOrdewellTool(this.opts.mcp, request.name)) {
14351
+ void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
14352
+ });
14353
+ return;
14354
+ }
13475
14355
  onEvent(request);
13476
14356
  void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
13477
14357
  });
@@ -13487,7 +14367,7 @@ var OpenCodeV2 = class {
13487
14367
  const request = this.permissionRequest(ask);
13488
14368
  if (!request || state.seen.has(`perm:${request.id}`)) return;
13489
14369
  state.seen.add(`perm:${request.id}`);
13490
- if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
14370
+ if (isOrdewellTool(this.opts.mcp, request.name) || this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
13491
14371
  onEvent({ ...request, decided: { decision: "allow" } });
13492
14372
  void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
13493
14373
  });
@@ -13506,6 +14386,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
13506
14386
  var RECOVERY_TIMEOUT_MS = 9e5;
13507
14387
  var STATUS_POLL_INTERVAL_MS = 1e3;
13508
14388
  var SERVER_USERNAME = "opencode";
14389
+ var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
14390
+ var MCP_STATUS_POLL_MS2 = 200;
13509
14391
  var DISABLED_TOOLS = {
13510
14392
  question: false,
13511
14393
  edit: false,
@@ -13571,6 +14453,12 @@ function unwrapFileToolOutput(output) {
13571
14453
  const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
13572
14454
  return inner ? inner[1] : output;
13573
14455
  }
14456
+ function editDiff2(tool2, state) {
14457
+ const { diff, exists } = state?.metadata ?? {};
14458
+ if (typeof diff === "string" && diff) return hunksOf(diff);
14459
+ const content = state?.input?.content;
14460
+ return tool2 === "write" && exists === false && typeof content === "string" ? markedLines(content, "+") : "";
14461
+ }
13574
14462
  function delay2(ms, signal) {
13575
14463
  return new Promise((resolve7) => {
13576
14464
  const timer = setTimeout(resolve7, ms);
@@ -13623,6 +14511,8 @@ var OpenCodeAdapter = class {
13623
14511
  openPermissions = /* @__PURE__ */ new Map();
13624
14512
  /** Set once the server turns out to speak the 2.x API, which then owns the session. */
13625
14513
  v2 = null;
14514
+ /** The Ordewell server this process was configured with; null when none was given or its config could not be merged. */
14515
+ ordewell = null;
13626
14516
  async start(opts) {
13627
14517
  if (opts.kind === "task") this.task = opts;
13628
14518
  else this.planner = opts;
@@ -13637,9 +14527,19 @@ var OpenCodeAdapter = class {
13637
14527
  throw new ExecutableNotFoundError("opencode", PATH);
13638
14528
  }
13639
14529
  const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
13640
- const password = randomBytes2(24).toString("base64url");
14530
+ const password = randomBytes5(24).toString("base64url");
14531
+ const ordewellConfig = opts.mcp ? mergeOrdewellConfig(workspaceEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT, opts.mcp) : null;
14532
+ if (opts.mcp && ordewellConfig === null) {
14533
+ console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
14534
+ }
14535
+ this.ordewell = ordewellConfig === null ? null : opts.mcp ?? null;
13641
14536
  this.process = this.deps.spawn(launch.file, launch.args, {
13642
- env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
14537
+ env: runnerEnv(PATH, {
14538
+ ...workspaceEnv,
14539
+ ...ordewellConfig === null ? {} : { OPENCODE_CONFIG_CONTENT: ordewellConfig },
14540
+ OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
14541
+ OPENCODE_SERVER_PASSWORD: password
14542
+ }),
13643
14543
  stdio: ["pipe", "pipe", "pipe"],
13644
14544
  cwd: opts.cwd,
13645
14545
  windowsVerbatimArguments: launch.verbatim
@@ -13689,7 +14589,7 @@ ${this.stderrTail.trim()}` : ""}`);
13689
14589
  processEnded: this.processEnded,
13690
14590
  isExited: () => this.exited,
13691
14591
  exitMessage: () => this.exitMessage()
13692
- }, opts, this.role());
14592
+ }, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
13693
14593
  try {
13694
14594
  await this.v2.start();
13695
14595
  } catch (err) {
@@ -13742,12 +14642,12 @@ ${this.stderrTail.trim()}` : ""}`);
13742
14642
  ...this.planner?.effort ? { variant: this.planner.effort } : {},
13743
14643
  ...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
13744
14644
  };
13745
- const reply = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
14645
+ const reply2 = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
13746
14646
  if (signal?.aborted) {
13747
14647
  this.dispose();
13748
14648
  return;
13749
14649
  }
13750
- this.settle(reply, turn, onEvent);
14650
+ this.settle(reply2, turn, onEvent);
13751
14651
  } catch (err) {
13752
14652
  if (signal?.aborted) {
13753
14653
  this.dispose();
@@ -13853,13 +14753,13 @@ ${this.stderrTail.trim()}` : ""}`);
13853
14753
  break;
13854
14754
  }
13855
14755
  }
13856
- for (const reply of messages.slice(start)) {
13857
- const id = reply.info?.id;
13858
- if (reply.info?.role !== "assistant" || !id) continue;
13859
- for (const part of reply.parts ?? []) {
14756
+ for (const reply2 of messages.slice(start)) {
14757
+ const id = reply2.info?.id;
14758
+ if (reply2.info?.role !== "assistant" || !id) continue;
14759
+ for (const part of reply2.parts ?? []) {
13860
14760
  if (part.messageID === id) this.emitPart(part, turn, onEvent);
13861
14761
  }
13862
- this.countUsage(reply.info, turn, onEvent);
14762
+ this.countUsage(reply2.info, turn, onEvent);
13863
14763
  }
13864
14764
  }
13865
14765
  /**
@@ -13969,18 +14869,18 @@ ${this.stderrTail.trim()}` : ""}`);
13969
14869
  * their text, reasoning and usage — reach Ordewell over the stream or not at
13970
14870
  * all.
13971
14871
  */
13972
- settle(reply, turn, onEvent) {
13973
- const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
14872
+ settle(reply2, turn, onEvent) {
14873
+ const failure = typeof reply2?.error === "string" ? reply2.error : reply2?.error?.message ?? reply2?.info?.error?.data?.message;
13974
14874
  if (failure) {
13975
14875
  onEvent({ type: "error", message: failure });
13976
14876
  return;
13977
14877
  }
13978
- const assistantId = reply?.info?.id;
13979
- for (const part of reply?.parts ?? []) {
14878
+ const assistantId = reply2?.info?.id;
14879
+ for (const part of reply2?.parts ?? []) {
13980
14880
  if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
13981
14881
  this.emitPart(part, turn, onEvent);
13982
14882
  }
13983
- if (reply?.info) this.countUsage(reply.info, turn, onEvent);
14883
+ if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
13984
14884
  if (assistantId) this.lastAssistantId = assistantId;
13985
14885
  onEvent({ type: "turn_end" });
13986
14886
  }
@@ -14057,7 +14957,7 @@ ${this.stderrTail.trim()}` : ""}`);
14057
14957
  type: "tool_result",
14058
14958
  id: callId,
14059
14959
  name,
14060
- output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
14960
+ output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
14061
14961
  success: status === "completed",
14062
14962
  subagentId
14063
14963
  });
@@ -14191,6 +15091,11 @@ ${this.stderrTail.trim()}` : ""}`);
14191
15091
  const id = ask.id;
14192
15092
  if (!id || seen.has(`perm:${id}`)) return;
14193
15093
  seen.add(`perm:${id}`);
15094
+ if (isOrdewellTool(this.ordewell, permissionName(ask))) {
15095
+ void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "once" }).catch(() => {
15096
+ });
15097
+ return;
15098
+ }
14194
15099
  onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
14195
15100
  void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
14196
15101
  });
@@ -14217,7 +15122,7 @@ ${this.stderrTail.trim()}` : ""}`);
14217
15122
  ...ask.always?.length ? { suggestions: ask.always } : {},
14218
15123
  ...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
14219
15124
  };
14220
- if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
15125
+ if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
14221
15126
  onEvent({ ...request, decided: { decision: "allow" } });
14222
15127
  void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
14223
15128
  });
@@ -14230,10 +15135,10 @@ ${this.stderrTail.trim()}` : ""}`);
14230
15135
  * `POST /permission/:id/reply` is the current answer. The per-session path
14231
15136
  * it replaced is tried only when a server too old to know the new one 404s.
14232
15137
  */
14233
- async replyPermission(id, sessionId, reply) {
14234
- const response = await this.request("POST", `/permission/${id}/reply`, reply);
15138
+ async replyPermission(id, sessionId, reply2) {
15139
+ const response = await this.request("POST", `/permission/${id}/reply`, reply2);
14235
15140
  if (response.status !== 404) return;
14236
- await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply.reply });
15141
+ await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
14237
15142
  }
14238
15143
  /**
14239
15144
  * Open `/event` for one turn and wait until it is connected. The stream
@@ -14312,6 +15217,19 @@ ${this.stderrTail.trim()}` : ""}`);
14312
15217
  nativeSessionId() {
14313
15218
  return this.sessionId;
14314
15219
  }
15220
+ /** Asks the server, which lists every MCP server it has with the state of its connection. */
15221
+ async mcpAttached() {
15222
+ if (!this.ordewell || !this.baseUrl) return false;
15223
+ const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS3;
15224
+ while (Date.now() < deadline && !this.exited) {
15225
+ const servers = await this.json("GET", "/mcp").catch(() => null);
15226
+ const status = servers?.[this.ordewell.name]?.status;
15227
+ if (status === "connected") return true;
15228
+ if (status !== void 0 && status !== "pending") return false;
15229
+ await delay2(MCP_STATUS_POLL_MS2);
15230
+ }
15231
+ return false;
15232
+ }
14315
15233
  dispose() {
14316
15234
  if (this.disposed) return;
14317
15235
  this.disposed = true;
@@ -14334,9 +15252,13 @@ var TASK_MODE_ADAPTERS = {
14334
15252
  codex: (deps) => new CodexAdapter(deps),
14335
15253
  opencode: (deps) => new OpenCodeAdapter(deps)
14336
15254
  };
15255
+ var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
14337
15256
  function supportsTaskMode(runner) {
14338
15257
  return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
14339
15258
  }
15259
+ function takesOrdewellTools(runner) {
15260
+ return ORDEWELL_TOOL_RUNNERS.has(runner);
15261
+ }
14340
15262
  function createTaskAdapter(runner, deps) {
14341
15263
  if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
14342
15264
  return TASK_MODE_ADAPTERS[runner](deps);
@@ -14349,6 +15271,9 @@ function routeTransport(requested, runner, registry) {
14349
15271
  const name = registry?.get(runner)?.manifest.displayName ?? runner;
14350
15272
  return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
14351
15273
  }
15274
+ function givesCompletionTool(requested, runner, registry) {
15275
+ return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
15276
+ }
14352
15277
  var TransportRouter = class {
14353
15278
  constructor(runners) {
14354
15279
  this.runners = runners;
@@ -14471,6 +15396,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
14471
15396
  this.emit("onTaskSettled", { taskId });
14472
15397
  this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
14473
15398
  });
15399
+ this.verifier.onCheckpointWithdrawn((taskId) => {
15400
+ if (!this.atCheckpoint(taskId)) return;
15401
+ this.store.markInProgress(taskId);
15402
+ this.emit("onTaskChanged");
15403
+ });
14474
15404
  this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
14475
15405
  }
14476
15406
  /**
@@ -14557,7 +15487,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
14557
15487
  this.notifications.warn(message);
14558
15488
  };
14559
15489
  if (resolved.blockedEnvrc) {
14560
- warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path13.dirname(resolved.blockedEnvrc)} to use them.`);
15490
+ 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.`);
14561
15491
  }
14562
15492
  if (resolved.trackedEnvFile) {
14563
15493
  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.`);
@@ -15388,17 +16318,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
15388
16318
  this.abandonSpawn(task, attempt);
15389
16319
  return false;
15390
16320
  }
15391
- 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, {
16321
+ const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
16322
+ const completionTool = givesCompletionTool(transport, attempt.runner, this.registry);
16323
+ 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, {
15392
16324
  planMapEnabled: this.config.planMapEnabled,
15393
16325
  // A merge to resolve is not new behaviour to drive test-first.
15394
16326
  tddEnabled: !attempt.repair && this.tddEnabled(),
15395
16327
  // An ops task's effects outlive a failed attempt and are never rolled
15396
16328
  // back, so the next one is told what the last one did (ADR-0020).
15397
- previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
16329
+ previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
16330
+ completionTool
15398
16331
  });
15399
16332
  this.lingering.close(task.id);
15400
16333
  const env = await this.envForTask(cwd);
15401
- const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
15402
16334
  const session = await this.terminalRunner.spawn({
15403
16335
  taskId: task.id,
15404
16336
  runner: attempt.runner,
@@ -15413,7 +16345,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
15413
16345
  title: task.title,
15414
16346
  env,
15415
16347
  transport,
15416
- resumeSessionId: attempt.continuation?.resumeSessionId
16348
+ resumeSessionId: attempt.continuation?.resumeSessionId,
16349
+ attempt: attempt.attempt
15417
16350
  });
15418
16351
  if (this.attempts.get(task.id) !== attempt) {
15419
16352
  this.abandonSpawn(task, attempt, session);
@@ -15711,52 +16644,6 @@ import { exec as exec3, spawn } from "child_process";
15711
16644
  import { promisify as promisify3 } from "util";
15712
16645
  import * as fsSync3 from "fs";
15713
16646
  import * as osMod from "os";
15714
-
15715
- // src/utils/globalDataDir.ts
15716
- import * as fs7 from "fs";
15717
- import * as path14 from "path";
15718
- import * as os5 from "os";
15719
- function globalDataDir() {
15720
- return path14.join(os5.homedir(), ".ordewell");
15721
- }
15722
- function copyDirSync(src, dest) {
15723
- fs7.mkdirSync(dest, { recursive: true });
15724
- const entries = fs7.readdirSync(src, { withFileTypes: true });
15725
- for (const entry of entries) {
15726
- const srcPath = path14.join(src, entry.name);
15727
- const destPath = path14.join(dest, entry.name);
15728
- if (entry.isDirectory()) {
15729
- copyDirSync(srcPath, destPath);
15730
- } else {
15731
- fs7.copyFileSync(srcPath, destPath);
15732
- }
15733
- }
15734
- }
15735
- var migrated = false;
15736
- function migrateOldConfigDir() {
15737
- if (migrated) return;
15738
- migrated = true;
15739
- const oldDir = path14.join(os5.homedir(), ".config", "ordewell");
15740
- if (!fs7.existsSync(oldDir)) return;
15741
- const newDir = globalDataDir();
15742
- const liftFile = (name) => {
15743
- const oldFile = path14.join(oldDir, name);
15744
- const newFile = path14.join(newDir, name);
15745
- if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
15746
- fs7.mkdirSync(newDir, { recursive: true });
15747
- fs7.copyFileSync(oldFile, newFile);
15748
- }
15749
- };
15750
- liftFile("settings.json");
15751
- liftFile(".env");
15752
- const oldPlugins = path14.join(oldDir, "plugins");
15753
- const newPlugins = path14.join(newDir, "plugins");
15754
- if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
15755
- copyDirSync(oldPlugins, newPlugins);
15756
- }
15757
- }
15758
-
15759
- // src/services/ModelDiscovery.ts
15760
16647
  var execAsync3 = promisify3(exec3);
15761
16648
  var defaultExec2 = async (command, options) => {
15762
16649
  const PATH = await augmentedPath();
@@ -16963,6 +17850,7 @@ var CliAgentAiService = class {
16963
17850
  };
16964
17851
  this.makeAdapter = deps.createAdapter ?? defaultAdapter;
16965
17852
  this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
17853
+ this.mcpServer = deps.mcpServer;
16966
17854
  }
16967
17855
  config;
16968
17856
  runner;
@@ -16977,6 +17865,11 @@ var CliAgentAiService = class {
16977
17865
  */
16978
17866
  lastNativeSessionId = null;
16979
17867
  conversation = null;
17868
+ mcpServer;
17869
+ /** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
17870
+ plannerToken = null;
17871
+ /** Whether the running planner was spawned with its tools and the CLI reported them connected. */
17872
+ toolsAttached = false;
16980
17873
  activeAbort = null;
16981
17874
  /**
16982
17875
  * Every subagent this conversation has reported starting, and finishing.
@@ -16999,6 +17892,10 @@ var CliAgentAiService = class {
16999
17892
  if (!this.conversation) return true;
17000
17893
  return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
17001
17894
  }
17895
+ /** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
17896
+ plannerToolsAttached() {
17897
+ return this.toolsAttached;
17898
+ }
17002
17899
  /** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
17003
17900
  nativeSessionId() {
17004
17901
  return this.adapter?.nativeSessionId() ?? null;
@@ -17006,8 +17903,8 @@ var CliAgentAiService = class {
17006
17903
  reset() {
17007
17904
  this.activeAbort?.abort();
17008
17905
  this.activeAbort = null;
17009
- this.adapter?.dispose();
17010
- this.adapter = null;
17906
+ this.disposeAdapter();
17907
+ this.toolsAttached = false;
17011
17908
  this.lastNativeSessionId = null;
17012
17909
  this.conversation = null;
17013
17910
  this.subagents.started.clear();
@@ -17034,9 +17931,23 @@ var CliAgentAiService = class {
17034
17931
  model: this.plannerModel(),
17035
17932
  effort: this.config.plannerThinkingEffort
17036
17933
  };
17037
- await this.startAdapter(startOptions);
17934
+ const tools = req.plannerTools && this.mcpServer ? {
17935
+ offer: req.plannerTools,
17936
+ systemPrompt: buildConversationSystemPrompt(
17937
+ req.goal,
17938
+ contextStr,
17939
+ req.modelsByRunner,
17940
+ req.runners,
17941
+ req.runnerModes,
17942
+ req.autonomousDefault ?? true,
17943
+ req.verificationEnabled ?? false,
17944
+ { harness: true, isolatedExecution: req.isolatedExecution, plannerTools: true }
17945
+ )
17946
+ } : void 0;
17947
+ await this.startAdapter(startOptions, tools);
17038
17948
  this.conversation = {
17039
17949
  startOptions,
17950
+ tools,
17040
17951
  runners: req.runners,
17041
17952
  runnerModes: req.runnerModes,
17042
17953
  autonomousDefault: req.autonomousDefault
@@ -17241,10 +18152,7 @@ var CliAgentAiService = class {
17241
18152
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
17242
18153
  }
17243
18154
  this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
17244
- if (error || signal?.aborted) {
17245
- adapter.dispose();
17246
- this.adapter = null;
17247
- }
18155
+ if (error || signal?.aborted) this.disposeAdapter();
17248
18156
  return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
17249
18157
  }
17250
18158
  // --- Process lifecycle ---
@@ -17253,13 +18161,50 @@ var CliAgentAiService = class {
17253
18161
  * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
17254
18162
  * mode from here without changing this signature.
17255
18163
  */
17256
- async startAdapter(opts) {
18164
+ async startAdapter(opts, tools) {
17257
18165
  const adapter = this.makeAdapter(this.runner, this.processDeps);
17258
18166
  if (!adapter) throw new Error(`No planner adapter is available for "${this.runner}".`);
18167
+ this.toolsAttached = false;
18168
+ if (tools && this.mcpServer && adapter.mcpAttached) {
18169
+ if (await this.startWithTools(adapter, opts, tools, this.mcpServer)) return adapter;
18170
+ return this.startAdapter(opts);
18171
+ }
17259
18172
  await adapter.start(opts);
17260
18173
  this.adapter = adapter;
17261
18174
  return adapter;
17262
18175
  }
18176
+ /** Spawn with the Ordewell server injected. False, with nothing left running, when it did not connect. */
18177
+ async startWithTools(adapter, opts, tools, server) {
18178
+ let credential;
18179
+ try {
18180
+ credential = await server.issuePlannerToken({ sessionId: tools.offer.sessionId }, tools.offer.handler);
18181
+ } catch {
18182
+ return false;
18183
+ }
18184
+ try {
18185
+ await adapter.start({ ...opts, systemPrompt: tools.systemPrompt, mcp: mcpClientConfig(credential) });
18186
+ if (await adapter.mcpAttached()) {
18187
+ this.adapter = adapter;
18188
+ this.plannerToken = credential.token;
18189
+ this.toolsAttached = true;
18190
+ return true;
18191
+ }
18192
+ } catch (err) {
18193
+ adapter.dispose();
18194
+ server.revoke(credential.token);
18195
+ throw err;
18196
+ }
18197
+ adapter.dispose();
18198
+ server.revoke(credential.token);
18199
+ return false;
18200
+ }
18201
+ /** Kill the planner process, and with it the token it was spawned with. */
18202
+ disposeAdapter() {
18203
+ this.adapter?.dispose();
18204
+ this.adapter = null;
18205
+ if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
18206
+ this.plannerToken = null;
18207
+ }
17263
18208
  /**
17264
18209
  * The live agent process, restarted from its own session id if it died
17265
18210
  * between turns. Resume is a hint: when it fails, the caller's next
@@ -17270,7 +18215,7 @@ var CliAgentAiService = class {
17270
18215
  if (this.adapter) return this.adapter;
17271
18216
  const conversation = this.conversation;
17272
18217
  if (!conversation) throw new Error("No active planner conversation.");
17273
- await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
18218
+ await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
17274
18219
  return this.adapter;
17275
18220
  }
17276
18221
  plannerModel() {
@@ -17289,7 +18234,9 @@ var CliAgentAiService = class {
17289
18234
  const previous = this.adapter;
17290
18235
  const previousConversation = this.conversation;
17291
18236
  const previousSessionId = this.lastNativeSessionId;
18237
+ const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
17292
18238
  this.adapter = null;
18239
+ this.plannerToken = null;
17293
18240
  const startOptions = {
17294
18241
  kind: "planner",
17295
18242
  cwd: this.workspaceRoot(),
@@ -17319,6 +18266,8 @@ var CliAgentAiService = class {
17319
18266
  } finally {
17320
18267
  oneShotAdapter?.dispose();
17321
18268
  this.adapter = previous;
18269
+ this.plannerToken = previousTools.token;
18270
+ this.toolsAttached = previousTools.attached;
17322
18271
  this.conversation = previousConversation;
17323
18272
  this.lastNativeSessionId = previousSessionId;
17324
18273
  }
@@ -17414,6 +18363,173 @@ function createAiService(config, deps) {
17414
18363
  return new OpenAiService(config);
17415
18364
  }
17416
18365
 
18366
+ // src/services/plannerTools.ts
18367
+ function runnersOf(tasks) {
18368
+ return [...new Set(flattenTasks(tasks).filter((t) => t.type !== "user").map((t) => t.assignedRunner))];
18369
+ }
18370
+ function plannerToolHandler(host) {
18371
+ return {
18372
+ async listRunners() {
18373
+ const catalog = await host.liveCatalog();
18374
+ return answer({
18375
+ autonomy: autonomyLevelLabel(catalog.autonomousDefault),
18376
+ runners: catalog.runners.map((id) => {
18377
+ const modes = catalog.modes[id] ?? [];
18378
+ const offered = [...filteredBuildModes(modes, catalog.autonomousDefault), ...modes.filter((m) => m.id === "plan")];
18379
+ return {
18380
+ id,
18381
+ defaultMode: resolveDefaultMode(modes, catalog.autonomousDefault) ?? null,
18382
+ modes: offered.map(({ id: modeId2, label, description }) => ({ id: modeId2, label, description }))
18383
+ };
18384
+ })
18385
+ });
18386
+ },
18387
+ async listModels({ runner }) {
18388
+ const catalog = await host.liveCatalog();
18389
+ if (!catalog.runners.includes(runner)) return notEnabled(runner, catalog.runners);
18390
+ return answer({
18391
+ runner,
18392
+ models: (catalog.models[runner] ?? []).map((m) => ({
18393
+ modelId: m.modelId,
18394
+ modelLabel: m.modelLabel,
18395
+ variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
18396
+ }))
18397
+ });
18398
+ },
18399
+ async submitPlan({ tasks }) {
18400
+ const catalog = await host.liveCatalog();
18401
+ const result = validatePlanTasks({ tasks }, catalog.runners, catalog.modes, catalog.autonomousDefault);
18402
+ if (!result.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: result.errors, enabledRunners: catalog.runners }) };
18403
+ const runners = runnersOf(result.tasks);
18404
+ const coerced = coercions(result.tasks, host.coerce(result.tasks, runners));
18405
+ if (!host.submitPlan(result.tasks, runners)) {
18406
+ return { isError: true, text: JSON.stringify({ ok: false, errors: [{ field: "tasks", message: "No planning turn is open to take the plan." }] }) };
18407
+ }
18408
+ return answer({
18409
+ ok: true,
18410
+ tasks: result.tasks.length,
18411
+ coerced,
18412
+ next: "The plan is committed when this reply ends. Tell the user briefly what you submitted; do not repeat the plan as JSON."
18413
+ });
18414
+ },
18415
+ async taskQuery({ tasks, fields, catalog }) {
18416
+ const query = { tasks: (tasks ?? []).map(String), fields: fields ?? [...TASK_QUERY_TASK_FIELDS], catalog: catalog === true };
18417
+ return read(host, taskQuerySignature(query), async () => ({
18418
+ tasks: query.tasks.map((ref) => taskDetail(host.tasks(), ref, query.fields ?? [])),
18419
+ ...query.catalog ? { catalog: catalogDetail(await host.liveCatalog()) } : {}
18420
+ }));
18421
+ },
18422
+ async taskOutput({ task: ref, lines, since }) {
18423
+ const task = findTask(host.tasks(), String(ref));
18424
+ if (!task) return { isError: true, text: JSON.stringify({ ok: false, error: `No task matches "${ref}" in the current plan.` }) };
18425
+ const maxLines = Math.min(lines ?? OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX);
18426
+ return read(host, JSON.stringify(["output", task.id, maxLines, since ?? null]), async () => {
18427
+ const head = { task: { id: task.id, order: task.order, title: task.title, status: task.status } };
18428
+ const tail = host.liveOutput(task.id, { maxLines, sinceOffset: since });
18429
+ if (tail?.running) {
18430
+ const fitted = fitTail(tail.text, OUTPUT_TAIL_MAX_CHARS);
18431
+ return { ...head, running: true, output: fitted.text, nextOffset: tail.nextOffset, ...fitted.trimmed ? { trimmedToFit: true } : {} };
18432
+ }
18433
+ return {
18434
+ ...head,
18435
+ running: false,
18436
+ reason: "This task is not running, so there is no live output: its outcome is below.",
18437
+ verdict: verdictDetail(task.verdict),
18438
+ outputSummary: outputSummaryDetail(task),
18439
+ lastAttempt: host.lastAttempt(task.id)
18440
+ };
18441
+ });
18442
+ },
18443
+ async editPlan({ ops }) {
18444
+ const outcome = await host.editPlan(ops);
18445
+ if (!outcome.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: outcome.errors.map(opError) }) };
18446
+ return answer({
18447
+ ok: true,
18448
+ summary: outcome.summary,
18449
+ queued: outcome.queued,
18450
+ 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."
18451
+ });
18452
+ }
18453
+ };
18454
+ }
18455
+ function opError(message) {
18456
+ const match = /^op (\d+) \((\w+)\): ([\s\S]*)$/.exec(message);
18457
+ return match ? { op: Number(match[1]), kind: match[2], message: match[3] } : { message };
18458
+ }
18459
+ async function read(host, signature, answerRead) {
18460
+ const result = await host.read(signature, answerRead);
18461
+ if (result.status === "refused") {
18462
+ 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." }) };
18463
+ }
18464
+ return answer({ ...result.value, ...result.landNow ? { note: TASK_READ_ANSWER_OR_EDIT } : {} });
18465
+ }
18466
+ function verdictDetail(verdict) {
18467
+ if (!verdict) return null;
18468
+ return {
18469
+ outcome: verdict.outcome,
18470
+ reason: verdict.reason,
18471
+ checks: verdict.checks.map((c) => ({ name: c.name, result: c.skipped ? "skipped" : c.passed ? "pass" : "fail" }))
18472
+ };
18473
+ }
18474
+ function outputSummaryDetail({ outputSummary }) {
18475
+ return outputSummary ? { reviewReason: outputSummary.reviewReason, ...outputSummary.logTail ? { logTail: outputSummary.logTail } : {} } : null;
18476
+ }
18477
+ function taskDetail(plan, ref, fields) {
18478
+ const task = findTask(plan, ref);
18479
+ if (!task) return { ref, error: "no task matches this reference in the current plan." };
18480
+ const wants = (field) => fields.includes(field);
18481
+ return {
18482
+ id: task.id,
18483
+ order: task.order,
18484
+ title: task.title,
18485
+ status: task.status,
18486
+ type: task.type === "user" ? "user" : "ai",
18487
+ ...wants("description") ? { description: task.description || null } : {},
18488
+ ...wants("prompt") ? { prompt: task.prompt || null } : {},
18489
+ ...wants("userSteps") ? { userSteps: task.userSteps?.length ? task.userSteps.map((s) => ({ order: s.order, instruction: s.instruction, completed: s.completed === true })) : null } : {},
18490
+ ...wants("verdict") ? { verdict: verdictDetail(task.verdict) } : {},
18491
+ ...wants("outputSummary") ? { outputSummary: outputSummaryDetail(task) } : {},
18492
+ ...wants("userStoriesCovered") ? { userStoriesCovered: task.userStoriesCovered?.length ? task.userStoriesCovered : null } : {}
18493
+ };
18494
+ }
18495
+ function catalogDetail(catalog) {
18496
+ return {
18497
+ runners: catalog.runners.map((id) => {
18498
+ const modes = catalog.modes[id] ?? [];
18499
+ const defaultMode = resolveDefaultMode(modes, catalog.autonomousDefault);
18500
+ return {
18501
+ id,
18502
+ models: (catalog.models[id] ?? []).map((m) => ({
18503
+ modelId: m.modelId,
18504
+ modelLabel: m.modelLabel,
18505
+ variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
18506
+ })),
18507
+ modes: modes.map((m) => ({ id: m.id, label: m.label, description: m.description, default: m.id === defaultMode }))
18508
+ };
18509
+ })
18510
+ };
18511
+ }
18512
+ function answer(body) {
18513
+ return { text: JSON.stringify(body) };
18514
+ }
18515
+ function notEnabled(runner, enabled) {
18516
+ return { isError: true, text: `Runner "${runner}" is not enabled. Enabled runners: ${enabled.join(", ") || "(none)"}.` };
18517
+ }
18518
+ function coercions(submitted, landed) {
18519
+ return submitted.flatMap((task, i) => {
18520
+ const after = landed[i];
18521
+ const out = [];
18522
+ if (after.assignedRunner !== task.assignedRunner) {
18523
+ out.push({ taskId: task.id, field: "assignedRunner", from: task.assignedRunner, to: after.assignedRunner });
18524
+ }
18525
+ const [fromModel, toModel] = [task.assignedModel?.modelId ?? null, after.assignedModel?.modelId ?? null];
18526
+ if (fromModel !== toModel) out.push({ taskId: task.id, field: "assignedModel", from: fromModel, to: toModel });
18527
+ const [fromEffort, toEffort] = [task.assignedModel?.thinkingEffort ?? null, after.assignedModel?.thinkingEffort ?? null];
18528
+ if (fromEffort !== toEffort) out.push({ taskId: task.id, field: "thinkingEffort", from: fromEffort, to: toEffort });
18529
+ return out;
18530
+ });
18531
+ }
18532
+
17417
18533
  // src/services/PlannerConversation.ts
17418
18534
  import { v4 as uuidv45 } from "uuid";
17419
18535
 
@@ -17430,8 +18546,8 @@ function summaryRequest(planLines) {
17430
18546
  ...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
17431
18547
  ].join("\n");
17432
18548
  }
17433
- function extractSummary(reply) {
17434
- const matches = [...reply.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
18549
+ function extractSummary(reply2) {
18550
+ const matches = [...reply2.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
17435
18551
  const summary = matches.at(-1)?.[1].trim();
17436
18552
  return summary || null;
17437
18553
  }
@@ -17450,6 +18566,7 @@ function keptTail(transcript) {
17450
18566
 
17451
18567
  // src/services/PlannerConversation.ts
17452
18568
  var CATALOG_MODEL_CAP = 100;
18569
+ 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.";
17453
18570
  var MAX_TASK_QUERIES = 3;
17454
18571
  var MAX_TASK_QUERIES_HARD = 6;
17455
18572
  function freshReadBudget() {
@@ -17479,7 +18596,7 @@ var PlannerConversation = class {
17479
18596
  savedInBackground = 0;
17480
18597
  turnsInFlight = 0;
17481
18598
  compacting = false;
17482
- openTurnId = null;
18599
+ openTurn = null;
17483
18600
  get transcript() {
17484
18601
  return this.host.plan()?.conversationHistory ?? [];
17485
18602
  }
@@ -17489,7 +18606,48 @@ var PlannerConversation = class {
17489
18606
  }
17490
18607
  /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
17491
18608
  get currentTurnId() {
17492
- return this.openTurnId ?? void 0;
18609
+ return this.openTurn?.stream.turnId;
18610
+ }
18611
+ /**
18612
+ * Hand the open turn a plan (already validated) or a task edit. The turn
18613
+ * settles with it exactly as with the same plan or ops parsed out of its
18614
+ * reply, and it wins over any envelope that reply carries. A later
18615
+ * submission replaces an earlier one. False when no user turn is open.
18616
+ */
18617
+ submit(submission) {
18618
+ if (!this.openTurn) return false;
18619
+ this.openTurn.submitted = submission;
18620
+ return true;
18621
+ }
18622
+ /**
18623
+ * The edit the open turn already holds, for a further edit in the same reply
18624
+ * to join rather than replace. Empty when it holds nothing; null when it holds
18625
+ * a whole plan, which an edit cannot be layered on.
18626
+ */
18627
+ pendingOps() {
18628
+ const held = this.openTurn?.submitted;
18629
+ if (!held) return [];
18630
+ return held.kind === "task_ops" ? held.ops : null;
18631
+ }
18632
+ /** Whether the turn would park this edit until a batch boundary instead of applying it as it settles. */
18633
+ editWouldQueue(ops) {
18634
+ return this.editTouchesLiveWork({ kind: "task_ops", ops, text: "", researchLog: [] });
18635
+ }
18636
+ /**
18637
+ * A read the planner makes through a tool. It spends the same per-turn budget
18638
+ * as an envelope read, so a planner cannot read more by switching channels:
18639
+ * past the soft limit, or on a repeated question, the answer still comes but
18640
+ * is told to land the turn; at the hard limit it is refused. `signature`
18641
+ * names the question, for the repeat check.
18642
+ */
18643
+ async read(signature, answer2) {
18644
+ const reads = this.openTurn?.reads;
18645
+ if (!reads) return { status: "answered", value: await answer2(), landNow: false };
18646
+ if (reads.answered >= MAX_TASK_QUERIES_HARD) return { status: "refused" };
18647
+ const landNow = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
18648
+ reads.seen.add(signature);
18649
+ reads.answered++;
18650
+ return { status: "answered", value: await answer2(), landNow };
17493
18651
  }
17494
18652
  /** Whether the model still holds this conversation in memory. */
17495
18653
  get isActive() {
@@ -17777,15 +18935,16 @@ ${message}` : message;
17777
18935
  async userTurn(prompt, signal, run) {
17778
18936
  const turnId = uuidv45();
17779
18937
  const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
17780
- this.openTurnId = turnId;
18938
+ const turn = { stream, signal, reads: freshReadBudget() };
18939
+ this.openTurn = turn;
17781
18940
  this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
17782
18941
  let outcome = "error";
17783
18942
  try {
17784
- const settled = await this.inTurn(() => run({ stream, signal, reads: freshReadBudget() }));
18943
+ const settled = await this.inTurn(() => run(turn));
17785
18944
  outcome = settled.outcome;
17786
18945
  return settled.plan;
17787
18946
  } finally {
17788
- if (this.openTurnId === turnId) this.openTurnId = null;
18947
+ if (this.openTurn === turn) this.openTurn = null;
17789
18948
  this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
17790
18949
  }
17791
18950
  }
@@ -17836,10 +18995,11 @@ ${message}` : message;
17836
18995
  * ops JSON still gets its two corrective retries; charging it for the read
17837
18996
  * would cost it the chance to fix the edit.
17838
18997
  */
17839
- async drainTaskQueries(turn, { reads, signal, stream }) {
18998
+ async drainTaskQueries(turn, userTurn) {
18999
+ const { reads, signal, stream } = userTurn;
17840
19000
  const ai = this.host.aiService();
17841
19001
  const carried = [];
17842
- let current = turn;
19002
+ let current = this.claimSubmission(turn, userTurn);
17843
19003
  while (current.kind === "task_query") {
17844
19004
  carried.push(...current.researchLog);
17845
19005
  if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
@@ -17853,17 +19013,30 @@ ${message}` : message;
17853
19013
  const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
17854
19014
  reads.seen.add(signature);
17855
19015
  reads.answered++;
17856
- const answer = this.taskQueryAnswer(current.query);
17857
- current = await ai.continueConversation(
17858
- insist ? `${answer}
19016
+ const answer2 = this.taskQueryAnswer(current.query);
19017
+ current = this.claimSubmission(await ai.continueConversation(
19018
+ insist ? `${answer2}
17859
19019
 
17860
- ${TASK_QUERY_ANSWER_OR_OPS}` : answer,
19020
+ ${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
17861
19021
  stream.sink(),
17862
19022
  signal
17863
- );
19023
+ ), userTurn);
17864
19024
  }
17865
19025
  return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
17866
19026
  }
19027
+ /**
19028
+ * The backend's answer, with what was handed in while it ran standing in
19029
+ * for whatever its reply carried. Taken once: a corrective re-send after a
19030
+ * rejected edit must not settle on the same submission again. A stopped
19031
+ * turn commits nothing, the same as a stopped reply with a plan in it.
19032
+ */
19033
+ claimSubmission(turn, userTurn) {
19034
+ const submitted = userTurn.submitted;
19035
+ userTurn.submitted = void 0;
19036
+ if (!submitted || userTurn.signal?.aborted) return turn;
19037
+ const { text, researchLog } = turn;
19038
+ return submitted.kind === "plan" ? { kind: "plan", tasks: submitted.tasks, text, researchLog } : { kind: "task_ops", ops: submitted.ops, text, researchLog };
19039
+ }
17867
19040
  /**
17868
19041
  * Render one read out of live state. Never persisted to the transcript: the
17869
19042
  * detail is context for the planner's next reply, and re-sending it on every
@@ -17907,7 +19080,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17907
19080
  return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
17908
19081
  },
17909
19082
  maxRepairs: 2,
17910
- onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
19083
+ onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
17911
19084
  });
17912
19085
  if ("plan" in settled) {
17913
19086
  await this.host.afterEdit();
@@ -17970,10 +19143,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17970
19143
  *
17971
19144
  * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
17972
19145
  * hard bound on every turn, and reads the plan's runners live, so a runner
17973
- * admitted mid-session by a retarget is shown like every other.
19146
+ * admitted mid-session by a retarget is shown like every other. A planner
19147
+ * with Ordewell's tools gets a reminder to read the catalog instead.
17974
19148
  */
17975
19149
  catalogBlock() {
17976
19150
  if (!this.host.plan()) return null;
19151
+ if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
17977
19152
  const { runners, models, modes, autonomousDefault } = this.host.catalog();
17978
19153
  const modelLines = runners.map((runner) => {
17979
19154
  const list = models[runner] ?? [];
@@ -18008,6 +19183,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
18008
19183
  const tasks = this.host.tasks();
18009
19184
  const lines = this.currentPlanLines();
18010
19185
  if (!lines) return null;
19186
+ const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
18011
19187
  const locked = tasks.filter((t) => t.status === "in_progress");
18012
19188
  const execNote = this.host.hasLiveWork() ? `
18013
19189
  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.` : "";
@@ -18017,8 +19193,8 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
18017
19193
  "</current_plan>",
18018
19194
  "The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
18019
19195
  "- To answer a question or discuss, reply in plain prose (no JSON).",
18020
- TASK_QUERY_REMINDER,
18021
- ...taskOpsProtocol(execNote)
19196
+ tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
19197
+ ...taskOpsProtocol(execNote, tools)
18022
19198
  ].filter(Boolean).join("\n");
18023
19199
  }
18024
19200
  /** One line per task with stable references — null until the plan has tasks. */
@@ -18294,6 +19470,7 @@ function retargetTaskRunner(task, runner, catalog) {
18294
19470
  }
18295
19471
 
18296
19472
  // src/services/SkillsService.ts
19473
+ import { createHash } from "crypto";
18297
19474
  import * as fs9 from "fs";
18298
19475
  import * as path18 from "path";
18299
19476
 
@@ -18311,6 +19488,11 @@ function builtinSkillsDir() {
18311
19488
  // src/services/SkillsService.ts
18312
19489
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
18313
19490
  var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
19491
+ var PRIOR_BUILTIN_HASHES = {
19492
+ grilling: ["d5d4cb8589fbb4a0f3296bae15033f2297ef682aaf5615a3dee258451f96e5c3"],
19493
+ "improve-codebase-architecture": ["b5154b5e3baca9a5f244634453e83706d742011c31163648c2b8395a2524dd9c"]
19494
+ };
19495
+ var SEED_MANIFEST = ".seeded.json";
18314
19496
  function parseSkillFile(filePath, name, source) {
18315
19497
  const raw = fs9.readFileSync(filePath, "utf8");
18316
19498
  const frontmatter = {};
@@ -18350,6 +19532,13 @@ function copyDirSync2(src, dest) {
18350
19532
  else fs9.copyFileSync(srcPath, destPath);
18351
19533
  }
18352
19534
  }
19535
+ function fileHash(file) {
19536
+ try {
19537
+ return createHash("sha256").update(fs9.readFileSync(file)).digest("hex");
19538
+ } catch {
19539
+ return void 0;
19540
+ }
19541
+ }
18353
19542
  function readDir(dir, source) {
18354
19543
  if (!fs9.existsSync(dir)) return [];
18355
19544
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
@@ -18374,21 +19563,59 @@ var SkillsService = class {
18374
19563
  globalDir() {
18375
19564
  return path18.join(globalDataDir(), "skills");
18376
19565
  }
19566
+ readSeedManifest() {
19567
+ try {
19568
+ const parsed = JSON.parse(fs9.readFileSync(path18.join(this.globalDir(), SEED_MANIFEST), "utf8"));
19569
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19570
+ return Object.fromEntries(
19571
+ Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
19572
+ );
19573
+ }
19574
+ } catch {
19575
+ }
19576
+ return {};
19577
+ }
19578
+ recordSeed(name, hash) {
19579
+ const manifest = this.readSeedManifest();
19580
+ if (manifest[name] === hash) return;
19581
+ manifest[name] = hash;
19582
+ try {
19583
+ fs9.mkdirSync(this.globalDir(), { recursive: true });
19584
+ fs9.writeFileSync(path18.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
19585
+ } catch (err) {
19586
+ console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
19587
+ }
19588
+ }
19589
+ /** A seed the user never edited: safe to replace with a newer built-in. */
19590
+ isUntouchedSeed(name, currentHash) {
19591
+ if (currentHash === void 0) return false;
19592
+ return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
19593
+ }
18377
19594
  seed(name) {
18378
19595
  const dest = path18.join(this.globalDir(), name);
18379
- if (fs9.existsSync(dest)) return false;
19596
+ const exists = fs9.existsSync(dest);
18380
19597
  let srcDir;
18381
19598
  try {
18382
19599
  srcDir = path18.join(builtinSkillsDir(), name);
18383
19600
  } catch (err) {
18384
- console.warn(`Could not locate built-in skills dir: ${String(err)}`);
19601
+ if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
18385
19602
  return false;
18386
19603
  }
18387
19604
  if (!fs9.existsSync(srcDir)) {
18388
- console.warn(`Built-in skill not found in package: ${name}`);
19605
+ if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
18389
19606
  return false;
18390
19607
  }
19608
+ const srcHash = fileHash(path18.join(srcDir, "SKILL.md"));
19609
+ if (exists) {
19610
+ const currentHash = fileHash(path18.join(dest, "SKILL.md"));
19611
+ if (currentHash !== void 0 && currentHash === srcHash) {
19612
+ this.recordSeed(name, currentHash);
19613
+ return false;
19614
+ }
19615
+ if (!this.isUntouchedSeed(name, currentHash)) return false;
19616
+ }
18391
19617
  copyDirSync2(srcDir, dest);
19618
+ if (srcHash) this.recordSeed(name, srcHash);
18392
19619
  return true;
18393
19620
  }
18394
19621
  seedBuiltinSkill(name) {
@@ -18682,9 +19909,9 @@ import * as fs10 from "fs";
18682
19909
  import * as path19 from "path";
18683
19910
 
18684
19911
  // src/utils/sessionId.ts
18685
- import { randomBytes as randomBytes3 } from "crypto";
19912
+ import { randomBytes as randomBytes6 } from "crypto";
18686
19913
  function mintSessionId() {
18687
- return `session-${randomBytes3(16).toString("hex")}`;
19914
+ return `session-${randomBytes6(16).toString("hex")}`;
18688
19915
  }
18689
19916
 
18690
19917
  // src/utils/sessionStore.ts
@@ -19021,7 +20248,12 @@ function lastAttemptDigest(location, taskId) {
19021
20248
  return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
19022
20249
  }
19023
20250
  function sessionRuntimeSettings(settings) {
19024
- return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
20251
+ return {
20252
+ ...plannerRuntimeToggles(settings),
20253
+ modelAllowlist: settings.modelAllowlist,
20254
+ runnerTransport: settings.runnerTransport,
20255
+ enabledRunners: settings.enabledRunners
20256
+ };
19025
20257
  }
19026
20258
  var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
19027
20259
  function resolveSkillInvocation(text, skillsService) {
@@ -19045,14 +20277,14 @@ function resolveSkillInvocation(text, skillsService) {
19045
20277
  return `${lead}${skill.content}${punct}`;
19046
20278
  });
19047
20279
  }
19048
- function liveAiService(config, workspaceRoot) {
20280
+ function liveAiService(config, workspaceRoot, mcpServer) {
19049
20281
  let live = null;
19050
20282
  let liveProvider = null;
19051
20283
  return () => {
19052
20284
  const provider = config.aiProvider;
19053
20285
  if (live && liveProvider === provider) return live;
19054
20286
  live?.reset();
19055
- live = createAiService(config, { workspaceRoot });
20287
+ live = createAiService(config, { workspaceRoot, mcpServer });
19056
20288
  liveProvider = provider;
19057
20289
  return live;
19058
20290
  };
@@ -19062,7 +20294,7 @@ function isPlannerApproval(request) {
19062
20294
  }
19063
20295
  function createSession(deps) {
19064
20296
  const pinnedAiService = deps.aiService;
19065
- const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
20297
+ const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
19066
20298
  const store = new PlanStore();
19067
20299
  const taskLogs = new TaskLogRecorder({
19068
20300
  broadcast: deps.broadcast,
@@ -19192,6 +20424,16 @@ var Session = class {
19192
20424
  currentSessionId;
19193
20425
  skillsService;
19194
20426
  conversation;
20427
+ plannerTools = plannerToolHandler({
20428
+ liveCatalog: () => this.liveCatalog(),
20429
+ coerce: (tasks, runners) => coerceAssignments(tasks, this.allowlist(), runners, this.models()),
20430
+ submitPlan: (tasks, runners) => this.submitPlanFromTool(tasks, runners),
20431
+ editPlan: (ops) => this.editPlanFromTool(ops),
20432
+ tasks: () => this.store.planTasks,
20433
+ read: (signature, answer2) => this.conversation.read(signature, answer2),
20434
+ liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
20435
+ lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId)
20436
+ });
19195
20437
  constructor(parts) {
19196
20438
  this.config = parts.config;
19197
20439
  this.registry = parts.registry;
@@ -19220,17 +20462,7 @@ var Session = class {
19220
20462
  aiService: () => this.aiService(),
19221
20463
  onProgress: (p) => this.events.progress(p),
19222
20464
  opening: (runners) => this.conversationOpening(runners),
19223
- catalog: () => {
19224
- const runners = this.plan?.runners ?? [];
19225
- return {
19226
- runners,
19227
- // Allowlist-filtered: neither the per-turn block nor a read may offer
19228
- // a model the planner is forbidden to assign.
19229
- models: filterModelsForPrompt(this.models(), this.allowlist()),
19230
- modes: this.runnerModesFor(runners),
19231
- autonomousDefault: this.config.autonomousMode
19232
- };
19233
- },
20465
+ catalog: () => this.catalogOf(this.plan?.runners ?? []),
19234
20466
  tasks: () => this.store.planTasks,
19235
20467
  liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
19236
20468
  hasLiveWork: () => this.hasLiveWork,
@@ -19254,8 +20486,8 @@ var Session = class {
19254
20486
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
19255
20487
  * path is identical regardless of who is looking.
19256
20488
  */
19257
- resolveApproval(id, answer) {
19258
- return this.approvals.resolve(id, answer);
20489
+ resolveApproval(id, answer2) {
20490
+ return this.approvals.resolve(id, answer2);
19259
20491
  }
19260
20492
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
19261
20493
  outstandingApprovals() {
@@ -19375,6 +20607,27 @@ var Session = class {
19375
20607
  this.modelsCache = {};
19376
20608
  this.remintSessionId();
19377
20609
  }
20610
+ /**
20611
+ * What a planner may assign right now: every enabled runner, its modes, and
20612
+ * its allowlisted models — read as of this call, so a runner enabled or an
20613
+ * allowlist edited since planning started is in it (#69). Discovers a
20614
+ * runner's models the first time it is asked for.
20615
+ */
20616
+ async liveCatalog() {
20617
+ const runners = this.enabledRunners();
20618
+ this.modelsCache = { ...this.modelsCache, ...await this.modelResolver.modelsForRunners(runners) };
20619
+ return this.catalogOf(runners);
20620
+ }
20621
+ catalogOf(runners) {
20622
+ return {
20623
+ runners,
20624
+ // Allowlist-filtered: neither the per-turn block nor a read may offer
20625
+ // a model the planner is forbidden to assign.
20626
+ models: filterModelsForPrompt(this.models(), this.allowlist()),
20627
+ modes: this.runnerModesFor(runners),
20628
+ autonomousDefault: this.config.autonomousMode
20629
+ };
20630
+ }
19378
20631
  runnerModesFor(runners) {
19379
20632
  return runnerModesFrom(this.registry, runners);
19380
20633
  }
@@ -19386,13 +20639,17 @@ var Session = class {
19386
20639
  * allowlist here — {@link checkModelAndModeValidity} narrows by
19387
20640
  * allowlist itself, the same way `coerceAssignments` does.
19388
20641
  */
19389
- editCatalog() {
20642
+ editCatalog(runners = this.plan?.runners ?? []) {
19390
20643
  return {
19391
20644
  modelsByRunner: this.models(),
19392
- runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
20645
+ runnerModes: this.runnerModesFor(runners),
19393
20646
  perRunnerAllowlist: this.allowlist()
19394
20647
  };
19395
20648
  }
20649
+ /** The runners enabled right now — a toggle made since the session was built counts (#69). */
20650
+ enabledRunners() {
20651
+ return this.settingsFn().enabledRunners ?? this.config.enabledRunners;
20652
+ }
19396
20653
  /**
19397
20654
  * The allowlist in force right now. Unset means no restriction — falling
19398
20655
  * back to the one planning started under would keep a restriction the user
@@ -19494,7 +20751,7 @@ var Session = class {
19494
20751
  this.goal = goal;
19495
20752
  this.remintSessionId();
19496
20753
  this.beginFreshPlan();
19497
- const enabled = this.config.enabledRunners;
20754
+ const enabled = this.enabledRunners();
19498
20755
  const chosenRunners = runners.filter((r) => enabled.includes(r));
19499
20756
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
19500
20757
  const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
@@ -19538,7 +20795,7 @@ var Session = class {
19538
20795
  this.goal = this.resolveSkillInvocation(goal);
19539
20796
  this.remintSessionId();
19540
20797
  this.beginFreshPlan();
19541
- const enabled = this.config.enabledRunners;
20798
+ const enabled = this.enabledRunners();
19542
20799
  const chosenRunners = runners.filter((r) => enabled.includes(r));
19543
20800
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
19544
20801
  const opening = await this.conversationOpening(chosenRunners);
@@ -19616,6 +20873,49 @@ var Session = class {
19616
20873
  rewindTargets() {
19617
20874
  return this.conversation.rewindTargets();
19618
20875
  }
20876
+ /**
20877
+ * Hand the planner turn in flight a validated plan or a task edit; the turn
20878
+ * commits it as it would the same JSON in the planner's reply. False when
20879
+ * no turn is open.
20880
+ */
20881
+ submitToTurn(submission) {
20882
+ return this.conversation.submit(submission);
20883
+ }
20884
+ /**
20885
+ * A plan the planner submitted through its tool, already checked against the
20886
+ * live catalog. Its runners join `plan.runners` here, so a runner enabled
20887
+ * since planning started is not snapped back by the commit (#69).
20888
+ */
20889
+ submitPlanFromTool(tasks, runners) {
20890
+ if (!this.plan || !this.conversation.submit({ kind: "plan", tasks })) return false;
20891
+ for (const runner of runners) this.admitRunner(runner, this.modelsCache[runner] ?? []);
20892
+ return true;
20893
+ }
20894
+ /**
20895
+ * A task edit the planner made through its tool. Checked against the live
20896
+ * catalog, then handed to the open turn, which commits it as it would the
20897
+ * same ops in a taskOps reply — including parking it behind a running batch,
20898
+ * which the envelope does before any check, so a queued edit is not checked
20899
+ * here either. Edits made in one reply join one batch.
20900
+ */
20901
+ async editPlanFromTool(ops) {
20902
+ const refuse = (message) => ({ ok: false, errors: [message] });
20903
+ const catalog = await this.liveCatalog();
20904
+ if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
20905
+ const held = this.conversation.pendingOps();
20906
+ 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.");
20907
+ const batch = [...held, ...ops];
20908
+ const queued = this.conversation.editWouldQueue(batch);
20909
+ let summary = [];
20910
+ if (!queued) {
20911
+ const result = applyTaskOps(this.store.planTasks, batch, catalog.runners, this.editCatalog(catalog.runners));
20912
+ if (!result.ok) return { ok: false, errors: result.errors };
20913
+ summary = result.summary;
20914
+ for (const runner of runnersOf(result.tasks)) this.admitRunner(runner, this.modelsCache[runner] ?? []);
20915
+ }
20916
+ if (!this.conversation.submit({ kind: "task_ops", ops: batch })) return refuse("No planning turn is open to take the edit.");
20917
+ return { ok: true, summary, queued };
20918
+ }
19619
20919
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
19620
20920
  get isConversationActive() {
19621
20921
  return this.conversation.isActive;
@@ -19661,7 +20961,8 @@ var Session = class {
19661
20961
  // usage line can show context fill (#49). Unknown stays absent.
19662
20962
  contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
19663
20963
  fs: this.fsAdapter,
19664
- fetcher: this.fetcher
20964
+ fetcher: this.fetcher,
20965
+ plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
19665
20966
  };
19666
20967
  }
19667
20968
  /**
@@ -19779,7 +21080,7 @@ var Session = class {
19779
21080
  ])
19780
21081
  );
19781
21082
  try {
19782
- const modelsByRunner = await this.modelResolver.modelsForRunners(this.config.enabledRunners);
21083
+ const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
19783
21084
  const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
19784
21085
  const { modelAllowlist } = this.settingsFn();
19785
21086
  const result = await this.planner.modifyDuringExecution({
@@ -20126,7 +21427,7 @@ var Session = class {
20126
21427
  if (!this.plan) throw new Error("No active plan state");
20127
21428
  const check = canMergeTasks(this.store.planTasks, taskIds);
20128
21429
  if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
20129
- const prompt = buildMergePrompt(taskIds, this.store.planTasks);
21430
+ const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
20130
21431
  return this.continueConversation(prompt, options);
20131
21432
  }
20132
21433
  /**
@@ -20138,7 +21439,7 @@ var Session = class {
20138
21439
  if (!this.plan) throw new Error("No active plan state");
20139
21440
  const check = canSplitTask(this.store.planTasks, taskId);
20140
21441
  if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
20141
- const prompt = buildSplitPrompt(taskId, this.store.planTasks);
21442
+ const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
20142
21443
  return this.continueConversation(prompt, options);
20143
21444
  }
20144
21445
  loadPlan(plan, goal, workspace, opts) {
@@ -20178,41 +21479,6 @@ var Session = class {
20178
21479
  }
20179
21480
  };
20180
21481
 
20181
- // src/utils/privateFile.ts
20182
- import { closeSync, mkdirSync as mkdirSync10, openSync, renameSync, unlinkSync as unlinkSync2, writeSync } from "fs";
20183
- import { basename as basename2, dirname as dirname7, join as join18 } from "path";
20184
- import { randomBytes as randomBytes4 } from "crypto";
20185
- var DIR_MODE = 448;
20186
- var FILE_MODE = 384;
20187
- var POSIX_MODES = process.platform !== "win32";
20188
- function ensurePrivateDir(dir) {
20189
- if (POSIX_MODES) {
20190
- mkdirSync10(dir, { recursive: true, mode: DIR_MODE });
20191
- return;
20192
- }
20193
- mkdirSync10(dir, { recursive: true });
20194
- }
20195
- function writePrivateFile(filePath, content) {
20196
- const dir = dirname7(filePath);
20197
- ensurePrivateDir(dir);
20198
- const tempPath = join18(dir, `.${basename2(filePath)}.${randomBytes4(8).toString("hex")}.tmp`);
20199
- const fd = openSync(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
20200
- try {
20201
- writeSync(fd, content);
20202
- } finally {
20203
- closeSync(fd);
20204
- }
20205
- try {
20206
- renameSync(tempPath, filePath);
20207
- } catch (err) {
20208
- try {
20209
- unlinkSync2(tempPath);
20210
- } catch {
20211
- }
20212
- throw err;
20213
- }
20214
- }
20215
-
20216
21482
  // src/services/buildRunnerArgs.ts
20217
21483
  function buildRunnerInvocation(opts) {
20218
21484
  const entry = opts.registry.get(opts.runner);
@@ -20469,9 +21735,9 @@ var HeadlessRunner = class extends AbstractRunner {
20469
21735
  // src/services/TmuxRunner.ts
20470
21736
  import { execFile as execFile5 } from "child_process";
20471
21737
  import { promisify as promisify5 } from "util";
20472
- import { tmpdir } from "os";
20473
- import { join as join19 } from "path";
20474
- import { existsSync as existsSync11, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
21738
+ import { tmpdir as tmpdir2 } from "os";
21739
+ import { join as join21 } from "path";
21740
+ import { existsSync as existsSync11, readFileSync as readFileSync10, unlinkSync as unlinkSync4, writeFileSync as writeFileSync9 } from "fs";
20475
21741
 
20476
21742
  // src/utils/tmux.ts
20477
21743
  import { execSync as execSync2 } from "child_process";
@@ -20569,7 +21835,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20569
21835
  * lets the user keep poking at a finished task's terminal.
20570
21836
  */
20571
21837
  async start(command, args, cwd, env) {
20572
- writeFileSync7(this.logPath, "");
21838
+ writeFileSync9(this.logPath, "");
20573
21839
  const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
20574
21840
  const inner = [command, ...args].map(posixShellQuote).join(" ");
20575
21841
  const prefix = envAssignments ? `env ${envAssignments} ` : "";
@@ -20592,7 +21858,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20592
21858
  readLog() {
20593
21859
  let content;
20594
21860
  try {
20595
- content = existsSync11(this.logPath) ? readFileSync9(this.logPath, "utf8") : "";
21861
+ content = existsSync11(this.logPath) ? readFileSync10(this.logPath, "utf8") : "";
20596
21862
  } catch {
20597
21863
  return false;
20598
21864
  }
@@ -20648,7 +21914,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20648
21914
  this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
20649
21915
  });
20650
21916
  try {
20651
- unlinkSync3(this.logPath);
21917
+ unlinkSync4(this.logPath);
20652
21918
  } catch {
20653
21919
  }
20654
21920
  this.baseHandleExit(code);
@@ -20685,7 +21951,7 @@ var TmuxRunner = class extends AbstractRunner {
20685
21951
  this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
20686
21952
  this.resolvePath = deps.resolvePath ?? augmentedPath;
20687
21953
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
20688
- this.logDir = deps.logDir ?? tmpdir();
21954
+ this.logDir = deps.logDir ?? tmpdir2();
20689
21955
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
20690
21956
  this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
20691
21957
  }
@@ -20805,7 +22071,7 @@ var TmuxRunner = class extends AbstractRunner {
20805
22071
  windowName,
20806
22072
  this.execFileImpl,
20807
22073
  this.pollIntervalMs,
20808
- join19(this.logDir, `${id}.log`),
22074
+ join21(this.logDir, `${id}.log`),
20809
22075
  this.socket
20810
22076
  );
20811
22077
  invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
@@ -20833,9 +22099,9 @@ var KEY_ARGS = {
20833
22099
  };
20834
22100
  var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
20835
22101
  function toolLine(name, args) {
20836
- const { tool } = mapAgentTool(name);
20837
- const normalized = normalizeAgentArgs(tool, args);
20838
- const preferred = KEY_ARGS[tool];
22102
+ const { tool: tool2 } = mapAgentTool(name);
22103
+ const normalized = normalizeAgentArgs(tool2, args);
22104
+ const preferred = KEY_ARGS[tool2];
20839
22105
  for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
20840
22106
  const value = normalized[key];
20841
22107
  if (typeof value !== "string") continue;
@@ -20898,6 +22164,7 @@ var StructuredSession = class extends AbstractTerminalSession {
20898
22164
  constructor(id, taskId, launch) {
20899
22165
  super(id, taskId);
20900
22166
  this.launch = launch;
22167
+ this.startOptions = launch.startOptions;
20901
22168
  this.text = new PlainTextChannel((text) => {
20902
22169
  this.output += text;
20903
22170
  this.outputEmitter.emit("output", text);
@@ -20926,9 +22193,20 @@ var StructuredSession = class extends AbstractTerminalSession {
20926
22193
  * requests — while an approval must be answerable by id across the session.
20927
22194
  */
20928
22195
  permissions = /* @__PURE__ */ new Map();
22196
+ /** The launch's start options, with this attempt's server once its token is issued. */
22197
+ startOptions;
22198
+ /** This attempt's MCP token, until the session ends (ADR-0022, A2). */
22199
+ toolToken = null;
22200
+ checkpointHandler = null;
20929
22201
  /** Start the runner and send the task's prompt as its first turn. */
20930
22202
  async start(prompt) {
20931
- await this.startAdapter(this.launch.startOptions);
22203
+ await this.issueTools();
22204
+ try {
22205
+ await this.startAdapter(this.startOptions);
22206
+ } catch (err) {
22207
+ this.revokeTools();
22208
+ throw err;
22209
+ }
20932
22210
  this.state = "working";
20933
22211
  setImmediate(() => {
20934
22212
  if (!this.exited) this.deliver(prompt);
@@ -20937,6 +22215,49 @@ var StructuredSession = class extends AbstractTerminalSession {
20937
22215
  getOutput() {
20938
22216
  return this.output;
20939
22217
  }
22218
+ onTaskComplete(listener) {
22219
+ this.structuredEmitter.on("taskComplete", listener);
22220
+ }
22221
+ onToolCheckpoint(handler) {
22222
+ this.checkpointHandler = handler;
22223
+ }
22224
+ /**
22225
+ * A server that cannot start costs the task its tools, not its run: the
22226
+ * prompt still teaches the marker (ADR-0022, S2).
22227
+ */
22228
+ async issueTools() {
22229
+ const tools = this.launch.tools;
22230
+ if (!tools) return;
22231
+ try {
22232
+ const credential = await tools.server.issueTaskToken(tools.scope, {
22233
+ taskComplete: async (report) => {
22234
+ this.structuredEmitter.emit("taskComplete", report);
22235
+ return { text: "Recorded. End your turn now." };
22236
+ },
22237
+ checkpoint: async ({ question }, { signal }) => {
22238
+ if (!this.checkpointHandler) return { text: "checkpoint is not available in this session.", isError: true };
22239
+ return checkpointReply(await this.checkpointHandler(question, signal));
22240
+ }
22241
+ });
22242
+ if (this.exited) {
22243
+ tools.server.revoke(credential.token);
22244
+ return;
22245
+ }
22246
+ this.toolToken = credential.token;
22247
+ this.startOptions = { ...this.startOptions, mcp: mcpClientConfig(credential) };
22248
+ } catch (err) {
22249
+ console.error(`[structured] No Ordewell tools for task ${this.taskId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`);
22250
+ }
22251
+ }
22252
+ revokeTools() {
22253
+ if (this.toolToken === null) return;
22254
+ this.launch.tools?.server.revoke(this.toolToken);
22255
+ this.toolToken = null;
22256
+ }
22257
+ baseHandleExit(code) {
22258
+ this.revokeTools();
22259
+ super.baseHandleExit(code);
22260
+ }
20940
22261
  /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
20941
22262
  write(text) {
20942
22263
  const message = text.trim();
@@ -21034,12 +22355,12 @@ var StructuredSession = class extends AbstractTerminalSession {
21034
22355
  * session id after an abort.
21035
22356
  */
21036
22357
  async restartInterrupted(turn) {
21037
- const resumeSessionId = this.nativeSessionId() ?? this.launch.startOptions.resumeSessionId;
22358
+ const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
21038
22359
  this.generation += 1;
21039
22360
  turn.abort.abort();
21040
22361
  if (this.adapter) this.withdrawPermissions(this.adapter);
21041
22362
  try {
21042
- await this.startAdapter({ ...this.launch.startOptions, resumeSessionId });
22363
+ await this.startAdapter({ ...this.startOptions, resumeSessionId });
21043
22364
  } catch (err) {
21044
22365
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
21045
22366
  this.endTurn(turn, "interrupted");
@@ -21177,11 +22498,13 @@ var StructuredRunner = class extends AbstractRunner {
21177
22498
  processDeps;
21178
22499
  createAdapter;
21179
22500
  interruptGraceMs;
22501
+ mcp;
21180
22502
  constructor(deps = {}) {
21181
22503
  super();
21182
22504
  this.processDeps = deps.process ?? {};
21183
22505
  this.createAdapter = deps.createAdapter ?? createTaskAdapter;
21184
22506
  this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
22507
+ this.mcp = deps.mcp ?? sharedMcpServer();
21185
22508
  }
21186
22509
  async spawn(opts) {
21187
22510
  const manifest = opts.registry?.get(opts.runner)?.manifest;
@@ -21208,7 +22531,8 @@ var StructuredRunner = class extends AbstractRunner {
21208
22531
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
21209
22532
  resumeSessionId: opts.resumeSessionId
21210
22533
  },
21211
- interruptGraceMs: this.interruptGraceMs
22534
+ interruptGraceMs: this.interruptGraceMs,
22535
+ ...takesOrdewellTools(opts.runner) ? { tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } } } : {}
21212
22536
  });
21213
22537
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
21214
22538
  this.registerSession(id, session);
@@ -21222,73 +22546,12 @@ var StructuredRunner = class extends AbstractRunner {
21222
22546
  }
21223
22547
  };
21224
22548
 
21225
- // src/utils/daemonToken.ts
21226
- import { randomBytes as randomBytes5, timingSafeEqual } from "crypto";
21227
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync4 } from "fs";
21228
- import { join as join20 } from "path";
21229
- var BEARER_PREFIX = "bearer ";
21230
- var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
21231
- var DAEMON_SUBPROTOCOL = "ordewell.v1";
21232
- function configDir() {
21233
- return globalDataDir();
21234
- }
21235
- function daemonTokenPath(port) {
21236
- return join20(configDir(), `server-${port}.token`);
21237
- }
21238
- function mintDaemonToken(port) {
21239
- const token = randomBytes5(32).toString("base64url");
21240
- const file = daemonTokenPath(port);
21241
- writePrivateFile(file, token);
21242
- return { token, file };
21243
- }
21244
- function readDaemonToken(port) {
21245
- try {
21246
- const token = readFileSync10(daemonTokenPath(port), "utf8").trim();
21247
- return token === "" ? void 0 : token;
21248
- } catch {
21249
- return void 0;
21250
- }
21251
- }
21252
- function clearDaemonToken(port) {
21253
- try {
21254
- unlinkSync4(daemonTokenPath(port));
21255
- } catch {
21256
- }
21257
- }
21258
- function bearerHeaderValue(token) {
21259
- return `Bearer ${token}`;
21260
- }
21261
- function tokenSubprotocols(token) {
21262
- return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
21263
- }
21264
- function extractPresentedToken(carriers) {
21265
- const authorization = carriers.authorization;
21266
- if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
21267
- const value = authorization.slice(BEARER_PREFIX.length).trim();
21268
- if (value !== "") return value;
21269
- }
21270
- for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
21271
- const value = offered.trim();
21272
- if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
21273
- const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
21274
- if (token !== "") return token;
21275
- }
21276
- return void 0;
21277
- }
21278
- function tokensMatch(presented, expected) {
21279
- if (presented === void 0) return false;
21280
- const a = Buffer.from(presented, "utf8");
21281
- const b = Buffer.from(expected, "utf8");
21282
- if (a.length !== b.length) return false;
21283
- return timingSafeEqual(a, b);
21284
- }
21285
-
21286
22549
  // src/plugins/RunnerRegistry.ts
21287
22550
  import * as fs13 from "fs";
21288
22551
  import * as path23 from "path";
21289
22552
  import * as os6 from "os";
21290
22553
  import { execFileSync } from "child_process";
21291
- import { randomBytes as randomBytes6 } from "crypto";
22554
+ import { randomBytes as randomBytes7 } from "crypto";
21292
22555
 
21293
22556
  // src/plugins/FsPluginStore.ts
21294
22557
  import * as fs12 from "fs";
@@ -21312,8 +22575,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
21312
22575
  assertPlainPluginName(name);
21313
22576
  const root = path21.resolve(pluginsDir);
21314
22577
  const dest = path21.resolve(root, name);
21315
- const relative5 = path21.relative(root, dest);
21316
- if (relative5 !== name || relative5 === "" || path21.isAbsolute(relative5)) {
22578
+ const relative6 = path21.relative(root, dest);
22579
+ if (relative6 !== name || relative6 === "" || path21.isAbsolute(relative6)) {
21317
22580
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
21318
22581
  }
21319
22582
  return dest;
@@ -21927,7 +23190,7 @@ var RunnerRegistry = class {
21927
23190
  }
21928
23191
  installFromGit(url) {
21929
23192
  const validated = assertInstallablePluginUrl(url);
21930
- const tmpDir = path23.join(os6.tmpdir(), `ordewell-plugin-${randomBytes6(12).toString("hex")}`);
23193
+ const tmpDir = path23.join(os6.tmpdir(), `ordewell-plugin-${randomBytes7(12).toString("hex")}`);
21931
23194
  this.store.ensureDir(tmpDir);
21932
23195
  try {
21933
23196
  try {
@@ -22346,11 +23609,15 @@ export {
22346
23609
  NO_TURN,
22347
23610
  OPENCODE_MANIFEST,
22348
23611
  ORCHESTRATOR_SHORTCUTS,
23612
+ ORDEWELL_MCP_PATH,
23613
+ ORDEWELL_MCP_SERVER_NAME,
22349
23614
  ORDEWELL_SETTABLE_ENV,
22350
23615
  OUTPUT_LINES_DEFAULT,
22351
23616
  OUTPUT_LINES_MAX,
22352
23617
  OpenAiService,
22353
23618
  OpenCodeAdapter,
23619
+ OrdewellMcpServer,
23620
+ PLANNER_TOOLS,
22354
23621
  PLAN_ENVELOPE_KEY,
22355
23622
  PLUGIN_NAME_PATTERN,
22356
23623
  PROVIDER_CREDENTIAL_ENV,
@@ -22387,6 +23654,7 @@ export {
22387
23654
  TASK_QUERY_FIELDS,
22388
23655
  TASK_QUERY_PROTOCOL,
22389
23656
  TASK_QUERY_REMINDER,
23657
+ TASK_TOOLS,
22390
23658
  TRUNCATED_PLAN_REPAIR_INSTRUCTION,
22391
23659
  TaskControlError,
22392
23660
  TaskLogRecorder,
@@ -22470,10 +23738,14 @@ export {
22470
23738
  daemonTokenPath,
22471
23739
  defaultLogger,
22472
23740
  definitionPattern,
23741
+ defuseMarkers,
22473
23742
  deleteSession,
22474
23743
  dependencyCandidates,
22475
23744
  dependentsOf,
22476
23745
  describeMergeResult,
23746
+ diffRows,
23747
+ diffStat,
23748
+ diffSummary,
22477
23749
  digestTaskLog,
22478
23750
  discoverGeminiModels,
22479
23751
  drainNext,
@@ -22536,6 +23808,7 @@ export {
22536
23808
  loadState,
22537
23809
  looksLikePlanAttempt,
22538
23810
  mapAgentTool,
23811
+ mcpClientConfig,
22539
23812
  migrateLegacyPlan,
22540
23813
  migrateOldConfigDir,
22541
23814
  migratePlanIsolation,
@@ -22551,6 +23824,7 @@ export {
22551
23824
  opensWithJsonObject,
22552
23825
  outputLines,
22553
23826
  outputPreview,
23827
+ ownerOnlyConfigFile,
22554
23828
  parseAutonomyLevel,
22555
23829
  parseMaxParallel,
22556
23830
  parsePartialPlan,
@@ -22609,6 +23883,7 @@ export {
22609
23883
  serializeTaskStatus,
22610
23884
  sessionDataDir,
22611
23885
  sessionRuntimeSettings,
23886
+ sharedMcpServer,
22612
23887
  stateExists,
22613
23888
  stopTurn,
22614
23889
  stripAnsi,
@@ -22642,6 +23917,7 @@ export {
22642
23917
  usageLine,
22643
23918
  validateModifiedPlan,
22644
23919
  validatePlanModification,
23920
+ validatePlanTasks,
22645
23921
  warningsText,
22646
23922
  wellKnownBinDirs,
22647
23923
  windowsCommandLine,