@ordewell/core 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/{IFileSystem-DQEKyC6H.d.ts → IFileSystem-D4ADfTrX.d.ts} +1 -1
  3. package/dist/{IFileSystem-Qj2MWHn_.d.mts → IFileSystem-DgpX0BU7.d.mts} +1 -1
  4. package/dist/{ModeResolver-ByNpC0EP.d.ts → ModeResolver-BEcRAz1c.d.ts} +3 -3
  5. package/dist/{ModeResolver-rqaY_4Ex.d.mts → ModeResolver-Cv01ZDd9.d.mts} +3 -3
  6. package/dist/{Task-xqdxugt2.d.mts → Task-CoKhvBa1.d.mts} +275 -2
  7. package/dist/{Task-xqdxugt2.d.ts → Task-CoKhvBa1.d.ts} +275 -2
  8. package/dist/{chunk-M4KXBEFJ.mjs → chunk-J3YSTPCE.mjs} +41 -41
  9. package/dist/chunk-J3YSTPCE.mjs.map +1 -0
  10. package/dist/{chunk-I6U3PVHL.mjs → chunk-LSGY7AIZ.mjs} +74 -23
  11. package/dist/chunk-LSGY7AIZ.mjs.map +1 -0
  12. package/dist/{chunk-JRZPSNRO.mjs → chunk-YU4UKKKI.mjs} +1 -1
  13. package/dist/chunk-YU4UKKKI.mjs.map +1 -0
  14. package/dist/index.d.mts +250 -34
  15. package/dist/index.d.ts +250 -34
  16. package/dist/index.js +2011 -670
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +1650 -365
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/order-labels.d.mts +2 -1
  21. package/dist/order-labels.d.ts +2 -1
  22. package/dist/{parsing-DDZ1rUYa.d.mts → parsing-B4OCHH1_.d.mts} +24 -3
  23. package/dist/{parsing-COPG8-Ep.d.ts → parsing-DMdOq-BV.d.ts} +24 -3
  24. package/dist/parsing.d.mts +4 -3
  25. package/dist/parsing.d.ts +4 -3
  26. package/dist/parsing.js +36 -38
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +2 -2
  29. package/dist/{plan-utils-Br8UAoiu.d.ts → plan-utils-BdYPRBno.d.ts} +28 -3
  30. package/dist/{plan-utils-DMgajMvd.d.mts → plan-utils-CD1Xu65s.d.mts} +28 -3
  31. package/dist/plan-utils.d.mts +4 -3
  32. package/dist/plan-utils.d.ts +4 -3
  33. package/dist/plan-utils.js +111 -58
  34. package/dist/plan-utils.js.map +1 -1
  35. package/dist/plan-utils.mjs +8 -2
  36. package/dist/testing.d.mts +11 -2
  37. package/dist/testing.d.ts +11 -2
  38. package/dist/testing.js +17 -0
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +17 -0
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +4 -2
  43. package/dist/chunk-I6U3PVHL.mjs.map +0 -1
  44. package/dist/chunk-JRZPSNRO.mjs.map +0 -1
  45. package/dist/chunk-M4KXBEFJ.mjs.map +0 -1
package/dist/index.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";
@@ -2345,7 +2349,10 @@ var WRAPPER_FAMILY = {
2345
2349
  // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
2346
2350
  // name arrives as its first argument.
2347
2351
  busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
2348
- command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
2352
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] },
2353
+ // Runs the named shell builtin, so `builtin eval …` is an `eval`. Classified
2354
+ // on its own name it only asked, and a grant for `builtin echo` covered it.
2355
+ builtin: {}
2349
2356
  };
2350
2357
  var XARGS_SPEC = {
2351
2358
  valueFlags: [
@@ -3075,6 +3082,9 @@ function refusalFor(seg) {
3075
3082
  if (seg.binary === "source" || seg.binary === ".") {
3076
3083
  return `"${seg.binary}" runs a file's contents as commands, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
3077
3084
  }
3085
+ if (seg.binary === "enable" && seg.args.some((a) => /^-[a-zA-Z]*f/.test(a))) {
3086
+ return `"enable -f" loads a shared library into the shell, which runs its code. Use the read-only research tools, or describe it as a task.`;
3087
+ }
3078
3088
  if (SHELL_KEYWORDS.includes(seg.binary)) {
3079
3089
  return `"${seg.binary}" is a shell keyword or compound-command opener. This classifier only inspects the command that runs first, and a keyword hides the real one from it. Run the inner command directly, or describe it as a task.`;
3080
3090
  }
@@ -3185,6 +3195,9 @@ function classifyCommand(command, opts = {}) {
3185
3195
  }
3186
3196
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
3187
3197
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
3198
+ if (nonAuto.some((u) => u.seg.binary === "cd" || u.seg.binary !== "command" && SHELL_STATE_COMMANDS.has(u.seg.binary))) {
3199
+ return { tier: "ask", scope: trimmed };
3200
+ }
3188
3201
  const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
3189
3202
  return { tier: "ask", scope };
3190
3203
  }
@@ -3805,12 +3818,12 @@ var ApprovalPolicy = class {
3805
3818
  if (this.generation === gen) this.inFlight.delete(openKey);
3806
3819
  });
3807
3820
  this.inFlight.set(openKey, pending);
3808
- const answer = await pending;
3821
+ const answer2 = await pending;
3809
3822
  if (this.generation === gen) {
3810
- if (answer) open.forEach((s) => this.granted.add(s));
3823
+ if (answer2) open.forEach((s) => this.granted.add(s));
3811
3824
  else this.refused.add(key);
3812
3825
  }
3813
- return decide(answer, "asked");
3826
+ return decide(answer2, "asked");
3814
3827
  }
3815
3828
  /** Scopes the user has granted this session — for display and for persistence. */
3816
3829
  grantedScopes() {
@@ -3871,10 +3884,10 @@ var PendingApprovals = class {
3871
3884
  * settled. "Allow for this task" on a request that did not offer it is a
3872
3885
  * plain allow: no grant is made that the requester never proposed.
3873
3886
  */
3874
- resolve(id, answer) {
3887
+ resolve(id, answer2) {
3875
3888
  const entry = this.entries.get(id);
3876
3889
  if (!entry) return false;
3877
- const decision = toApprovalDecision(answer);
3890
+ const decision = toApprovalDecision(answer2);
3878
3891
  entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
3879
3892
  return true;
3880
3893
  }
@@ -4112,9 +4125,9 @@ function applyHeadLimit(stdout, headLimit) {
4112
4125
  }
4113
4126
  function formatSearchOutput(capped, root, opts) {
4114
4127
  if (capped.rows.length === 0) return opts.emptyMessage;
4115
- const sep2 = opts.sep ?? path3.sep;
4116
- const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
4117
- 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");
4118
4131
  if (!capped.truncated) return body;
4119
4132
  const hint = opts.hint ? ` ${opts.hint}` : "";
4120
4133
  return `${body}
@@ -5069,28 +5082,28 @@ function posixBinDirs() {
5069
5082
  ];
5070
5083
  }
5071
5084
  function windowsBinDirs(env, home) {
5072
- const join25 = (...parts) => path4.win32.join(...parts);
5073
- 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)] : [];
5074
5087
  const appData = env.APPDATA;
5075
5088
  const localAppData = env.LOCALAPPDATA;
5076
5089
  const programData = env.ProgramData;
5077
5090
  return [
5078
5091
  // Native per-user installers, PowerShell-driven and otherwise.
5079
- join25(home, ".local", "bin"),
5080
- join25(home, ".opencode", "bin"),
5081
- join25(home, ".claude", "bin"),
5082
- join25(home, ".codex", "bin"),
5092
+ join26(home, ".local", "bin"),
5093
+ join26(home, ".opencode", "bin"),
5094
+ join26(home, ".claude", "bin"),
5095
+ join26(home, ".codex", "bin"),
5083
5096
  ...under(localAppData, "Programs", "claude"),
5084
5097
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
5085
5098
  // literal spelling is kept as a fallback for a host that does not export
5086
5099
  // APPDATA (a service account, a stripped CI container).
5087
5100
  ...under(appData, "npm"),
5088
- join25(home, "AppData", "Roaming", "npm"),
5101
+ join26(home, "AppData", "Roaming", "npm"),
5089
5102
  ...under(localAppData, "pnpm"),
5090
5103
  ...under(localAppData, "Yarn", "bin"),
5091
- join25(home, ".bun", "bin"),
5104
+ join26(home, ".bun", "bin"),
5092
5105
  // Windows package managers.
5093
- join25(home, "scoop", "shims"),
5106
+ join26(home, "scoop", "shims"),
5094
5107
  ...under(env.SCOOP, "shims"),
5095
5108
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
5096
5109
  ...under(programData, "chocolatey", "bin"),
@@ -5100,7 +5113,7 @@ function windowsBinDirs(env, home) {
5100
5113
  ...under(localAppData, "Microsoft", "WindowsApps"),
5101
5114
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
5102
5115
  ...under(localAppData, "Volta", "bin"),
5103
- join25(home, ".volta", "bin")
5116
+ join26(home, ".volta", "bin")
5104
5117
  ];
5105
5118
  }
5106
5119
  function wellKnownBinDirs(deps = {}) {
@@ -5588,18 +5601,18 @@ var GitWorktreeIsolation = class {
5588
5601
  */
5589
5602
  sharedPaths(workspaceRoot, isolated) {
5590
5603
  if (isolated.includes(SELF_REPO)) return [];
5591
- const shared = [];
5604
+ const shared2 = [];
5592
5605
  const walk = (rel) => {
5593
5606
  for (const name of listDir(path8.join(workspaceRoot, rel))) {
5594
5607
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
5595
5608
  const child = rel ? `${rel}/${name}` : name;
5596
5609
  if (isolated.includes(child)) continue;
5597
5610
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
5598
- else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
5611
+ else if (resolves(path8.join(workspaceRoot, child))) shared2.push(child);
5599
5612
  }
5600
5613
  };
5601
5614
  walk("");
5602
- return shared;
5615
+ return shared2;
5603
5616
  }
5604
5617
  prepare(task, run) {
5605
5618
  return this.admin(run.workspaceRoot, async () => {
@@ -6427,10 +6440,12 @@ var TASK_QUERY_FIELDS = [
6427
6440
  "userStoriesCovered",
6428
6441
  "output"
6429
6442
  ];
6443
+ var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
6430
6444
  var OUTPUT_LINES_DEFAULT = 80;
6431
6445
  var OUTPUT_LINES_MAX = 400;
6432
6446
  var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
6433
6447
  var ANSWER_TAIL_RESERVE = 4e3;
6448
+ var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
6434
6449
  function textHasTaskQuery(text) {
6435
6450
  return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
6436
6451
  }
@@ -6506,6 +6521,7 @@ function taskQuerySignature(query) {
6506
6521
  return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
6507
6522
  }
6508
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.";
6509
6525
  var TASK_QUERY_PROTOCOL = [
6510
6526
  "READING A TASK BEFORE YOU EDIT IT:",
6511
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:",
@@ -6517,7 +6533,16 @@ var TASK_QUERY_PROTOCOL = [
6517
6533
  "Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
6518
6534
  "Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
6519
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
+ ];
6520
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.";
6521
6546
  function findTask(tasks, ref) {
6522
6547
  const byId = tasks.find((t) => t.id === ref);
6523
6548
  if (byId) return byId;
@@ -6726,7 +6751,7 @@ function verificationModeBlock() {
6726
6751
  ].join("\n");
6727
6752
  }
6728
6753
  function researchPhaseBlock(harnessMode) {
6729
- const shared = [
6754
+ const shared2 = [
6730
6755
  "RESEARCH PHASE:",
6731
6756
  "- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
6732
6757
  ];
@@ -6740,7 +6765,7 @@ function researchPhaseBlock(harnessMode) {
6740
6765
  "- Read key source files to understand architecture"
6741
6766
  ];
6742
6767
  return [
6743
- ...shared,
6768
+ ...shared2,
6744
6769
  ...toolSpecific,
6745
6770
  "- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
6746
6771
  "- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
@@ -6802,28 +6827,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
6802
6827
  verificationBlock
6803
6828
  );
6804
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
+ ];
6805
6839
  function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
6840
+ const tools = variant.plannerTools ?? false;
6806
6841
  return [
6807
- "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.`,
6808
6843
  "",
6809
6844
  "WORKFLOW:",
6810
6845
  "1. Explore the workspace with tools to understand the codebase.",
6811
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.",
6812
6847
  "3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
6813
- '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.',
6814
6849
  "",
6815
6850
  researchPhaseBlock(variant.harness ?? false),
6816
6851
  verificationBlock,
6817
6852
  "",
6818
6853
  "OUTLINE PHASE:",
6819
- "- 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"}.`,
6820
6855
  "- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
6821
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"',
6822
6857
  "- The user may reply with adjustments. Revise the outline and re-present it.",
6823
- "- 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.",
6824
6859
  "",
6825
6860
  "PLAN FORMAT:",
6826
- "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:",
6827
6862
  "",
6828
6863
  "{",
6829
6864
  ' "tasks": [',
@@ -6837,8 +6872,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6837
6872
  ' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
6838
6873
  ' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
6839
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" },`,
6840
- ' "assignedRunner": "' + runners.join("|") + '",',
6841
- ' "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) + '",',
6842
6877
  ' "autonomy": "AFK|HITL",',
6843
6878
  ' "sliceType": "AFK|HITL",',
6844
6879
  ' "ops": true,',
@@ -6870,8 +6905,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6870
6905
  ...opsTaskSection(true),
6871
6906
  "",
6872
6907
  "RULES:",
6873
- "- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
6874
- "- 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
+ ],
6875
6915
  "- Reference specific files and patterns from your research in task prompts.",
6876
6916
  ...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
6877
6917
  "",
@@ -6879,23 +6919,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6879
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.",
6880
6920
  "- Follow the existing code style, naming conventions, and architectural patterns of the project.",
6881
6921
  "",
6882
- "MODEL ASSIGNMENT:",
6883
- "Available models:",
6884
- modelsJson,
6885
- "",
6886
- runnerInstruction(runners),
6922
+ ...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
6887
6923
  "MODEL SELECTION GUIDELINES:",
6888
6924
  "- Use stronger models for complex refactoring, architecture changes, security-critical code.",
6889
6925
  "- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
6890
- `- 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.`,
6891
6927
  "",
6892
6928
  "TASK MODE:",
6893
- 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,
6894
6930
  "",
6895
- // Shared by both variants on purpose: the read channel is a text envelope
6896
- // exactly so a harness planner, which Ordewell cannot hand tools to, speaks
6897
- // the same protocol as an API-backed one (ADR-0009).
6898
- ...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,
6899
6935
  "",
6900
6936
  context ? `PROJECT CONTEXT:
6901
6937
  ${context}
@@ -7198,7 +7234,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
7198
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.');
7199
7235
  return sections.join("\n");
7200
7236
  }
7201
- function buildMergePrompt(taskIds, tasks) {
7237
+ function buildMergePrompt(taskIds, tasks, tools = false) {
7202
7238
  const idSet = new Set(taskIds);
7203
7239
  const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
7204
7240
  const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
@@ -7206,22 +7242,22 @@ function buildMergePrompt(taskIds, tasks) {
7206
7242
  `Merge these tasks into ONE combined task: ${refs}.`,
7207
7243
  "Write a clear combined title, description, and prompt that cover all of their work.",
7208
7244
  "The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
7209
- '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.',
7210
- 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
7211
- ` {"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":"..."}}}]}`,
7212
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.`
7213
7249
  ].join("\n");
7214
7250
  }
7215
- function buildSplitPrompt(taskId, tasks) {
7251
+ function buildSplitPrompt(taskId, tasks, tools = false) {
7216
7252
  const task = tasks.find((t) => t.id === taskId);
7217
7253
  if (!task) return `Split task ${taskId} into smaller tasks.`;
7218
7254
  return [
7219
7255
  `Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
7220
7256
  "Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
7221
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.",
7222
- '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).',
7223
- 'Reply with ONLY a taskOps JSON object using a single "split" op:',
7224
- ` {"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":"..."}},...]}]}`,
7225
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.`
7226
7262
  ].join("\n");
7227
7263
  }
@@ -7275,15 +7311,15 @@ ${task.prompt ?? task.description}`;
7275
7311
 
7276
7312
  // src/services/PlanRepair.ts
7277
7313
  async function repairLoop(opts) {
7278
- let reply = await opts.first();
7314
+ let reply2 = await opts.first();
7279
7315
  for (let repairs = 0; ; repairs++) {
7280
- const verdict = await opts.interpret(reply);
7316
+ const verdict = await opts.interpret(reply2);
7281
7317
  if ("done" in verdict) return verdict.done;
7282
7318
  if (repairs >= opts.maxRepairs) {
7283
- 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 });
7284
7320
  }
7285
7321
  const { corrective } = verdict.retry;
7286
- reply = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
7322
+ reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
7287
7323
  }
7288
7324
  }
7289
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.";
@@ -7399,34 +7435,34 @@ async function settleReply(opts) {
7399
7435
  }
7400
7436
  if (attempt.failure !== void 0) return { done: message(attempt.failure) };
7401
7437
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
7402
- const reply = classifyPlannerReply(attempt.text, opts.classify);
7403
- switch (reply.kind) {
7438
+ const reply2 = classifyPlannerReply(attempt.text, opts.classify);
7439
+ switch (reply2.kind) {
7404
7440
  case "plan":
7405
- return { done: { kind: "plan", tasks: reply.tasks, text: said(attempt), researchLog } };
7441
+ return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
7406
7442
  case "task_ops":
7407
- 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 } };
7408
7444
  // A read is answered by the conversation, which owns the plan and the
7409
7445
  // catalog, so it leaves here the way a plan or an edit does.
7410
7446
  case "task_query":
7411
- 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 } };
7412
7448
  case "prose":
7413
7449
  return { done: asProse(attempt) };
7414
7450
  }
7415
7451
  if (opts.signal?.aborted) return { done: asProse(attempt) };
7416
- const errors = [reply.error.message];
7417
- switch (reply.kind) {
7452
+ const errors = [reply2.error.message];
7453
+ switch (reply2.kind) {
7418
7454
  case "broken_task_ops":
7419
- return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply.error.message), cause: reply.error } };
7455
+ return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
7420
7456
  case "broken_task_query":
7421
- return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply.error.message), cause: reply.error } };
7457
+ return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
7422
7458
  case "broken_plan": {
7423
- const corrective = reply.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply.error.message);
7424
- 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 } };
7425
7461
  }
7426
7462
  }
7427
7463
  },
7428
7464
  maxRepairs: MAX_JSON_REPAIRS,
7429
- onExhausted: ({ reply }) => asProse(reply)
7465
+ onExhausted: ({ reply: reply2 }) => asProse(reply2)
7430
7466
  });
7431
7467
  }
7432
7468
  var said = (attempt) => attempt.fullText ?? attempt.text;
@@ -7653,8 +7689,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
7653
7689
  const blocks = [{ tool: null, lines: [] }];
7654
7690
  for (const line of resultsText.split("\n")) {
7655
7691
  const m = headerRe.exec(line);
7656
- const tool = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
7657
- 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] });
7658
7694
  else blocks[blocks.length - 1].lines.push(line);
7659
7695
  }
7660
7696
  const texts = blocks.map((b) => b.lines.join("\n"));
@@ -8147,9 +8183,9 @@ ${llmOutput}
8147
8183
  return { tasks: null, researchLog, researchResults };
8148
8184
  }
8149
8185
  const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
8150
- const reply = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
8151
- if (reply.kind === "plan") {
8152
- 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 };
8153
8189
  }
8154
8190
  if (!turn.hasToolCalls) break;
8155
8191
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
@@ -8868,7 +8904,7 @@ ${collected.aiflowContext}
8868
8904
  };
8869
8905
 
8870
8906
  // src/services/TaskOrchestrator.ts
8871
- import * as path13 from "path";
8907
+ import * as path14 from "path";
8872
8908
 
8873
8909
  // src/services/promptAugment.ts
8874
8910
  var MAX_TAIL_CHARS = 500;
@@ -8986,10 +9022,14 @@ function renderPreviousAttempt(output) {
8986
9022
  tail
8987
9023
  ].join("\n");
8988
9024
  }
8989
- 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}`;
8990
9030
  return `
8991
9031
 
8992
- 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}`;
8993
9033
  }
8994
9034
  function renderTddInstruction() {
8995
9035
  return [
@@ -9010,7 +9050,21 @@ function renderTddInstruction() {
9010
9050
  ].join("\n");
9011
9051
  }
9012
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 `>>>`";
9013
- 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
+ }
9014
9068
  return [
9015
9069
  "## Human-in-the-loop checkpoints",
9016
9070
  "",
@@ -9042,9 +9096,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
9042
9096
  blocks.push(renderTddInstruction());
9043
9097
  }
9044
9098
  if (isHitlTask(task)) {
9045
- blocks.push(renderCheckpointInstruction());
9099
+ blocks.push(renderCheckpointInstruction(opts?.completionTool));
9046
9100
  }
9047
- const marker2 = renderCompletionMarker(task);
9101
+ const marker2 = renderCompletionMarker(task, opts?.completionTool);
9048
9102
  if (blocks.length === 0) return basePrompt + marker2;
9049
9103
  return `${blocks.join("\n\n")}
9050
9104
 
@@ -9055,12 +9109,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
9055
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."
9056
9110
  ];
9057
9111
  if (isHitlTask(task)) {
9058
- 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}.`);
9059
9113
  }
9060
9114
  return `${message.trim()}
9061
9115
 
9062
9116
  ---
9063
- ${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
9117
+ ${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
9064
9118
  }
9065
9119
 
9066
9120
  // src/services/VerdictEngine.ts
@@ -9078,6 +9132,13 @@ var VerdictEngine = class {
9078
9132
  pausedSessions = /* @__PURE__ */ new Map();
9079
9133
  listeners = [];
9080
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();
9081
9142
  idleListeners = [];
9082
9143
  /**
9083
9144
  * Per-task generation. Replaced on every watch(), clear() and verdict.
@@ -9108,6 +9169,9 @@ var VerdictEngine = class {
9108
9169
  onCheckpoint(listener) {
9109
9170
  this.checkpointListeners.push(listener);
9110
9171
  }
9172
+ onCheckpointWithdrawn(listener) {
9173
+ this.withdrawnListeners.push(listener);
9174
+ }
9111
9175
  onIdleChange(listener) {
9112
9176
  this.idleListeners.push(listener);
9113
9177
  }
@@ -9169,6 +9233,11 @@ ${line}
9169
9233
  }
9170
9234
  approveCheckpoint(taskId) {
9171
9235
  this.resumeIdle(taskId);
9236
+ const toolCall = this.toolCheckpoints.get(taskId);
9237
+ if (toolCall) {
9238
+ toolCall({ kind: "continue" });
9239
+ return;
9240
+ }
9172
9241
  const session = this.pausedSessions.get(taskId);
9173
9242
  if (session) {
9174
9243
  session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
@@ -9177,6 +9246,11 @@ ${line}
9177
9246
  }
9178
9247
  rejectCheckpoint(taskId, reason) {
9179
9248
  this.resumeIdle(taskId);
9249
+ const toolCall = this.toolCheckpoints.get(taskId);
9250
+ if (toolCall) {
9251
+ toolCall({ kind: "rejected", reason });
9252
+ return;
9253
+ }
9180
9254
  const session = this.pausedSessions.get(taskId);
9181
9255
  if (session) {
9182
9256
  session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
@@ -9187,7 +9261,10 @@ ${line}
9187
9261
  * Attach to a spawned session: scan the output tail for the task's completion
9188
9262
  * marker (delivering a verdict immediately while leaving interactive sessions
9189
9263
  * open), scan for checkpoint markers, and on exit produce a failed verdict
9190
- * 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.
9191
9268
  */
9192
9269
  watch(task, session) {
9193
9270
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
@@ -9217,6 +9294,8 @@ ${line}
9217
9294
  else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
9218
9295
  else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
9219
9296
  });
9297
+ session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
9298
+ session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
9220
9299
  }
9221
9300
  session.onExit((exitCode) => {
9222
9301
  if (this.generations.get(task.id) !== gen) return;
@@ -9225,6 +9304,52 @@ ${line}
9225
9304
  const verdict = this.decide(task, exitCode);
9226
9305
  for (const l of this.listeners) l(task.id, verdict);
9227
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." });
9228
9353
  }
9229
9354
  approvalOpened(taskId, approvalId) {
9230
9355
  const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
@@ -9258,6 +9383,7 @@ ${line}
9258
9383
  this.markerTails.delete(taskId);
9259
9384
  this.checkpointCarry.delete(taskId);
9260
9385
  this.pausedSessions.delete(taskId);
9386
+ this.withdrawToolCheckpoint(taskId);
9261
9387
  this.idlePaused.delete(taskId);
9262
9388
  this.openApprovals.delete(taskId);
9263
9389
  this.clearIdle(taskId);
@@ -9293,6 +9419,7 @@ ${line}
9293
9419
  this.markerTails.clear();
9294
9420
  this.checkpointCarry.clear();
9295
9421
  this.pausedSessions.clear();
9422
+ for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
9296
9423
  for (const timer of this.idleTimers.values()) clearTimeout(timer);
9297
9424
  this.idleTimers.clear();
9298
9425
  this.idleSince.clear();
@@ -9346,6 +9473,28 @@ ${line}
9346
9473
  };
9347
9474
  }
9348
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
+ }
9349
9498
 
9350
9499
  // src/services/transcriptCapture.ts
9351
9500
  import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync4 } from "fs";
@@ -9509,10 +9658,10 @@ async function fromDirenv(cwd, deps) {
9509
9658
  }
9510
9659
  return { env, blocked: null };
9511
9660
  }
9512
- function findUp(start, relative5, deps) {
9661
+ function findUp(start, relative6, deps) {
9513
9662
  let dir = path9.resolve(start);
9514
9663
  for (; ; ) {
9515
- const candidate = path9.join(dir, relative5);
9664
+ const candidate = path9.join(dir, relative6);
9516
9665
  if (deps.readFile(candidate) !== null) return candidate;
9517
9666
  const parent = path9.dirname(dir);
9518
9667
  if (parent === dir) return null;
@@ -9761,6 +9910,11 @@ var BufferedTaskOutputSource = class {
9761
9910
  session.onExit(() => {
9762
9911
  capture.exited = true;
9763
9912
  });
9913
+ if (isStructuredSession(session)) {
9914
+ session.onTaskComplete(({ summary }) => {
9915
+ if (capture.attached && summary.trim()) capture.reported = summary.trim();
9916
+ });
9917
+ }
9764
9918
  }
9765
9919
  detach(taskId) {
9766
9920
  const capture = this.captures.get(taskId);
@@ -9771,6 +9925,8 @@ var BufferedTaskOutputSource = class {
9771
9925
  this.captures.clear();
9772
9926
  }
9773
9927
  async finalText(attempt, doneToken) {
9928
+ const reported = this.captures.get(attempt.taskId)?.reported;
9929
+ if (reported) return defuseMarkers(reported);
9774
9930
  if (attempt.cwd && attempt.completionMarker) {
9775
9931
  const transcript = await this.transcripts.finalAssistantText({
9776
9932
  runner: attempt.runner,
@@ -10610,8 +10766,8 @@ var IsolationRunController = class {
10610
10766
  this.resolvers = {};
10611
10767
  this.reportedCopies.clear();
10612
10768
  this.listener.changed();
10613
- const shared = sharedPathsNotice(this.run);
10614
- if (shared) this.tell("info", shared);
10769
+ const shared2 = sharedPathsNotice(this.run);
10770
+ if (shared2) this.tell("info", shared2);
10615
10771
  }
10616
10772
  reportCopies(copied) {
10617
10773
  const fresh = copied.filter((p) => !this.reportedCopies.has(p));
@@ -11783,6 +11939,502 @@ ${tail}` : ""}`;
11783
11939
  }
11784
11940
  };
11785
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
+
11786
12438
  // src/services/harness/ClaudeCodeAdapter.ts
11787
12439
  var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
11788
12440
  var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
@@ -11797,7 +12449,10 @@ var PROTOCOL_ARGS = [
11797
12449
  ];
11798
12450
  var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
11799
12451
  var FOLLOW_ON_TURN_GRACE_MS = 5e3;
12452
+ var MCP_ATTACH_TIMEOUT_MS = 1e4;
12453
+ var MCP_STATUS_POLL_MS = 100;
11800
12454
  var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
12455
+ var ORDEWELL_TASK_TOOLS = ["task_complete", "checkpoint"];
11801
12456
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
11802
12457
  function usageRecord(usage, model, subagentId) {
11803
12458
  const record = {
@@ -11830,10 +12485,16 @@ function flattenContent(content) {
11830
12485
  }
11831
12486
  return "";
11832
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
+ }
11833
12494
  var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11834
12495
  agentId = "claude-code";
11835
- interruptCount = 0;
11836
- /** Interrupts sent and not yet acknowledged, by request id. */
12496
+ controlCount = 0;
12497
+ /** Control requests sent and not yet answered, by request id. */
11837
12498
  pendingControl = /* @__PURE__ */ new Map();
11838
12499
  /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
11839
12500
  interruptRequested = false;
@@ -11872,6 +12533,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11872
12533
  requestedMode = null;
11873
12534
  /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
11874
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;
11875
12540
  spawnSpec(opts) {
11876
12541
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
11877
12542
  const args = [
@@ -11884,6 +12549,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11884
12549
  "--append-system-prompt",
11885
12550
  opts.systemPrompt
11886
12551
  ];
12552
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, PLANNER_TOOLS.map((t) => t.name)));
11887
12553
  if (opts.model) args.push("--model", opts.model);
11888
12554
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
11889
12555
  if (opts.resumeSessionId) {
@@ -11919,30 +12585,78 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11919
12585
  this.reportedCostUsd = void 0;
11920
12586
  this.pendingResume = opts.resumeSessionId;
11921
12587
  }
12588
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, ORDEWELL_TASK_TOOLS));
11922
12589
  return { command: "claude", args };
11923
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
+ }
11924
12633
  /**
11925
12634
  * Claude Code's soft interrupt: the turn stops, the process and its session
11926
12635
  * stay. The CLI acknowledges on the control channel, then closes the turn
11927
12636
  * with an `error_during_execution` result, which {@link handleLine} reports
11928
12637
  * as an interrupted `turn_end`.
11929
12638
  */
11930
- interrupt(timeoutMs) {
11931
- if (!this.process) return Promise.resolve(false);
11932
- this.interruptCount += 1;
11933
- const requestId = `ordewell-interrupt-${this.interruptCount}`;
12639
+ async interrupt(timeoutMs) {
12640
+ if (!this.process) return false;
11934
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}`;
11935
12649
  return new Promise((resolve7) => {
11936
- const settle = (ok) => {
12650
+ const settle = (response) => {
11937
12651
  if (!this.pendingControl.delete(requestId)) return;
11938
12652
  clearTimeout(timer);
11939
- resolve7(ok);
12653
+ resolve7(response);
11940
12654
  };
11941
- const timer = setTimeout(() => settle(false), timeoutMs);
12655
+ const timer = setTimeout(() => settle(null), timeoutMs);
11942
12656
  timer.unref?.();
11943
12657
  this.pendingControl.set(requestId, settle);
11944
- void this.processEnded.then(() => settle(false));
11945
- 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 });
11946
12660
  });
11947
12661
  }
11948
12662
  /**
@@ -11977,6 +12691,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11977
12691
  }
11978
12692
  dispose() {
11979
12693
  this.clearFollowOnTimer();
12694
+ this.removeMcpConfig();
11980
12695
  super.dispose();
11981
12696
  }
11982
12697
  clearFollowOnTimer() {
@@ -12027,12 +12742,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12027
12742
  emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
12028
12743
  return;
12029
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
+ }
12030
12751
  const suggestions = msg.request.permission_suggestions ?? [];
12031
12752
  this.openPermissions.set(id, { input, suggestions });
12032
12753
  emit({
12033
12754
  type: "permission_request",
12034
12755
  id,
12035
- name: msg.request.tool_name ?? "unknown",
12756
+ name,
12036
12757
  detail: JSON.stringify(input),
12037
12758
  input,
12038
12759
  suggestions,
@@ -12048,7 +12769,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12048
12769
  }
12049
12770
  case "control_response": {
12050
12771
  const requestId = msg.response?.request_id;
12051
- if (requestId) this.pendingControl.get(requestId)?.(msg.response?.subtype === "success");
12772
+ if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
12052
12773
  return;
12053
12774
  }
12054
12775
  case "assistant":
@@ -12076,6 +12797,7 @@ ${block.text}` : block.text });
12076
12797
  if (block.type !== "tool_result") continue;
12077
12798
  const id = block.tool_use_id ?? "";
12078
12799
  const output = flattenContent(block.content);
12800
+ const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
12079
12801
  const subagent = subagentId ? void 0 : this.openSubagents.get(id);
12080
12802
  if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
12081
12803
  emit({ type: "background_agent", id });
@@ -12083,7 +12805,7 @@ ${block.text}` : block.text });
12083
12805
  } else if (subagent) {
12084
12806
  this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
12085
12807
  }
12086
- 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 });
12087
12809
  }
12088
12810
  return;
12089
12811
  case "system":
@@ -12219,6 +12941,9 @@ ${block.text}` : block.text });
12219
12941
  }
12220
12942
  };
12221
12943
 
12944
+ // src/services/harness/CodexAdapter.ts
12945
+ import { isAbsolute as isAbsolute4, relative as relative4, sep as sep2 } from "path";
12946
+
12222
12947
  // src/services/harness/codexSandbox.ts
12223
12948
  var PROBE_TIMEOUT_MS = 1e4;
12224
12949
  var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
@@ -12278,6 +13003,17 @@ function runProbe(deps, cwd, env, flags) {
12278
13003
 
12279
13004
  // src/services/harness/CodexAdapter.ts
12280
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
+ }
12281
13017
  function subagentOutcome(status) {
12282
13018
  switch (status) {
12283
13019
  case "completed":
@@ -12311,7 +13047,29 @@ var DECLINE_RESULTS = {
12311
13047
  execCommandApproval: { decision: "denied" },
12312
13048
  applyPatchApproval: { decision: "denied" }
12313
13049
  };
12314
- var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
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" };
12315
13073
  var TASK_APPROVALS = {
12316
13074
  "item/commandExecution/requestApproval": {
12317
13075
  name: "shell",
@@ -12385,11 +13143,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
12385
13143
  * names only the item, and a person deciding needs to see the files.
12386
13144
  */
12387
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();
12388
13148
  /** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
12389
13149
  pendingRequests = /* @__PURE__ */ new Map();
12390
13150
  resumeAttempted = false;
12391
13151
  resumeFallbackSent = false;
12392
13152
  sandbox = "default";
13153
+ /** The Ordewell server's startup state, as Codex last reported it for this thread. */
13154
+ mcpStartup = null;
13155
+ mcpStartupSettled = null;
12393
13156
  /**
12394
13157
  * Subagent threads this session has spawned, keyed by the child thread id.
12395
13158
  * Codex runs a subagent in its own thread and replays both threads' events on
@@ -12398,7 +13161,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
12398
13161
  subagents = /* @__PURE__ */ new Map();
12399
13162
  spawnSpec(opts) {
12400
13163
  this.startOpts = opts;
12401
- 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 } : {} };
12402
13167
  }
12403
13168
  /**
12404
13169
  * `initialize`, then `thread/start`, both before the first user message. A
@@ -12465,7 +13230,11 @@ ${this.exitMessage()}`);
12465
13230
  cwd: opts?.cwd,
12466
13231
  ...opts?.model ? { model: opts.model } : {}
12467
13232
  };
12468
- 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 } : {};
12469
13238
  if (opts?.kind === "task") {
12470
13239
  const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
12471
13240
  return {
@@ -12473,14 +13242,15 @@ ${this.exitMessage()}`);
12473
13242
  sandbox: opts.flags.permissionMode,
12474
13243
  ...approvalPolicy ? { approvalPolicy } : {},
12475
13244
  ...approvalsReviewer ? { approvalsReviewer } : {},
12476
- ...legacyLandlock
13245
+ ...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
13246
+ ...threadConfig
12477
13247
  };
12478
13248
  }
12479
13249
  return {
12480
13250
  ...common,
12481
13251
  sandbox: "read-only",
12482
13252
  approvalPolicy: "never",
12483
- ...legacyLandlock,
13253
+ ...threadConfig,
12484
13254
  // `developerInstructions` layers on top of Codex's own base prompt, the
12485
13255
  // way Claude Code's `--append-system-prompt` does. `baseInstructions`
12486
13256
  // replaces it — which takes Codex's description of its own tools with
@@ -12489,6 +13259,32 @@ ${this.exitMessage()}`);
12489
13259
  developerInstructions: opts?.systemPrompt
12490
13260
  };
12491
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
+ }
12492
13288
  effort() {
12493
13289
  const opts = this.startOpts;
12494
13290
  return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
@@ -12660,6 +13456,13 @@ ${this.exitMessage()}`);
12660
13456
  if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
12661
13457
  return;
12662
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
+ }
12663
13466
  case "thread/tokenUsage/updated":
12664
13467
  this.emitUsage(msg.params, emit);
12665
13468
  return;
@@ -12798,6 +13601,10 @@ ${this.exitMessage()}`);
12798
13601
  const method = msg.method;
12799
13602
  const params = msg.params ?? {};
12800
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
+ }
12801
13608
  if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
12802
13609
  const id = String(msg.id);
12803
13610
  const input = this.approvalInput(method, params);
@@ -12838,6 +13645,10 @@ ${this.exitMessage()}`);
12838
13645
  */
12839
13646
  answerServerRequest(msg, emit) {
12840
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
+ }
12841
13652
  const result = DECLINE_RESULTS[method];
12842
13653
  if (result) {
12843
13654
  this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
@@ -12864,7 +13675,7 @@ ${this.exitMessage()}`);
12864
13675
  emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
12865
13676
  return;
12866
13677
  case "mcpToolCall":
12867
- 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 } : {} });
12868
13679
  return;
12869
13680
  case "dynamicToolCall":
12870
13681
  emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
@@ -12876,7 +13687,7 @@ ${this.exitMessage()}`);
12876
13687
  const paths = (item.changes ?? []).map((change) => change.path).filter((path26) => !!path26);
12877
13688
  this.fileChangePaths.set(item.id, paths);
12878
13689
  if (this.startOpts?.kind === "task") {
12879
- 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);
12880
13691
  }
12881
13692
  return;
12882
13693
  }
@@ -12933,7 +13744,7 @@ ${item.text}` : item.text });
12933
13744
  emit({
12934
13745
  type: "tool_result",
12935
13746
  id,
12936
- name: item.tool ?? "tool",
13747
+ name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
12937
13748
  output: item.error ?? flattenText(item.result) ?? "",
12938
13749
  success: item.status !== "error" && item.success !== false && !item.error,
12939
13750
  ...subagentId ? { subagentId } : {}
@@ -12947,8 +13758,11 @@ ${item.text}` : item.text });
12947
13758
  case "fileChange":
12948
13759
  this.fileChangePaths.delete(id);
12949
13760
  if (this.startOpts?.kind === "task") {
12950
- const diff = (item.changes ?? []).map((change) => change.diff ?? "").join("\n");
12951
- 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);
12952
13766
  return;
12953
13767
  }
12954
13768
  emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
@@ -12957,6 +13771,34 @@ ${item.text}` : item.text });
12957
13771
  return;
12958
13772
  }
12959
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
+ }
12960
13802
  /**
12961
13803
  * A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
12962
13804
  * subagent. The call itself is planner-level tool activity; the lifecycle it
@@ -12996,7 +13838,45 @@ ${item.text}` : item.text });
12996
13838
  };
12997
13839
 
12998
13840
  // src/services/harness/OpenCodeAdapter.ts
12999
- 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
+ }
13000
13880
 
13001
13881
  // src/services/harness/OpenCodeV2.ts
13002
13882
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
@@ -13079,12 +13959,16 @@ var OpenCodeV2 = class {
13079
13959
  const created = await this.host.json("POST", "/api/session", {
13080
13960
  agent: this.agent,
13081
13961
  ...this.model ? { model: this.model } : {},
13082
- permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
13962
+ permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
13083
13963
  });
13084
13964
  if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
13085
13965
  this.sessionId = created.data.id;
13086
13966
  await this.applyInstructions();
13087
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
+ }
13088
13972
  /** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
13089
13973
  async applyInstructions() {
13090
13974
  if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
@@ -13222,8 +14106,8 @@ var OpenCodeV2 = class {
13222
14106
  });
13223
14107
  return true;
13224
14108
  }
13225
- async replyPermission(id, sessionId, reply) {
13226
- 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);
13227
14111
  }
13228
14112
  async openStream(state, turn, onEvent, onActivity) {
13229
14113
  const streamAbort = new AbortController();
@@ -13463,6 +14347,11 @@ var OpenCodeV2 = class {
13463
14347
  const request = this.permissionRequest(ask);
13464
14348
  if (!request || state.seen.has(`perm:${request.id}`)) return;
13465
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
+ }
13466
14355
  onEvent(request);
13467
14356
  void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
13468
14357
  });
@@ -13478,7 +14367,7 @@ var OpenCodeV2 = class {
13478
14367
  const request = this.permissionRequest(ask);
13479
14368
  if (!request || state.seen.has(`perm:${request.id}`)) return;
13480
14369
  state.seen.add(`perm:${request.id}`);
13481
- 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) {
13482
14371
  onEvent({ ...request, decided: { decision: "allow" } });
13483
14372
  void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
13484
14373
  });
@@ -13497,6 +14386,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
13497
14386
  var RECOVERY_TIMEOUT_MS = 9e5;
13498
14387
  var STATUS_POLL_INTERVAL_MS = 1e3;
13499
14388
  var SERVER_USERNAME = "opencode";
14389
+ var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
14390
+ var MCP_STATUS_POLL_MS2 = 200;
13500
14391
  var DISABLED_TOOLS = {
13501
14392
  question: false,
13502
14393
  edit: false,
@@ -13562,6 +14453,12 @@ function unwrapFileToolOutput(output) {
13562
14453
  const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
13563
14454
  return inner ? inner[1] : output;
13564
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
+ }
13565
14462
  function delay2(ms, signal) {
13566
14463
  return new Promise((resolve7) => {
13567
14464
  const timer = setTimeout(resolve7, ms);
@@ -13614,6 +14511,8 @@ var OpenCodeAdapter = class {
13614
14511
  openPermissions = /* @__PURE__ */ new Map();
13615
14512
  /** Set once the server turns out to speak the 2.x API, which then owns the session. */
13616
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;
13617
14516
  async start(opts) {
13618
14517
  if (opts.kind === "task") this.task = opts;
13619
14518
  else this.planner = opts;
@@ -13628,9 +14527,19 @@ var OpenCodeAdapter = class {
13628
14527
  throw new ExecutableNotFoundError("opencode", PATH);
13629
14528
  }
13630
14529
  const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
13631
- 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;
13632
14536
  this.process = this.deps.spawn(launch.file, launch.args, {
13633
- 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
+ }),
13634
14543
  stdio: ["pipe", "pipe", "pipe"],
13635
14544
  cwd: opts.cwd,
13636
14545
  windowsVerbatimArguments: launch.verbatim
@@ -13680,7 +14589,7 @@ ${this.stderrTail.trim()}` : ""}`);
13680
14589
  processEnded: this.processEnded,
13681
14590
  isExited: () => this.exited,
13682
14591
  exitMessage: () => this.exitMessage()
13683
- }, opts, this.role());
14592
+ }, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
13684
14593
  try {
13685
14594
  await this.v2.start();
13686
14595
  } catch (err) {
@@ -13733,12 +14642,12 @@ ${this.stderrTail.trim()}` : ""}`);
13733
14642
  ...this.planner?.effort ? { variant: this.planner.effort } : {},
13734
14643
  ...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
13735
14644
  };
13736
- 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);
13737
14646
  if (signal?.aborted) {
13738
14647
  this.dispose();
13739
14648
  return;
13740
14649
  }
13741
- this.settle(reply, turn, onEvent);
14650
+ this.settle(reply2, turn, onEvent);
13742
14651
  } catch (err) {
13743
14652
  if (signal?.aborted) {
13744
14653
  this.dispose();
@@ -13844,13 +14753,13 @@ ${this.stderrTail.trim()}` : ""}`);
13844
14753
  break;
13845
14754
  }
13846
14755
  }
13847
- for (const reply of messages.slice(start)) {
13848
- const id = reply.info?.id;
13849
- if (reply.info?.role !== "assistant" || !id) continue;
13850
- 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 ?? []) {
13851
14760
  if (part.messageID === id) this.emitPart(part, turn, onEvent);
13852
14761
  }
13853
- this.countUsage(reply.info, turn, onEvent);
14762
+ this.countUsage(reply2.info, turn, onEvent);
13854
14763
  }
13855
14764
  }
13856
14765
  /**
@@ -13960,18 +14869,18 @@ ${this.stderrTail.trim()}` : ""}`);
13960
14869
  * their text, reasoning and usage — reach Ordewell over the stream or not at
13961
14870
  * all.
13962
14871
  */
13963
- settle(reply, turn, onEvent) {
13964
- 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;
13965
14874
  if (failure) {
13966
14875
  onEvent({ type: "error", message: failure });
13967
14876
  return;
13968
14877
  }
13969
- const assistantId = reply?.info?.id;
13970
- for (const part of reply?.parts ?? []) {
14878
+ const assistantId = reply2?.info?.id;
14879
+ for (const part of reply2?.parts ?? []) {
13971
14880
  if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
13972
14881
  this.emitPart(part, turn, onEvent);
13973
14882
  }
13974
- if (reply?.info) this.countUsage(reply.info, turn, onEvent);
14883
+ if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
13975
14884
  if (assistantId) this.lastAssistantId = assistantId;
13976
14885
  onEvent({ type: "turn_end" });
13977
14886
  }
@@ -14048,7 +14957,7 @@ ${this.stderrTail.trim()}` : ""}`);
14048
14957
  type: "tool_result",
14049
14958
  id: callId,
14050
14959
  name,
14051
- output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
14960
+ output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
14052
14961
  success: status === "completed",
14053
14962
  subagentId
14054
14963
  });
@@ -14182,6 +15091,11 @@ ${this.stderrTail.trim()}` : ""}`);
14182
15091
  const id = ask.id;
14183
15092
  if (!id || seen.has(`perm:${id}`)) return;
14184
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
+ }
14185
15099
  onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
14186
15100
  void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
14187
15101
  });
@@ -14208,7 +15122,7 @@ ${this.stderrTail.trim()}` : ""}`);
14208
15122
  ...ask.always?.length ? { suggestions: ask.always } : {},
14209
15123
  ...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
14210
15124
  };
14211
- if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
15125
+ if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
14212
15126
  onEvent({ ...request, decided: { decision: "allow" } });
14213
15127
  void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
14214
15128
  });
@@ -14221,10 +15135,10 @@ ${this.stderrTail.trim()}` : ""}`);
14221
15135
  * `POST /permission/:id/reply` is the current answer. The per-session path
14222
15136
  * it replaced is tried only when a server too old to know the new one 404s.
14223
15137
  */
14224
- async replyPermission(id, sessionId, reply) {
14225
- 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);
14226
15140
  if (response.status !== 404) return;
14227
- await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply.reply });
15141
+ await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
14228
15142
  }
14229
15143
  /**
14230
15144
  * Open `/event` for one turn and wait until it is connected. The stream
@@ -14303,6 +15217,19 @@ ${this.stderrTail.trim()}` : ""}`);
14303
15217
  nativeSessionId() {
14304
15218
  return this.sessionId;
14305
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
+ }
14306
15233
  dispose() {
14307
15234
  if (this.disposed) return;
14308
15235
  this.disposed = true;
@@ -14325,9 +15252,13 @@ var TASK_MODE_ADAPTERS = {
14325
15252
  codex: (deps) => new CodexAdapter(deps),
14326
15253
  opencode: (deps) => new OpenCodeAdapter(deps)
14327
15254
  };
15255
+ var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
14328
15256
  function supportsTaskMode(runner) {
14329
15257
  return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
14330
15258
  }
15259
+ function takesOrdewellTools(runner) {
15260
+ return ORDEWELL_TOOL_RUNNERS.has(runner);
15261
+ }
14331
15262
  function createTaskAdapter(runner, deps) {
14332
15263
  if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
14333
15264
  return TASK_MODE_ADAPTERS[runner](deps);
@@ -14340,6 +15271,9 @@ function routeTransport(requested, runner, registry) {
14340
15271
  const name = registry?.get(runner)?.manifest.displayName ?? runner;
14341
15272
  return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
14342
15273
  }
15274
+ function givesCompletionTool(requested, runner, registry) {
15275
+ return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
15276
+ }
14343
15277
  var TransportRouter = class {
14344
15278
  constructor(runners) {
14345
15279
  this.runners = runners;
@@ -14462,6 +15396,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
14462
15396
  this.emit("onTaskSettled", { taskId });
14463
15397
  this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
14464
15398
  });
15399
+ this.verifier.onCheckpointWithdrawn((taskId) => {
15400
+ if (!this.atCheckpoint(taskId)) return;
15401
+ this.store.markInProgress(taskId);
15402
+ this.emit("onTaskChanged");
15403
+ });
14465
15404
  this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
14466
15405
  }
14467
15406
  /**
@@ -14548,7 +15487,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
14548
15487
  this.notifications.warn(message);
14549
15488
  };
14550
15489
  if (resolved.blockedEnvrc) {
14551
- 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.`);
14552
15491
  }
14553
15492
  if (resolved.trackedEnvFile) {
14554
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.`);
@@ -15379,17 +16318,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
15379
16318
  this.abandonSpawn(task, attempt);
15380
16319
  return false;
15381
16320
  }
15382
- 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, {
15383
16324
  planMapEnabled: this.config.planMapEnabled,
15384
16325
  // A merge to resolve is not new behaviour to drive test-first.
15385
16326
  tddEnabled: !attempt.repair && this.tddEnabled(),
15386
16327
  // An ops task's effects outlive a failed attempt and are never rolled
15387
16328
  // back, so the next one is told what the last one did (ADR-0020).
15388
- previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
16329
+ previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
16330
+ completionTool
15389
16331
  });
15390
16332
  this.lingering.close(task.id);
15391
16333
  const env = await this.envForTask(cwd);
15392
- const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
15393
16334
  const session = await this.terminalRunner.spawn({
15394
16335
  taskId: task.id,
15395
16336
  runner: attempt.runner,
@@ -15404,7 +16345,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
15404
16345
  title: task.title,
15405
16346
  env,
15406
16347
  transport,
15407
- resumeSessionId: attempt.continuation?.resumeSessionId
16348
+ resumeSessionId: attempt.continuation?.resumeSessionId,
16349
+ attempt: attempt.attempt
15408
16350
  });
15409
16351
  if (this.attempts.get(task.id) !== attempt) {
15410
16352
  this.abandonSpawn(task, attempt, session);
@@ -15702,52 +16644,6 @@ import { exec as exec3, spawn } from "child_process";
15702
16644
  import { promisify as promisify3 } from "util";
15703
16645
  import * as fsSync3 from "fs";
15704
16646
  import * as osMod from "os";
15705
-
15706
- // src/utils/globalDataDir.ts
15707
- import * as fs7 from "fs";
15708
- import * as path14 from "path";
15709
- import * as os5 from "os";
15710
- function globalDataDir() {
15711
- return path14.join(os5.homedir(), ".ordewell");
15712
- }
15713
- function copyDirSync(src, dest) {
15714
- fs7.mkdirSync(dest, { recursive: true });
15715
- const entries = fs7.readdirSync(src, { withFileTypes: true });
15716
- for (const entry of entries) {
15717
- const srcPath = path14.join(src, entry.name);
15718
- const destPath = path14.join(dest, entry.name);
15719
- if (entry.isDirectory()) {
15720
- copyDirSync(srcPath, destPath);
15721
- } else {
15722
- fs7.copyFileSync(srcPath, destPath);
15723
- }
15724
- }
15725
- }
15726
- var migrated = false;
15727
- function migrateOldConfigDir() {
15728
- if (migrated) return;
15729
- migrated = true;
15730
- const oldDir = path14.join(os5.homedir(), ".config", "ordewell");
15731
- if (!fs7.existsSync(oldDir)) return;
15732
- const newDir = globalDataDir();
15733
- const liftFile = (name) => {
15734
- const oldFile = path14.join(oldDir, name);
15735
- const newFile = path14.join(newDir, name);
15736
- if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
15737
- fs7.mkdirSync(newDir, { recursive: true });
15738
- fs7.copyFileSync(oldFile, newFile);
15739
- }
15740
- };
15741
- liftFile("settings.json");
15742
- liftFile(".env");
15743
- const oldPlugins = path14.join(oldDir, "plugins");
15744
- const newPlugins = path14.join(newDir, "plugins");
15745
- if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
15746
- copyDirSync(oldPlugins, newPlugins);
15747
- }
15748
- }
15749
-
15750
- // src/services/ModelDiscovery.ts
15751
16647
  var execAsync3 = promisify3(exec3);
15752
16648
  var defaultExec2 = async (command, options) => {
15753
16649
  const PATH = await augmentedPath();
@@ -16954,6 +17850,7 @@ var CliAgentAiService = class {
16954
17850
  };
16955
17851
  this.makeAdapter = deps.createAdapter ?? defaultAdapter;
16956
17852
  this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
17853
+ this.mcpServer = deps.mcpServer;
16957
17854
  }
16958
17855
  config;
16959
17856
  runner;
@@ -16968,6 +17865,11 @@ var CliAgentAiService = class {
16968
17865
  */
16969
17866
  lastNativeSessionId = null;
16970
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;
16971
17873
  activeAbort = null;
16972
17874
  /**
16973
17875
  * Every subagent this conversation has reported starting, and finishing.
@@ -16990,6 +17892,10 @@ var CliAgentAiService = class {
16990
17892
  if (!this.conversation) return true;
16991
17893
  return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
16992
17894
  }
17895
+ /** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
17896
+ plannerToolsAttached() {
17897
+ return this.toolsAttached;
17898
+ }
16993
17899
  /** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
16994
17900
  nativeSessionId() {
16995
17901
  return this.adapter?.nativeSessionId() ?? null;
@@ -16997,8 +17903,8 @@ var CliAgentAiService = class {
16997
17903
  reset() {
16998
17904
  this.activeAbort?.abort();
16999
17905
  this.activeAbort = null;
17000
- this.adapter?.dispose();
17001
- this.adapter = null;
17906
+ this.disposeAdapter();
17907
+ this.toolsAttached = false;
17002
17908
  this.lastNativeSessionId = null;
17003
17909
  this.conversation = null;
17004
17910
  this.subagents.started.clear();
@@ -17025,9 +17931,23 @@ var CliAgentAiService = class {
17025
17931
  model: this.plannerModel(),
17026
17932
  effort: this.config.plannerThinkingEffort
17027
17933
  };
17028
- 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);
17029
17948
  this.conversation = {
17030
17949
  startOptions,
17950
+ tools,
17031
17951
  runners: req.runners,
17032
17952
  runnerModes: req.runnerModes,
17033
17953
  autonomousDefault: req.autonomousDefault
@@ -17232,10 +18152,7 @@ var CliAgentAiService = class {
17232
18152
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
17233
18153
  }
17234
18154
  this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
17235
- if (error || signal?.aborted) {
17236
- adapter.dispose();
17237
- this.adapter = null;
17238
- }
18155
+ if (error || signal?.aborted) this.disposeAdapter();
17239
18156
  return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
17240
18157
  }
17241
18158
  // --- Process lifecycle ---
@@ -17244,13 +18161,50 @@ var CliAgentAiService = class {
17244
18161
  * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
17245
18162
  * mode from here without changing this signature.
17246
18163
  */
17247
- async startAdapter(opts) {
18164
+ async startAdapter(opts, tools) {
17248
18165
  const adapter = this.makeAdapter(this.runner, this.processDeps);
17249
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
+ }
17250
18172
  await adapter.start(opts);
17251
18173
  this.adapter = adapter;
17252
18174
  return adapter;
17253
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
+ }
17254
18208
  /**
17255
18209
  * The live agent process, restarted from its own session id if it died
17256
18210
  * between turns. Resume is a hint: when it fails, the caller's next
@@ -17261,7 +18215,7 @@ var CliAgentAiService = class {
17261
18215
  if (this.adapter) return this.adapter;
17262
18216
  const conversation = this.conversation;
17263
18217
  if (!conversation) throw new Error("No active planner conversation.");
17264
- await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
18218
+ await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
17265
18219
  return this.adapter;
17266
18220
  }
17267
18221
  plannerModel() {
@@ -17280,7 +18234,9 @@ var CliAgentAiService = class {
17280
18234
  const previous = this.adapter;
17281
18235
  const previousConversation = this.conversation;
17282
18236
  const previousSessionId = this.lastNativeSessionId;
18237
+ const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
17283
18238
  this.adapter = null;
18239
+ this.plannerToken = null;
17284
18240
  const startOptions = {
17285
18241
  kind: "planner",
17286
18242
  cwd: this.workspaceRoot(),
@@ -17310,6 +18266,8 @@ var CliAgentAiService = class {
17310
18266
  } finally {
17311
18267
  oneShotAdapter?.dispose();
17312
18268
  this.adapter = previous;
18269
+ this.plannerToken = previousTools.token;
18270
+ this.toolsAttached = previousTools.attached;
17313
18271
  this.conversation = previousConversation;
17314
18272
  this.lastNativeSessionId = previousSessionId;
17315
18273
  }
@@ -17405,6 +18363,173 @@ function createAiService(config, deps) {
17405
18363
  return new OpenAiService(config);
17406
18364
  }
17407
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
+
17408
18533
  // src/services/PlannerConversation.ts
17409
18534
  import { v4 as uuidv45 } from "uuid";
17410
18535
 
@@ -17421,8 +18546,8 @@ function summaryRequest(planLines) {
17421
18546
  ...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
17422
18547
  ].join("\n");
17423
18548
  }
17424
- function extractSummary(reply) {
17425
- 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"))];
17426
18551
  const summary = matches.at(-1)?.[1].trim();
17427
18552
  return summary || null;
17428
18553
  }
@@ -17441,6 +18566,7 @@ function keptTail(transcript) {
17441
18566
 
17442
18567
  // src/services/PlannerConversation.ts
17443
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.";
17444
18570
  var MAX_TASK_QUERIES = 3;
17445
18571
  var MAX_TASK_QUERIES_HARD = 6;
17446
18572
  function freshReadBudget() {
@@ -17470,7 +18596,7 @@ var PlannerConversation = class {
17470
18596
  savedInBackground = 0;
17471
18597
  turnsInFlight = 0;
17472
18598
  compacting = false;
17473
- openTurnId = null;
18599
+ openTurn = null;
17474
18600
  get transcript() {
17475
18601
  return this.host.plan()?.conversationHistory ?? [];
17476
18602
  }
@@ -17480,7 +18606,48 @@ var PlannerConversation = class {
17480
18606
  }
17481
18607
  /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
17482
18608
  get currentTurnId() {
17483
- 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 };
17484
18651
  }
17485
18652
  /** Whether the model still holds this conversation in memory. */
17486
18653
  get isActive() {
@@ -17768,15 +18935,16 @@ ${message}` : message;
17768
18935
  async userTurn(prompt, signal, run) {
17769
18936
  const turnId = uuidv45();
17770
18937
  const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
17771
- this.openTurnId = turnId;
18938
+ const turn = { stream, signal, reads: freshReadBudget() };
18939
+ this.openTurn = turn;
17772
18940
  this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
17773
18941
  let outcome = "error";
17774
18942
  try {
17775
- const settled = await this.inTurn(() => run({ stream, signal, reads: freshReadBudget() }));
18943
+ const settled = await this.inTurn(() => run(turn));
17776
18944
  outcome = settled.outcome;
17777
18945
  return settled.plan;
17778
18946
  } finally {
17779
- if (this.openTurnId === turnId) this.openTurnId = null;
18947
+ if (this.openTurn === turn) this.openTurn = null;
17780
18948
  this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
17781
18949
  }
17782
18950
  }
@@ -17827,10 +18995,11 @@ ${message}` : message;
17827
18995
  * ops JSON still gets its two corrective retries; charging it for the read
17828
18996
  * would cost it the chance to fix the edit.
17829
18997
  */
17830
- async drainTaskQueries(turn, { reads, signal, stream }) {
18998
+ async drainTaskQueries(turn, userTurn) {
18999
+ const { reads, signal, stream } = userTurn;
17831
19000
  const ai = this.host.aiService();
17832
19001
  const carried = [];
17833
- let current = turn;
19002
+ let current = this.claimSubmission(turn, userTurn);
17834
19003
  while (current.kind === "task_query") {
17835
19004
  carried.push(...current.researchLog);
17836
19005
  if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
@@ -17844,17 +19013,30 @@ ${message}` : message;
17844
19013
  const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
17845
19014
  reads.seen.add(signature);
17846
19015
  reads.answered++;
17847
- const answer = this.taskQueryAnswer(current.query);
17848
- current = await ai.continueConversation(
17849
- insist ? `${answer}
19016
+ const answer2 = this.taskQueryAnswer(current.query);
19017
+ current = this.claimSubmission(await ai.continueConversation(
19018
+ insist ? `${answer2}
17850
19019
 
17851
- ${TASK_QUERY_ANSWER_OR_OPS}` : answer,
19020
+ ${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
17852
19021
  stream.sink(),
17853
19022
  signal
17854
- );
19023
+ ), userTurn);
17855
19024
  }
17856
19025
  return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
17857
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
+ }
17858
19040
  /**
17859
19041
  * Render one read out of live state. Never persisted to the transcript: the
17860
19042
  * detail is context for the planner's next reply, and re-sending it on every
@@ -17898,7 +19080,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17898
19080
  return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
17899
19081
  },
17900
19082
  maxRepairs: 2,
17901
- onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
19083
+ onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
17902
19084
  });
17903
19085
  if ("plan" in settled) {
17904
19086
  await this.host.afterEdit();
@@ -17961,10 +19143,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17961
19143
  *
17962
19144
  * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
17963
19145
  * hard bound on every turn, and reads the plan's runners live, so a runner
17964
- * 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.
17965
19148
  */
17966
19149
  catalogBlock() {
17967
19150
  if (!this.host.plan()) return null;
19151
+ if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
17968
19152
  const { runners, models, modes, autonomousDefault } = this.host.catalog();
17969
19153
  const modelLines = runners.map((runner) => {
17970
19154
  const list = models[runner] ?? [];
@@ -17999,6 +19183,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17999
19183
  const tasks = this.host.tasks();
18000
19184
  const lines = this.currentPlanLines();
18001
19185
  if (!lines) return null;
19186
+ const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
18002
19187
  const locked = tasks.filter((t) => t.status === "in_progress");
18003
19188
  const execNote = this.host.hasLiveWork() ? `
18004
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.` : "";
@@ -18008,8 +19193,8 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
18008
19193
  "</current_plan>",
18009
19194
  "The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
18010
19195
  "- To answer a question or discuss, reply in plain prose (no JSON).",
18011
- TASK_QUERY_REMINDER,
18012
- ...taskOpsProtocol(execNote)
19196
+ tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
19197
+ ...taskOpsProtocol(execNote, tools)
18013
19198
  ].filter(Boolean).join("\n");
18014
19199
  }
18015
19200
  /** One line per task with stable references — null until the plan has tasks. */
@@ -18285,6 +19470,7 @@ function retargetTaskRunner(task, runner, catalog) {
18285
19470
  }
18286
19471
 
18287
19472
  // src/services/SkillsService.ts
19473
+ import { createHash } from "crypto";
18288
19474
  import * as fs9 from "fs";
18289
19475
  import * as path18 from "path";
18290
19476
 
@@ -18302,6 +19488,11 @@ function builtinSkillsDir() {
18302
19488
  // src/services/SkillsService.ts
18303
19489
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
18304
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";
18305
19496
  function parseSkillFile(filePath, name, source) {
18306
19497
  const raw = fs9.readFileSync(filePath, "utf8");
18307
19498
  const frontmatter = {};
@@ -18341,6 +19532,13 @@ function copyDirSync2(src, dest) {
18341
19532
  else fs9.copyFileSync(srcPath, destPath);
18342
19533
  }
18343
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
+ }
18344
19542
  function readDir(dir, source) {
18345
19543
  if (!fs9.existsSync(dir)) return [];
18346
19544
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
@@ -18365,21 +19563,59 @@ var SkillsService = class {
18365
19563
  globalDir() {
18366
19564
  return path18.join(globalDataDir(), "skills");
18367
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
+ }
18368
19594
  seed(name) {
18369
19595
  const dest = path18.join(this.globalDir(), name);
18370
- if (fs9.existsSync(dest)) return false;
19596
+ const exists = fs9.existsSync(dest);
18371
19597
  let srcDir;
18372
19598
  try {
18373
19599
  srcDir = path18.join(builtinSkillsDir(), name);
18374
19600
  } catch (err) {
18375
- 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)}`);
18376
19602
  return false;
18377
19603
  }
18378
19604
  if (!fs9.existsSync(srcDir)) {
18379
- console.warn(`Built-in skill not found in package: ${name}`);
19605
+ if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
18380
19606
  return false;
18381
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
+ }
18382
19617
  copyDirSync2(srcDir, dest);
19618
+ if (srcHash) this.recordSeed(name, srcHash);
18383
19619
  return true;
18384
19620
  }
18385
19621
  seedBuiltinSkill(name) {
@@ -18673,9 +19909,9 @@ import * as fs10 from "fs";
18673
19909
  import * as path19 from "path";
18674
19910
 
18675
19911
  // src/utils/sessionId.ts
18676
- import { randomBytes as randomBytes3 } from "crypto";
19912
+ import { randomBytes as randomBytes6 } from "crypto";
18677
19913
  function mintSessionId() {
18678
- return `session-${randomBytes3(16).toString("hex")}`;
19914
+ return `session-${randomBytes6(16).toString("hex")}`;
18679
19915
  }
18680
19916
 
18681
19917
  // src/utils/sessionStore.ts
@@ -19012,7 +20248,12 @@ function lastAttemptDigest(location, taskId) {
19012
20248
  return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
19013
20249
  }
19014
20250
  function sessionRuntimeSettings(settings) {
19015
- 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
+ };
19016
20257
  }
19017
20258
  var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
19018
20259
  function resolveSkillInvocation(text, skillsService) {
@@ -19036,14 +20277,14 @@ function resolveSkillInvocation(text, skillsService) {
19036
20277
  return `${lead}${skill.content}${punct}`;
19037
20278
  });
19038
20279
  }
19039
- function liveAiService(config, workspaceRoot) {
20280
+ function liveAiService(config, workspaceRoot, mcpServer) {
19040
20281
  let live = null;
19041
20282
  let liveProvider = null;
19042
20283
  return () => {
19043
20284
  const provider = config.aiProvider;
19044
20285
  if (live && liveProvider === provider) return live;
19045
20286
  live?.reset();
19046
- live = createAiService(config, { workspaceRoot });
20287
+ live = createAiService(config, { workspaceRoot, mcpServer });
19047
20288
  liveProvider = provider;
19048
20289
  return live;
19049
20290
  };
@@ -19053,7 +20294,7 @@ function isPlannerApproval(request) {
19053
20294
  }
19054
20295
  function createSession(deps) {
19055
20296
  const pinnedAiService = deps.aiService;
19056
- const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
20297
+ const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
19057
20298
  const store = new PlanStore();
19058
20299
  const taskLogs = new TaskLogRecorder({
19059
20300
  broadcast: deps.broadcast,
@@ -19183,6 +20424,16 @@ var Session = class {
19183
20424
  currentSessionId;
19184
20425
  skillsService;
19185
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
+ });
19186
20437
  constructor(parts) {
19187
20438
  this.config = parts.config;
19188
20439
  this.registry = parts.registry;
@@ -19211,17 +20462,7 @@ var Session = class {
19211
20462
  aiService: () => this.aiService(),
19212
20463
  onProgress: (p) => this.events.progress(p),
19213
20464
  opening: (runners) => this.conversationOpening(runners),
19214
- catalog: () => {
19215
- const runners = this.plan?.runners ?? [];
19216
- return {
19217
- runners,
19218
- // Allowlist-filtered: neither the per-turn block nor a read may offer
19219
- // a model the planner is forbidden to assign.
19220
- models: filterModelsForPrompt(this.models(), this.allowlist()),
19221
- modes: this.runnerModesFor(runners),
19222
- autonomousDefault: this.config.autonomousMode
19223
- };
19224
- },
20465
+ catalog: () => this.catalogOf(this.plan?.runners ?? []),
19225
20466
  tasks: () => this.store.planTasks,
19226
20467
  liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
19227
20468
  hasLiveWork: () => this.hasLiveWork,
@@ -19245,8 +20486,8 @@ var Session = class {
19245
20486
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
19246
20487
  * path is identical regardless of who is looking.
19247
20488
  */
19248
- resolveApproval(id, answer) {
19249
- return this.approvals.resolve(id, answer);
20489
+ resolveApproval(id, answer2) {
20490
+ return this.approvals.resolve(id, answer2);
19250
20491
  }
19251
20492
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
19252
20493
  outstandingApprovals() {
@@ -19366,6 +20607,27 @@ var Session = class {
19366
20607
  this.modelsCache = {};
19367
20608
  this.remintSessionId();
19368
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
+ }
19369
20631
  runnerModesFor(runners) {
19370
20632
  return runnerModesFrom(this.registry, runners);
19371
20633
  }
@@ -19377,13 +20639,17 @@ var Session = class {
19377
20639
  * allowlist here — {@link checkModelAndModeValidity} narrows by
19378
20640
  * allowlist itself, the same way `coerceAssignments` does.
19379
20641
  */
19380
- editCatalog() {
20642
+ editCatalog(runners = this.plan?.runners ?? []) {
19381
20643
  return {
19382
20644
  modelsByRunner: this.models(),
19383
- runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
20645
+ runnerModes: this.runnerModesFor(runners),
19384
20646
  perRunnerAllowlist: this.allowlist()
19385
20647
  };
19386
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
+ }
19387
20653
  /**
19388
20654
  * The allowlist in force right now. Unset means no restriction — falling
19389
20655
  * back to the one planning started under would keep a restriction the user
@@ -19485,7 +20751,7 @@ var Session = class {
19485
20751
  this.goal = goal;
19486
20752
  this.remintSessionId();
19487
20753
  this.beginFreshPlan();
19488
- const enabled = this.config.enabledRunners;
20754
+ const enabled = this.enabledRunners();
19489
20755
  const chosenRunners = runners.filter((r) => enabled.includes(r));
19490
20756
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
19491
20757
  const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
@@ -19529,7 +20795,7 @@ var Session = class {
19529
20795
  this.goal = this.resolveSkillInvocation(goal);
19530
20796
  this.remintSessionId();
19531
20797
  this.beginFreshPlan();
19532
- const enabled = this.config.enabledRunners;
20798
+ const enabled = this.enabledRunners();
19533
20799
  const chosenRunners = runners.filter((r) => enabled.includes(r));
19534
20800
  if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
19535
20801
  const opening = await this.conversationOpening(chosenRunners);
@@ -19607,6 +20873,49 @@ var Session = class {
19607
20873
  rewindTargets() {
19608
20874
  return this.conversation.rewindTargets();
19609
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
+ }
19610
20919
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
19611
20920
  get isConversationActive() {
19612
20921
  return this.conversation.isActive;
@@ -19652,7 +20961,8 @@ var Session = class {
19652
20961
  // usage line can show context fill (#49). Unknown stays absent.
19653
20962
  contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
19654
20963
  fs: this.fsAdapter,
19655
- fetcher: this.fetcher
20964
+ fetcher: this.fetcher,
20965
+ plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
19656
20966
  };
19657
20967
  }
19658
20968
  /**
@@ -19770,7 +21080,7 @@ var Session = class {
19770
21080
  ])
19771
21081
  );
19772
21082
  try {
19773
- const modelsByRunner = await this.modelResolver.modelsForRunners(this.config.enabledRunners);
21083
+ const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
19774
21084
  const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
19775
21085
  const { modelAllowlist } = this.settingsFn();
19776
21086
  const result = await this.planner.modifyDuringExecution({
@@ -20117,7 +21427,7 @@ var Session = class {
20117
21427
  if (!this.plan) throw new Error("No active plan state");
20118
21428
  const check = canMergeTasks(this.store.planTasks, taskIds);
20119
21429
  if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
20120
- const prompt = buildMergePrompt(taskIds, this.store.planTasks);
21430
+ const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
20121
21431
  return this.continueConversation(prompt, options);
20122
21432
  }
20123
21433
  /**
@@ -20129,7 +21439,7 @@ var Session = class {
20129
21439
  if (!this.plan) throw new Error("No active plan state");
20130
21440
  const check = canSplitTask(this.store.planTasks, taskId);
20131
21441
  if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
20132
- const prompt = buildSplitPrompt(taskId, this.store.planTasks);
21442
+ const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
20133
21443
  return this.continueConversation(prompt, options);
20134
21444
  }
20135
21445
  loadPlan(plan, goal, workspace, opts) {
@@ -20169,41 +21479,6 @@ var Session = class {
20169
21479
  }
20170
21480
  };
20171
21481
 
20172
- // src/utils/privateFile.ts
20173
- import { closeSync, mkdirSync as mkdirSync10, openSync, renameSync, unlinkSync as unlinkSync2, writeSync } from "fs";
20174
- import { basename as basename2, dirname as dirname7, join as join18 } from "path";
20175
- import { randomBytes as randomBytes4 } from "crypto";
20176
- var DIR_MODE = 448;
20177
- var FILE_MODE = 384;
20178
- var POSIX_MODES = process.platform !== "win32";
20179
- function ensurePrivateDir(dir) {
20180
- if (POSIX_MODES) {
20181
- mkdirSync10(dir, { recursive: true, mode: DIR_MODE });
20182
- return;
20183
- }
20184
- mkdirSync10(dir, { recursive: true });
20185
- }
20186
- function writePrivateFile(filePath, content) {
20187
- const dir = dirname7(filePath);
20188
- ensurePrivateDir(dir);
20189
- const tempPath = join18(dir, `.${basename2(filePath)}.${randomBytes4(8).toString("hex")}.tmp`);
20190
- const fd = openSync(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
20191
- try {
20192
- writeSync(fd, content);
20193
- } finally {
20194
- closeSync(fd);
20195
- }
20196
- try {
20197
- renameSync(tempPath, filePath);
20198
- } catch (err) {
20199
- try {
20200
- unlinkSync2(tempPath);
20201
- } catch {
20202
- }
20203
- throw err;
20204
- }
20205
- }
20206
-
20207
21482
  // src/services/buildRunnerArgs.ts
20208
21483
  function buildRunnerInvocation(opts) {
20209
21484
  const entry = opts.registry.get(opts.runner);
@@ -20460,9 +21735,9 @@ var HeadlessRunner = class extends AbstractRunner {
20460
21735
  // src/services/TmuxRunner.ts
20461
21736
  import { execFile as execFile5 } from "child_process";
20462
21737
  import { promisify as promisify5 } from "util";
20463
- import { tmpdir } from "os";
20464
- import { join as join19 } from "path";
20465
- 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";
20466
21741
 
20467
21742
  // src/utils/tmux.ts
20468
21743
  import { execSync as execSync2 } from "child_process";
@@ -20560,7 +21835,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20560
21835
  * lets the user keep poking at a finished task's terminal.
20561
21836
  */
20562
21837
  async start(command, args, cwd, env) {
20563
- writeFileSync7(this.logPath, "");
21838
+ writeFileSync9(this.logPath, "");
20564
21839
  const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
20565
21840
  const inner = [command, ...args].map(posixShellQuote).join(" ");
20566
21841
  const prefix = envAssignments ? `env ${envAssignments} ` : "";
@@ -20583,7 +21858,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20583
21858
  readLog() {
20584
21859
  let content;
20585
21860
  try {
20586
- content = existsSync11(this.logPath) ? readFileSync9(this.logPath, "utf8") : "";
21861
+ content = existsSync11(this.logPath) ? readFileSync10(this.logPath, "utf8") : "";
20587
21862
  } catch {
20588
21863
  return false;
20589
21864
  }
@@ -20639,7 +21914,7 @@ var TmuxSession = class extends AbstractTerminalSession {
20639
21914
  this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
20640
21915
  });
20641
21916
  try {
20642
- unlinkSync3(this.logPath);
21917
+ unlinkSync4(this.logPath);
20643
21918
  } catch {
20644
21919
  }
20645
21920
  this.baseHandleExit(code);
@@ -20676,7 +21951,7 @@ var TmuxRunner = class extends AbstractRunner {
20676
21951
  this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
20677
21952
  this.resolvePath = deps.resolvePath ?? augmentedPath;
20678
21953
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
20679
- this.logDir = deps.logDir ?? tmpdir();
21954
+ this.logDir = deps.logDir ?? tmpdir2();
20680
21955
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
20681
21956
  this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
20682
21957
  }
@@ -20796,7 +22071,7 @@ var TmuxRunner = class extends AbstractRunner {
20796
22071
  windowName,
20797
22072
  this.execFileImpl,
20798
22073
  this.pollIntervalMs,
20799
- join19(this.logDir, `${id}.log`),
22074
+ join21(this.logDir, `${id}.log`),
20800
22075
  this.socket
20801
22076
  );
20802
22077
  invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
@@ -20824,9 +22099,9 @@ var KEY_ARGS = {
20824
22099
  };
20825
22100
  var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
20826
22101
  function toolLine(name, args) {
20827
- const { tool } = mapAgentTool(name);
20828
- const normalized = normalizeAgentArgs(tool, args);
20829
- const preferred = KEY_ARGS[tool];
22102
+ const { tool: tool2 } = mapAgentTool(name);
22103
+ const normalized = normalizeAgentArgs(tool2, args);
22104
+ const preferred = KEY_ARGS[tool2];
20830
22105
  for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
20831
22106
  const value = normalized[key];
20832
22107
  if (typeof value !== "string") continue;
@@ -20889,6 +22164,7 @@ var StructuredSession = class extends AbstractTerminalSession {
20889
22164
  constructor(id, taskId, launch) {
20890
22165
  super(id, taskId);
20891
22166
  this.launch = launch;
22167
+ this.startOptions = launch.startOptions;
20892
22168
  this.text = new PlainTextChannel((text) => {
20893
22169
  this.output += text;
20894
22170
  this.outputEmitter.emit("output", text);
@@ -20917,9 +22193,20 @@ var StructuredSession = class extends AbstractTerminalSession {
20917
22193
  * requests — while an approval must be answerable by id across the session.
20918
22194
  */
20919
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;
20920
22201
  /** Start the runner and send the task's prompt as its first turn. */
20921
22202
  async start(prompt) {
20922
- 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
+ }
20923
22210
  this.state = "working";
20924
22211
  setImmediate(() => {
20925
22212
  if (!this.exited) this.deliver(prompt);
@@ -20928,6 +22215,49 @@ var StructuredSession = class extends AbstractTerminalSession {
20928
22215
  getOutput() {
20929
22216
  return this.output;
20930
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
+ }
20931
22261
  /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
20932
22262
  write(text) {
20933
22263
  const message = text.trim();
@@ -21025,12 +22355,12 @@ var StructuredSession = class extends AbstractTerminalSession {
21025
22355
  * session id after an abort.
21026
22356
  */
21027
22357
  async restartInterrupted(turn) {
21028
- const resumeSessionId = this.nativeSessionId() ?? this.launch.startOptions.resumeSessionId;
22358
+ const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
21029
22359
  this.generation += 1;
21030
22360
  turn.abort.abort();
21031
22361
  if (this.adapter) this.withdrawPermissions(this.adapter);
21032
22362
  try {
21033
- await this.startAdapter({ ...this.launch.startOptions, resumeSessionId });
22363
+ await this.startAdapter({ ...this.startOptions, resumeSessionId });
21034
22364
  } catch (err) {
21035
22365
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
21036
22366
  this.endTurn(turn, "interrupted");
@@ -21168,11 +22498,13 @@ var StructuredRunner = class extends AbstractRunner {
21168
22498
  processDeps;
21169
22499
  createAdapter;
21170
22500
  interruptGraceMs;
22501
+ mcp;
21171
22502
  constructor(deps = {}) {
21172
22503
  super();
21173
22504
  this.processDeps = deps.process ?? {};
21174
22505
  this.createAdapter = deps.createAdapter ?? createTaskAdapter;
21175
22506
  this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
22507
+ this.mcp = deps.mcp ?? sharedMcpServer();
21176
22508
  }
21177
22509
  async spawn(opts) {
21178
22510
  const manifest = opts.registry?.get(opts.runner)?.manifest;
@@ -21199,7 +22531,8 @@ var StructuredRunner = class extends AbstractRunner {
21199
22531
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
21200
22532
  resumeSessionId: opts.resumeSessionId
21201
22533
  },
21202
- 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 } } } : {}
21203
22536
  });
21204
22537
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
21205
22538
  this.registerSession(id, session);
@@ -21213,73 +22546,12 @@ var StructuredRunner = class extends AbstractRunner {
21213
22546
  }
21214
22547
  };
21215
22548
 
21216
- // src/utils/daemonToken.ts
21217
- import { randomBytes as randomBytes5, timingSafeEqual } from "crypto";
21218
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync4 } from "fs";
21219
- import { join as join20 } from "path";
21220
- var BEARER_PREFIX = "bearer ";
21221
- var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
21222
- var DAEMON_SUBPROTOCOL = "ordewell.v1";
21223
- function configDir() {
21224
- return globalDataDir();
21225
- }
21226
- function daemonTokenPath(port) {
21227
- return join20(configDir(), `server-${port}.token`);
21228
- }
21229
- function mintDaemonToken(port) {
21230
- const token = randomBytes5(32).toString("base64url");
21231
- const file = daemonTokenPath(port);
21232
- writePrivateFile(file, token);
21233
- return { token, file };
21234
- }
21235
- function readDaemonToken(port) {
21236
- try {
21237
- const token = readFileSync10(daemonTokenPath(port), "utf8").trim();
21238
- return token === "" ? void 0 : token;
21239
- } catch {
21240
- return void 0;
21241
- }
21242
- }
21243
- function clearDaemonToken(port) {
21244
- try {
21245
- unlinkSync4(daemonTokenPath(port));
21246
- } catch {
21247
- }
21248
- }
21249
- function bearerHeaderValue(token) {
21250
- return `Bearer ${token}`;
21251
- }
21252
- function tokenSubprotocols(token) {
21253
- return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
21254
- }
21255
- function extractPresentedToken(carriers) {
21256
- const authorization = carriers.authorization;
21257
- if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
21258
- const value = authorization.slice(BEARER_PREFIX.length).trim();
21259
- if (value !== "") return value;
21260
- }
21261
- for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
21262
- const value = offered.trim();
21263
- if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
21264
- const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
21265
- if (token !== "") return token;
21266
- }
21267
- return void 0;
21268
- }
21269
- function tokensMatch(presented, expected) {
21270
- if (presented === void 0) return false;
21271
- const a = Buffer.from(presented, "utf8");
21272
- const b = Buffer.from(expected, "utf8");
21273
- if (a.length !== b.length) return false;
21274
- return timingSafeEqual(a, b);
21275
- }
21276
-
21277
22549
  // src/plugins/RunnerRegistry.ts
21278
22550
  import * as fs13 from "fs";
21279
22551
  import * as path23 from "path";
21280
22552
  import * as os6 from "os";
21281
22553
  import { execFileSync } from "child_process";
21282
- import { randomBytes as randomBytes6 } from "crypto";
22554
+ import { randomBytes as randomBytes7 } from "crypto";
21283
22555
 
21284
22556
  // src/plugins/FsPluginStore.ts
21285
22557
  import * as fs12 from "fs";
@@ -21303,8 +22575,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
21303
22575
  assertPlainPluginName(name);
21304
22576
  const root = path21.resolve(pluginsDir);
21305
22577
  const dest = path21.resolve(root, name);
21306
- const relative5 = path21.relative(root, dest);
21307
- if (relative5 !== name || relative5 === "" || path21.isAbsolute(relative5)) {
22578
+ const relative6 = path21.relative(root, dest);
22579
+ if (relative6 !== name || relative6 === "" || path21.isAbsolute(relative6)) {
21308
22580
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
21309
22581
  }
21310
22582
  return dest;
@@ -21918,7 +23190,7 @@ var RunnerRegistry = class {
21918
23190
  }
21919
23191
  installFromGit(url) {
21920
23192
  const validated = assertInstallablePluginUrl(url);
21921
- 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")}`);
21922
23194
  this.store.ensureDir(tmpDir);
21923
23195
  try {
21924
23196
  try {
@@ -22337,11 +23609,15 @@ export {
22337
23609
  NO_TURN,
22338
23610
  OPENCODE_MANIFEST,
22339
23611
  ORCHESTRATOR_SHORTCUTS,
23612
+ ORDEWELL_MCP_PATH,
23613
+ ORDEWELL_MCP_SERVER_NAME,
22340
23614
  ORDEWELL_SETTABLE_ENV,
22341
23615
  OUTPUT_LINES_DEFAULT,
22342
23616
  OUTPUT_LINES_MAX,
22343
23617
  OpenAiService,
22344
23618
  OpenCodeAdapter,
23619
+ OrdewellMcpServer,
23620
+ PLANNER_TOOLS,
22345
23621
  PLAN_ENVELOPE_KEY,
22346
23622
  PLUGIN_NAME_PATTERN,
22347
23623
  PROVIDER_CREDENTIAL_ENV,
@@ -22378,6 +23654,7 @@ export {
22378
23654
  TASK_QUERY_FIELDS,
22379
23655
  TASK_QUERY_PROTOCOL,
22380
23656
  TASK_QUERY_REMINDER,
23657
+ TASK_TOOLS,
22381
23658
  TRUNCATED_PLAN_REPAIR_INSTRUCTION,
22382
23659
  TaskControlError,
22383
23660
  TaskLogRecorder,
@@ -22461,10 +23738,14 @@ export {
22461
23738
  daemonTokenPath,
22462
23739
  defaultLogger,
22463
23740
  definitionPattern,
23741
+ defuseMarkers,
22464
23742
  deleteSession,
22465
23743
  dependencyCandidates,
22466
23744
  dependentsOf,
22467
23745
  describeMergeResult,
23746
+ diffRows,
23747
+ diffStat,
23748
+ diffSummary,
22468
23749
  digestTaskLog,
22469
23750
  discoverGeminiModels,
22470
23751
  drainNext,
@@ -22527,6 +23808,7 @@ export {
22527
23808
  loadState,
22528
23809
  looksLikePlanAttempt,
22529
23810
  mapAgentTool,
23811
+ mcpClientConfig,
22530
23812
  migrateLegacyPlan,
22531
23813
  migrateOldConfigDir,
22532
23814
  migratePlanIsolation,
@@ -22542,6 +23824,7 @@ export {
22542
23824
  opensWithJsonObject,
22543
23825
  outputLines,
22544
23826
  outputPreview,
23827
+ ownerOnlyConfigFile,
22545
23828
  parseAutonomyLevel,
22546
23829
  parseMaxParallel,
22547
23830
  parsePartialPlan,
@@ -22600,6 +23883,7 @@ export {
22600
23883
  serializeTaskStatus,
22601
23884
  sessionDataDir,
22602
23885
  sessionRuntimeSettings,
23886
+ sharedMcpServer,
22603
23887
  stateExists,
22604
23888
  stopTurn,
22605
23889
  stripAnsi,
@@ -22633,6 +23917,7 @@ export {
22633
23917
  usageLine,
22634
23918
  validateModifiedPlan,
22635
23919
  validatePlanModification,
23920
+ validatePlanTasks,
22636
23921
  warningsText,
22637
23922
  wellKnownBinDirs,
22638
23923
  windowsCommandLine,