@ordewell/core 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{IFileSystem-DQEKyC6H.d.ts → IFileSystem-D4ADfTrX.d.ts} +1 -1
- package/dist/{IFileSystem-Qj2MWHn_.d.mts → IFileSystem-DgpX0BU7.d.mts} +1 -1
- package/dist/{ModeResolver-ByNpC0EP.d.ts → ModeResolver-BEcRAz1c.d.ts} +3 -3
- package/dist/{ModeResolver-rqaY_4Ex.d.mts → ModeResolver-Cv01ZDd9.d.mts} +3 -3
- package/dist/{Task-xqdxugt2.d.mts → Task-CoKhvBa1.d.mts} +275 -2
- package/dist/{Task-xqdxugt2.d.ts → Task-CoKhvBa1.d.ts} +275 -2
- package/dist/{chunk-M4KXBEFJ.mjs → chunk-J3YSTPCE.mjs} +41 -41
- package/dist/chunk-J3YSTPCE.mjs.map +1 -0
- package/dist/{chunk-I6U3PVHL.mjs → chunk-LSGY7AIZ.mjs} +74 -23
- package/dist/chunk-LSGY7AIZ.mjs.map +1 -0
- package/dist/{chunk-JRZPSNRO.mjs → chunk-YU4UKKKI.mjs} +1 -1
- package/dist/chunk-YU4UKKKI.mjs.map +1 -0
- package/dist/index.d.mts +250 -34
- package/dist/index.d.ts +250 -34
- package/dist/index.js +2002 -670
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1642 -366
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +2 -1
- package/dist/order-labels.d.ts +2 -1
- package/dist/{parsing-DDZ1rUYa.d.mts → parsing-B4OCHH1_.d.mts} +24 -3
- package/dist/{parsing-COPG8-Ep.d.ts → parsing-DMdOq-BV.d.ts} +24 -3
- package/dist/parsing.d.mts +4 -3
- package/dist/parsing.d.ts +4 -3
- package/dist/parsing.js +36 -38
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-Br8UAoiu.d.ts → plan-utils-BdYPRBno.d.ts} +28 -3
- package/dist/{plan-utils-DMgajMvd.d.mts → plan-utils-CD1Xu65s.d.mts} +28 -3
- package/dist/plan-utils.d.mts +4 -3
- package/dist/plan-utils.d.ts +4 -3
- package/dist/plan-utils.js +111 -58
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +8 -2
- package/dist/testing.d.mts +11 -2
- package/dist/testing.d.ts +11 -2
- package/dist/testing.js +17 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +17 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +4 -2
- package/dist/chunk-I6U3PVHL.mjs.map +0 -1
- package/dist/chunk-JRZPSNRO.mjs.map +0 -1
- package/dist/chunk-M4KXBEFJ.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -79,11 +79,15 @@ __export(index_exports, {
|
|
|
79
79
|
NO_TURN: () => NO_TURN,
|
|
80
80
|
OPENCODE_MANIFEST: () => OPENCODE_MANIFEST,
|
|
81
81
|
ORCHESTRATOR_SHORTCUTS: () => ORCHESTRATOR_SHORTCUTS,
|
|
82
|
+
ORDEWELL_MCP_PATH: () => ORDEWELL_MCP_PATH,
|
|
83
|
+
ORDEWELL_MCP_SERVER_NAME: () => ORDEWELL_MCP_SERVER_NAME,
|
|
82
84
|
ORDEWELL_SETTABLE_ENV: () => ORDEWELL_SETTABLE_ENV,
|
|
83
85
|
OUTPUT_LINES_DEFAULT: () => OUTPUT_LINES_DEFAULT,
|
|
84
86
|
OUTPUT_LINES_MAX: () => OUTPUT_LINES_MAX,
|
|
85
87
|
OpenAiService: () => OpenAiService,
|
|
86
88
|
OpenCodeAdapter: () => OpenCodeAdapter,
|
|
89
|
+
OrdewellMcpServer: () => OrdewellMcpServer,
|
|
90
|
+
PLANNER_TOOLS: () => PLANNER_TOOLS,
|
|
87
91
|
PLAN_ENVELOPE_KEY: () => PLAN_ENVELOPE_KEY,
|
|
88
92
|
PLUGIN_NAME_PATTERN: () => PLUGIN_NAME_PATTERN,
|
|
89
93
|
PROVIDER_CREDENTIAL_ENV: () => PROVIDER_CREDENTIAL_ENV,
|
|
@@ -120,6 +124,7 @@ __export(index_exports, {
|
|
|
120
124
|
TASK_QUERY_FIELDS: () => TASK_QUERY_FIELDS,
|
|
121
125
|
TASK_QUERY_PROTOCOL: () => TASK_QUERY_PROTOCOL,
|
|
122
126
|
TASK_QUERY_REMINDER: () => TASK_QUERY_REMINDER,
|
|
127
|
+
TASK_TOOLS: () => TASK_TOOLS,
|
|
123
128
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION: () => TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
124
129
|
TaskControlError: () => TaskControlError,
|
|
125
130
|
TaskLogRecorder: () => TaskLogRecorder,
|
|
@@ -203,10 +208,14 @@ __export(index_exports, {
|
|
|
203
208
|
daemonTokenPath: () => daemonTokenPath,
|
|
204
209
|
defaultLogger: () => defaultLogger,
|
|
205
210
|
definitionPattern: () => definitionPattern,
|
|
211
|
+
defuseMarkers: () => defuseMarkers,
|
|
206
212
|
deleteSession: () => deleteSession,
|
|
207
213
|
dependencyCandidates: () => dependencyCandidates,
|
|
208
214
|
dependentsOf: () => dependentsOf,
|
|
209
215
|
describeMergeResult: () => describeMergeResult,
|
|
216
|
+
diffRows: () => diffRows,
|
|
217
|
+
diffStat: () => diffStat,
|
|
218
|
+
diffSummary: () => diffSummary,
|
|
210
219
|
digestTaskLog: () => digestTaskLog,
|
|
211
220
|
discoverGeminiModels: () => discoverGeminiModels,
|
|
212
221
|
drainNext: () => drainNext,
|
|
@@ -269,6 +278,7 @@ __export(index_exports, {
|
|
|
269
278
|
loadState: () => loadState,
|
|
270
279
|
looksLikePlanAttempt: () => looksLikePlanAttempt,
|
|
271
280
|
mapAgentTool: () => mapAgentTool,
|
|
281
|
+
mcpClientConfig: () => mcpClientConfig,
|
|
272
282
|
migrateLegacyPlan: () => migrateLegacyPlan,
|
|
273
283
|
migrateOldConfigDir: () => migrateOldConfigDir,
|
|
274
284
|
migratePlanIsolation: () => migratePlanIsolation,
|
|
@@ -284,6 +294,7 @@ __export(index_exports, {
|
|
|
284
294
|
opensWithJsonObject: () => opensWithJsonObject,
|
|
285
295
|
outputLines: () => outputLines,
|
|
286
296
|
outputPreview: () => outputPreview,
|
|
297
|
+
ownerOnlyConfigFile: () => ownerOnlyConfigFile,
|
|
287
298
|
parseAutonomyLevel: () => parseAutonomyLevel,
|
|
288
299
|
parseMaxParallel: () => parseMaxParallel,
|
|
289
300
|
parsePartialPlan: () => parsePartialPlan,
|
|
@@ -342,6 +353,7 @@ __export(index_exports, {
|
|
|
342
353
|
serializeTaskStatus: () => serializeTaskStatus,
|
|
343
354
|
sessionDataDir: () => sessionDataDir,
|
|
344
355
|
sessionRuntimeSettings: () => sessionRuntimeSettings,
|
|
356
|
+
sharedMcpServer: () => sharedMcpServer,
|
|
345
357
|
stateExists: () => stateExists,
|
|
346
358
|
stopTurn: () => stopTurn,
|
|
347
359
|
stripAnsi: () => stripAnsi,
|
|
@@ -375,6 +387,7 @@ __export(index_exports, {
|
|
|
375
387
|
usageLine: () => usageLine,
|
|
376
388
|
validateModifiedPlan: () => validateModifiedPlan,
|
|
377
389
|
validatePlanModification: () => validatePlanModification,
|
|
390
|
+
validatePlanTasks: () => validatePlanTasks,
|
|
378
391
|
warningsText: () => warningsText,
|
|
379
392
|
wellKnownBinDirs: () => wellKnownBinDirs,
|
|
380
393
|
windowsCommandLine: () => windowsCommandLine,
|
|
@@ -709,13 +722,13 @@ function validateModifiedPlan(original, modified) {
|
|
|
709
722
|
return warnings;
|
|
710
723
|
}
|
|
711
724
|
function warningsText(w) {
|
|
712
|
-
const
|
|
713
|
-
if (w.deletedCompleted.length)
|
|
714
|
-
if (w.changedCompleted.length)
|
|
715
|
-
if (w.deletedInProgress.length)
|
|
716
|
-
if (w.modifiedInProgress.length)
|
|
717
|
-
if (w.brokenDependencies.length)
|
|
718
|
-
return
|
|
725
|
+
const lines2 = [];
|
|
726
|
+
if (w.deletedCompleted.length) lines2.push(`Completed tasks deleted: ${w.deletedCompleted.join(", ")}`);
|
|
727
|
+
if (w.changedCompleted.length) lines2.push(`Completed tasks modified: ${w.changedCompleted.join(", ")}`);
|
|
728
|
+
if (w.deletedInProgress.length) lines2.push(`In-progress tasks deleted: ${w.deletedInProgress.join(", ")}`);
|
|
729
|
+
if (w.modifiedInProgress.length) lines2.push(`In-progress tasks modified: ${w.modifiedInProgress.join(", ")}`);
|
|
730
|
+
if (w.brokenDependencies.length) lines2.push(`Broken dependencies: ${w.brokenDependencies.join(", ")}`);
|
|
731
|
+
return lines2.length > 0 ? lines2.join("\n") : null;
|
|
719
732
|
}
|
|
720
733
|
|
|
721
734
|
// src/models/Usage.ts
|
|
@@ -775,14 +788,14 @@ function capLine(line) {
|
|
|
775
788
|
return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
776
789
|
}
|
|
777
790
|
function trimToolOutput(output) {
|
|
778
|
-
const
|
|
779
|
-
const long =
|
|
780
|
-
if (
|
|
781
|
-
const omittedLines =
|
|
791
|
+
const lines2 = output.split("\n");
|
|
792
|
+
const long = lines2.some((l) => l.length > MAX_LINE_CHARS);
|
|
793
|
+
if (lines2.length <= HEAD_LINES + TAIL_LINES) return long ? { output: lines2.map(capLine).join("\n") } : { output };
|
|
794
|
+
const omittedLines = lines2.length - HEAD_LINES - TAIL_LINES;
|
|
782
795
|
const kept = [
|
|
783
|
-
...
|
|
796
|
+
...lines2.slice(0, HEAD_LINES),
|
|
784
797
|
`\u2026 ${omittedLines} line${omittedLines === 1 ? "" : "s"} omitted \u2026`,
|
|
785
|
-
...
|
|
798
|
+
...lines2.slice(-TAIL_LINES)
|
|
786
799
|
];
|
|
787
800
|
return { output: kept.map(capLine).join("\n"), omittedLines };
|
|
788
801
|
}
|
|
@@ -862,15 +875,15 @@ function digestTaskLog(events, maxCalls) {
|
|
|
862
875
|
else if (event.type === "text") lastText = event.text;
|
|
863
876
|
else if (event.type === "error") lastError = event.message;
|
|
864
877
|
}
|
|
865
|
-
const
|
|
878
|
+
const lines2 = [];
|
|
866
879
|
for (const event of events) {
|
|
867
880
|
if (event.type !== "tool_call") continue;
|
|
868
881
|
const outcome = outcomes.get(event.id);
|
|
869
882
|
const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
|
|
870
|
-
|
|
883
|
+
lines2.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
|
|
871
884
|
}
|
|
872
|
-
const omitted =
|
|
873
|
-
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...
|
|
885
|
+
const omitted = lines2.length - maxCalls;
|
|
886
|
+
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines2.slice(omitted)] : lines2;
|
|
874
887
|
const gap = () => out.length > 0 ? [""] : [];
|
|
875
888
|
const said2 = lastText?.trim();
|
|
876
889
|
if (said2) {
|
|
@@ -908,9 +921,9 @@ var SEARCH_EXCLUSIONS = [
|
|
|
908
921
|
];
|
|
909
922
|
|
|
910
923
|
// src/interfaces/IApproval.ts
|
|
911
|
-
function toApprovalDecision(
|
|
912
|
-
if (typeof
|
|
913
|
-
return
|
|
924
|
+
function toApprovalDecision(answer2) {
|
|
925
|
+
if (typeof answer2 === "boolean") return { decision: answer2 ? "allow" : "deny" };
|
|
926
|
+
return answer2;
|
|
914
927
|
}
|
|
915
928
|
function isGranted(decision) {
|
|
916
929
|
return decision.decision !== "deny";
|
|
@@ -4456,12 +4469,12 @@ var ApprovalPolicy = class {
|
|
|
4456
4469
|
if (this.generation === gen) this.inFlight.delete(openKey);
|
|
4457
4470
|
});
|
|
4458
4471
|
this.inFlight.set(openKey, pending);
|
|
4459
|
-
const
|
|
4472
|
+
const answer2 = await pending;
|
|
4460
4473
|
if (this.generation === gen) {
|
|
4461
|
-
if (
|
|
4474
|
+
if (answer2) open.forEach((s) => this.granted.add(s));
|
|
4462
4475
|
else this.refused.add(key);
|
|
4463
4476
|
}
|
|
4464
|
-
return decide(
|
|
4477
|
+
return decide(answer2, "asked");
|
|
4465
4478
|
}
|
|
4466
4479
|
/** Scopes the user has granted this session — for display and for persistence. */
|
|
4467
4480
|
grantedScopes() {
|
|
@@ -4522,10 +4535,10 @@ var PendingApprovals = class {
|
|
|
4522
4535
|
* settled. "Allow for this task" on a request that did not offer it is a
|
|
4523
4536
|
* plain allow: no grant is made that the requester never proposed.
|
|
4524
4537
|
*/
|
|
4525
|
-
resolve(id,
|
|
4538
|
+
resolve(id, answer2) {
|
|
4526
4539
|
const entry = this.entries.get(id);
|
|
4527
4540
|
if (!entry) return false;
|
|
4528
|
-
const decision = toApprovalDecision(
|
|
4541
|
+
const decision = toApprovalDecision(answer2);
|
|
4529
4542
|
entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
|
|
4530
4543
|
return true;
|
|
4531
4544
|
}
|
|
@@ -4570,6 +4583,10 @@ var KNOWN_TOOLS = {
|
|
|
4570
4583
|
list: "list_dir",
|
|
4571
4584
|
fetch: "fetch"
|
|
4572
4585
|
};
|
|
4586
|
+
var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "patch", "add", "update", "delete", "filechange"]);
|
|
4587
|
+
function isFileEditTool(name) {
|
|
4588
|
+
return FILE_EDIT_TOOLS.has(name.trim().toLowerCase().replace(/[_-]/g, ""));
|
|
4589
|
+
}
|
|
4573
4590
|
function mapAgentTool(name) {
|
|
4574
4591
|
const normalized = name.trim().toLowerCase().replace(/[_-]/g, "");
|
|
4575
4592
|
const direct = KNOWN_TOOLS[normalized] ?? KNOWN_TOOLS[name.trim().toLowerCase()];
|
|
@@ -4578,25 +4595,25 @@ function mapAgentTool(name) {
|
|
|
4578
4595
|
}
|
|
4579
4596
|
return { tool: "agent_tool", toolLabel: name };
|
|
4580
4597
|
}
|
|
4581
|
-
function normalizeAgentArgs(
|
|
4598
|
+
function normalizeAgentArgs(tool2, args) {
|
|
4582
4599
|
const out = { ...args };
|
|
4583
4600
|
const alias = (from, to) => {
|
|
4584
4601
|
if (out[to] === void 0 && out[from] !== void 0) out[to] = out[from];
|
|
4585
4602
|
};
|
|
4586
|
-
if (
|
|
4603
|
+
if (tool2 === "read_file" || tool2 === "list_dir") {
|
|
4587
4604
|
alias("filePath", "path");
|
|
4588
4605
|
alias("file_path", "path");
|
|
4589
4606
|
alias("target", "path");
|
|
4590
4607
|
}
|
|
4591
|
-
if (
|
|
4608
|
+
if (tool2 === "bash") {
|
|
4592
4609
|
alias("cmd", "command");
|
|
4593
4610
|
alias("script", "command");
|
|
4594
4611
|
if (typeof out.command !== "string" && Array.isArray(out.command)) {
|
|
4595
4612
|
out.command = out.command.join(" ");
|
|
4596
4613
|
}
|
|
4597
4614
|
}
|
|
4598
|
-
if (
|
|
4599
|
-
if (
|
|
4615
|
+
if (tool2 === "fetch") alias("uri", "url");
|
|
4616
|
+
if (tool2 === "web_search") alias("q", "query");
|
|
4600
4617
|
return out;
|
|
4601
4618
|
}
|
|
4602
4619
|
|
|
@@ -4765,13 +4782,13 @@ function renderTerminalOutput(raw) {
|
|
|
4765
4782
|
function renderCleanCapture(raw, doneToken) {
|
|
4766
4783
|
const rendered = renderTerminalOutput(raw).replace(/[\s─-▟]+$/gm, "");
|
|
4767
4784
|
if (!doneToken) return rendered.trim();
|
|
4768
|
-
const
|
|
4785
|
+
const lines2 = rendered.split("\n");
|
|
4769
4786
|
const flat = (s) => flattenTerminalOutput(s);
|
|
4770
|
-
for (let i = 0; i <
|
|
4771
|
-
const own = flat(
|
|
4772
|
-
if (own) return
|
|
4773
|
-
const window = flat(
|
|
4774
|
-
if (window.includes(doneToken)) return
|
|
4787
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
4788
|
+
const own = flat(lines2[i]).includes(doneToken);
|
|
4789
|
+
if (own) return lines2.slice(0, i).join("\n").trim();
|
|
4790
|
+
const window = flat(lines2[i]) + (i + 1 < lines2.length ? flat(lines2[i + 1]) : "");
|
|
4791
|
+
if (window.includes(doneToken)) return lines2.slice(0, i + 1).join("\n").trim();
|
|
4775
4792
|
}
|
|
4776
4793
|
return rendered.trim();
|
|
4777
4794
|
}
|
|
@@ -4802,9 +4819,9 @@ function oneLine(text2) {
|
|
|
4802
4819
|
return text2.replace(/\s+/g, " ").trim();
|
|
4803
4820
|
}
|
|
4804
4821
|
function firstLineOf(command) {
|
|
4805
|
-
const
|
|
4806
|
-
const more =
|
|
4807
|
-
return more > 0 ? `${
|
|
4822
|
+
const lines2 = command.trim().split(/\r?\n/);
|
|
4823
|
+
const more = lines2.length - 1;
|
|
4824
|
+
return more > 0 ? `${lines2[0]} \u2026 +${more} line${more === 1 ? "" : "s"}` : lines2[0];
|
|
4808
4825
|
}
|
|
4809
4826
|
function text(value) {
|
|
4810
4827
|
return typeof value === "string" ? value : "";
|
|
@@ -4820,8 +4837,8 @@ function hintArg(args) {
|
|
|
4820
4837
|
const firstText = Object.values(args).find((v) => typeof v === "string" && v.trim() !== "");
|
|
4821
4838
|
return firstText ? oneLine(firstText) : "";
|
|
4822
4839
|
}
|
|
4823
|
-
function keyArgOf(
|
|
4824
|
-
switch (
|
|
4840
|
+
function keyArgOf(tool2, args) {
|
|
4841
|
+
switch (tool2) {
|
|
4825
4842
|
case "bash":
|
|
4826
4843
|
return firstLineOf(text(args.command));
|
|
4827
4844
|
case "read_file":
|
|
@@ -4845,20 +4862,62 @@ function keyArgOf(tool, args) {
|
|
|
4845
4862
|
return hintArg(args);
|
|
4846
4863
|
}
|
|
4847
4864
|
}
|
|
4848
|
-
function toolHeadline(
|
|
4849
|
-
const name = toolLabel?.trim() || DISPLAY_NAMES[
|
|
4865
|
+
function toolHeadline(tool2, args, toolLabel) {
|
|
4866
|
+
const name = toolLabel?.trim() || DISPLAY_NAMES[tool2] || tool2;
|
|
4850
4867
|
const parsed = parseArgs(args);
|
|
4851
|
-
return { name, keyArg: parsed ? keyArgOf(
|
|
4868
|
+
return { name, keyArg: parsed ? keyArgOf(tool2, parsed) : oneLine(args) };
|
|
4852
4869
|
}
|
|
4853
4870
|
function outputLines(output) {
|
|
4854
|
-
const
|
|
4855
|
-
while (
|
|
4856
|
-
return
|
|
4871
|
+
const lines2 = output.replace(/\r\n?/g, "\n").replace(ANSI_OR_CTRL_RE, "").split("\n");
|
|
4872
|
+
while (lines2.length > 0 && lines2[lines2.length - 1].trim() === "") lines2.pop();
|
|
4873
|
+
return lines2;
|
|
4857
4874
|
}
|
|
4858
4875
|
function outputPreview(output, maxLines) {
|
|
4859
|
-
const
|
|
4860
|
-
const shown =
|
|
4861
|
-
return { lines: shown, hiddenLineCount:
|
|
4876
|
+
const lines2 = outputLines(output);
|
|
4877
|
+
const shown = lines2.slice(0, Math.max(0, maxLines));
|
|
4878
|
+
return { lines: shown, hiddenLineCount: lines2.length - shown.length };
|
|
4879
|
+
}
|
|
4880
|
+
function diffStat(output) {
|
|
4881
|
+
const lines2 = outputLines(output);
|
|
4882
|
+
let added = 0;
|
|
4883
|
+
let removed = 0;
|
|
4884
|
+
for (const line of lines2) {
|
|
4885
|
+
if (line.startsWith("+")) added += 1;
|
|
4886
|
+
else if (line.startsWith("-")) removed += 1;
|
|
4887
|
+
else if (!(line.startsWith("@@") || line.startsWith(" ") || line === "" || line.startsWith("\\"))) return null;
|
|
4888
|
+
}
|
|
4889
|
+
return added + removed > 0 ? { added, removed } : null;
|
|
4890
|
+
}
|
|
4891
|
+
var HUNK_RANGE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
4892
|
+
function diffRows(output) {
|
|
4893
|
+
const rows = [];
|
|
4894
|
+
let oldLine = 1;
|
|
4895
|
+
let newLine = 1;
|
|
4896
|
+
for (const line of outputLines(output)) {
|
|
4897
|
+
const range2 = HUNK_RANGE.exec(line);
|
|
4898
|
+
if (range2) {
|
|
4899
|
+
if (rows.length > 0) rows.push({ kind: "gap", text: "" });
|
|
4900
|
+
oldLine = Number(range2[1]);
|
|
4901
|
+
newLine = Number(range2[2]);
|
|
4902
|
+
} else if (line.startsWith("+")) {
|
|
4903
|
+
rows.push({ kind: "added", line: newLine++, text: line.slice(1) });
|
|
4904
|
+
} else if (line.startsWith("-")) {
|
|
4905
|
+
rows.push({ kind: "removed", line: oldLine++, text: line.slice(1) });
|
|
4906
|
+
} else if (!line.startsWith("\\")) {
|
|
4907
|
+
rows.push({ kind: "context", line: newLine, text: line.slice(1) });
|
|
4908
|
+
oldLine += 1;
|
|
4909
|
+
newLine += 1;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
return rows;
|
|
4913
|
+
}
|
|
4914
|
+
function lines(count) {
|
|
4915
|
+
return `${count} line${count === 1 ? "" : "s"}`;
|
|
4916
|
+
}
|
|
4917
|
+
function diffSummary({ added, removed }) {
|
|
4918
|
+
if (!removed) return `Added ${lines(added)}`;
|
|
4919
|
+
if (!added) return `Removed ${lines(removed)}`;
|
|
4920
|
+
return `Added ${lines(added)}, removed ${lines(removed)}`;
|
|
4862
4921
|
}
|
|
4863
4922
|
|
|
4864
4923
|
// src/conversation/records.ts
|
|
@@ -4887,17 +4946,18 @@ var STATUS_OF_OUTCOME = {
|
|
|
4887
4946
|
not_executed: "interrupted"
|
|
4888
4947
|
};
|
|
4889
4948
|
function finishedTool(call, output, outcome) {
|
|
4890
|
-
|
|
4949
|
+
const diff = outcome === "success" && isFileEditTool(call.toolLabel ?? call.tool) ? diffStat(output) : null;
|
|
4950
|
+
return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output, outputLineCount: outputLines(output).length, ...diff ? { diff } : {} };
|
|
4891
4951
|
}
|
|
4892
4952
|
function settledTool(call, step) {
|
|
4893
4953
|
return finishedTool(call, step.result, step.outcome);
|
|
4894
4954
|
}
|
|
4895
|
-
function pendingTool(id, { tool, toolLabel, args, toolCallId, turnId }) {
|
|
4955
|
+
function pendingTool(id, { tool: tool2, toolLabel, args, toolCallId, turnId }) {
|
|
4896
4956
|
return {
|
|
4897
4957
|
type: "tool",
|
|
4898
4958
|
id,
|
|
4899
|
-
tool,
|
|
4900
|
-
headline: toolHeadline(
|
|
4959
|
+
tool: tool2,
|
|
4960
|
+
headline: toolHeadline(tool2, args, toolLabel),
|
|
4901
4961
|
args,
|
|
4902
4962
|
status: "pending",
|
|
4903
4963
|
output: "",
|
|
@@ -5087,12 +5147,12 @@ function thinkingWhole(view, { text: text2, subagentId }) {
|
|
|
5087
5147
|
return streamWhole(placed, lane, thinkingStream(subagentId), text2, (id) => thinkingBlock(id, blockText(text2), false, subagentId));
|
|
5088
5148
|
}
|
|
5089
5149
|
function announceTool(view, { id: toolCallId, name, args, subagentId }) {
|
|
5090
|
-
const { tool, toolLabel } = mapAgentTool(name);
|
|
5150
|
+
const { tool: tool2, toolLabel } = mapAgentTool(name);
|
|
5091
5151
|
const [placed, lane] = laneOf(view, subagentId);
|
|
5092
5152
|
return appendOutput(placed, lane, (id) => {
|
|
5093
|
-
const call = pendingTool(id, { tool, toolLabel, args, toolCallId });
|
|
5153
|
+
const call = pendingTool(id, { tool: tool2, toolLabel, args, toolCallId });
|
|
5094
5154
|
const parsed = parseArgs2(args);
|
|
5095
|
-
return parsed ? { ...call, headline: toolHeadline(
|
|
5155
|
+
return parsed ? { ...call, headline: toolHeadline(tool2, JSON.stringify(normalizeAgentArgs(tool2, parsed)), toolLabel) } : call;
|
|
5096
5156
|
});
|
|
5097
5157
|
}
|
|
5098
5158
|
function parseArgs2(args) {
|
|
@@ -5132,21 +5192,21 @@ function reportUsage(view, { record }) {
|
|
|
5132
5192
|
const next = { ...view, usage };
|
|
5133
5193
|
return isMeasured(usage.totals) ? setUsageLine(next, usageLine(usage)) : next;
|
|
5134
5194
|
}
|
|
5135
|
-
function runnerToolSubject(
|
|
5136
|
-
const { tool: mapped, toolLabel } = mapAgentTool(
|
|
5195
|
+
function runnerToolSubject(tool2, args) {
|
|
5196
|
+
const { tool: mapped, toolLabel } = mapAgentTool(tool2);
|
|
5137
5197
|
const parsed = parseArgs2(args);
|
|
5138
5198
|
const headline = toolHeadline(mapped, parsed ? JSON.stringify(normalizeAgentArgs(mapped, parsed)) : args, toolLabel);
|
|
5139
5199
|
return headline.keyArg ? `${headline.name}(${headline.keyArg})` : headline.name;
|
|
5140
5200
|
}
|
|
5141
|
-
function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId }) {
|
|
5201
|
+
function requestApproval(view, { approvalId, tool: tool2, args, allowForTask, toolCallId }) {
|
|
5142
5202
|
if (view.blocks.some((b) => b.type === "approval" && b.approvalId === approvalId)) return view;
|
|
5143
5203
|
return append(view, (id) => ({
|
|
5144
5204
|
type: "approval",
|
|
5145
5205
|
id,
|
|
5146
5206
|
approvalId,
|
|
5147
5207
|
kind: "runner_tool",
|
|
5148
|
-
subject: runnerToolSubject(
|
|
5149
|
-
scope:
|
|
5208
|
+
subject: runnerToolSubject(tool2, args),
|
|
5209
|
+
scope: tool2,
|
|
5150
5210
|
status: "pending",
|
|
5151
5211
|
...allowForTask ? { allowForTask } : {},
|
|
5152
5212
|
...toolCallId ? { toolCallId } : {}
|
|
@@ -5440,9 +5500,9 @@ function applyHeadLimit(stdout, headLimit) {
|
|
|
5440
5500
|
}
|
|
5441
5501
|
function formatSearchOutput(capped, root, opts) {
|
|
5442
5502
|
if (capped.rows.length === 0) return opts.emptyMessage;
|
|
5443
|
-
const
|
|
5444
|
-
const prefix = root ===
|
|
5445
|
-
const body = prefix ===
|
|
5503
|
+
const sep3 = opts.sep ?? import_node_path.default.sep;
|
|
5504
|
+
const prefix = root === sep3 || root.endsWith(sep3) ? root : `${root}${sep3}`;
|
|
5505
|
+
const body = prefix === sep3 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
|
|
5446
5506
|
if (!capped.truncated) return body;
|
|
5447
5507
|
const hint = opts.hint ? ` ${opts.hint}` : "";
|
|
5448
5508
|
return `${body}
|
|
@@ -6492,28 +6552,28 @@ function posixBinDirs() {
|
|
|
6492
6552
|
];
|
|
6493
6553
|
}
|
|
6494
6554
|
function windowsBinDirs(env, home) {
|
|
6495
|
-
const
|
|
6496
|
-
const under = (base, ...parts) => base ? [
|
|
6555
|
+
const join27 = (...parts) => path5.win32.join(...parts);
|
|
6556
|
+
const under = (base, ...parts) => base ? [join27(base, ...parts)] : [];
|
|
6497
6557
|
const appData = env.APPDATA;
|
|
6498
6558
|
const localAppData = env.LOCALAPPDATA;
|
|
6499
6559
|
const programData = env.ProgramData;
|
|
6500
6560
|
return [
|
|
6501
6561
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6562
|
+
join27(home, ".local", "bin"),
|
|
6563
|
+
join27(home, ".opencode", "bin"),
|
|
6564
|
+
join27(home, ".claude", "bin"),
|
|
6565
|
+
join27(home, ".codex", "bin"),
|
|
6506
6566
|
...under(localAppData, "Programs", "claude"),
|
|
6507
6567
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
6508
6568
|
// literal spelling is kept as a fallback for a host that does not export
|
|
6509
6569
|
// APPDATA (a service account, a stripped CI container).
|
|
6510
6570
|
...under(appData, "npm"),
|
|
6511
|
-
|
|
6571
|
+
join27(home, "AppData", "Roaming", "npm"),
|
|
6512
6572
|
...under(localAppData, "pnpm"),
|
|
6513
6573
|
...under(localAppData, "Yarn", "bin"),
|
|
6514
|
-
|
|
6574
|
+
join27(home, ".bun", "bin"),
|
|
6515
6575
|
// Windows package managers.
|
|
6516
|
-
|
|
6576
|
+
join27(home, "scoop", "shims"),
|
|
6517
6577
|
...under(env.SCOOP, "shims"),
|
|
6518
6578
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
6519
6579
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -6523,7 +6583,7 @@ function windowsBinDirs(env, home) {
|
|
|
6523
6583
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
6524
6584
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
6525
6585
|
...under(localAppData, "Volta", "bin"),
|
|
6526
|
-
|
|
6586
|
+
join27(home, ".volta", "bin")
|
|
6527
6587
|
];
|
|
6528
6588
|
}
|
|
6529
6589
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -7011,18 +7071,18 @@ var GitWorktreeIsolation = class {
|
|
|
7011
7071
|
*/
|
|
7012
7072
|
sharedPaths(workspaceRoot, isolated) {
|
|
7013
7073
|
if (isolated.includes(SELF_REPO)) return [];
|
|
7014
|
-
const
|
|
7074
|
+
const shared2 = [];
|
|
7015
7075
|
const walk = (rel) => {
|
|
7016
7076
|
for (const name of listDir(path9.join(workspaceRoot, rel))) {
|
|
7017
7077
|
if (!rel && (name === STATE_DIR || name === ".git")) continue;
|
|
7018
7078
|
const child = rel ? `${rel}/${name}` : name;
|
|
7019
7079
|
if (isolated.includes(child)) continue;
|
|
7020
7080
|
if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
|
|
7021
|
-
else if (resolves(path9.join(workspaceRoot, child)))
|
|
7081
|
+
else if (resolves(path9.join(workspaceRoot, child))) shared2.push(child);
|
|
7022
7082
|
}
|
|
7023
7083
|
};
|
|
7024
7084
|
walk("");
|
|
7025
|
-
return
|
|
7085
|
+
return shared2;
|
|
7026
7086
|
}
|
|
7027
7087
|
prepare(task, run) {
|
|
7028
7088
|
return this.admin(run.workspaceRoot, async () => {
|
|
@@ -7836,13 +7896,14 @@ function filteredBuildModes(modes, autonomousDefault) {
|
|
|
7836
7896
|
return buildModes;
|
|
7837
7897
|
}
|
|
7838
7898
|
function autonomyLevelLabel(autonomous) {
|
|
7839
|
-
return autonomous ? "Full
|
|
7899
|
+
return autonomous ? "Full" : "Guarded";
|
|
7840
7900
|
}
|
|
7841
7901
|
function parseAutonomyLevel(arg) {
|
|
7842
7902
|
switch (arg?.trim().toLowerCase()) {
|
|
7843
7903
|
case "full":
|
|
7844
7904
|
case "on":
|
|
7845
7905
|
return true;
|
|
7906
|
+
case "guarded":
|
|
7846
7907
|
case "auto":
|
|
7847
7908
|
case "off":
|
|
7848
7909
|
return false;
|
|
@@ -7851,7 +7912,7 @@ function parseAutonomyLevel(arg) {
|
|
|
7851
7912
|
}
|
|
7852
7913
|
}
|
|
7853
7914
|
function buildModeGuide(runnerModes, autonomousDefault = true) {
|
|
7854
|
-
const
|
|
7915
|
+
const lines2 = [];
|
|
7855
7916
|
for (const [runnerId, modes] of Object.entries(runnerModes)) {
|
|
7856
7917
|
if (!modes || modes.length === 0) continue;
|
|
7857
7918
|
const filtered = filteredBuildModes(modes, autonomousDefault);
|
|
@@ -7860,13 +7921,13 @@ function buildModeGuide(runnerModes, autonomousDefault = true) {
|
|
|
7860
7921
|
const tag = isDefaultMode(m, autonomousDefault) ? " (DEFAULT)" : "";
|
|
7861
7922
|
return `${m.id}${tag} (${m.description})`;
|
|
7862
7923
|
}).join(", ");
|
|
7863
|
-
|
|
7924
|
+
lines2.push(`- ${runnerId}: ${modeStr}`);
|
|
7864
7925
|
}
|
|
7865
|
-
if (
|
|
7926
|
+
if (lines2.length === 0) return "";
|
|
7866
7927
|
return [
|
|
7867
7928
|
`Autonomy level: ${autonomyLevelLabel(autonomousDefault)}.`,
|
|
7868
7929
|
"AVAILABLE MODES PER RUNNER:",
|
|
7869
|
-
...
|
|
7930
|
+
...lines2,
|
|
7870
7931
|
"",
|
|
7871
7932
|
'For each AI task, use the mode marked (DEFAULT) unless the task needs a different compatible mode listed above. Avoid the "plan" mode (read-only) unless the user explicitly requested analysis-only.'
|
|
7872
7933
|
].join("\n");
|
|
@@ -8107,10 +8168,12 @@ var TASK_QUERY_FIELDS = [
|
|
|
8107
8168
|
"userStoriesCovered",
|
|
8108
8169
|
"output"
|
|
8109
8170
|
];
|
|
8171
|
+
var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
|
|
8110
8172
|
var OUTPUT_LINES_DEFAULT = 80;
|
|
8111
8173
|
var OUTPUT_LINES_MAX = 400;
|
|
8112
8174
|
var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
|
|
8113
8175
|
var ANSWER_TAIL_RESERVE = 4e3;
|
|
8176
|
+
var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
|
|
8114
8177
|
function textHasTaskQuery(text2) {
|
|
8115
8178
|
return text2.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
8116
8179
|
}
|
|
@@ -8186,6 +8249,7 @@ function taskQuerySignature(query) {
|
|
|
8186
8249
|
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
|
|
8187
8250
|
}
|
|
8188
8251
|
var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
|
|
8252
|
+
var TASK_READ_ANSWER_OR_EDIT = "You have now read everything you asked for. Do not read again this turn: answer the user in prose, or call edit_plan for the change you came to make.";
|
|
8189
8253
|
var TASK_QUERY_PROTOCOL = [
|
|
8190
8254
|
"READING A TASK BEFORE YOU EDIT IT:",
|
|
8191
8255
|
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
|
|
@@ -8197,7 +8261,16 @@ var TASK_QUERY_PROTOCOL = [
|
|
|
8197
8261
|
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
8198
8262
|
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
8199
8263
|
];
|
|
8264
|
+
var TASK_READ_TOOLS_PROTOCOL = [
|
|
8265
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
8266
|
+
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. Read with these tools; they change nothing:",
|
|
8267
|
+
'- task_query: "tasks" are one or more task references ("<id or #order>"). Ask for everything you need in ONE call. "fields" is optional (omit it to get description, prompt, userSteps, verdict, outputSummary and userStoriesCovered). "catalog": true also returns every runner with its models, thinking-effort variants and modes.',
|
|
8268
|
+
`- task_output: the recent output of a task that is running right now \u2014 the way to diagnose a task that looks stuck mid-execution. "lines" (default 80, at most 400) sets how many; pass an answer's "nextOffset" back as "since" to read only what follows. A task that is not running answers with its verdict, output summary and a digest of its last attempt.`,
|
|
8269
|
+
"Three reads per user message; after that every answer also tells you to land the turn, and after six they are refused. Do not ask the same question twice.",
|
|
8270
|
+
"Once a plan exists, change it with edit_plan \u2014 every turn shows you its operations. Never reply with taskOps or taskQuery JSON."
|
|
8271
|
+
];
|
|
8200
8272
|
var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
|
|
8273
|
+
var TASK_READ_TOOLS_REMINDER = "- To READ a task in full (prompt, user steps, verdict, user stories), a running task's output, or the whole model/mode catalog before editing, call task_query or task_output \u2014 they change nothing \u2014 and then call edit_plan with your ops.";
|
|
8201
8274
|
function findTask(tasks, ref) {
|
|
8202
8275
|
const byId = tasks.find((t) => t.id === ref);
|
|
8203
8276
|
if (byId) return byId;
|
|
@@ -8211,13 +8284,13 @@ function findTask(tasks, ref) {
|
|
|
8211
8284
|
function fitTail(text2, budget) {
|
|
8212
8285
|
if (text2.length <= budget) return { text: text2, trimmed: false };
|
|
8213
8286
|
if (budget <= 0) return { text: "", trimmed: true };
|
|
8214
|
-
const
|
|
8287
|
+
const lines2 = text2.split("\n");
|
|
8215
8288
|
const kept = [];
|
|
8216
8289
|
let total = 0;
|
|
8217
|
-
for (let i =
|
|
8218
|
-
const cost =
|
|
8290
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
8291
|
+
const cost = lines2[i].length + (kept.length > 0 ? 1 : 0);
|
|
8219
8292
|
if (total + cost > budget) break;
|
|
8220
|
-
kept.unshift(
|
|
8293
|
+
kept.unshift(lines2[i]);
|
|
8221
8294
|
total += cost;
|
|
8222
8295
|
}
|
|
8223
8296
|
if (kept.length === 0) return { text: text2.slice(text2.length - budget), trimmed: true };
|
|
@@ -8268,34 +8341,34 @@ function renderField(task, field, ctx) {
|
|
|
8268
8341
|
}
|
|
8269
8342
|
}
|
|
8270
8343
|
function renderTask(task, fields, ctx) {
|
|
8271
|
-
const
|
|
8344
|
+
const lines2 = [
|
|
8272
8345
|
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
8273
8346
|
...fields.filter((f) => f !== "output").flatMap((f) => renderField(task, f, ctx)),
|
|
8274
8347
|
...fields.includes("output") ? renderLiveOutput(task.id, ctx) : [],
|
|
8275
8348
|
""
|
|
8276
8349
|
];
|
|
8277
|
-
ctx.used +=
|
|
8278
|
-
return
|
|
8350
|
+
ctx.used += lines2.reduce((n, l) => n + l.length + 1, 0);
|
|
8351
|
+
return lines2;
|
|
8279
8352
|
}
|
|
8280
8353
|
function renderCatalog(catalog) {
|
|
8281
|
-
const
|
|
8354
|
+
const lines2 = [];
|
|
8282
8355
|
for (const runner of catalog.runners) {
|
|
8283
8356
|
const models = catalog.models[runner] ?? [];
|
|
8284
|
-
|
|
8285
|
-
if (models.length === 0)
|
|
8357
|
+
lines2.push(`${runner} models:`);
|
|
8358
|
+
if (models.length === 0) lines2.push(" (none discovered)");
|
|
8286
8359
|
for (const m of models) {
|
|
8287
8360
|
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
8288
|
-
|
|
8361
|
+
lines2.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
8289
8362
|
}
|
|
8290
8363
|
const modes = catalog.modes[runner] ?? [];
|
|
8291
|
-
|
|
8292
|
-
if (modes.length === 0)
|
|
8364
|
+
lines2.push(`${runner} task modes:`);
|
|
8365
|
+
if (modes.length === 0) lines2.push(" (none declared)");
|
|
8293
8366
|
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
8294
8367
|
for (const mode of modes) {
|
|
8295
|
-
|
|
8368
|
+
lines2.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
8296
8369
|
}
|
|
8297
8370
|
}
|
|
8298
|
-
return
|
|
8371
|
+
return lines2;
|
|
8299
8372
|
}
|
|
8300
8373
|
function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
|
|
8301
8374
|
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
@@ -8329,7 +8402,7 @@ function renderTaskQueryAnswer(query, tasks, catalog, liveOutput) {
|
|
|
8329
8402
|
|
|
8330
8403
|
// src/services/PlanPrompts.ts
|
|
8331
8404
|
function buildResearchToolsPrompt() {
|
|
8332
|
-
const
|
|
8405
|
+
const lines2 = [
|
|
8333
8406
|
"You have access to the following tools to explore the workspace:",
|
|
8334
8407
|
"",
|
|
8335
8408
|
"read_file(path: string, offset?: number, limit?: number) - Read file contents with line numbers and optional pagination. offset is a 0-based line number (default 0). limit is max lines (default 2000). Output lines are prefixed with their line numbers. When a file has more lines than requested, a hint shows the offset to use next. Files larger than ~1 MB are rejected \u2014 use grep instead.",
|
|
@@ -8349,7 +8422,7 @@ function buildResearchToolsPrompt() {
|
|
|
8349
8422
|
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
8350
8423
|
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
8351
8424
|
];
|
|
8352
|
-
return
|
|
8425
|
+
return lines2.join("\n");
|
|
8353
8426
|
}
|
|
8354
8427
|
function buildSubagentSystemPrompt() {
|
|
8355
8428
|
return [
|
|
@@ -8366,18 +8439,18 @@ function buildSubagentSystemPrompt() {
|
|
|
8366
8439
|
].join("\n");
|
|
8367
8440
|
}
|
|
8368
8441
|
function buildModeGuideForRunners(runners, _autonomousDefault) {
|
|
8369
|
-
const
|
|
8442
|
+
const lines2 = ['Assign a "taskMode" to each AI task based on the runner:'];
|
|
8370
8443
|
for (const r of runners) {
|
|
8371
8444
|
if (r === "claude-code") {
|
|
8372
|
-
|
|
8445
|
+
lines2.push('- For claude-code: "acceptEdits" (edit automatically, recommended), "auto" (a classifier approves or blocks each action), "default" (ask before edits), "plan" (read-only analysis), "bypassPermissions" (skip all prompts, CI only).');
|
|
8373
8446
|
} else if (r === "opencode") {
|
|
8374
|
-
|
|
8447
|
+
lines2.push('- For opencode: "build" (full access agent), "plan" (read-only analysis).');
|
|
8375
8448
|
} else {
|
|
8376
|
-
|
|
8449
|
+
lines2.push(`- For ${r}: use the modes from the runner's manifest.`);
|
|
8377
8450
|
}
|
|
8378
8451
|
}
|
|
8379
|
-
|
|
8380
|
-
return
|
|
8452
|
+
lines2.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
|
|
8453
|
+
return lines2.join("\n");
|
|
8381
8454
|
}
|
|
8382
8455
|
function buildModeExamplesForRunners(runners) {
|
|
8383
8456
|
return runners.map((r) => {
|
|
@@ -8406,7 +8479,7 @@ function verificationModeBlock() {
|
|
|
8406
8479
|
].join("\n");
|
|
8407
8480
|
}
|
|
8408
8481
|
function researchPhaseBlock(harnessMode) {
|
|
8409
|
-
const
|
|
8482
|
+
const shared2 = [
|
|
8410
8483
|
"RESEARCH PHASE:",
|
|
8411
8484
|
"- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
|
|
8412
8485
|
];
|
|
@@ -8420,7 +8493,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
8420
8493
|
"- Read key source files to understand architecture"
|
|
8421
8494
|
];
|
|
8422
8495
|
return [
|
|
8423
|
-
...
|
|
8496
|
+
...shared2,
|
|
8424
8497
|
...toolSpecific,
|
|
8425
8498
|
"- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
|
|
8426
8499
|
"- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
|
|
@@ -8482,28 +8555,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
|
|
|
8482
8555
|
verificationBlock
|
|
8483
8556
|
);
|
|
8484
8557
|
}
|
|
8558
|
+
var PLANNER_TOOLS_CATALOG = [
|
|
8559
|
+
"RUNNERS, MODELS AND MODES:",
|
|
8560
|
+
"Which runners, models and modes you may assign can change while you plan: the user may enable a runner or edit the model allowlist at any time. Read them just before you submit, never from memory:",
|
|
8561
|
+
"1. Call list_runners: the enabled runners, each with its task modes and default mode.",
|
|
8562
|
+
"2. Call list_models for each runner you will assign: its allowed models, with their thinking-effort variants.",
|
|
8563
|
+
`3. Call submit_plan with the whole plan. "assignedRunner" must be a runner list_runners returned, "assignedModel.modelId" a modelId list_models returned for that runner, "taskMode" one of that runner's modes.`,
|
|
8564
|
+
'4. If submit_plan returns errors, fix exactly what each one names and call it again in the same reply. If it reports "coerced" changes, tell the user what was changed.',
|
|
8565
|
+
""
|
|
8566
|
+
];
|
|
8485
8567
|
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
|
|
8568
|
+
const tools = variant.plannerTools ?? false;
|
|
8486
8569
|
return [
|
|
8487
|
-
|
|
8570
|
+
`You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans ${tools ? "through the submit_plan tool" : "as JSON"}. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.`,
|
|
8488
8571
|
"",
|
|
8489
8572
|
"WORKFLOW:",
|
|
8490
8573
|
"1. Explore the workspace with tools to understand the codebase.",
|
|
8491
8574
|
"2. Ask clarifying questions grounded in your findings. Reference actual files. When the goal is vague, or a decision would materially change the outcome (storage engine, library choice, scope, API shape), ask the user BEFORE planning \u2014 never silently assume. Clear, fully-specified goals need no questions.",
|
|
8492
8575
|
"3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
|
|
8493
|
-
'4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
8576
|
+
tools ? "4. After the user confirms the outline, call list_runners and list_models, then submit the final task plan with submit_plan." : '4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
8494
8577
|
"",
|
|
8495
8578
|
researchPhaseBlock(variant.harness ?? false),
|
|
8496
8579
|
verificationBlock,
|
|
8497
8580
|
"",
|
|
8498
8581
|
"OUTLINE PHASE:",
|
|
8499
|
-
|
|
8582
|
+
`- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to ${tools ? "submit_plan" : "JSON"}.`,
|
|
8500
8583
|
"- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
|
|
8501
8584
|
'- Example: "1. Set up database schema (src/db/schema.ts) \u2014 creates tables for users and sessions\n2. Add auth middleware (src/auth/middleware.ts) \u2014 validates JWTs on protected routes\n3. Build login page (src/ui/Login.tsx) \u2014 form component with validation"',
|
|
8502
8585
|
"- The user may reply with adjustments. Revise the outline and re-present it.",
|
|
8503
|
-
"- When the user confirms the outline, emit the task plan JSON.",
|
|
8586
|
+
tools ? "- When the user confirms the outline, submit the plan with submit_plan." : "- When the user confirms the outline, emit the task plan JSON.",
|
|
8504
8587
|
"",
|
|
8505
8588
|
"PLAN FORMAT:",
|
|
8506
|
-
"Generate a task plan using this JSON format:",
|
|
8589
|
+
tools ? 'submit_plan takes {"tasks": [...]}, every task in this shape:' : "Generate a task plan using this JSON format:",
|
|
8507
8590
|
"",
|
|
8508
8591
|
"{",
|
|
8509
8592
|
' "tasks": [',
|
|
@@ -8517,8 +8600,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
8517
8600
|
' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
|
|
8518
8601
|
' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
|
|
8519
8602
|
` "assignedModel": { "modelId": "model-id", "modelLabel": "Model Label", "thinkingEffort": "a variant id from that model's variants list \u2014 omit if the model has none" },`,
|
|
8520
|
-
' "assignedRunner": "' + runners.join("|") + '",',
|
|
8521
|
-
' "taskMode": "' + modeExamples + '",',
|
|
8603
|
+
' "assignedRunner": "' + (tools ? "a runner id from list_runners" : runners.join("|")) + '",',
|
|
8604
|
+
' "taskMode": "' + (tools ? "one of that runner's modes from list_runners" : modeExamples) + '",',
|
|
8522
8605
|
' "autonomy": "AFK|HITL",',
|
|
8523
8606
|
' "sliceType": "AFK|HITL",',
|
|
8524
8607
|
' "ops": true,',
|
|
@@ -8550,8 +8633,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
8550
8633
|
...opsTaskSection(true),
|
|
8551
8634
|
"",
|
|
8552
8635
|
"RULES:",
|
|
8553
|
-
|
|
8554
|
-
|
|
8636
|
+
...tools ? [
|
|
8637
|
+
"- Submit the plan ONLY through submit_plan. Never write it as JSON in your reply.",
|
|
8638
|
+
"- Show the outline BEFORE submitting. Do not skip the outline."
|
|
8639
|
+
] : [
|
|
8640
|
+
"- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
|
|
8641
|
+
"- Emit the outline BEFORE the JSON. Do not skip the outline."
|
|
8642
|
+
],
|
|
8555
8643
|
"- Reference specific files and patterns from your research in task prompts.",
|
|
8556
8644
|
...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
|
|
8557
8645
|
"",
|
|
@@ -8559,23 +8647,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
8559
8647
|
"- When suggesting libraries, frameworks, or patterns, first verify they already exist in the codebase. NEVER assume a library is available just because it is well-known. Check package.json, imports, or surrounding files first.",
|
|
8560
8648
|
"- Follow the existing code style, naming conventions, and architectural patterns of the project.",
|
|
8561
8649
|
"",
|
|
8562
|
-
"MODEL ASSIGNMENT:",
|
|
8563
|
-
"Available models:",
|
|
8564
|
-
modelsJson,
|
|
8565
|
-
"",
|
|
8566
|
-
runnerInstruction(runners),
|
|
8650
|
+
...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
|
|
8567
8651
|
"MODEL SELECTION GUIDELINES:",
|
|
8568
8652
|
"- Use stronger models for complex refactoring, architecture changes, security-critical code.",
|
|
8569
8653
|
"- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
|
|
8570
|
-
`- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list above (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
|
|
8654
|
+
`- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list ${tools ? "from list_models" : "above"} (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
|
|
8571
8655
|
"",
|
|
8572
8656
|
"TASK MODE:",
|
|
8573
|
-
modeGuide,
|
|
8657
|
+
tools ? `Use each runner's default mode from list_runners unless the task needs a different mode it lists. Avoid "plan" (read-only) unless the user asked for analysis only.` : modeGuide,
|
|
8574
8658
|
"",
|
|
8575
|
-
//
|
|
8576
|
-
//
|
|
8577
|
-
//
|
|
8578
|
-
...TASK_QUERY_PROTOCOL,
|
|
8659
|
+
// The envelope is shared by both variants on purpose: a harness planner
|
|
8660
|
+
// Ordewell could not hand tools to speaks the same protocol as an
|
|
8661
|
+
// API-backed one (ADR-0009). One it did hand them to reads through them.
|
|
8662
|
+
...tools ? TASK_READ_TOOLS_PROTOCOL : TASK_QUERY_PROTOCOL,
|
|
8579
8663
|
"",
|
|
8580
8664
|
context ? `PROJECT CONTEXT:
|
|
8581
8665
|
${context}
|
|
@@ -8817,25 +8901,25 @@ function executionLogBlock(executionLog) {
|
|
|
8817
8901
|
if (executionLog.length === 0) return "";
|
|
8818
8902
|
const completed = executionLog.filter((t) => t.status === "completed");
|
|
8819
8903
|
const failed = executionLog.filter((t) => t.status === "failed");
|
|
8820
|
-
const
|
|
8821
|
-
|
|
8904
|
+
const lines2 = ["EXECUTION LOG SUMMARY:"];
|
|
8905
|
+
lines2.push(`Total entries: ${executionLog.length} (${completed.length} completed, ${failed.length} failed)`);
|
|
8822
8906
|
if (completed.length > 0) {
|
|
8823
|
-
|
|
8824
|
-
|
|
8907
|
+
lines2.push("");
|
|
8908
|
+
lines2.push("COMPLETED TASKS:");
|
|
8825
8909
|
for (const t of completed) {
|
|
8826
8910
|
const verdict = t.verdict ? ` \u2014 ${t.verdict.outcome.toUpperCase()}: ${t.verdict.reason}` : "";
|
|
8827
|
-
|
|
8911
|
+
lines2.push(`- [${t.id}] ${t.title}${verdict}`);
|
|
8828
8912
|
}
|
|
8829
8913
|
}
|
|
8830
8914
|
if (failed.length > 0) {
|
|
8831
|
-
|
|
8832
|
-
|
|
8915
|
+
lines2.push("");
|
|
8916
|
+
lines2.push("FAILED TASKS (eligible for retry):");
|
|
8833
8917
|
for (const t of failed) {
|
|
8834
8918
|
const verdict = t.verdict ? ` \u2014 ${t.verdict.reason}` : "";
|
|
8835
|
-
|
|
8919
|
+
lines2.push(`- [${t.id}] ${t.title} (retry #${t.retryCount})${verdict}`);
|
|
8836
8920
|
}
|
|
8837
8921
|
}
|
|
8838
|
-
return
|
|
8922
|
+
return lines2.join("\n");
|
|
8839
8923
|
}
|
|
8840
8924
|
function pendingEditRulesBlock() {
|
|
8841
8925
|
return [
|
|
@@ -8878,7 +8962,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
|
|
|
8878
8962
|
sections.push('Return the COMPLETE modified pending tasks as a JSON object with a single "tasks" array. Include all pending tasks, not just the modified ones.\nDo NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object.');
|
|
8879
8963
|
return sections.join("\n");
|
|
8880
8964
|
}
|
|
8881
|
-
function buildMergePrompt(taskIds, tasks) {
|
|
8965
|
+
function buildMergePrompt(taskIds, tasks, tools = false) {
|
|
8882
8966
|
const idSet = new Set(taskIds);
|
|
8883
8967
|
const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
|
|
8884
8968
|
const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
|
|
@@ -8886,22 +8970,22 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
8886
8970
|
`Merge these tasks into ONE combined task: ${refs}.`,
|
|
8887
8971
|
"Write a clear combined title, description, and prompt that cover all of their work.",
|
|
8888
8972
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
8889
|
-
|
|
8890
|
-
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
8891
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
8973
|
+
`Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. Prefer the strongest model if the merged work is complex.`,
|
|
8974
|
+
tools ? 'Call edit_plan with a single "merge" op:' : 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
8975
|
+
` {"${tools ? "ops" : "taskOps"}":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
8892
8976
|
`If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
|
|
8893
8977
|
].join("\n");
|
|
8894
8978
|
}
|
|
8895
|
-
function buildSplitPrompt(taskId, tasks) {
|
|
8979
|
+
function buildSplitPrompt(taskId, tasks, tools = false) {
|
|
8896
8980
|
const task = tasks.find((t) => t.id === taskId);
|
|
8897
8981
|
if (!task) return `Split task ${taskId} into smaller tasks.`;
|
|
8898
8982
|
return [
|
|
8899
8983
|
`Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
|
|
8900
8984
|
"Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
|
|
8901
8985
|
"The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
|
|
8902
|
-
|
|
8903
|
-
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
8904
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
8986
|
+
`Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).`,
|
|
8987
|
+
tools ? 'Call edit_plan with a single "split" op:' : 'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
8988
|
+
` {"${tools ? "ops" : "taskOps"}":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
8905
8989
|
`If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
|
|
8906
8990
|
].join("\n");
|
|
8907
8991
|
}
|
|
@@ -8984,25 +9068,37 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = tr
|
|
|
8984
9068
|
const detail = err instanceof Error ? err.message : String(err);
|
|
8985
9069
|
throw new PlanParseError(`Plan response was not valid JSON: ${detail}`, raw);
|
|
8986
9070
|
}
|
|
8987
|
-
|
|
8988
|
-
|
|
9071
|
+
const result = validatePlanTasks(parsed, runners, runnerModes, autonomousDefault);
|
|
9072
|
+
if (result.ok) return result.tasks;
|
|
9073
|
+
const [first] = result.errors;
|
|
9074
|
+
throw new PlanParseError(first.message, raw, { semantic: first.taskId !== void 0 });
|
|
9075
|
+
}
|
|
9076
|
+
function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = true) {
|
|
9077
|
+
const rawTasks = isRecord(obj) ? obj.tasks : void 0;
|
|
9078
|
+
if (!Array.isArray(rawTasks)) {
|
|
9079
|
+
return { ok: false, errors: [{ field: "tasks", message: "Invalid plan: missing tasks array" }] };
|
|
8989
9080
|
}
|
|
8990
|
-
const rawTasks = parsed.tasks;
|
|
8991
9081
|
if (rawTasks.length === 0) {
|
|
8992
|
-
|
|
9082
|
+
return { ok: false, errors: [{ field: "tasks", message: "Plan contained no tasks" }] };
|
|
8993
9083
|
}
|
|
8994
|
-
const tasks = rawTasks.map((t) => parseTask(t, runners, runnerModes, autonomousDefault));
|
|
8995
|
-
|
|
9084
|
+
const tasks = rawTasks.map((t) => parseTask(asRecord(t), runners, runnerModes, autonomousDefault));
|
|
9085
|
+
const errors = verticalSliceErrors(tasks);
|
|
8996
9086
|
for (const t of tasks) {
|
|
8997
9087
|
if (!runners.includes(t.assignedRunner)) {
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
{
|
|
9002
|
-
);
|
|
9088
|
+
errors.push({
|
|
9089
|
+
taskId: t.id,
|
|
9090
|
+
field: "assignedRunner",
|
|
9091
|
+
message: `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
|
|
9092
|
+
});
|
|
9003
9093
|
}
|
|
9004
9094
|
}
|
|
9005
|
-
return tasks;
|
|
9095
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, tasks };
|
|
9096
|
+
}
|
|
9097
|
+
function isRecord(value) {
|
|
9098
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9099
|
+
}
|
|
9100
|
+
function asRecord(value) {
|
|
9101
|
+
return isRecord(value) ? value : {};
|
|
9006
9102
|
}
|
|
9007
9103
|
function looksLikePlanAttempt(raw) {
|
|
9008
9104
|
const { matches, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
@@ -9027,12 +9123,12 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
|
|
|
9027
9123
|
// description/title — same fallback TaskOps.applyTaskOps uses for `add`, so
|
|
9028
9124
|
// a task never silently ends up unschedulable (getReadyTasks requires prompt).
|
|
9029
9125
|
prompt: raw.prompt ? String(raw.prompt) : description || title || void 0,
|
|
9030
|
-
userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map((s) => ({
|
|
9126
|
+
userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map(asRecord).map((s) => ({
|
|
9031
9127
|
order: Number(s.order ?? 0),
|
|
9032
9128
|
instruction: String(s.instruction ?? ""),
|
|
9033
9129
|
completed: false
|
|
9034
9130
|
})) : void 0,
|
|
9035
|
-
subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(s, runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
|
|
9131
|
+
subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(asRecord(s), runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
|
|
9036
9132
|
assignedModel: raw.assignedModel ? {
|
|
9037
9133
|
modelId: String(raw.assignedModel.modelId ?? ""),
|
|
9038
9134
|
modelLabel: String(raw.assignedModel.modelLabel ?? ""),
|
|
@@ -9053,42 +9149,28 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
|
|
|
9053
9149
|
ops: !inherited && taskType === "ai" && raw.ops === true
|
|
9054
9150
|
});
|
|
9055
9151
|
}
|
|
9056
|
-
function
|
|
9152
|
+
function verticalSliceErrors(tasks, parentTitle) {
|
|
9057
9153
|
const where = (t) => parentTitle ? `subtask "${t.title}" of "${parentTitle}"` : `Task "${t.title}"`;
|
|
9058
|
-
const
|
|
9154
|
+
const errors = [];
|
|
9059
9155
|
for (const t of tasks) {
|
|
9156
|
+
const fail = (field, message) => errors.push({ taskId: t.id, field, message });
|
|
9060
9157
|
if (!t.sliceType) {
|
|
9061
|
-
|
|
9062
|
-
`${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`,
|
|
9063
|
-
raw,
|
|
9064
|
-
semantic
|
|
9065
|
-
);
|
|
9158
|
+
fail("sliceType", `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`);
|
|
9066
9159
|
}
|
|
9067
9160
|
if (t.type === "ai" && !t.autonomy) {
|
|
9068
|
-
|
|
9069
|
-
`AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`,
|
|
9070
|
-
raw,
|
|
9071
|
-
semantic
|
|
9072
|
-
);
|
|
9161
|
+
fail("autonomy", `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`);
|
|
9073
9162
|
}
|
|
9074
9163
|
if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
|
|
9075
|
-
|
|
9076
|
-
`${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`,
|
|
9077
|
-
raw,
|
|
9078
|
-
semantic
|
|
9079
|
-
);
|
|
9164
|
+
fail("userSteps", `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
|
|
9080
9165
|
}
|
|
9081
|
-
if (t.type === "user" && t.sliceType !== "HITL") {
|
|
9082
|
-
|
|
9083
|
-
`User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`,
|
|
9084
|
-
raw,
|
|
9085
|
-
semantic
|
|
9086
|
-
);
|
|
9166
|
+
if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
|
|
9167
|
+
fail("sliceType", `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
|
|
9087
9168
|
}
|
|
9088
9169
|
if (t.subtasks.length > 0) {
|
|
9089
|
-
|
|
9170
|
+
errors.push(...verticalSliceErrors(t.subtasks, t.title));
|
|
9090
9171
|
}
|
|
9091
9172
|
}
|
|
9173
|
+
return errors;
|
|
9092
9174
|
}
|
|
9093
9175
|
function checkUniqueIds(ctx) {
|
|
9094
9176
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9391,11 +9473,11 @@ var UPDATABLE_FIELDS = [
|
|
|
9391
9473
|
"sliceType",
|
|
9392
9474
|
"ops"
|
|
9393
9475
|
];
|
|
9394
|
-
function taskOpsProtocol(executionNote = "") {
|
|
9476
|
+
function taskOpsProtocol(executionNote = "", tools = false) {
|
|
9395
9477
|
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
9396
9478
|
return [
|
|
9397
|
-
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
9398
|
-
' {"taskOps": [',
|
|
9479
|
+
tools ? "- To modify specific tasks, call edit_plan, never a JSON reply. Its argument is:" : "- To modify specific tasks, reply with ONLY this JSON object:",
|
|
9480
|
+
tools ? ' {"ops": [' : ' {"taskOps": [',
|
|
9399
9481
|
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
9400
9482
|
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"ops"?},"handle"?:"<name>"},',
|
|
9401
9483
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
@@ -9404,11 +9486,12 @@ function taskOpsProtocol(executionNote = "") {
|
|
|
9404
9486
|
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
9405
9487
|
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
9406
9488
|
" ]}",
|
|
9407
|
-
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
9489
|
+
tools ? "- For sweeping changes, you may instead call submit_plan with the full plan." : '- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
9408
9490
|
'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
|
|
9409
9491
|
'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
|
|
9410
|
-
|
|
9492
|
+
`When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models>"}.`,
|
|
9411
9493
|
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
9494
|
+
...tools ? ["edit_plan checks the batch when you call it and answers with what it will change, or with the error to fix \u2014 call it again in the same reply. The edit is applied when your reply ends; edits you make in one reply join one batch."] : [],
|
|
9412
9495
|
'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + executionNote,
|
|
9413
9496
|
'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
|
|
9414
9497
|
'"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
|
|
@@ -9640,21 +9723,21 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9640
9723
|
const handleOwner = /* @__PURE__ */ new Map();
|
|
9641
9724
|
const handleId = /* @__PURE__ */ new Map();
|
|
9642
9725
|
for (const [i, op] of ops.entries()) {
|
|
9643
|
-
const
|
|
9644
|
-
if (
|
|
9645
|
-
if (typeof
|
|
9726
|
+
const handle2 = op.handle;
|
|
9727
|
+
if (handle2 === void 0) continue;
|
|
9728
|
+
if (typeof handle2 !== "string" || !handle2.trim()) {
|
|
9646
9729
|
errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
|
|
9647
9730
|
continue;
|
|
9648
9731
|
}
|
|
9649
|
-
if (handleOwner.has(
|
|
9650
|
-
errors.push(`op ${i + 1} (${op.op}): handle "${
|
|
9732
|
+
if (handleOwner.has(handle2)) {
|
|
9733
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle2}" is already used earlier in this batch`);
|
|
9651
9734
|
continue;
|
|
9652
9735
|
}
|
|
9653
|
-
if (refCollidesWithExisting(
|
|
9654
|
-
errors.push(`op ${i + 1} (${op.op}): handle "${
|
|
9736
|
+
if (refCollidesWithExisting(handle2, originalFlatAll)) {
|
|
9737
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle2}" collides with an existing task reference`);
|
|
9655
9738
|
continue;
|
|
9656
9739
|
}
|
|
9657
|
-
handleOwner.set(
|
|
9740
|
+
handleOwner.set(handle2, i);
|
|
9658
9741
|
}
|
|
9659
9742
|
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
9660
9743
|
const resolveDeps = (deps, opIndex) => {
|
|
@@ -10021,15 +10104,15 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
10021
10104
|
|
|
10022
10105
|
// src/services/PlanRepair.ts
|
|
10023
10106
|
async function repairLoop(opts) {
|
|
10024
|
-
let
|
|
10107
|
+
let reply2 = await opts.first();
|
|
10025
10108
|
for (let repairs = 0; ; repairs++) {
|
|
10026
|
-
const verdict = await opts.interpret(
|
|
10109
|
+
const verdict = await opts.interpret(reply2);
|
|
10027
10110
|
if ("done" in verdict) return verdict.done;
|
|
10028
10111
|
if (repairs >= opts.maxRepairs) {
|
|
10029
|
-
return opts.onExhausted({ reply, errors: verdict.retry.errors, cause: verdict.retry.cause });
|
|
10112
|
+
return opts.onExhausted({ reply: reply2, errors: verdict.retry.errors, cause: verdict.retry.cause });
|
|
10030
10113
|
}
|
|
10031
10114
|
const { corrective } = verdict.retry;
|
|
10032
|
-
|
|
10115
|
+
reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
|
|
10033
10116
|
}
|
|
10034
10117
|
}
|
|
10035
10118
|
var JSON_REPAIR_INSTRUCTION = "Your previous response could not be parsed as JSON. Re-send ONLY the JSON object \u2014 no prose before or after, no explanation, no markdown code fences.";
|
|
@@ -10145,34 +10228,34 @@ async function settleReply(opts) {
|
|
|
10145
10228
|
}
|
|
10146
10229
|
if (attempt.failure !== void 0) return { done: message(attempt.failure) };
|
|
10147
10230
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
10148
|
-
const
|
|
10149
|
-
switch (
|
|
10231
|
+
const reply2 = classifyPlannerReply(attempt.text, opts.classify);
|
|
10232
|
+
switch (reply2.kind) {
|
|
10150
10233
|
case "plan":
|
|
10151
|
-
return { done: { kind: "plan", tasks:
|
|
10234
|
+
return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
|
|
10152
10235
|
case "task_ops":
|
|
10153
|
-
return { done: { kind: "task_ops", ops:
|
|
10236
|
+
return { done: { kind: "task_ops", ops: reply2.ops, text: said(attempt), researchLog } };
|
|
10154
10237
|
// A read is answered by the conversation, which owns the plan and the
|
|
10155
10238
|
// catalog, so it leaves here the way a plan or an edit does.
|
|
10156
10239
|
case "task_query":
|
|
10157
|
-
return { done: { kind: "task_query", query:
|
|
10240
|
+
return { done: { kind: "task_query", query: reply2.query, text: said(attempt), researchLog } };
|
|
10158
10241
|
case "prose":
|
|
10159
10242
|
return { done: asProse(attempt) };
|
|
10160
10243
|
}
|
|
10161
10244
|
if (opts.signal?.aborted) return { done: asProse(attempt) };
|
|
10162
|
-
const errors = [
|
|
10163
|
-
switch (
|
|
10245
|
+
const errors = [reply2.error.message];
|
|
10246
|
+
switch (reply2.kind) {
|
|
10164
10247
|
case "broken_task_ops":
|
|
10165
|
-
return { retry: { errors, corrective: reEmitTaskOpsPrompt(
|
|
10248
|
+
return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
|
|
10166
10249
|
case "broken_task_query":
|
|
10167
|
-
return { retry: { errors, corrective: reEmitTaskQueryPrompt(
|
|
10250
|
+
return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
|
|
10168
10251
|
case "broken_plan": {
|
|
10169
|
-
const corrective =
|
|
10170
|
-
return { retry: { errors, corrective, cause:
|
|
10252
|
+
const corrective = reply2.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply2.error.message);
|
|
10253
|
+
return { retry: { errors, corrective, cause: reply2.error } };
|
|
10171
10254
|
}
|
|
10172
10255
|
}
|
|
10173
10256
|
},
|
|
10174
10257
|
maxRepairs: MAX_JSON_REPAIRS,
|
|
10175
|
-
onExhausted: ({ reply }) => asProse(
|
|
10258
|
+
onExhausted: ({ reply: reply2 }) => asProse(reply2)
|
|
10176
10259
|
});
|
|
10177
10260
|
}
|
|
10178
10261
|
var said = (attempt) => attempt.fullText ?? attempt.text;
|
|
@@ -10399,8 +10482,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
|
|
|
10399
10482
|
const blocks = [{ tool: null, lines: [] }];
|
|
10400
10483
|
for (const line of resultsText.split("\n")) {
|
|
10401
10484
|
const m = headerRe.exec(line);
|
|
10402
|
-
const
|
|
10403
|
-
if (
|
|
10485
|
+
const tool2 = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
|
|
10486
|
+
if (tool2) blocks.push({ tool: tool2, lines: [line] });
|
|
10404
10487
|
else blocks[blocks.length - 1].lines.push(line);
|
|
10405
10488
|
}
|
|
10406
10489
|
const texts = blocks.map((b) => b.lines.join("\n"));
|
|
@@ -10494,9 +10577,9 @@ function ellipsize(text2, max) {
|
|
|
10494
10577
|
const oneLine2 = text2.replace(/\s+/g, " ").trim();
|
|
10495
10578
|
return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
|
|
10496
10579
|
}
|
|
10497
|
-
function summarizeToolCall(
|
|
10498
|
-
const display = toolLabel?.trim() ||
|
|
10499
|
-
if (
|
|
10580
|
+
function summarizeToolCall(tool2, argsJson, toolLabel) {
|
|
10581
|
+
const display = toolLabel?.trim() || tool2;
|
|
10582
|
+
if (tool2 === "agent_tool") {
|
|
10500
10583
|
try {
|
|
10501
10584
|
const args = JSON.parse(argsJson);
|
|
10502
10585
|
const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
|
|
@@ -10505,7 +10588,7 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
|
10505
10588
|
}
|
|
10506
10589
|
return display;
|
|
10507
10590
|
}
|
|
10508
|
-
if (
|
|
10591
|
+
if (tool2 === "spawn_research_agent") {
|
|
10509
10592
|
try {
|
|
10510
10593
|
const args = JSON.parse(argsJson);
|
|
10511
10594
|
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
@@ -10517,19 +10600,19 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
|
10517
10600
|
}
|
|
10518
10601
|
try {
|
|
10519
10602
|
const args = JSON.parse(argsJson);
|
|
10520
|
-
if (
|
|
10603
|
+
if (tool2 === "find_symbol" && args.symbol) {
|
|
10521
10604
|
return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
|
|
10522
10605
|
}
|
|
10523
|
-
if (
|
|
10606
|
+
if (tool2 === "web_search" && args.query) {
|
|
10524
10607
|
return `web_search "${ellipsize(String(args.query), 50)}"`;
|
|
10525
10608
|
}
|
|
10526
|
-
if (
|
|
10609
|
+
if (tool2 === "fetch" && args.url) {
|
|
10527
10610
|
return `fetch ${ellipsize(String(args.url), 60)}`;
|
|
10528
10611
|
}
|
|
10529
|
-
if (
|
|
10612
|
+
if (tool2 === "glob" && args.pattern) {
|
|
10530
10613
|
return `glob ${ellipsize(String(args.pattern), 50)}`;
|
|
10531
10614
|
}
|
|
10532
|
-
if (
|
|
10615
|
+
if (tool2 === "grep" && args.pattern) {
|
|
10533
10616
|
const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
|
|
10534
10617
|
const scope = args.include ? ` in ${args.include}` : "";
|
|
10535
10618
|
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
@@ -10962,9 +11045,9 @@ ${llmOutput}
|
|
|
10962
11045
|
return { tasks: null, researchLog, researchResults };
|
|
10963
11046
|
}
|
|
10964
11047
|
const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
|
|
10965
|
-
const
|
|
10966
|
-
if (
|
|
10967
|
-
return { tasks:
|
|
11048
|
+
const reply2 = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
|
|
11049
|
+
if (reply2.kind === "plan") {
|
|
11050
|
+
return { tasks: reply2.tasks, researchLog, researchResults };
|
|
10968
11051
|
}
|
|
10969
11052
|
if (!turn.hasToolCalls) break;
|
|
10970
11053
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
@@ -11681,7 +11764,7 @@ ${collected.aiflowContext}
|
|
|
11681
11764
|
};
|
|
11682
11765
|
|
|
11683
11766
|
// src/services/TaskOrchestrator.ts
|
|
11684
|
-
var
|
|
11767
|
+
var path15 = __toESM(require("path"));
|
|
11685
11768
|
|
|
11686
11769
|
// src/services/promptAugment.ts
|
|
11687
11770
|
var MAX_TAIL_CHARS = 500;
|
|
@@ -11768,7 +11851,7 @@ function renderPlanMap(planTasks, currentTaskId, opts) {
|
|
|
11768
11851
|
const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
|
|
11769
11852
|
const { window, omitted } = pickWindow(sorted, currentIdx, max);
|
|
11770
11853
|
const currentTask = sorted.find((r) => r.task.id === currentTaskId)?.task;
|
|
11771
|
-
const
|
|
11854
|
+
const lines2 = window.map((row) => {
|
|
11772
11855
|
const isCurrent = row.task.id === currentTaskId;
|
|
11773
11856
|
const tag = planMapStatus(row.task, isCurrent);
|
|
11774
11857
|
const order = row.parent ? row.label : row.label.padStart(2, " ");
|
|
@@ -11785,7 +11868,7 @@ You are running as: ${currentTask.assignedRunner}` : "";
|
|
|
11785
11868
|
"",
|
|
11786
11869
|
"This is for context only \u2014 do ONLY the task marked `\u2190 you are here`. Future tasks will handle their own scope; do not preempt them.",
|
|
11787
11870
|
"",
|
|
11788
|
-
|
|
11871
|
+
lines2.join("\n") + footer + runnerNote
|
|
11789
11872
|
].join("\n");
|
|
11790
11873
|
}
|
|
11791
11874
|
function renderPreviousAttempt(output) {
|
|
@@ -11799,10 +11882,14 @@ function renderPreviousAttempt(output) {
|
|
|
11799
11882
|
tail
|
|
11800
11883
|
].join("\n");
|
|
11801
11884
|
}
|
|
11802
|
-
function renderCompletionMarker(task) {
|
|
11885
|
+
function renderCompletionMarker(task, completionTool = false) {
|
|
11886
|
+
const howto = `Build it by writing \`<<<ORDEWELL_\` immediately followed by \`DONE_${task.completionMarker}>>>\` \u2014 joined into a single unbroken token, with no space, quote, or any other character between the two parts.`;
|
|
11887
|
+
if (!completionTool) return `
|
|
11888
|
+
|
|
11889
|
+
When you have fully completed this task, print one final line containing only the completion marker. ${howto}`;
|
|
11803
11890
|
return `
|
|
11804
11891
|
|
|
11805
|
-
When you have fully completed this task,
|
|
11892
|
+
When you have fully completed this task, call the \`task_complete\` tool with status \`done\` and a summary of what you did; the tasks that depend on this one are given that summary. If you cannot complete it, call \`task_complete\` with status \`blocked\` or \`failed\` and the reason instead, and print no marker. After a \`done\` call, or if the tool is not available to you, also print one final line containing only the completion marker. ${howto}`;
|
|
11806
11893
|
}
|
|
11807
11894
|
function renderTddInstruction() {
|
|
11808
11895
|
return [
|
|
@@ -11823,7 +11910,21 @@ function renderTddInstruction() {
|
|
|
11823
11910
|
].join("\n");
|
|
11824
11911
|
}
|
|
11825
11912
|
var CHECKPOINT_MARKER_HOWTO = "Build it by writing `<<<ORDEWELL_` immediately followed by `CHECKPOINT:` \u2014 no space, quote, or any other character between those two parts \u2014 then a brief summary of what you are about to do and why human input is needed, closed with `>>>`";
|
|
11826
|
-
function renderCheckpointInstruction() {
|
|
11913
|
+
function renderCheckpointInstruction(checkpointTool = false) {
|
|
11914
|
+
if (checkpointTool) {
|
|
11915
|
+
return [
|
|
11916
|
+
"## Human-in-the-loop checkpoints",
|
|
11917
|
+
"",
|
|
11918
|
+
"When you reach a decision point that requires human judgment \u2014 before destructive",
|
|
11919
|
+
"operations, after major design decisions, or when multiple viable paths exist \u2014 pause",
|
|
11920
|
+
"and request input:",
|
|
11921
|
+
"",
|
|
11922
|
+
"1. Call the `checkpoint` tool with a brief summary of what you are about to do and why human input is needed. The call waits for the human, and its result is their answer",
|
|
11923
|
+
"2. If the result is `continue`: proceed with the action you described",
|
|
11924
|
+
"3. If the result starts with `rejected:`: the rest is why. Adjust your approach and call `checkpoint` again if needed",
|
|
11925
|
+
`4. If the \`checkpoint\` tool is not available to you, or the call fails, fall back to the marker: print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}`
|
|
11926
|
+
].join("\n");
|
|
11927
|
+
}
|
|
11827
11928
|
return [
|
|
11828
11929
|
"## Human-in-the-loop checkpoints",
|
|
11829
11930
|
"",
|
|
@@ -11855,9 +11956,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
11855
11956
|
blocks.push(renderTddInstruction());
|
|
11856
11957
|
}
|
|
11857
11958
|
if (isHitlTask(task)) {
|
|
11858
|
-
blocks.push(renderCheckpointInstruction());
|
|
11959
|
+
blocks.push(renderCheckpointInstruction(opts?.completionTool));
|
|
11859
11960
|
}
|
|
11860
|
-
const marker2 = renderCompletionMarker(task);
|
|
11961
|
+
const marker2 = renderCompletionMarker(task, opts?.completionTool);
|
|
11861
11962
|
if (blocks.length === 0) return basePrompt + marker2;
|
|
11862
11963
|
return `${blocks.join("\n\n")}
|
|
11863
11964
|
|
|
@@ -11868,12 +11969,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
|
|
|
11868
11969
|
opts.ops ? "(Ordewell) You are continuing this task in the same session, in the same checkout. What your earlier attempt did outside the repository was not undone: check what already exists before acting again." : "(Ordewell) You are continuing this task in the same session. Your working directory was recreated from the integration branch: work from your earlier attempt is there only if it landed, so check the files before relying on them."
|
|
11869
11970
|
];
|
|
11870
11971
|
if (isHitlTask(task)) {
|
|
11871
|
-
reminder.push(`If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
|
|
11972
|
+
reminder.push(opts.completionTool ? `If you reach a decision that needs human judgment, call the \`checkpoint\` tool: its result is \`continue\` or \`rejected: <why>\`. Only if the tool is not available to you, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.` : `If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
|
|
11872
11973
|
}
|
|
11873
11974
|
return `${message.trim()}
|
|
11874
11975
|
|
|
11875
11976
|
---
|
|
11876
|
-
${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
|
|
11977
|
+
${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
|
|
11877
11978
|
}
|
|
11878
11979
|
|
|
11879
11980
|
// src/services/VerdictEngine.ts
|
|
@@ -11891,6 +11992,13 @@ var VerdictEngine = class {
|
|
|
11891
11992
|
pausedSessions = /* @__PURE__ */ new Map();
|
|
11892
11993
|
listeners = [];
|
|
11893
11994
|
checkpointListeners = [];
|
|
11995
|
+
withdrawnListeners = [];
|
|
11996
|
+
/**
|
|
11997
|
+
* The open `checkpoint` tool call per task (ADR-0022, V5): settling it is how
|
|
11998
|
+
* an answer reaches a runner that asked through the tool, where the marker's
|
|
11999
|
+
* answer is typed into the session instead.
|
|
12000
|
+
*/
|
|
12001
|
+
toolCheckpoints = /* @__PURE__ */ new Map();
|
|
11894
12002
|
idleListeners = [];
|
|
11895
12003
|
/**
|
|
11896
12004
|
* Per-task generation. Replaced on every watch(), clear() and verdict.
|
|
@@ -11921,6 +12029,9 @@ var VerdictEngine = class {
|
|
|
11921
12029
|
onCheckpoint(listener) {
|
|
11922
12030
|
this.checkpointListeners.push(listener);
|
|
11923
12031
|
}
|
|
12032
|
+
onCheckpointWithdrawn(listener) {
|
|
12033
|
+
this.withdrawnListeners.push(listener);
|
|
12034
|
+
}
|
|
11924
12035
|
onIdleChange(listener) {
|
|
11925
12036
|
this.idleListeners.push(listener);
|
|
11926
12037
|
}
|
|
@@ -11982,6 +12093,11 @@ ${line}
|
|
|
11982
12093
|
}
|
|
11983
12094
|
approveCheckpoint(taskId) {
|
|
11984
12095
|
this.resumeIdle(taskId);
|
|
12096
|
+
const toolCall = this.toolCheckpoints.get(taskId);
|
|
12097
|
+
if (toolCall) {
|
|
12098
|
+
toolCall({ kind: "continue" });
|
|
12099
|
+
return;
|
|
12100
|
+
}
|
|
11985
12101
|
const session = this.pausedSessions.get(taskId);
|
|
11986
12102
|
if (session) {
|
|
11987
12103
|
session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
|
|
@@ -11990,6 +12106,11 @@ ${line}
|
|
|
11990
12106
|
}
|
|
11991
12107
|
rejectCheckpoint(taskId, reason) {
|
|
11992
12108
|
this.resumeIdle(taskId);
|
|
12109
|
+
const toolCall = this.toolCheckpoints.get(taskId);
|
|
12110
|
+
if (toolCall) {
|
|
12111
|
+
toolCall({ kind: "rejected", reason });
|
|
12112
|
+
return;
|
|
12113
|
+
}
|
|
11993
12114
|
const session = this.pausedSessions.get(taskId);
|
|
11994
12115
|
if (session) {
|
|
11995
12116
|
session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
|
|
@@ -12000,7 +12121,10 @@ ${line}
|
|
|
12000
12121
|
* Attach to a spawned session: scan the output tail for the task's completion
|
|
12001
12122
|
* marker (delivering a verdict immediately while leaving interactive sessions
|
|
12002
12123
|
* open), scan for checkpoint markers, and on exit produce a failed verdict
|
|
12003
|
-
* when the marker was never observed.
|
|
12124
|
+
* when the marker was never observed. A structured session's `task_complete`
|
|
12125
|
+
* call is evidence too (ADR-0022, V2): whichever signal comes first decides.
|
|
12126
|
+
*
|
|
12127
|
+
* Returns the attempt's generation, what {@link signalComplete} is checked against.
|
|
12004
12128
|
*/
|
|
12005
12129
|
watch(task, session) {
|
|
12006
12130
|
const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
|
|
@@ -12030,6 +12154,8 @@ ${line}
|
|
|
12030
12154
|
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
12031
12155
|
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
12032
12156
|
});
|
|
12157
|
+
session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
|
|
12158
|
+
session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
|
|
12033
12159
|
}
|
|
12034
12160
|
session.onExit((exitCode) => {
|
|
12035
12161
|
if (this.generations.get(task.id) !== gen) return;
|
|
@@ -12038,6 +12164,52 @@ ${line}
|
|
|
12038
12164
|
const verdict = this.decide(task, exitCode);
|
|
12039
12165
|
for (const l of this.listeners) l(task.id, verdict);
|
|
12040
12166
|
});
|
|
12167
|
+
return gen;
|
|
12168
|
+
}
|
|
12169
|
+
/**
|
|
12170
|
+
* The runner's own `task_complete` call (ADR-0022, V1/V3): settles the
|
|
12171
|
+
* attempt exactly as the marker does, unless that attempt is no longer the
|
|
12172
|
+
* task's current one or another signal already settled it.
|
|
12173
|
+
*/
|
|
12174
|
+
signalComplete(taskId, generation, report) {
|
|
12175
|
+
if (this.generations.get(taskId) !== generation) return;
|
|
12176
|
+
this.forget(taskId);
|
|
12177
|
+
this.bumpGeneration(taskId);
|
|
12178
|
+
const verdict = reportedVerdict(report);
|
|
12179
|
+
for (const l of this.listeners) l(taskId, verdict);
|
|
12180
|
+
}
|
|
12181
|
+
/**
|
|
12182
|
+
* The runner's `checkpoint` call (ADR-0022, V5): raised through the same
|
|
12183
|
+
* listeners as the marker, so the task waits on the user the same way, and
|
|
12184
|
+
* settled by {@link approveCheckpoint} or {@link rejectCheckpoint}. It is
|
|
12185
|
+
* withdrawn, never left hanging, once the attempt is over or the call goes.
|
|
12186
|
+
*/
|
|
12187
|
+
raiseCheckpoint(taskId, generation, question, signal) {
|
|
12188
|
+
if (this.generations.get(taskId) !== generation || signal.aborted) {
|
|
12189
|
+
return Promise.resolve({ kind: "withdrawn", why: "this attempt has ended." });
|
|
12190
|
+
}
|
|
12191
|
+
if (this.toolCheckpoints.has(taskId)) {
|
|
12192
|
+
return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
|
|
12193
|
+
}
|
|
12194
|
+
return new Promise((resolve7) => {
|
|
12195
|
+
const settle = (answer2) => {
|
|
12196
|
+
signal.removeEventListener("abort", onAbort);
|
|
12197
|
+
if (this.toolCheckpoints.get(taskId) === settle) this.toolCheckpoints.delete(taskId);
|
|
12198
|
+
resolve7(answer2);
|
|
12199
|
+
};
|
|
12200
|
+
const onAbort = () => {
|
|
12201
|
+
settle({ kind: "withdrawn", why: "the call was cancelled." });
|
|
12202
|
+
if (this.generations.get(taskId) !== generation) return;
|
|
12203
|
+
this.resumeIdle(taskId);
|
|
12204
|
+
for (const l of this.withdrawnListeners) l(taskId);
|
|
12205
|
+
};
|
|
12206
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12207
|
+
this.toolCheckpoints.set(taskId, settle);
|
|
12208
|
+
for (const l of this.checkpointListeners) l(taskId, question.trim());
|
|
12209
|
+
});
|
|
12210
|
+
}
|
|
12211
|
+
withdrawToolCheckpoint(taskId) {
|
|
12212
|
+
this.toolCheckpoints.get(taskId)?.({ kind: "withdrawn", why: "this attempt has ended." });
|
|
12041
12213
|
}
|
|
12042
12214
|
approvalOpened(taskId, approvalId) {
|
|
12043
12215
|
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
@@ -12071,6 +12243,7 @@ ${line}
|
|
|
12071
12243
|
this.markerTails.delete(taskId);
|
|
12072
12244
|
this.checkpointCarry.delete(taskId);
|
|
12073
12245
|
this.pausedSessions.delete(taskId);
|
|
12246
|
+
this.withdrawToolCheckpoint(taskId);
|
|
12074
12247
|
this.idlePaused.delete(taskId);
|
|
12075
12248
|
this.openApprovals.delete(taskId);
|
|
12076
12249
|
this.clearIdle(taskId);
|
|
@@ -12106,6 +12279,7 @@ ${line}
|
|
|
12106
12279
|
this.markerTails.clear();
|
|
12107
12280
|
this.checkpointCarry.clear();
|
|
12108
12281
|
this.pausedSessions.clear();
|
|
12282
|
+
for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
|
|
12109
12283
|
for (const timer of this.idleTimers.values()) clearTimeout(timer);
|
|
12110
12284
|
this.idleTimers.clear();
|
|
12111
12285
|
this.idleSince.clear();
|
|
@@ -12159,6 +12333,28 @@ ${line}
|
|
|
12159
12333
|
};
|
|
12160
12334
|
}
|
|
12161
12335
|
};
|
|
12336
|
+
function reportedVerdict(report) {
|
|
12337
|
+
const checks = [
|
|
12338
|
+
{
|
|
12339
|
+
name: "task_complete",
|
|
12340
|
+
passed: report.status === "done",
|
|
12341
|
+
skipped: false,
|
|
12342
|
+
detail: `the runner called task_complete with status "${report.status}"`
|
|
12343
|
+
},
|
|
12344
|
+
{
|
|
12345
|
+
name: "completion_marker",
|
|
12346
|
+
passed: report.status === "done",
|
|
12347
|
+
skipped: true,
|
|
12348
|
+
detail: "bypassed \u2014 the runner reported through task_complete"
|
|
12349
|
+
}
|
|
12350
|
+
];
|
|
12351
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12352
|
+
if (report.status === "done") {
|
|
12353
|
+
return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt };
|
|
12354
|
+
}
|
|
12355
|
+
const why = report.reason?.trim() || "no reason given";
|
|
12356
|
+
return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt };
|
|
12357
|
+
}
|
|
12162
12358
|
|
|
12163
12359
|
// src/services/transcriptCapture.ts
|
|
12164
12360
|
var import_fs = require("fs");
|
|
@@ -12322,10 +12518,10 @@ async function fromDirenv(cwd, deps) {
|
|
|
12322
12518
|
}
|
|
12323
12519
|
return { env, blocked: null };
|
|
12324
12520
|
}
|
|
12325
|
-
function findUp(start,
|
|
12521
|
+
function findUp(start, relative6, deps) {
|
|
12326
12522
|
let dir = path10.resolve(start);
|
|
12327
12523
|
for (; ; ) {
|
|
12328
|
-
const candidate = path10.join(dir,
|
|
12524
|
+
const candidate = path10.join(dir, relative6);
|
|
12329
12525
|
if (deps.readFile(candidate) !== null) return candidate;
|
|
12330
12526
|
const parent = path10.dirname(dir);
|
|
12331
12527
|
if (parent === dir) return null;
|
|
@@ -12531,10 +12727,10 @@ function codexFinal(home, query, maxChars) {
|
|
|
12531
12727
|
function codexLastAssistant(file, query) {
|
|
12532
12728
|
const raw = (0, import_fs.readFileSync)(file, "utf8");
|
|
12533
12729
|
if (!raw.includes(query.marker)) return null;
|
|
12534
|
-
const
|
|
12730
|
+
const lines2 = raw.split("\n").filter((l) => l.trim());
|
|
12535
12731
|
let sawCwd = false;
|
|
12536
12732
|
let last = null;
|
|
12537
|
-
for (const line of
|
|
12733
|
+
for (const line of lines2) {
|
|
12538
12734
|
let rec;
|
|
12539
12735
|
try {
|
|
12540
12736
|
rec = JSON.parse(line);
|
|
@@ -12574,6 +12770,11 @@ var BufferedTaskOutputSource = class {
|
|
|
12574
12770
|
session.onExit(() => {
|
|
12575
12771
|
capture.exited = true;
|
|
12576
12772
|
});
|
|
12773
|
+
if (isStructuredSession(session)) {
|
|
12774
|
+
session.onTaskComplete(({ summary }) => {
|
|
12775
|
+
if (capture.attached && summary.trim()) capture.reported = summary.trim();
|
|
12776
|
+
});
|
|
12777
|
+
}
|
|
12577
12778
|
}
|
|
12578
12779
|
detach(taskId) {
|
|
12579
12780
|
const capture = this.captures.get(taskId);
|
|
@@ -12584,6 +12785,8 @@ var BufferedTaskOutputSource = class {
|
|
|
12584
12785
|
this.captures.clear();
|
|
12585
12786
|
}
|
|
12586
12787
|
async finalText(attempt, doneToken) {
|
|
12788
|
+
const reported = this.captures.get(attempt.taskId)?.reported;
|
|
12789
|
+
if (reported) return defuseMarkers(reported);
|
|
12587
12790
|
if (attempt.cwd && attempt.completionMarker) {
|
|
12588
12791
|
const transcript = await this.transcripts.finalAssistantText({
|
|
12589
12792
|
runner: attempt.runner,
|
|
@@ -12601,8 +12804,8 @@ var BufferedTaskOutputSource = class {
|
|
|
12601
12804
|
if (!capture) return null;
|
|
12602
12805
|
const total = capture.dropped + capture.raw.length;
|
|
12603
12806
|
const from = Math.min(Math.max(0, (opts.sinceOffset ?? 0) - capture.dropped), capture.raw.length);
|
|
12604
|
-
const
|
|
12605
|
-
const kept = opts.maxLines > 0 ?
|
|
12807
|
+
const lines2 = renderCleanCapture(capture.raw.slice(from)).split("\n");
|
|
12808
|
+
const kept = opts.maxLines > 0 ? lines2.slice(-opts.maxLines) : [];
|
|
12606
12809
|
return {
|
|
12607
12810
|
text: kept.join("\n"),
|
|
12608
12811
|
nextOffset: total,
|
|
@@ -13423,8 +13626,8 @@ var IsolationRunController = class {
|
|
|
13423
13626
|
this.resolvers = {};
|
|
13424
13627
|
this.reportedCopies.clear();
|
|
13425
13628
|
this.listener.changed();
|
|
13426
|
-
const
|
|
13427
|
-
if (
|
|
13629
|
+
const shared2 = sharedPathsNotice(this.run);
|
|
13630
|
+
if (shared2) this.tell("info", shared2);
|
|
13428
13631
|
}
|
|
13429
13632
|
reportCopies(copied) {
|
|
13430
13633
|
const fresh = copied.filter((p) => !this.reportedCopies.has(p));
|
|
@@ -14596,166 +14799,724 @@ ${tail}` : ""}`;
|
|
|
14596
14799
|
}
|
|
14597
14800
|
};
|
|
14598
14801
|
|
|
14599
|
-
// src/services/harness/
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
"
|
|
14604
|
-
"--input-format",
|
|
14605
|
-
"stream-json",
|
|
14606
|
-
"--output-format",
|
|
14607
|
-
"stream-json",
|
|
14608
|
-
"--verbose",
|
|
14609
|
-
"--include-partial-messages"
|
|
14610
|
-
];
|
|
14611
|
-
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
14612
|
-
var FOLLOW_ON_TURN_GRACE_MS = 5e3;
|
|
14613
|
-
var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
|
|
14614
|
-
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
14615
|
-
function usageRecord(usage, model, subagentId) {
|
|
14616
|
-
const record = {
|
|
14617
|
-
source: "claude-code",
|
|
14618
|
-
...partedPromptUsage({ uncached: usage.input_tokens, cacheRead: usage.cache_read_input_tokens, cacheWrite: usage.cache_creation_input_tokens })
|
|
14619
|
-
};
|
|
14620
|
-
if (model) record.model = model;
|
|
14621
|
-
if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
|
|
14622
|
-
if (subagentId) record.subagentId = subagentId;
|
|
14623
|
-
return record;
|
|
14624
|
-
}
|
|
14625
|
-
function subagentFinalCall(result) {
|
|
14626
|
-
if (typeof result !== "object" || result === null) return null;
|
|
14627
|
-
const { usage, resolvedModel } = result;
|
|
14628
|
-
if (typeof usage !== "object" || usage === null) return null;
|
|
14629
|
-
return { usage, model: typeof resolvedModel === "string" ? resolvedModel : void 0 };
|
|
14802
|
+
// src/services/harness/fileDiff.ts
|
|
14803
|
+
function markedLines(text2, mark) {
|
|
14804
|
+
if (!text2) return text2;
|
|
14805
|
+
const body = text2.endsWith("\n") ? text2.slice(0, -1) : text2;
|
|
14806
|
+
return `${body.split("\n").map((line) => `${mark}${line}`).join("\n")}${body === text2 ? "" : "\n"}`;
|
|
14630
14807
|
}
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14808
|
+
var HUNK_HEADER = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/;
|
|
14809
|
+
function hunksOf(diff) {
|
|
14810
|
+
const kept = [];
|
|
14811
|
+
let oldLeft = 0;
|
|
14812
|
+
let newLeft = 0;
|
|
14813
|
+
for (const line of diff.split("\n")) {
|
|
14814
|
+
if (oldLeft > 0 || newLeft > 0 || line.startsWith("\\")) {
|
|
14815
|
+
kept.push(line);
|
|
14816
|
+
if (line.startsWith("\\")) continue;
|
|
14817
|
+
if (!line.startsWith("+")) oldLeft -= 1;
|
|
14818
|
+
if (!line.startsWith("-")) newLeft -= 1;
|
|
14819
|
+
continue;
|
|
14820
|
+
}
|
|
14821
|
+
const header = HUNK_HEADER.exec(line);
|
|
14822
|
+
if (!header) continue;
|
|
14823
|
+
kept.push(line);
|
|
14824
|
+
oldLeft = header[1] === void 0 ? 1 : Number(header[1]);
|
|
14825
|
+
newLeft = header[2] === void 0 ? 1 : Number(header[2]);
|
|
14826
|
+
}
|
|
14827
|
+
return kept.length ? `${kept.join("\n")}${diff.endsWith("\n") ? "\n" : ""}` : diff;
|
|
14635
14828
|
}
|
|
14636
|
-
function
|
|
14637
|
-
return
|
|
14829
|
+
function range(start, lines2) {
|
|
14830
|
+
return lines2 === 1 ? `${start}` : `${start},${lines2}`;
|
|
14638
14831
|
}
|
|
14639
|
-
function
|
|
14640
|
-
if (
|
|
14641
|
-
|
|
14642
|
-
|
|
14832
|
+
function structuredPatchText(patch) {
|
|
14833
|
+
if (!Array.isArray(patch)) return "";
|
|
14834
|
+
let text2 = "";
|
|
14835
|
+
for (const hunk of patch) {
|
|
14836
|
+
if (!hunk || typeof hunk !== "object") return "";
|
|
14837
|
+
const { oldStart, oldLines, newStart, newLines, lines: lines2 } = hunk;
|
|
14838
|
+
if (typeof oldStart !== "number" || typeof oldLines !== "number" || typeof newStart !== "number" || typeof newLines !== "number") return "";
|
|
14839
|
+
if (!Array.isArray(lines2) || !lines2.every((line) => typeof line === "string")) return "";
|
|
14840
|
+
text2 += `@@ -${range(oldStart, oldLines)} +${range(newStart, newLines)} @@
|
|
14841
|
+
${lines2.map((line) => `${line}
|
|
14842
|
+
`).join("")}`;
|
|
14643
14843
|
}
|
|
14644
|
-
return
|
|
14844
|
+
return text2;
|
|
14645
14845
|
}
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
/**
|
|
14677
|
-
* A task's turn whose `result` arrived while background work was still open.
|
|
14678
|
-
* The CLI reports that result when the model stops talking, then opens a turn
|
|
14679
|
-
* of its own when the work finishes; a turn ended at the first result would
|
|
14680
|
-
* lose everything said after it, the completion marker included.
|
|
14681
|
-
*/
|
|
14682
|
-
resultHeld = false;
|
|
14683
|
-
followOnTimer = null;
|
|
14684
|
-
/** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
|
|
14685
|
-
requestedMode = null;
|
|
14686
|
-
/** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
|
|
14687
|
-
pendingResume = null;
|
|
14688
|
-
spawnSpec(opts) {
|
|
14689
|
-
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
14690
|
-
const args = [
|
|
14691
|
-
...PROTOCOL_ARGS,
|
|
14692
|
-
// The read-only guarantee, enforced at spawn rather than by prompt.
|
|
14693
|
-
"--permission-mode",
|
|
14694
|
-
"plan",
|
|
14695
|
-
"--disallowedTools",
|
|
14696
|
-
DISALLOWED_TOOLS.join(","),
|
|
14697
|
-
"--append-system-prompt",
|
|
14698
|
-
opts.systemPrompt
|
|
14699
|
-
];
|
|
14700
|
-
if (opts.model) args.push("--model", opts.model);
|
|
14701
|
-
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
14702
|
-
if (opts.resumeSessionId) {
|
|
14703
|
-
args.push("--resume", opts.resumeSessionId);
|
|
14704
|
-
this.reportedCostUsd = void 0;
|
|
14705
|
-
this.pendingResume = opts.resumeSessionId;
|
|
14846
|
+
|
|
14847
|
+
// src/services/mcp/OrdewellMcpServer.ts
|
|
14848
|
+
var import_http = require("http");
|
|
14849
|
+
var import_crypto4 = require("crypto");
|
|
14850
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
14851
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
14852
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
14853
|
+
|
|
14854
|
+
// src/utils/daemonToken.ts
|
|
14855
|
+
var import_crypto3 = require("crypto");
|
|
14856
|
+
var import_fs3 = require("fs");
|
|
14857
|
+
var import_path2 = require("path");
|
|
14858
|
+
|
|
14859
|
+
// src/utils/globalDataDir.ts
|
|
14860
|
+
var fs7 = __toESM(require("fs"));
|
|
14861
|
+
var path14 = __toESM(require("path"));
|
|
14862
|
+
var os5 = __toESM(require("os"));
|
|
14863
|
+
function globalDataDir() {
|
|
14864
|
+
return path14.join(os5.homedir(), ".ordewell");
|
|
14865
|
+
}
|
|
14866
|
+
function copyDirSync(src, dest) {
|
|
14867
|
+
fs7.mkdirSync(dest, { recursive: true });
|
|
14868
|
+
const entries = fs7.readdirSync(src, { withFileTypes: true });
|
|
14869
|
+
for (const entry of entries) {
|
|
14870
|
+
const srcPath = path14.join(src, entry.name);
|
|
14871
|
+
const destPath = path14.join(dest, entry.name);
|
|
14872
|
+
if (entry.isDirectory()) {
|
|
14873
|
+
copyDirSync(srcPath, destPath);
|
|
14874
|
+
} else {
|
|
14875
|
+
fs7.copyFileSync(srcPath, destPath);
|
|
14706
14876
|
}
|
|
14707
|
-
return { command: "claude", args };
|
|
14708
14877
|
}
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
const
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
"--permission-mode",
|
|
14724
|
-
opts.flags.permissionMode,
|
|
14725
|
-
"--disallowedTools",
|
|
14726
|
-
TASK_DISALLOWED_TOOLS.join(","),
|
|
14727
|
-
...opts.flags.effort ? claudeThinkingArgs(opts.flags.effort) : []
|
|
14728
|
-
];
|
|
14729
|
-
if (opts.model) args.push("--model", opts.model);
|
|
14730
|
-
if (opts.resumeSessionId) {
|
|
14731
|
-
args.push("--resume", opts.resumeSessionId);
|
|
14732
|
-
this.reportedCostUsd = void 0;
|
|
14733
|
-
this.pendingResume = opts.resumeSessionId;
|
|
14878
|
+
}
|
|
14879
|
+
var migrated = false;
|
|
14880
|
+
function migrateOldConfigDir() {
|
|
14881
|
+
if (migrated) return;
|
|
14882
|
+
migrated = true;
|
|
14883
|
+
const oldDir = path14.join(os5.homedir(), ".config", "ordewell");
|
|
14884
|
+
if (!fs7.existsSync(oldDir)) return;
|
|
14885
|
+
const newDir = globalDataDir();
|
|
14886
|
+
const liftFile = (name) => {
|
|
14887
|
+
const oldFile = path14.join(oldDir, name);
|
|
14888
|
+
const newFile = path14.join(newDir, name);
|
|
14889
|
+
if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
|
|
14890
|
+
fs7.mkdirSync(newDir, { recursive: true });
|
|
14891
|
+
fs7.copyFileSync(oldFile, newFile);
|
|
14734
14892
|
}
|
|
14735
|
-
|
|
14893
|
+
};
|
|
14894
|
+
liftFile("settings.json");
|
|
14895
|
+
liftFile(".env");
|
|
14896
|
+
const oldPlugins = path14.join(oldDir, "plugins");
|
|
14897
|
+
const newPlugins = path14.join(newDir, "plugins");
|
|
14898
|
+
if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
|
|
14899
|
+
copyDirSync(oldPlugins, newPlugins);
|
|
14736
14900
|
}
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14901
|
+
}
|
|
14902
|
+
|
|
14903
|
+
// src/utils/privateFile.ts
|
|
14904
|
+
var import_fs2 = require("fs");
|
|
14905
|
+
var import_path = require("path");
|
|
14906
|
+
var import_crypto2 = require("crypto");
|
|
14907
|
+
var DIR_MODE = 448;
|
|
14908
|
+
var FILE_MODE = 384;
|
|
14909
|
+
var POSIX_MODES = process.platform !== "win32";
|
|
14910
|
+
function ensurePrivateDir(dir) {
|
|
14911
|
+
if (POSIX_MODES) {
|
|
14912
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
|
|
14913
|
+
return;
|
|
14914
|
+
}
|
|
14915
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
14916
|
+
}
|
|
14917
|
+
function writePrivateFile(filePath, content) {
|
|
14918
|
+
const dir = (0, import_path.dirname)(filePath);
|
|
14919
|
+
ensurePrivateDir(dir);
|
|
14920
|
+
const tempPath = (0, import_path.join)(dir, `.${(0, import_path.basename)(filePath)}.${(0, import_crypto2.randomBytes)(8).toString("hex")}.tmp`);
|
|
14921
|
+
const fd = (0, import_fs2.openSync)(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
|
|
14922
|
+
try {
|
|
14923
|
+
(0, import_fs2.writeSync)(fd, content);
|
|
14924
|
+
} finally {
|
|
14925
|
+
(0, import_fs2.closeSync)(fd);
|
|
14926
|
+
}
|
|
14927
|
+
try {
|
|
14928
|
+
(0, import_fs2.renameSync)(tempPath, filePath);
|
|
14929
|
+
} catch (err) {
|
|
14930
|
+
try {
|
|
14931
|
+
(0, import_fs2.unlinkSync)(tempPath);
|
|
14932
|
+
} catch {
|
|
14933
|
+
}
|
|
14934
|
+
throw err;
|
|
14935
|
+
}
|
|
14936
|
+
}
|
|
14937
|
+
|
|
14938
|
+
// src/utils/daemonToken.ts
|
|
14939
|
+
var BEARER_PREFIX = "bearer ";
|
|
14940
|
+
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
14941
|
+
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
14942
|
+
function configDir() {
|
|
14943
|
+
return globalDataDir();
|
|
14944
|
+
}
|
|
14945
|
+
function daemonTokenPath(port) {
|
|
14946
|
+
return (0, import_path2.join)(configDir(), `server-${port}.token`);
|
|
14947
|
+
}
|
|
14948
|
+
function mintDaemonToken(port) {
|
|
14949
|
+
const token = (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
14950
|
+
const file = daemonTokenPath(port);
|
|
14951
|
+
writePrivateFile(file, token);
|
|
14952
|
+
return { token, file };
|
|
14953
|
+
}
|
|
14954
|
+
function readDaemonToken(port) {
|
|
14955
|
+
try {
|
|
14956
|
+
const token = (0, import_fs3.readFileSync)(daemonTokenPath(port), "utf8").trim();
|
|
14957
|
+
return token === "" ? void 0 : token;
|
|
14958
|
+
} catch {
|
|
14959
|
+
return void 0;
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
function clearDaemonToken(port) {
|
|
14963
|
+
try {
|
|
14964
|
+
(0, import_fs3.unlinkSync)(daemonTokenPath(port));
|
|
14965
|
+
} catch {
|
|
14966
|
+
}
|
|
14967
|
+
}
|
|
14968
|
+
function bearerHeaderValue(token) {
|
|
14969
|
+
return `Bearer ${token}`;
|
|
14970
|
+
}
|
|
14971
|
+
function tokenSubprotocols(token) {
|
|
14972
|
+
return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
|
|
14973
|
+
}
|
|
14974
|
+
function extractPresentedToken(carriers) {
|
|
14975
|
+
const authorization = carriers.authorization;
|
|
14976
|
+
if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
|
|
14977
|
+
const value = authorization.slice(BEARER_PREFIX.length).trim();
|
|
14978
|
+
if (value !== "") return value;
|
|
14979
|
+
}
|
|
14980
|
+
for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
|
|
14981
|
+
const value = offered.trim();
|
|
14982
|
+
if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
|
|
14983
|
+
const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
|
|
14984
|
+
if (token !== "") return token;
|
|
14985
|
+
}
|
|
14986
|
+
return void 0;
|
|
14987
|
+
}
|
|
14988
|
+
function tokensMatch(presented, expected) {
|
|
14989
|
+
if (presented === void 0) return false;
|
|
14990
|
+
const a = Buffer.from(presented, "utf8");
|
|
14991
|
+
const b = Buffer.from(expected, "utf8");
|
|
14992
|
+
if (a.length !== b.length) return false;
|
|
14993
|
+
return (0, import_crypto3.timingSafeEqual)(a, b);
|
|
14994
|
+
}
|
|
14995
|
+
|
|
14996
|
+
// src/services/mcp/tools.ts
|
|
14997
|
+
var import_v4 = require("zod/v4");
|
|
14998
|
+
var taskCompleteInput = import_v4.z.object({
|
|
14999
|
+
status: import_v4.z.enum(["done", "blocked", "failed"]).describe("'done' only when the task is fully complete. 'blocked' or 'failed' end the attempt without passing."),
|
|
15000
|
+
summary: import_v4.z.string().describe("What was done, handed to the tasks that depend on this one."),
|
|
15001
|
+
reason: import_v4.z.string().optional().describe("Why the task is blocked or failed. Expected for anything but 'done'.")
|
|
15002
|
+
});
|
|
15003
|
+
var checkpointInput = import_v4.z.object({
|
|
15004
|
+
question: import_v4.z.string().min(1).describe("The question for the user. The call returns their answer.")
|
|
15005
|
+
});
|
|
15006
|
+
var listRunnersInput = import_v4.z.object({});
|
|
15007
|
+
var listModelsInput = import_v4.z.object({
|
|
15008
|
+
runner: import_v4.z.string().min(1).describe("A runner id from list_runners.")
|
|
15009
|
+
});
|
|
15010
|
+
var taskRef = import_v4.z.union([import_v4.z.string().min(1), import_v4.z.number().int().min(0)]).describe('A task id, "#order", a bare order, or a title.');
|
|
15011
|
+
var modelAssignment = import_v4.z.looseObject({
|
|
15012
|
+
modelId: import_v4.z.string(),
|
|
15013
|
+
modelLabel: import_v4.z.string().optional(),
|
|
15014
|
+
thinkingEffort: import_v4.z.string().optional().describe("A variant id from that model's variants list.")
|
|
15015
|
+
});
|
|
15016
|
+
var planTask = import_v4.z.looseObject({
|
|
15017
|
+
id: import_v4.z.string().optional(),
|
|
15018
|
+
order: import_v4.z.number().optional(),
|
|
15019
|
+
title: import_v4.z.string(),
|
|
15020
|
+
description: import_v4.z.string().optional(),
|
|
15021
|
+
type: import_v4.z.enum(["ai", "user"]).optional(),
|
|
15022
|
+
dependencies: import_v4.z.array(import_v4.z.string()).optional(),
|
|
15023
|
+
prompt: import_v4.z.string().optional(),
|
|
15024
|
+
userSteps: import_v4.z.array(import_v4.z.looseObject({ order: import_v4.z.number().optional(), instruction: import_v4.z.string() })).optional(),
|
|
15025
|
+
assignedRunner: import_v4.z.string().optional(),
|
|
15026
|
+
assignedModel: modelAssignment.optional(),
|
|
15027
|
+
taskMode: import_v4.z.string().optional(),
|
|
15028
|
+
autonomy: import_v4.z.enum(["AFK", "HITL"]).optional(),
|
|
15029
|
+
sliceType: import_v4.z.enum(["AFK", "HITL"]).optional(),
|
|
15030
|
+
ops: import_v4.z.boolean().optional(),
|
|
15031
|
+
userStoriesCovered: import_v4.z.array(import_v4.z.string()).optional(),
|
|
15032
|
+
subtasks: import_v4.z.array(import_v4.z.record(import_v4.z.string(), import_v4.z.unknown())).optional()
|
|
15033
|
+
});
|
|
15034
|
+
var submitPlanInput = import_v4.z.object({
|
|
15035
|
+
tasks: import_v4.z.array(planTask).min(1).describe("The whole plan, in order.")
|
|
15036
|
+
});
|
|
15037
|
+
var taskFields = import_v4.z.record(import_v4.z.string(), import_v4.z.unknown());
|
|
15038
|
+
var handle = import_v4.z.string().optional();
|
|
15039
|
+
var taskOp = import_v4.z.discriminatedUnion("op", [
|
|
15040
|
+
import_v4.z.object({ op: import_v4.z.literal("update"), taskId: taskRef, changes: taskFields }),
|
|
15041
|
+
import_v4.z.object({ op: import_v4.z.literal("add"), task: taskFields, handle }),
|
|
15042
|
+
import_v4.z.object({ op: import_v4.z.literal("remove"), taskId: taskRef }),
|
|
15043
|
+
import_v4.z.object({ op: import_v4.z.literal("reorder"), taskIds: import_v4.z.array(taskRef) }),
|
|
15044
|
+
import_v4.z.object({ op: import_v4.z.literal("merge"), taskIds: import_v4.z.array(taskRef), merged: taskFields, handle }),
|
|
15045
|
+
import_v4.z.object({ op: import_v4.z.literal("split"), taskId: taskRef, parts: import_v4.z.array(taskFields), handle }),
|
|
15046
|
+
import_v4.z.object({ op: import_v4.z.literal("rearm"), taskId: taskRef, changes: taskFields.optional() })
|
|
15047
|
+
]);
|
|
15048
|
+
var editPlanInput = import_v4.z.object({
|
|
15049
|
+
ops: import_v4.z.array(taskOp).min(1).describe("Applied as one batch; every ref resolves against the plan before any op runs.")
|
|
15050
|
+
});
|
|
15051
|
+
var taskQueryInput = import_v4.z.object({
|
|
15052
|
+
tasks: import_v4.z.array(taskRef).optional(),
|
|
15053
|
+
fields: import_v4.z.array(import_v4.z.enum(TASK_QUERY_TASK_FIELDS)).optional().describe("Omit to read every field. A task's output is read with task_output."),
|
|
15054
|
+
catalog: import_v4.z.boolean().optional().describe("Also return every runner with its models, thinking-effort variants and modes.")
|
|
15055
|
+
}).refine((q) => (q.tasks?.length ?? 0) > 0 || q.catalog === true, { message: "Name at least one task, or set catalog: true." });
|
|
15056
|
+
var taskOutputInput = import_v4.z.object({
|
|
15057
|
+
task: taskRef,
|
|
15058
|
+
lines: import_v4.z.number().int().min(1).optional().describe(`How many lines to return; the default is 80 and anything over ${OUTPUT_LINES_MAX} is cut to ${OUTPUT_LINES_MAX}.`),
|
|
15059
|
+
since: import_v4.z.number().int().min(0).optional().describe("A previous answer's nextOffset: return only what came after it.")
|
|
15060
|
+
});
|
|
15061
|
+
function checkpointReply(answer2) {
|
|
15062
|
+
switch (answer2.kind) {
|
|
15063
|
+
case "continue":
|
|
15064
|
+
return { text: "continue" };
|
|
15065
|
+
case "rejected":
|
|
15066
|
+
return { text: `rejected: ${answer2.reason}` };
|
|
15067
|
+
case "withdrawn":
|
|
15068
|
+
return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
|
|
15069
|
+
}
|
|
15070
|
+
}
|
|
15071
|
+
function tool(name, description, input, pick, annotations) {
|
|
15072
|
+
return {
|
|
15073
|
+
name,
|
|
15074
|
+
description,
|
|
15075
|
+
inputSchema: { ...import_v4.z.toJSONSchema(input, { io: "input" }), type: "object" },
|
|
15076
|
+
...annotations ? { annotations } : {},
|
|
15077
|
+
async call(handler, args, context) {
|
|
15078
|
+
const run = pick(handler);
|
|
15079
|
+
if (!run) return { text: `${name} is not available in this session.`, isError: true };
|
|
15080
|
+
const parsed = input.safeParse(args ?? {});
|
|
15081
|
+
if (!parsed.success) return { text: `Invalid ${name} arguments: ${import_v4.z.prettifyError(parsed.error)}`, isError: true };
|
|
15082
|
+
return run(parsed.data, context);
|
|
15083
|
+
}
|
|
15084
|
+
};
|
|
15085
|
+
}
|
|
15086
|
+
var TASK_TOOLS = [
|
|
15087
|
+
tool(
|
|
15088
|
+
"task_complete",
|
|
15089
|
+
"Report that this task has ended, and how. Call it once, as your last action.",
|
|
15090
|
+
taskCompleteInput,
|
|
15091
|
+
(h) => h.taskComplete?.bind(h)
|
|
15092
|
+
),
|
|
15093
|
+
tool(
|
|
15094
|
+
"checkpoint",
|
|
15095
|
+
"Ask the user a question and wait for the answer, which is this call's result.",
|
|
15096
|
+
checkpointInput,
|
|
15097
|
+
(h) => h.checkpoint?.bind(h)
|
|
15098
|
+
)
|
|
15099
|
+
];
|
|
15100
|
+
var PLANNER_READ_ONLY = { readOnlyHint: true };
|
|
15101
|
+
var PLANNER_TOOLS = [
|
|
15102
|
+
tool(
|
|
15103
|
+
"list_runners",
|
|
15104
|
+
"List the runners enabled right now, each with its modes and default mode. Call it just before submit_plan.",
|
|
15105
|
+
listRunnersInput,
|
|
15106
|
+
(h) => h.listRunners?.bind(h),
|
|
15107
|
+
PLANNER_READ_ONLY
|
|
15108
|
+
),
|
|
15109
|
+
tool(
|
|
15110
|
+
"list_models",
|
|
15111
|
+
"List the models a runner may use right now, with labels and thinking-effort variants.",
|
|
15112
|
+
listModelsInput,
|
|
15113
|
+
(h) => h.listModels?.bind(h),
|
|
15114
|
+
PLANNER_READ_ONLY
|
|
15115
|
+
),
|
|
15116
|
+
tool(
|
|
15117
|
+
"submit_plan",
|
|
15118
|
+
"Submit the whole plan. It is checked against the live runners and models; an error names the task, the field and what would be accepted.",
|
|
15119
|
+
submitPlanInput,
|
|
15120
|
+
(h) => h.submitPlan?.bind(h),
|
|
15121
|
+
PLANNER_READ_ONLY
|
|
15122
|
+
),
|
|
15123
|
+
tool(
|
|
15124
|
+
"edit_plan",
|
|
15125
|
+
"Change the current plan with task operations: update, add, remove, reorder, merge, split or rearm.",
|
|
15126
|
+
editPlanInput,
|
|
15127
|
+
(h) => h.editPlan?.bind(h),
|
|
15128
|
+
PLANNER_READ_ONLY
|
|
15129
|
+
),
|
|
15130
|
+
tool(
|
|
15131
|
+
"task_query",
|
|
15132
|
+
"Read the long fields of plan tasks that the plan summary leaves out (prompt, user steps, verdict, output summary), and optionally the live runner catalog. Read a task before you rewrite it.",
|
|
15133
|
+
taskQueryInput,
|
|
15134
|
+
(h) => h.taskQuery?.bind(h),
|
|
15135
|
+
PLANNER_READ_ONLY
|
|
15136
|
+
),
|
|
15137
|
+
tool(
|
|
15138
|
+
"task_output",
|
|
15139
|
+
"Read the recent output of a running task, to check what its runner is doing; paged by offset. A task that is not running answers with its verdict, output summary and a digest of its last attempt.",
|
|
15140
|
+
taskOutputInput,
|
|
15141
|
+
(h) => h.taskOutput?.bind(h),
|
|
15142
|
+
PLANNER_READ_ONLY
|
|
15143
|
+
)
|
|
15144
|
+
];
|
|
15145
|
+
|
|
15146
|
+
// src/services/mcp/OrdewellMcpServer.ts
|
|
15147
|
+
var ORDEWELL_MCP_PATH = "/mcp";
|
|
15148
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
15149
|
+
var OrdewellMcpServer = class {
|
|
15150
|
+
grants = /* @__PURE__ */ new Map();
|
|
15151
|
+
http;
|
|
15152
|
+
listening;
|
|
15153
|
+
boundUrl;
|
|
15154
|
+
boundHost;
|
|
15155
|
+
heartbeatMs;
|
|
15156
|
+
constructor(options = {}) {
|
|
15157
|
+
this.heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
15158
|
+
}
|
|
15159
|
+
/** The listener's URL, once the first token has started it. */
|
|
15160
|
+
get url() {
|
|
15161
|
+
return this.boundUrl;
|
|
15162
|
+
}
|
|
15163
|
+
issueTaskToken(scope, handler = {}) {
|
|
15164
|
+
return this.issue({ role: "task", scope: { ...scope }, handler, revoked: new AbortController() });
|
|
15165
|
+
}
|
|
15166
|
+
issuePlannerToken(scope, handler = {}) {
|
|
15167
|
+
return this.issue({ role: "planner", scope: { ...scope }, handler, revoked: new AbortController() });
|
|
15168
|
+
}
|
|
15169
|
+
/** Refuse every later request carrying `token`, and abort the calls it has in flight. */
|
|
15170
|
+
revoke(token) {
|
|
15171
|
+
const grant = this.grants.get(token);
|
|
15172
|
+
if (!grant) return;
|
|
15173
|
+
this.grants.delete(token);
|
|
15174
|
+
grant.revoked.abort();
|
|
15175
|
+
}
|
|
15176
|
+
async dispose() {
|
|
15177
|
+
for (const token of [...this.grants.keys()]) this.revoke(token);
|
|
15178
|
+
const http = this.http;
|
|
15179
|
+
this.http = void 0;
|
|
15180
|
+
this.listening = void 0;
|
|
15181
|
+
this.boundUrl = void 0;
|
|
15182
|
+
this.boundHost = void 0;
|
|
15183
|
+
if (!http) return;
|
|
15184
|
+
http.closeAllConnections();
|
|
15185
|
+
await new Promise((resolve7) => http.close(() => resolve7()));
|
|
15186
|
+
}
|
|
15187
|
+
async issue(grant) {
|
|
15188
|
+
const url = await this.start();
|
|
15189
|
+
const token = (0, import_crypto4.randomBytes)(32).toString("base64url");
|
|
15190
|
+
this.grants.set(token, grant);
|
|
15191
|
+
return { url, token };
|
|
15192
|
+
}
|
|
15193
|
+
start() {
|
|
15194
|
+
this.listening ??= new Promise((resolve7, reject) => {
|
|
15195
|
+
const http = (0, import_http.createServer)((req, res) => {
|
|
15196
|
+
this.handle(req, res).catch(() => {
|
|
15197
|
+
if (!res.headersSent) reply(res, 500, "Internal error");
|
|
15198
|
+
else res.destroy();
|
|
15199
|
+
});
|
|
15200
|
+
});
|
|
15201
|
+
http.once("error", (err) => {
|
|
15202
|
+
this.listening = void 0;
|
|
15203
|
+
reject(err);
|
|
15204
|
+
});
|
|
15205
|
+
http.listen(0, "127.0.0.1", () => {
|
|
15206
|
+
const address = http.address();
|
|
15207
|
+
if (address === null || typeof address === "string") {
|
|
15208
|
+
reject(new Error("Ordewell MCP server bound to no TCP port"));
|
|
15209
|
+
return;
|
|
15210
|
+
}
|
|
15211
|
+
this.boundHost = `127.0.0.1:${address.port}`;
|
|
15212
|
+
this.boundUrl = `http://${this.boundHost}${ORDEWELL_MCP_PATH}`;
|
|
15213
|
+
resolve7(this.boundUrl);
|
|
15214
|
+
});
|
|
15215
|
+
http.unref();
|
|
15216
|
+
this.http = http;
|
|
15217
|
+
});
|
|
15218
|
+
return this.listening;
|
|
15219
|
+
}
|
|
15220
|
+
async handle(req, res) {
|
|
15221
|
+
if (req.headers.host !== this.boundHost || req.headers.origin !== void 0) {
|
|
15222
|
+
reply(res, 403, "Forbidden");
|
|
15223
|
+
return;
|
|
15224
|
+
}
|
|
15225
|
+
const token = extractPresentedToken({ authorization: req.headers.authorization });
|
|
15226
|
+
const grant = token === void 0 ? void 0 : this.grants.get(token);
|
|
15227
|
+
if (!grant) {
|
|
15228
|
+
reply(res, 401, "Unauthorized");
|
|
15229
|
+
return;
|
|
15230
|
+
}
|
|
15231
|
+
if (new URL(req.url ?? "/", `http://${this.boundHost}`).pathname !== ORDEWELL_MCP_PATH) {
|
|
15232
|
+
reply(res, 404, "Not found");
|
|
15233
|
+
return;
|
|
15234
|
+
}
|
|
15235
|
+
const mcp = new import_server.Server({ name: "ordewell", version: "1" }, { capabilities: { tools: {} } });
|
|
15236
|
+
if (grant.role === "task") serveTools(mcp, TASK_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
15237
|
+
else serveTools(mcp, PLANNER_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
15238
|
+
const transport = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
15239
|
+
res.on("close", () => {
|
|
15240
|
+
void transport.close();
|
|
15241
|
+
void mcp.close();
|
|
15242
|
+
});
|
|
15243
|
+
await mcp.connect(transport);
|
|
15244
|
+
await transport.handleRequest(req, res);
|
|
15245
|
+
}
|
|
15246
|
+
};
|
|
15247
|
+
function serveTools(mcp, tools, handler, revoked, heartbeatMs) {
|
|
15248
|
+
mcp.setRequestHandler(import_types.ListToolsRequestSchema, () => ({
|
|
15249
|
+
tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, ...annotations ? { annotations } : {} }))
|
|
15250
|
+
}));
|
|
15251
|
+
mcp.setRequestHandler(import_types.CallToolRequestSchema, async (request, extra) => {
|
|
15252
|
+
const tool2 = tools.find((t) => t.name === request.params.name);
|
|
15253
|
+
if (!tool2) throw new import_types.McpError(import_types.ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
|
|
15254
|
+
const progressToken = request.params._meta?.progressToken;
|
|
15255
|
+
let beats = 0;
|
|
15256
|
+
const heartbeat = progressToken === void 0 ? void 0 : setInterval(() => {
|
|
15257
|
+
beats += 1;
|
|
15258
|
+
extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress: beats } }).catch(() => void 0);
|
|
15259
|
+
}, heartbeatMs);
|
|
15260
|
+
try {
|
|
15261
|
+
const result = await tool2.call(handler, request.params.arguments, { signal: AbortSignal.any([revoked, extra.signal]) });
|
|
15262
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError ?? false };
|
|
15263
|
+
} catch (err) {
|
|
15264
|
+
return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
|
|
15265
|
+
} finally {
|
|
15266
|
+
clearInterval(heartbeat);
|
|
15267
|
+
}
|
|
15268
|
+
});
|
|
15269
|
+
}
|
|
15270
|
+
function reply(res, status, message) {
|
|
15271
|
+
res.writeHead(status, { "content-type": "text/plain" }).end(message);
|
|
15272
|
+
}
|
|
15273
|
+
var shared;
|
|
15274
|
+
function sharedMcpServer() {
|
|
15275
|
+
shared ??= new OrdewellMcpServer();
|
|
15276
|
+
return shared;
|
|
15277
|
+
}
|
|
15278
|
+
|
|
15279
|
+
// src/services/mcp/clientConfig.ts
|
|
15280
|
+
var import_fs4 = require("fs");
|
|
15281
|
+
var import_os = require("os");
|
|
15282
|
+
var import_path3 = require("path");
|
|
15283
|
+
var ORDEWELL_MCP_SERVER_NAME = "ordewell";
|
|
15284
|
+
function mcpClientConfig(credential) {
|
|
15285
|
+
return {
|
|
15286
|
+
name: ORDEWELL_MCP_SERVER_NAME,
|
|
15287
|
+
url: credential.url,
|
|
15288
|
+
headers: { Authorization: bearerHeaderValue(credential.token) }
|
|
15289
|
+
};
|
|
15290
|
+
}
|
|
15291
|
+
function ownerOnlyConfigFile(fileName, contents) {
|
|
15292
|
+
const dir = (0, import_fs4.mkdtempSync)((0, import_path3.join)((0, import_os.tmpdir)(), "ordewell-mcp-"));
|
|
15293
|
+
const path27 = (0, import_path3.join)(dir, fileName);
|
|
15294
|
+
(0, import_fs4.writeFileSync)(path27, contents, { mode: 384 });
|
|
15295
|
+
return { path: path27, remove: () => (0, import_fs4.rmSync)(dir, { recursive: true, force: true }) };
|
|
15296
|
+
}
|
|
15297
|
+
|
|
15298
|
+
// src/services/harness/ClaudeCodeAdapter.ts
|
|
15299
|
+
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
15300
|
+
var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
|
|
15301
|
+
var PROTOCOL_ARGS = [
|
|
15302
|
+
"-p",
|
|
15303
|
+
"--input-format",
|
|
15304
|
+
"stream-json",
|
|
15305
|
+
"--output-format",
|
|
15306
|
+
"stream-json",
|
|
15307
|
+
"--verbose",
|
|
15308
|
+
"--include-partial-messages"
|
|
15309
|
+
];
|
|
15310
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
15311
|
+
var FOLLOW_ON_TURN_GRACE_MS = 5e3;
|
|
15312
|
+
var MCP_ATTACH_TIMEOUT_MS = 1e4;
|
|
15313
|
+
var MCP_STATUS_POLL_MS = 100;
|
|
15314
|
+
var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
|
|
15315
|
+
var ORDEWELL_TASK_TOOLS = ["task_complete", "checkpoint"];
|
|
15316
|
+
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
15317
|
+
function usageRecord(usage, model, subagentId) {
|
|
15318
|
+
const record = {
|
|
15319
|
+
source: "claude-code",
|
|
15320
|
+
...partedPromptUsage({ uncached: usage.input_tokens, cacheRead: usage.cache_read_input_tokens, cacheWrite: usage.cache_creation_input_tokens })
|
|
15321
|
+
};
|
|
15322
|
+
if (model) record.model = model;
|
|
15323
|
+
if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
|
|
15324
|
+
if (subagentId) record.subagentId = subagentId;
|
|
15325
|
+
return record;
|
|
15326
|
+
}
|
|
15327
|
+
function subagentFinalCall(result) {
|
|
15328
|
+
if (typeof result !== "object" || result === null) return null;
|
|
15329
|
+
const { usage, resolvedModel } = result;
|
|
15330
|
+
if (typeof usage !== "object" || usage === null) return null;
|
|
15331
|
+
return { usage, model: typeof resolvedModel === "string" ? resolvedModel : void 0 };
|
|
15332
|
+
}
|
|
15333
|
+
function notificationOutcome(status) {
|
|
15334
|
+
if (status === "completed") return "done";
|
|
15335
|
+
if (status === "killed" || status === "stopped") return "stopped";
|
|
15336
|
+
return "failed";
|
|
15337
|
+
}
|
|
15338
|
+
function blocksOf(msg) {
|
|
15339
|
+
return Array.isArray(msg.message?.content) ? msg.message.content : [];
|
|
15340
|
+
}
|
|
15341
|
+
function flattenContent(content) {
|
|
15342
|
+
if (typeof content === "string") return content;
|
|
15343
|
+
if (Array.isArray(content)) {
|
|
15344
|
+
return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").filter(Boolean).join("\n");
|
|
15345
|
+
}
|
|
15346
|
+
return "";
|
|
15347
|
+
}
|
|
15348
|
+
function editDiff(result) {
|
|
15349
|
+
if (!result || typeof result !== "object") return "";
|
|
15350
|
+
const { type, content, structuredPatch } = result;
|
|
15351
|
+
if (type === "create" && typeof content === "string") return markedLines(content, "+");
|
|
15352
|
+
return structuredPatchText(structuredPatch);
|
|
15353
|
+
}
|
|
15354
|
+
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
15355
|
+
agentId = "claude-code";
|
|
15356
|
+
controlCount = 0;
|
|
15357
|
+
/** Control requests sent and not yet answered, by request id. */
|
|
15358
|
+
pendingControl = /* @__PURE__ */ new Map();
|
|
15359
|
+
/** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
|
|
15360
|
+
interruptRequested = false;
|
|
15361
|
+
/** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
|
|
15362
|
+
openPermissions = /* @__PURE__ */ new Map();
|
|
15363
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
15364
|
+
turnHasText = false;
|
|
15365
|
+
/** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
|
|
15366
|
+
pendingBreak = false;
|
|
15367
|
+
/** The model answering the planner's current message, as its `message_start` named it. */
|
|
15368
|
+
plannerModel;
|
|
15369
|
+
/**
|
|
15370
|
+
* The session's `total_cost_usd` as last reported. Undefined after a resume:
|
|
15371
|
+
* the CLI restores the resumed session's running total, and what Ordewell
|
|
15372
|
+
* already counted of it is not ours to know here.
|
|
15373
|
+
*/
|
|
15374
|
+
reportedCostUsd = 0;
|
|
15375
|
+
/**
|
|
15376
|
+
* Subagents started and not yet finished, keyed by the `Agent` call's id.
|
|
15377
|
+
* Kept across turns: a backgrounded one reports after its turn has ended.
|
|
15378
|
+
*/
|
|
15379
|
+
openSubagents = /* @__PURE__ */ new Map();
|
|
15380
|
+
/** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
|
|
15381
|
+
countedSubagentMessages = /* @__PURE__ */ new Set();
|
|
15382
|
+
/** Background tasks (shells, agents) the CLI reports as still running, as of its last `background_tasks_changed`. */
|
|
15383
|
+
backgroundTaskCount = 0;
|
|
15384
|
+
/**
|
|
15385
|
+
* A task's turn whose `result` arrived while background work was still open.
|
|
15386
|
+
* The CLI reports that result when the model stops talking, then opens a turn
|
|
15387
|
+
* of its own when the work finishes; a turn ended at the first result would
|
|
15388
|
+
* lose everything said after it, the completion marker included.
|
|
15389
|
+
*/
|
|
15390
|
+
resultHeld = false;
|
|
15391
|
+
followOnTimer = null;
|
|
15392
|
+
/** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
|
|
15393
|
+
requestedMode = null;
|
|
15394
|
+
/** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
|
|
15395
|
+
pendingResume = null;
|
|
15396
|
+
/** The `--mcp-config` file of the running process, which holds its token; removed with the process. */
|
|
15397
|
+
mcpConfig = null;
|
|
15398
|
+
/** `mcp__ordewell__`, once this process was given the server. */
|
|
15399
|
+
ordewellToolPrefix = null;
|
|
15400
|
+
spawnSpec(opts) {
|
|
15401
|
+
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
15402
|
+
const args = [
|
|
15403
|
+
...PROTOCOL_ARGS,
|
|
15404
|
+
// The read-only guarantee, enforced at spawn rather than by prompt.
|
|
15405
|
+
"--permission-mode",
|
|
15406
|
+
"plan",
|
|
15407
|
+
"--disallowedTools",
|
|
15408
|
+
DISALLOWED_TOOLS.join(","),
|
|
15409
|
+
"--append-system-prompt",
|
|
15410
|
+
opts.systemPrompt
|
|
15411
|
+
];
|
|
15412
|
+
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, PLANNER_TOOLS.map((t) => t.name)));
|
|
15413
|
+
if (opts.model) args.push("--model", opts.model);
|
|
15414
|
+
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
15415
|
+
if (opts.resumeSessionId) {
|
|
15416
|
+
args.push("--resume", opts.resumeSessionId);
|
|
15417
|
+
this.reportedCostUsd = void 0;
|
|
15418
|
+
this.pendingResume = opts.resumeSessionId;
|
|
15419
|
+
}
|
|
15420
|
+
return { command: "claude", args };
|
|
15421
|
+
}
|
|
15422
|
+
/**
|
|
15423
|
+
* A task's run: the manifest decides what its mode and effort mean
|
|
15424
|
+
* (ADR-0001), and this adds only the protocol around them. No tool list and
|
|
15425
|
+
* no system prompt — the task's prompt is its first turn, as on the terminal
|
|
15426
|
+
* transport. `--permission-prompt-tool stdio` routes the questions the mode
|
|
15427
|
+
* leaves open to the control channel, where the adapter must answer them;
|
|
15428
|
+
* without it `-p` refuses them silently and nothing can ever surface one.
|
|
15429
|
+
*/
|
|
15430
|
+
taskSpawnSpec(opts) {
|
|
15431
|
+
this.requestedMode = opts.flags.permissionMode;
|
|
15432
|
+
const args = [
|
|
15433
|
+
...PROTOCOL_ARGS,
|
|
15434
|
+
"--permission-prompt-tool",
|
|
15435
|
+
"stdio",
|
|
15436
|
+
"--permission-mode",
|
|
15437
|
+
opts.flags.permissionMode,
|
|
15438
|
+
"--disallowedTools",
|
|
15439
|
+
TASK_DISALLOWED_TOOLS.join(","),
|
|
15440
|
+
...opts.flags.effort ? claudeThinkingArgs(opts.flags.effort) : []
|
|
15441
|
+
];
|
|
15442
|
+
if (opts.model) args.push("--model", opts.model);
|
|
15443
|
+
if (opts.resumeSessionId) {
|
|
15444
|
+
args.push("--resume", opts.resumeSessionId);
|
|
15445
|
+
this.reportedCostUsd = void 0;
|
|
15446
|
+
this.pendingResume = opts.resumeSessionId;
|
|
15447
|
+
}
|
|
15448
|
+
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, ORDEWELL_TASK_TOOLS));
|
|
15449
|
+
return { command: "claude", args };
|
|
15450
|
+
}
|
|
15451
|
+
/**
|
|
15452
|
+
* `--mcp-config` takes a path as well as inline JSON; the path keeps the
|
|
15453
|
+
* token out of the argv every local user can list (ADR-0022, A5). The file
|
|
15454
|
+
* lives exactly as long as the process that reads it. `alwaysLoad` because
|
|
15455
|
+
* the CLI otherwise defers MCP tools behind its tool search, and a model
|
|
15456
|
+
* that has to look an Ordewell tool up first tends to go on without it.
|
|
15457
|
+
*/
|
|
15458
|
+
ordewellServerArgs(mcp, tools) {
|
|
15459
|
+
this.removeMcpConfig();
|
|
15460
|
+
const config = { mcpServers: { [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } } };
|
|
15461
|
+
const file = ownerOnlyConfigFile("mcp.json", JSON.stringify(config));
|
|
15462
|
+
this.mcpConfig = file;
|
|
15463
|
+
void this.processEnded.then(() => file.remove());
|
|
15464
|
+
const prefix = `mcp__${mcp.name}__`;
|
|
15465
|
+
this.ordewellToolPrefix = prefix;
|
|
15466
|
+
return ["--mcp-config", file.path, "--allowedTools", tools.map((tool2) => `${prefix}${tool2}`).join(",")];
|
|
15467
|
+
}
|
|
15468
|
+
removeMcpConfig() {
|
|
15469
|
+
this.mcpConfig?.remove();
|
|
15470
|
+
this.mcpConfig = null;
|
|
15471
|
+
}
|
|
15472
|
+
async start(opts) {
|
|
15473
|
+
try {
|
|
15474
|
+
await super.start(opts);
|
|
15475
|
+
} catch (err) {
|
|
15476
|
+
this.removeMcpConfig();
|
|
15477
|
+
throw err;
|
|
15478
|
+
}
|
|
15479
|
+
}
|
|
15480
|
+
/** Asks the CLI itself, which reports `pending` until its connection attempt settles. */
|
|
15481
|
+
async mcpAttached() {
|
|
15482
|
+
if (!this.mcpConfig) return false;
|
|
15483
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS;
|
|
15484
|
+
while (Date.now() < deadline) {
|
|
15485
|
+
const answer2 = await this.control({ subtype: "mcp_status" }, deadline - Date.now());
|
|
15486
|
+
const status = answer2?.response?.mcpServers?.find((s) => s.name === ORDEWELL_MCP_SERVER_NAME)?.status;
|
|
15487
|
+
if (status === "connected") return true;
|
|
15488
|
+
if (status !== "pending") return false;
|
|
15489
|
+
await new Promise((resolve7) => setTimeout(resolve7, MCP_STATUS_POLL_MS));
|
|
15490
|
+
}
|
|
15491
|
+
return false;
|
|
15492
|
+
}
|
|
15493
|
+
/**
|
|
15494
|
+
* Claude Code's soft interrupt: the turn stops, the process and its session
|
|
15495
|
+
* stay. The CLI acknowledges on the control channel, then closes the turn
|
|
15496
|
+
* with an `error_during_execution` result, which {@link handleLine} reports
|
|
15497
|
+
* as an interrupted `turn_end`.
|
|
15498
|
+
*/
|
|
15499
|
+
async interrupt(timeoutMs) {
|
|
15500
|
+
if (!this.process) return false;
|
|
15501
|
+
this.interruptRequested = true;
|
|
15502
|
+
return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
|
|
15503
|
+
}
|
|
15504
|
+
/** Send a control request and wait for its answer; null when none came in time or the process ended. */
|
|
15505
|
+
control(request, timeoutMs) {
|
|
15506
|
+
if (!this.process) return Promise.resolve(null);
|
|
15507
|
+
this.controlCount += 1;
|
|
15508
|
+
const requestId = `ordewell-${request.subtype}-${this.controlCount}`;
|
|
15509
|
+
return new Promise((resolve7) => {
|
|
15510
|
+
const settle = (response) => {
|
|
15511
|
+
if (!this.pendingControl.delete(requestId)) return;
|
|
14751
15512
|
clearTimeout(timer);
|
|
14752
|
-
resolve7(
|
|
15513
|
+
resolve7(response);
|
|
14753
15514
|
};
|
|
14754
|
-
const timer = setTimeout(() => settle(
|
|
15515
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
14755
15516
|
timer.unref?.();
|
|
14756
15517
|
this.pendingControl.set(requestId, settle);
|
|
14757
|
-
void this.processEnded.then(() => settle(
|
|
14758
|
-
this.writeLine({ type: "control_request", request_id: requestId, request
|
|
15518
|
+
void this.processEnded.then(() => settle(null));
|
|
15519
|
+
this.writeLine({ type: "control_request", request_id: requestId, request });
|
|
14759
15520
|
});
|
|
14760
15521
|
}
|
|
14761
15522
|
/**
|
|
@@ -14790,6 +15551,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
14790
15551
|
}
|
|
14791
15552
|
dispose() {
|
|
14792
15553
|
this.clearFollowOnTimer();
|
|
15554
|
+
this.removeMcpConfig();
|
|
14793
15555
|
super.dispose();
|
|
14794
15556
|
}
|
|
14795
15557
|
clearFollowOnTimer() {
|
|
@@ -14840,12 +15602,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
14840
15602
|
emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
|
|
14841
15603
|
return;
|
|
14842
15604
|
}
|
|
15605
|
+
const name = msg.request.tool_name ?? "unknown";
|
|
15606
|
+
if (this.ordewellToolPrefix && name.startsWith(this.ordewellToolPrefix)) {
|
|
15607
|
+
this.writeLine({ type: "control_response", response: { subtype: "success", request_id: id, response: { behavior: "allow", updatedInput: input } } });
|
|
15608
|
+
emit({ type: "permission_request", id, name, detail: JSON.stringify(input), input, decided: { decision: "allow" } });
|
|
15609
|
+
return;
|
|
15610
|
+
}
|
|
14843
15611
|
const suggestions = msg.request.permission_suggestions ?? [];
|
|
14844
15612
|
this.openPermissions.set(id, { input, suggestions });
|
|
14845
15613
|
emit({
|
|
14846
15614
|
type: "permission_request",
|
|
14847
15615
|
id,
|
|
14848
|
-
name
|
|
15616
|
+
name,
|
|
14849
15617
|
detail: JSON.stringify(input),
|
|
14850
15618
|
input,
|
|
14851
15619
|
suggestions,
|
|
@@ -14861,7 +15629,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
14861
15629
|
}
|
|
14862
15630
|
case "control_response": {
|
|
14863
15631
|
const requestId = msg.response?.request_id;
|
|
14864
|
-
if (requestId) this.pendingControl.get(requestId)?.(msg.response
|
|
15632
|
+
if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
|
|
14865
15633
|
return;
|
|
14866
15634
|
}
|
|
14867
15635
|
case "assistant":
|
|
@@ -14889,6 +15657,7 @@ ${block.text}` : block.text });
|
|
|
14889
15657
|
if (block.type !== "tool_result") continue;
|
|
14890
15658
|
const id = block.tool_use_id ?? "";
|
|
14891
15659
|
const output = flattenContent(block.content);
|
|
15660
|
+
const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
|
|
14892
15661
|
const subagent = subagentId ? void 0 : this.openSubagents.get(id);
|
|
14893
15662
|
if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
14894
15663
|
emit({ type: "background_agent", id });
|
|
@@ -14896,7 +15665,7 @@ ${block.text}` : block.text });
|
|
|
14896
15665
|
} else if (subagent) {
|
|
14897
15666
|
this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
|
|
14898
15667
|
}
|
|
14899
|
-
emit({ type: "tool_result", id, name: "", output, success: block.is_error !== true, subagentId });
|
|
15668
|
+
emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
|
|
14900
15669
|
}
|
|
14901
15670
|
return;
|
|
14902
15671
|
case "system":
|
|
@@ -15032,6 +15801,9 @@ ${block.text}` : block.text });
|
|
|
15032
15801
|
}
|
|
15033
15802
|
};
|
|
15034
15803
|
|
|
15804
|
+
// src/services/harness/CodexAdapter.ts
|
|
15805
|
+
var import_path4 = require("path");
|
|
15806
|
+
|
|
15035
15807
|
// src/services/harness/codexSandbox.ts
|
|
15036
15808
|
var PROBE_TIMEOUT_MS = 1e4;
|
|
15037
15809
|
var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
|
|
@@ -15091,6 +15863,17 @@ function runProbe(deps, cwd, env, flags) {
|
|
|
15091
15863
|
|
|
15092
15864
|
// src/services/harness/CodexAdapter.ts
|
|
15093
15865
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
15866
|
+
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
15867
|
+
var CHANGE_NAMES = { add: "Add", update: "Update", delete: "Delete" };
|
|
15868
|
+
function changeName(change) {
|
|
15869
|
+
return CHANGE_NAMES[change.kind?.type ?? ""] ?? "file_change";
|
|
15870
|
+
}
|
|
15871
|
+
function changeDiff(change) {
|
|
15872
|
+
const diff = change.diff ?? "";
|
|
15873
|
+
if (change.kind?.type === "add") return markedLines(diff, "+");
|
|
15874
|
+
if (change.kind?.type === "delete") return markedLines(diff, "-");
|
|
15875
|
+
return diff;
|
|
15876
|
+
}
|
|
15094
15877
|
function subagentOutcome(status) {
|
|
15095
15878
|
switch (status) {
|
|
15096
15879
|
case "completed":
|
|
@@ -15124,6 +15907,28 @@ var DECLINE_RESULTS = {
|
|
|
15124
15907
|
execCommandApproval: { decision: "denied" },
|
|
15125
15908
|
applyPatchApproval: { decision: "denied" }
|
|
15126
15909
|
};
|
|
15910
|
+
function shownToolName(item) {
|
|
15911
|
+
const tool2 = item.tool ?? "mcp_tool";
|
|
15912
|
+
return item.server === ORDEWELL_MCP_SERVER_NAME ? `mcp__${ORDEWELL_MCP_SERVER_NAME}__${tool2}` : tool2;
|
|
15913
|
+
}
|
|
15914
|
+
function mcpHeaderEnv(mcp) {
|
|
15915
|
+
return Object.fromEntries(Object.keys(mcp.headers).map((header, i) => [header, `ORDEWELL_MCP_TOKEN_${i}`]));
|
|
15916
|
+
}
|
|
15917
|
+
function ordewellServerConfig(mcp) {
|
|
15918
|
+
return {
|
|
15919
|
+
url: mcp.url,
|
|
15920
|
+
env_http_headers: mcpHeaderEnv(mcp),
|
|
15921
|
+
default_tools_approval_mode: "approve"
|
|
15922
|
+
};
|
|
15923
|
+
}
|
|
15924
|
+
var TASK_TOOLS_NOTE = [
|
|
15925
|
+
"This task has two tools from the `ordewell` MCP server: `task_complete` and `checkpoint`.",
|
|
15926
|
+
"They are not in your tool list up front. Find them with the tool discovery you have (the `exec` tool's `ALL_TOOLS` list) and call them by their full names, `mcp__ordewell__task_complete` and `mcp__ordewell__checkpoint`.",
|
|
15927
|
+
"Look for them before you finish; the task is reported complete through `task_complete`."
|
|
15928
|
+
].join(" ");
|
|
15929
|
+
function isOrdewellElicitation(method, params) {
|
|
15930
|
+
return method === "mcpServer/elicitation/request" && params.serverName === ORDEWELL_MCP_SERVER_NAME;
|
|
15931
|
+
}
|
|
15127
15932
|
var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
|
|
15128
15933
|
var TASK_APPROVALS = {
|
|
15129
15934
|
"item/commandExecution/requestApproval": {
|
|
@@ -15198,11 +16003,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
15198
16003
|
* names only the item, and a person deciding needs to see the files.
|
|
15199
16004
|
*/
|
|
15200
16005
|
fileChangePaths = /* @__PURE__ */ new Map();
|
|
16006
|
+
/** The row each announced file of an in-flight `fileChange` item is shown under, by the file's shown path. */
|
|
16007
|
+
fileChangeRows = /* @__PURE__ */ new Map();
|
|
15201
16008
|
/** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
|
|
15202
16009
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
15203
16010
|
resumeAttempted = false;
|
|
15204
16011
|
resumeFallbackSent = false;
|
|
15205
16012
|
sandbox = "default";
|
|
16013
|
+
/** The Ordewell server's startup state, as Codex last reported it for this thread. */
|
|
16014
|
+
mcpStartup = null;
|
|
16015
|
+
mcpStartupSettled = null;
|
|
15206
16016
|
/**
|
|
15207
16017
|
* Subagent threads this session has spawned, keyed by the child thread id.
|
|
15208
16018
|
* Codex runs a subagent in its own thread and replays both threads' events on
|
|
@@ -15211,7 +16021,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
15211
16021
|
subagents = /* @__PURE__ */ new Map();
|
|
15212
16022
|
spawnSpec(opts) {
|
|
15213
16023
|
this.startOpts = opts;
|
|
15214
|
-
|
|
16024
|
+
const headers = opts.mcp ? mcpHeaderEnv(opts.mcp) : {};
|
|
16025
|
+
const env = Object.fromEntries(Object.entries(headers).map(([header, name]) => [name, opts.mcp.headers[header]]));
|
|
16026
|
+
return { command: "codex", args: ["app-server"], ...opts.mcp ? { env } : {} };
|
|
15215
16027
|
}
|
|
15216
16028
|
/**
|
|
15217
16029
|
* `initialize`, then `thread/start`, both before the first user message. A
|
|
@@ -15278,7 +16090,11 @@ ${this.exitMessage()}`);
|
|
|
15278
16090
|
cwd: opts?.cwd,
|
|
15279
16091
|
...opts?.model ? { model: opts.model } : {}
|
|
15280
16092
|
};
|
|
15281
|
-
const
|
|
16093
|
+
const config = {
|
|
16094
|
+
...this.sandbox === "legacy-landlock" ? { features: { use_legacy_landlock: true } } : {},
|
|
16095
|
+
...opts?.mcp ? { mcp_servers: { [opts.mcp.name]: ordewellServerConfig(opts.mcp) } } : {}
|
|
16096
|
+
};
|
|
16097
|
+
const threadConfig = Object.keys(config).length ? { config } : {};
|
|
15282
16098
|
if (opts?.kind === "task") {
|
|
15283
16099
|
const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
|
|
15284
16100
|
return {
|
|
@@ -15286,14 +16102,15 @@ ${this.exitMessage()}`);
|
|
|
15286
16102
|
sandbox: opts.flags.permissionMode,
|
|
15287
16103
|
...approvalPolicy ? { approvalPolicy } : {},
|
|
15288
16104
|
...approvalsReviewer ? { approvalsReviewer } : {},
|
|
15289
|
-
...
|
|
16105
|
+
...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
|
|
16106
|
+
...threadConfig
|
|
15290
16107
|
};
|
|
15291
16108
|
}
|
|
15292
16109
|
return {
|
|
15293
16110
|
...common,
|
|
15294
16111
|
sandbox: "read-only",
|
|
15295
16112
|
approvalPolicy: "never",
|
|
15296
|
-
...
|
|
16113
|
+
...threadConfig,
|
|
15297
16114
|
// `developerInstructions` layers on top of Codex's own base prompt, the
|
|
15298
16115
|
// way Claude Code's `--append-system-prompt` does. `baseInstructions`
|
|
15299
16116
|
// replaces it — which takes Codex's description of its own tools with
|
|
@@ -15302,6 +16119,32 @@ ${this.exitMessage()}`);
|
|
|
15302
16119
|
developerInstructions: opts?.systemPrompt
|
|
15303
16120
|
};
|
|
15304
16121
|
}
|
|
16122
|
+
/**
|
|
16123
|
+
* Codex's own startup report for the server, which it sends once the thread
|
|
16124
|
+
* has tried to connect (ADR-0022, S4). A report that came before this was
|
|
16125
|
+
* asked is kept, so asking late still gets the answer.
|
|
16126
|
+
*/
|
|
16127
|
+
async mcpAttached() {
|
|
16128
|
+
if (!this.startOpts?.mcp || !this.process) return false;
|
|
16129
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS2;
|
|
16130
|
+
while (this.mcpStartup === null || this.mcpStartup === "starting") {
|
|
16131
|
+
const left = deadline - Date.now();
|
|
16132
|
+
if (left <= 0) return false;
|
|
16133
|
+
const ended = await Promise.race([
|
|
16134
|
+
new Promise((resolve7) => {
|
|
16135
|
+
this.mcpStartupSettled = () => resolve7(false);
|
|
16136
|
+
}),
|
|
16137
|
+
this.processEnded.then(() => true),
|
|
16138
|
+
new Promise((resolve7) => {
|
|
16139
|
+
const t = setTimeout(() => resolve7(false), left);
|
|
16140
|
+
t.unref?.();
|
|
16141
|
+
})
|
|
16142
|
+
]);
|
|
16143
|
+
this.mcpStartupSettled = null;
|
|
16144
|
+
if (ended) return false;
|
|
16145
|
+
}
|
|
16146
|
+
return this.mcpStartup === "ready";
|
|
16147
|
+
}
|
|
15305
16148
|
effort() {
|
|
15306
16149
|
const opts = this.startOpts;
|
|
15307
16150
|
return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
|
|
@@ -15473,6 +16316,13 @@ ${this.exitMessage()}`);
|
|
|
15473
16316
|
if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
|
|
15474
16317
|
return;
|
|
15475
16318
|
}
|
|
16319
|
+
case "mcpServer/startupStatus/updated": {
|
|
16320
|
+
const startup = msg.params;
|
|
16321
|
+
if (startup?.name !== ORDEWELL_MCP_SERVER_NAME || !startup.status) return;
|
|
16322
|
+
this.mcpStartup = startup.status;
|
|
16323
|
+
this.mcpStartupSettled?.();
|
|
16324
|
+
return;
|
|
16325
|
+
}
|
|
15476
16326
|
case "thread/tokenUsage/updated":
|
|
15477
16327
|
this.emitUsage(msg.params, emit);
|
|
15478
16328
|
return;
|
|
@@ -15611,6 +16461,10 @@ ${this.exitMessage()}`);
|
|
|
15611
16461
|
const method = msg.method;
|
|
15612
16462
|
const params = msg.params ?? {};
|
|
15613
16463
|
const approval = TASK_APPROVALS[method];
|
|
16464
|
+
if (isOrdewellElicitation(method, params)) {
|
|
16465
|
+
this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
|
|
16466
|
+
return;
|
|
16467
|
+
}
|
|
15614
16468
|
if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
|
|
15615
16469
|
const id = String(msg.id);
|
|
15616
16470
|
const input = this.approvalInput(method, params);
|
|
@@ -15651,6 +16505,10 @@ ${this.exitMessage()}`);
|
|
|
15651
16505
|
*/
|
|
15652
16506
|
answerServerRequest(msg, emit) {
|
|
15653
16507
|
const method = msg.method;
|
|
16508
|
+
if (isOrdewellElicitation(method, msg.params ?? {})) {
|
|
16509
|
+
this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
|
|
16510
|
+
return;
|
|
16511
|
+
}
|
|
15654
16512
|
const result = DECLINE_RESULTS[method];
|
|
15655
16513
|
if (result) {
|
|
15656
16514
|
this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
|
|
@@ -15677,7 +16535,7 @@ ${this.exitMessage()}`);
|
|
|
15677
16535
|
emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
|
|
15678
16536
|
return;
|
|
15679
16537
|
case "mcpToolCall":
|
|
15680
|
-
emit({ type: "tool_call", id: item.id, name: item
|
|
16538
|
+
emit({ type: "tool_call", id: item.id, name: shownToolName(item), args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
15681
16539
|
return;
|
|
15682
16540
|
case "dynamicToolCall":
|
|
15683
16541
|
emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
@@ -15689,7 +16547,7 @@ ${this.exitMessage()}`);
|
|
|
15689
16547
|
const paths = (item.changes ?? []).map((change) => change.path).filter((path27) => !!path27);
|
|
15690
16548
|
this.fileChangePaths.set(item.id, paths);
|
|
15691
16549
|
if (this.startOpts?.kind === "task") {
|
|
15692
|
-
|
|
16550
|
+
for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
|
|
15693
16551
|
}
|
|
15694
16552
|
return;
|
|
15695
16553
|
}
|
|
@@ -15746,7 +16604,7 @@ ${item.text}` : item.text });
|
|
|
15746
16604
|
emit({
|
|
15747
16605
|
type: "tool_result",
|
|
15748
16606
|
id,
|
|
15749
|
-
name: item.tool ?? "tool",
|
|
16607
|
+
name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
|
|
15750
16608
|
output: item.error ?? flattenText(item.result) ?? "",
|
|
15751
16609
|
success: item.status !== "error" && item.success !== false && !item.error,
|
|
15752
16610
|
...subagentId ? { subagentId } : {}
|
|
@@ -15760,8 +16618,11 @@ ${item.text}` : item.text });
|
|
|
15760
16618
|
case "fileChange":
|
|
15761
16619
|
this.fileChangePaths.delete(id);
|
|
15762
16620
|
if (this.startOpts?.kind === "task") {
|
|
15763
|
-
const
|
|
15764
|
-
|
|
16621
|
+
for (const change of item.changes ?? []) {
|
|
16622
|
+
const row = this.announceFileChange(id, change, emit, subagentId);
|
|
16623
|
+
emit({ type: "tool_result", id: row, name: changeName(change), output: changeDiff(change), success: item.status === "completed", ...subagentId ? { subagentId } : {} });
|
|
16624
|
+
}
|
|
16625
|
+
this.fileChangeRows.delete(id);
|
|
15765
16626
|
return;
|
|
15766
16627
|
}
|
|
15767
16628
|
emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
|
|
@@ -15770,6 +16631,34 @@ ${item.text}` : item.text });
|
|
|
15770
16631
|
return;
|
|
15771
16632
|
}
|
|
15772
16633
|
}
|
|
16634
|
+
/**
|
|
16635
|
+
* One row per changed file: a patch is several files, and one row naming
|
|
16636
|
+
* them all reads as a single cut-off path. The first file keeps the item's
|
|
16637
|
+
* id, which an approval for the patch points at. Announcing a file twice
|
|
16638
|
+
* returns the row it already has, so a change first seen completed still
|
|
16639
|
+
* gets its call.
|
|
16640
|
+
*/
|
|
16641
|
+
announceFileChange(itemId, change, emit, subagentId) {
|
|
16642
|
+
const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
|
|
16643
|
+
this.fileChangeRows.set(itemId, rows);
|
|
16644
|
+
const path27 = this.shownChangePath(change);
|
|
16645
|
+
const known = rows.get(path27);
|
|
16646
|
+
if (known) return known;
|
|
16647
|
+
const row = rows.size === 0 ? itemId : `${itemId}:${path27}`;
|
|
16648
|
+
rows.set(path27, row);
|
|
16649
|
+
emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path27 }, ...subagentId ? { subagentId } : {} });
|
|
16650
|
+
return row;
|
|
16651
|
+
}
|
|
16652
|
+
/** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
|
|
16653
|
+
shownChangePath(change) {
|
|
16654
|
+
const shown = (path27) => {
|
|
16655
|
+
const cwd = this.startOpts?.cwd;
|
|
16656
|
+
const rel = cwd ? (0, import_path4.relative)(cwd, path27) : path27;
|
|
16657
|
+
return rel && rel !== ".." && !rel.startsWith(`..${import_path4.sep}`) && !(0, import_path4.isAbsolute)(rel) ? rel : path27;
|
|
16658
|
+
};
|
|
16659
|
+
const from = shown(change.path ?? "");
|
|
16660
|
+
return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
|
|
16661
|
+
}
|
|
15773
16662
|
/**
|
|
15774
16663
|
* A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
|
|
15775
16664
|
* subagent. The call itself is planner-level tool activity; the lifecycle it
|
|
@@ -15809,7 +16698,45 @@ ${item.text}` : item.text });
|
|
|
15809
16698
|
};
|
|
15810
16699
|
|
|
15811
16700
|
// src/services/harness/OpenCodeAdapter.ts
|
|
15812
|
-
var
|
|
16701
|
+
var import_crypto5 = require("crypto");
|
|
16702
|
+
|
|
16703
|
+
// src/services/harness/openCodeOrdewell.ts
|
|
16704
|
+
function ordewellToolPrefix(mcp) {
|
|
16705
|
+
return `${mcp.name}_`;
|
|
16706
|
+
}
|
|
16707
|
+
function isOrdewellTool(mcp, permission) {
|
|
16708
|
+
return !!mcp && !!permission && permission.startsWith(ordewellToolPrefix(mcp));
|
|
16709
|
+
}
|
|
16710
|
+
function isRecord2(value) {
|
|
16711
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16712
|
+
}
|
|
16713
|
+
function deepMerge(base, over) {
|
|
16714
|
+
const merged = { ...base };
|
|
16715
|
+
for (const [key, value] of Object.entries(over)) {
|
|
16716
|
+
const current = merged[key];
|
|
16717
|
+
merged[key] = isRecord2(current) && isRecord2(value) ? deepMerge(current, value) : value;
|
|
16718
|
+
}
|
|
16719
|
+
return merged;
|
|
16720
|
+
}
|
|
16721
|
+
function mergeOrdewellConfig(existing, mcp) {
|
|
16722
|
+
let base = {};
|
|
16723
|
+
if (existing?.trim()) {
|
|
16724
|
+
let parsed;
|
|
16725
|
+
try {
|
|
16726
|
+
parsed = JSON.parse(existing);
|
|
16727
|
+
} catch {
|
|
16728
|
+
return null;
|
|
16729
|
+
}
|
|
16730
|
+
if (!isRecord2(parsed)) return null;
|
|
16731
|
+
base = parsed;
|
|
16732
|
+
}
|
|
16733
|
+
const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
|
|
16734
|
+
const ours = {
|
|
16735
|
+
mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } },
|
|
16736
|
+
permission: { [`${ordewellToolPrefix(mcp)}*`]: "allow" }
|
|
16737
|
+
};
|
|
16738
|
+
return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
|
|
16739
|
+
}
|
|
15813
16740
|
|
|
15814
16741
|
// src/services/harness/OpenCodeV2.ts
|
|
15815
16742
|
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
@@ -15892,12 +16819,16 @@ var OpenCodeV2 = class {
|
|
|
15892
16819
|
const created = await this.host.json("POST", "/api/session", {
|
|
15893
16820
|
agent: this.agent,
|
|
15894
16821
|
...this.model ? { model: this.model } : {},
|
|
15895
|
-
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
16822
|
+
permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
|
|
15896
16823
|
});
|
|
15897
16824
|
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
15898
16825
|
this.sessionId = created.data.id;
|
|
15899
16826
|
await this.applyInstructions();
|
|
15900
16827
|
}
|
|
16828
|
+
/** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
|
|
16829
|
+
ordewellRules() {
|
|
16830
|
+
return this.opts.mcp ? [{ action: `${ordewellToolPrefix(this.opts.mcp)}*`, resource: "*", effect: "allow" }] : [];
|
|
16831
|
+
}
|
|
15901
16832
|
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
15902
16833
|
async applyInstructions() {
|
|
15903
16834
|
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
@@ -16035,8 +16966,8 @@ var OpenCodeV2 = class {
|
|
|
16035
16966
|
});
|
|
16036
16967
|
return true;
|
|
16037
16968
|
}
|
|
16038
|
-
async replyPermission(id, sessionId,
|
|
16039
|
-
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`,
|
|
16969
|
+
async replyPermission(id, sessionId, reply2) {
|
|
16970
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply2);
|
|
16040
16971
|
}
|
|
16041
16972
|
async openStream(state, turn, onEvent, onActivity) {
|
|
16042
16973
|
const streamAbort = new AbortController();
|
|
@@ -16276,6 +17207,11 @@ var OpenCodeV2 = class {
|
|
|
16276
17207
|
const request = this.permissionRequest(ask);
|
|
16277
17208
|
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16278
17209
|
state.seen.add(`perm:${request.id}`);
|
|
17210
|
+
if (isOrdewellTool(this.opts.mcp, request.name)) {
|
|
17211
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
17212
|
+
});
|
|
17213
|
+
return;
|
|
17214
|
+
}
|
|
16279
17215
|
onEvent(request);
|
|
16280
17216
|
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
16281
17217
|
});
|
|
@@ -16291,7 +17227,7 @@ var OpenCodeV2 = class {
|
|
|
16291
17227
|
const request = this.permissionRequest(ask);
|
|
16292
17228
|
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16293
17229
|
state.seen.add(`perm:${request.id}`);
|
|
16294
|
-
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
17230
|
+
if (isOrdewellTool(this.opts.mcp, request.name) || this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
16295
17231
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
16296
17232
|
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
16297
17233
|
});
|
|
@@ -16310,6 +17246,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
|
16310
17246
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
16311
17247
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
16312
17248
|
var SERVER_USERNAME = "opencode";
|
|
17249
|
+
var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
|
|
17250
|
+
var MCP_STATUS_POLL_MS2 = 200;
|
|
16313
17251
|
var DISABLED_TOOLS = {
|
|
16314
17252
|
question: false,
|
|
16315
17253
|
edit: false,
|
|
@@ -16326,20 +17264,20 @@ function splitModelId2(id) {
|
|
|
16326
17264
|
}
|
|
16327
17265
|
function describeError(err) {
|
|
16328
17266
|
const visited = /* @__PURE__ */ new Set();
|
|
16329
|
-
const
|
|
17267
|
+
const lines2 = [];
|
|
16330
17268
|
let current = err;
|
|
16331
17269
|
while (current && !visited.has(current)) {
|
|
16332
17270
|
visited.add(current);
|
|
16333
17271
|
if (!(current instanceof Error)) {
|
|
16334
|
-
|
|
17272
|
+
lines2.push(String(current));
|
|
16335
17273
|
break;
|
|
16336
17274
|
}
|
|
16337
17275
|
const code = current.code;
|
|
16338
17276
|
const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
|
|
16339
|
-
|
|
17277
|
+
lines2.push(`${current.message}${suffix}`);
|
|
16340
17278
|
current = current.cause;
|
|
16341
17279
|
}
|
|
16342
|
-
return
|
|
17280
|
+
return lines2.join(": ");
|
|
16343
17281
|
}
|
|
16344
17282
|
function permissionName(ask) {
|
|
16345
17283
|
return ask.permission ?? ask.action ?? "permission";
|
|
@@ -16375,6 +17313,12 @@ function unwrapFileToolOutput(output) {
|
|
|
16375
17313
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
16376
17314
|
return inner ? inner[1] : output;
|
|
16377
17315
|
}
|
|
17316
|
+
function editDiff2(tool2, state) {
|
|
17317
|
+
const { diff, exists } = state?.metadata ?? {};
|
|
17318
|
+
if (typeof diff === "string" && diff) return hunksOf(diff);
|
|
17319
|
+
const content = state?.input?.content;
|
|
17320
|
+
return tool2 === "write" && exists === false && typeof content === "string" ? markedLines(content, "+") : "";
|
|
17321
|
+
}
|
|
16378
17322
|
function delay2(ms, signal) {
|
|
16379
17323
|
return new Promise((resolve7) => {
|
|
16380
17324
|
const timer = setTimeout(resolve7, ms);
|
|
@@ -16427,6 +17371,8 @@ var OpenCodeAdapter = class {
|
|
|
16427
17371
|
openPermissions = /* @__PURE__ */ new Map();
|
|
16428
17372
|
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
16429
17373
|
v2 = null;
|
|
17374
|
+
/** The Ordewell server this process was configured with; null when none was given or its config could not be merged. */
|
|
17375
|
+
ordewell = null;
|
|
16430
17376
|
async start(opts) {
|
|
16431
17377
|
if (opts.kind === "task") this.task = opts;
|
|
16432
17378
|
else this.planner = opts;
|
|
@@ -16441,9 +17387,19 @@ var OpenCodeAdapter = class {
|
|
|
16441
17387
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
16442
17388
|
}
|
|
16443
17389
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
16444
|
-
const password = (0,
|
|
17390
|
+
const password = (0, import_crypto5.randomBytes)(24).toString("base64url");
|
|
17391
|
+
const ordewellConfig = opts.mcp ? mergeOrdewellConfig(workspaceEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT, opts.mcp) : null;
|
|
17392
|
+
if (opts.mcp && ordewellConfig === null) {
|
|
17393
|
+
console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
|
|
17394
|
+
}
|
|
17395
|
+
this.ordewell = ordewellConfig === null ? null : opts.mcp ?? null;
|
|
16445
17396
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
16446
|
-
env: runnerEnv(PATH, {
|
|
17397
|
+
env: runnerEnv(PATH, {
|
|
17398
|
+
...workspaceEnv,
|
|
17399
|
+
...ordewellConfig === null ? {} : { OPENCODE_CONFIG_CONTENT: ordewellConfig },
|
|
17400
|
+
OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
|
|
17401
|
+
OPENCODE_SERVER_PASSWORD: password
|
|
17402
|
+
}),
|
|
16447
17403
|
stdio: ["pipe", "pipe", "pipe"],
|
|
16448
17404
|
cwd: opts.cwd,
|
|
16449
17405
|
windowsVerbatimArguments: launch.verbatim
|
|
@@ -16493,7 +17449,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16493
17449
|
processEnded: this.processEnded,
|
|
16494
17450
|
isExited: () => this.exited,
|
|
16495
17451
|
exitMessage: () => this.exitMessage()
|
|
16496
|
-
}, opts, this.role());
|
|
17452
|
+
}, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
|
|
16497
17453
|
try {
|
|
16498
17454
|
await this.v2.start();
|
|
16499
17455
|
} catch (err) {
|
|
@@ -16546,12 +17502,12 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16546
17502
|
...this.planner?.effort ? { variant: this.planner.effort } : {},
|
|
16547
17503
|
...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
|
|
16548
17504
|
};
|
|
16549
|
-
const
|
|
17505
|
+
const reply2 = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
|
|
16550
17506
|
if (signal?.aborted) {
|
|
16551
17507
|
this.dispose();
|
|
16552
17508
|
return;
|
|
16553
17509
|
}
|
|
16554
|
-
this.settle(
|
|
17510
|
+
this.settle(reply2, turn, onEvent);
|
|
16555
17511
|
} catch (err) {
|
|
16556
17512
|
if (signal?.aborted) {
|
|
16557
17513
|
this.dispose();
|
|
@@ -16657,13 +17613,13 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16657
17613
|
break;
|
|
16658
17614
|
}
|
|
16659
17615
|
}
|
|
16660
|
-
for (const
|
|
16661
|
-
const id =
|
|
16662
|
-
if (
|
|
16663
|
-
for (const part of
|
|
17616
|
+
for (const reply2 of messages.slice(start)) {
|
|
17617
|
+
const id = reply2.info?.id;
|
|
17618
|
+
if (reply2.info?.role !== "assistant" || !id) continue;
|
|
17619
|
+
for (const part of reply2.parts ?? []) {
|
|
16664
17620
|
if (part.messageID === id) this.emitPart(part, turn, onEvent);
|
|
16665
17621
|
}
|
|
16666
|
-
this.countUsage(
|
|
17622
|
+
this.countUsage(reply2.info, turn, onEvent);
|
|
16667
17623
|
}
|
|
16668
17624
|
}
|
|
16669
17625
|
/**
|
|
@@ -16773,18 +17729,18 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16773
17729
|
* their text, reasoning and usage — reach Ordewell over the stream or not at
|
|
16774
17730
|
* all.
|
|
16775
17731
|
*/
|
|
16776
|
-
settle(
|
|
16777
|
-
const failure = typeof
|
|
17732
|
+
settle(reply2, turn, onEvent) {
|
|
17733
|
+
const failure = typeof reply2?.error === "string" ? reply2.error : reply2?.error?.message ?? reply2?.info?.error?.data?.message;
|
|
16778
17734
|
if (failure) {
|
|
16779
17735
|
onEvent({ type: "error", message: failure });
|
|
16780
17736
|
return;
|
|
16781
17737
|
}
|
|
16782
|
-
const assistantId =
|
|
16783
|
-
for (const part of
|
|
17738
|
+
const assistantId = reply2?.info?.id;
|
|
17739
|
+
for (const part of reply2?.parts ?? []) {
|
|
16784
17740
|
if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
|
|
16785
17741
|
this.emitPart(part, turn, onEvent);
|
|
16786
17742
|
}
|
|
16787
|
-
if (
|
|
17743
|
+
if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
|
|
16788
17744
|
if (assistantId) this.lastAssistantId = assistantId;
|
|
16789
17745
|
onEvent({ type: "turn_end" });
|
|
16790
17746
|
}
|
|
@@ -16861,7 +17817,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16861
17817
|
type: "tool_result",
|
|
16862
17818
|
id: callId,
|
|
16863
17819
|
name,
|
|
16864
|
-
output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
|
|
17820
|
+
output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
|
|
16865
17821
|
success: status === "completed",
|
|
16866
17822
|
subagentId
|
|
16867
17823
|
});
|
|
@@ -16995,6 +17951,11 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16995
17951
|
const id = ask.id;
|
|
16996
17952
|
if (!id || seen.has(`perm:${id}`)) return;
|
|
16997
17953
|
seen.add(`perm:${id}`);
|
|
17954
|
+
if (isOrdewellTool(this.ordewell, permissionName(ask))) {
|
|
17955
|
+
void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "once" }).catch(() => {
|
|
17956
|
+
});
|
|
17957
|
+
return;
|
|
17958
|
+
}
|
|
16998
17959
|
onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
|
|
16999
17960
|
void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
|
|
17000
17961
|
});
|
|
@@ -17021,7 +17982,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
17021
17982
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
17022
17983
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
17023
17984
|
};
|
|
17024
|
-
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
17985
|
+
if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
17025
17986
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
17026
17987
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
17027
17988
|
});
|
|
@@ -17034,10 +17995,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
17034
17995
|
* `POST /permission/:id/reply` is the current answer. The per-session path
|
|
17035
17996
|
* it replaced is tried only when a server too old to know the new one 404s.
|
|
17036
17997
|
*/
|
|
17037
|
-
async replyPermission(id, sessionId,
|
|
17038
|
-
const response = await this.request("POST", `/permission/${id}/reply`,
|
|
17998
|
+
async replyPermission(id, sessionId, reply2) {
|
|
17999
|
+
const response = await this.request("POST", `/permission/${id}/reply`, reply2);
|
|
17039
18000
|
if (response.status !== 404) return;
|
|
17040
|
-
await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response:
|
|
18001
|
+
await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
|
|
17041
18002
|
}
|
|
17042
18003
|
/**
|
|
17043
18004
|
* Open `/event` for one turn and wait until it is connected. The stream
|
|
@@ -17116,6 +18077,19 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
17116
18077
|
nativeSessionId() {
|
|
17117
18078
|
return this.sessionId;
|
|
17118
18079
|
}
|
|
18080
|
+
/** Asks the server, which lists every MCP server it has with the state of its connection. */
|
|
18081
|
+
async mcpAttached() {
|
|
18082
|
+
if (!this.ordewell || !this.baseUrl) return false;
|
|
18083
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS3;
|
|
18084
|
+
while (Date.now() < deadline && !this.exited) {
|
|
18085
|
+
const servers = await this.json("GET", "/mcp").catch(() => null);
|
|
18086
|
+
const status = servers?.[this.ordewell.name]?.status;
|
|
18087
|
+
if (status === "connected") return true;
|
|
18088
|
+
if (status !== void 0 && status !== "pending") return false;
|
|
18089
|
+
await delay2(MCP_STATUS_POLL_MS2);
|
|
18090
|
+
}
|
|
18091
|
+
return false;
|
|
18092
|
+
}
|
|
17119
18093
|
dispose() {
|
|
17120
18094
|
if (this.disposed) return;
|
|
17121
18095
|
this.disposed = true;
|
|
@@ -17138,9 +18112,13 @@ var TASK_MODE_ADAPTERS = {
|
|
|
17138
18112
|
codex: (deps) => new CodexAdapter(deps),
|
|
17139
18113
|
opencode: (deps) => new OpenCodeAdapter(deps)
|
|
17140
18114
|
};
|
|
18115
|
+
var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
|
|
17141
18116
|
function supportsTaskMode(runner) {
|
|
17142
18117
|
return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
|
|
17143
18118
|
}
|
|
18119
|
+
function takesOrdewellTools(runner) {
|
|
18120
|
+
return ORDEWELL_TOOL_RUNNERS.has(runner);
|
|
18121
|
+
}
|
|
17144
18122
|
function createTaskAdapter(runner, deps) {
|
|
17145
18123
|
if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
|
|
17146
18124
|
return TASK_MODE_ADAPTERS[runner](deps);
|
|
@@ -17153,6 +18131,9 @@ function routeTransport(requested, runner, registry) {
|
|
|
17153
18131
|
const name = registry?.get(runner)?.manifest.displayName ?? runner;
|
|
17154
18132
|
return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
|
|
17155
18133
|
}
|
|
18134
|
+
function givesCompletionTool(requested, runner, registry) {
|
|
18135
|
+
return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
|
|
18136
|
+
}
|
|
17156
18137
|
var TransportRouter = class {
|
|
17157
18138
|
constructor(runners) {
|
|
17158
18139
|
this.runners = runners;
|
|
@@ -17291,6 +18272,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
17291
18272
|
this.emit("onTaskSettled", { taskId });
|
|
17292
18273
|
this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
|
|
17293
18274
|
});
|
|
18275
|
+
this.verifier.onCheckpointWithdrawn((taskId) => {
|
|
18276
|
+
if (!this.atCheckpoint(taskId)) return;
|
|
18277
|
+
this.store.markInProgress(taskId);
|
|
18278
|
+
this.emit("onTaskChanged");
|
|
18279
|
+
});
|
|
17294
18280
|
this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
|
|
17295
18281
|
}
|
|
17296
18282
|
/**
|
|
@@ -17377,7 +18363,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
17377
18363
|
this.notifications.warn(message);
|
|
17378
18364
|
};
|
|
17379
18365
|
if (resolved.blockedEnvrc) {
|
|
17380
|
-
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${
|
|
18366
|
+
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path15.dirname(resolved.blockedEnvrc)} to use them.`);
|
|
17381
18367
|
}
|
|
17382
18368
|
if (resolved.trackedEnvFile) {
|
|
17383
18369
|
warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
|
|
@@ -18208,17 +19194,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
18208
19194
|
this.abandonSpawn(task, attempt);
|
|
18209
19195
|
return false;
|
|
18210
19196
|
}
|
|
18211
|
-
const
|
|
19197
|
+
const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
|
|
19198
|
+
const completionTool = givesCompletionTool(transport, attempt.runner, this.registry);
|
|
19199
|
+
const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message, { ops: attempt.ops, completionTool }) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
|
|
18212
19200
|
planMapEnabled: this.config.planMapEnabled,
|
|
18213
19201
|
// A merge to resolve is not new behaviour to drive test-first.
|
|
18214
19202
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
18215
19203
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
18216
19204
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
18217
|
-
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
19205
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
|
|
19206
|
+
completionTool
|
|
18218
19207
|
});
|
|
18219
19208
|
this.lingering.close(task.id);
|
|
18220
19209
|
const env = await this.envForTask(cwd);
|
|
18221
|
-
const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
|
|
18222
19210
|
const session = await this.terminalRunner.spawn({
|
|
18223
19211
|
taskId: task.id,
|
|
18224
19212
|
runner: attempt.runner,
|
|
@@ -18233,7 +19221,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
18233
19221
|
title: task.title,
|
|
18234
19222
|
env,
|
|
18235
19223
|
transport,
|
|
18236
|
-
resumeSessionId: attempt.continuation?.resumeSessionId
|
|
19224
|
+
resumeSessionId: attempt.continuation?.resumeSessionId,
|
|
19225
|
+
attempt: attempt.attempt
|
|
18237
19226
|
});
|
|
18238
19227
|
if (this.attempts.get(task.id) !== attempt) {
|
|
18239
19228
|
this.abandonSpawn(task, attempt, session);
|
|
@@ -18521,62 +19510,16 @@ var Planner = class {
|
|
|
18521
19510
|
${errors.map((e) => `- ${e}`).join("\n")}`
|
|
18522
19511
|
);
|
|
18523
19512
|
}
|
|
18524
|
-
});
|
|
18525
|
-
}
|
|
18526
|
-
};
|
|
18527
|
-
|
|
18528
|
-
// src/services/ModelDiscovery.ts
|
|
18529
|
-
var path16 = __toESM(require("path"));
|
|
18530
|
-
var import_child_process5 = require("child_process");
|
|
18531
|
-
var import_util3 = require("util");
|
|
18532
|
-
var fsSync3 = __toESM(require("fs"));
|
|
18533
|
-
var osMod = __toESM(require("os"));
|
|
18534
|
-
|
|
18535
|
-
// src/utils/globalDataDir.ts
|
|
18536
|
-
var fs7 = __toESM(require("fs"));
|
|
18537
|
-
var path15 = __toESM(require("path"));
|
|
18538
|
-
var os5 = __toESM(require("os"));
|
|
18539
|
-
function globalDataDir() {
|
|
18540
|
-
return path15.join(os5.homedir(), ".ordewell");
|
|
18541
|
-
}
|
|
18542
|
-
function copyDirSync(src, dest) {
|
|
18543
|
-
fs7.mkdirSync(dest, { recursive: true });
|
|
18544
|
-
const entries = fs7.readdirSync(src, { withFileTypes: true });
|
|
18545
|
-
for (const entry of entries) {
|
|
18546
|
-
const srcPath = path15.join(src, entry.name);
|
|
18547
|
-
const destPath = path15.join(dest, entry.name);
|
|
18548
|
-
if (entry.isDirectory()) {
|
|
18549
|
-
copyDirSync(srcPath, destPath);
|
|
18550
|
-
} else {
|
|
18551
|
-
fs7.copyFileSync(srcPath, destPath);
|
|
18552
|
-
}
|
|
18553
|
-
}
|
|
18554
|
-
}
|
|
18555
|
-
var migrated = false;
|
|
18556
|
-
function migrateOldConfigDir() {
|
|
18557
|
-
if (migrated) return;
|
|
18558
|
-
migrated = true;
|
|
18559
|
-
const oldDir = path15.join(os5.homedir(), ".config", "ordewell");
|
|
18560
|
-
if (!fs7.existsSync(oldDir)) return;
|
|
18561
|
-
const newDir = globalDataDir();
|
|
18562
|
-
const liftFile = (name) => {
|
|
18563
|
-
const oldFile = path15.join(oldDir, name);
|
|
18564
|
-
const newFile = path15.join(newDir, name);
|
|
18565
|
-
if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
|
|
18566
|
-
fs7.mkdirSync(newDir, { recursive: true });
|
|
18567
|
-
fs7.copyFileSync(oldFile, newFile);
|
|
18568
|
-
}
|
|
18569
|
-
};
|
|
18570
|
-
liftFile("settings.json");
|
|
18571
|
-
liftFile(".env");
|
|
18572
|
-
const oldPlugins = path15.join(oldDir, "plugins");
|
|
18573
|
-
const newPlugins = path15.join(newDir, "plugins");
|
|
18574
|
-
if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
|
|
18575
|
-
copyDirSync(oldPlugins, newPlugins);
|
|
19513
|
+
});
|
|
18576
19514
|
}
|
|
18577
|
-
}
|
|
19515
|
+
};
|
|
18578
19516
|
|
|
18579
19517
|
// src/services/ModelDiscovery.ts
|
|
19518
|
+
var path16 = __toESM(require("path"));
|
|
19519
|
+
var import_child_process5 = require("child_process");
|
|
19520
|
+
var import_util3 = require("util");
|
|
19521
|
+
var fsSync3 = __toESM(require("fs"));
|
|
19522
|
+
var osMod = __toESM(require("os"));
|
|
18580
19523
|
var execAsync3 = (0, import_util3.promisify)(import_child_process5.exec);
|
|
18581
19524
|
var defaultExec2 = async (command, options) => {
|
|
18582
19525
|
const PATH = await augmentedPath();
|
|
@@ -18952,8 +19895,8 @@ function parseClaudeHelp(stdout) {
|
|
|
18952
19895
|
return models;
|
|
18953
19896
|
}
|
|
18954
19897
|
function parseOpencodeModels(stdout, manifest) {
|
|
18955
|
-
const
|
|
18956
|
-
if (
|
|
19898
|
+
const lines2 = stdout.split("\n").map((l) => l.trim()).filter((l) => /^[A-Za-z0-9._-]+\/[^\s"{}]+$/.test(l));
|
|
19899
|
+
if (lines2.length === 0) return [];
|
|
18957
19900
|
const preferred = manifest.modelDiscovery.preferredPatterns;
|
|
18958
19901
|
const prefLabels = /* @__PURE__ */ new Map();
|
|
18959
19902
|
if (preferred) {
|
|
@@ -18963,14 +19906,14 @@ function parseOpencodeModels(stdout, manifest) {
|
|
|
18963
19906
|
}
|
|
18964
19907
|
const seen = /* @__PURE__ */ new Set();
|
|
18965
19908
|
const models = [];
|
|
18966
|
-
for (const id of
|
|
19909
|
+
for (const id of lines2) {
|
|
18967
19910
|
if (prefLabels.has(id) && !seen.has(id)) {
|
|
18968
19911
|
seen.add(id);
|
|
18969
19912
|
const provider = id.split("/")[0];
|
|
18970
19913
|
models.push({ modelId: id, modelLabel: prefLabels.get(id), runnerProvider: provider, variants: [] });
|
|
18971
19914
|
}
|
|
18972
19915
|
}
|
|
18973
|
-
for (const id of
|
|
19916
|
+
for (const id of lines2) {
|
|
18974
19917
|
if (!seen.has(id)) {
|
|
18975
19918
|
seen.add(id);
|
|
18976
19919
|
const provider = id.split("/")[0];
|
|
@@ -19783,6 +20726,7 @@ var CliAgentAiService = class {
|
|
|
19783
20726
|
};
|
|
19784
20727
|
this.makeAdapter = deps.createAdapter ?? defaultAdapter;
|
|
19785
20728
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
20729
|
+
this.mcpServer = deps.mcpServer;
|
|
19786
20730
|
}
|
|
19787
20731
|
config;
|
|
19788
20732
|
runner;
|
|
@@ -19797,6 +20741,11 @@ var CliAgentAiService = class {
|
|
|
19797
20741
|
*/
|
|
19798
20742
|
lastNativeSessionId = null;
|
|
19799
20743
|
conversation = null;
|
|
20744
|
+
mcpServer;
|
|
20745
|
+
/** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
|
|
20746
|
+
plannerToken = null;
|
|
20747
|
+
/** Whether the running planner was spawned with its tools and the CLI reported them connected. */
|
|
20748
|
+
toolsAttached = false;
|
|
19800
20749
|
activeAbort = null;
|
|
19801
20750
|
/**
|
|
19802
20751
|
* Every subagent this conversation has reported starting, and finishing.
|
|
@@ -19819,6 +20768,10 @@ var CliAgentAiService = class {
|
|
|
19819
20768
|
if (!this.conversation) return true;
|
|
19820
20769
|
return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
|
|
19821
20770
|
}
|
|
20771
|
+
/** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
|
|
20772
|
+
plannerToolsAttached() {
|
|
20773
|
+
return this.toolsAttached;
|
|
20774
|
+
}
|
|
19822
20775
|
/** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
|
|
19823
20776
|
nativeSessionId() {
|
|
19824
20777
|
return this.adapter?.nativeSessionId() ?? null;
|
|
@@ -19826,8 +20779,8 @@ var CliAgentAiService = class {
|
|
|
19826
20779
|
reset() {
|
|
19827
20780
|
this.activeAbort?.abort();
|
|
19828
20781
|
this.activeAbort = null;
|
|
19829
|
-
this.
|
|
19830
|
-
this.
|
|
20782
|
+
this.disposeAdapter();
|
|
20783
|
+
this.toolsAttached = false;
|
|
19831
20784
|
this.lastNativeSessionId = null;
|
|
19832
20785
|
this.conversation = null;
|
|
19833
20786
|
this.subagents.started.clear();
|
|
@@ -19854,9 +20807,23 @@ var CliAgentAiService = class {
|
|
|
19854
20807
|
model: this.plannerModel(),
|
|
19855
20808
|
effort: this.config.plannerThinkingEffort
|
|
19856
20809
|
};
|
|
19857
|
-
|
|
20810
|
+
const tools = req.plannerTools && this.mcpServer ? {
|
|
20811
|
+
offer: req.plannerTools,
|
|
20812
|
+
systemPrompt: buildConversationSystemPrompt(
|
|
20813
|
+
req.goal,
|
|
20814
|
+
contextStr,
|
|
20815
|
+
req.modelsByRunner,
|
|
20816
|
+
req.runners,
|
|
20817
|
+
req.runnerModes,
|
|
20818
|
+
req.autonomousDefault ?? true,
|
|
20819
|
+
req.verificationEnabled ?? false,
|
|
20820
|
+
{ harness: true, isolatedExecution: req.isolatedExecution, plannerTools: true }
|
|
20821
|
+
)
|
|
20822
|
+
} : void 0;
|
|
20823
|
+
await this.startAdapter(startOptions, tools);
|
|
19858
20824
|
this.conversation = {
|
|
19859
20825
|
startOptions,
|
|
20826
|
+
tools,
|
|
19860
20827
|
runners: req.runners,
|
|
19861
20828
|
runnerModes: req.runnerModes,
|
|
19862
20829
|
autonomousDefault: req.autonomousDefault
|
|
@@ -20061,10 +21028,7 @@ var CliAgentAiService = class {
|
|
|
20061
21028
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
20062
21029
|
}
|
|
20063
21030
|
this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
|
|
20064
|
-
if (error || signal?.aborted)
|
|
20065
|
-
adapter.dispose();
|
|
20066
|
-
this.adapter = null;
|
|
20067
|
-
}
|
|
21031
|
+
if (error || signal?.aborted) this.disposeAdapter();
|
|
20068
21032
|
return { text: text2, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
20069
21033
|
}
|
|
20070
21034
|
// --- Process lifecycle ---
|
|
@@ -20073,13 +21037,50 @@ var CliAgentAiService = class {
|
|
|
20073
21037
|
* type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
|
|
20074
21038
|
* mode from here without changing this signature.
|
|
20075
21039
|
*/
|
|
20076
|
-
async startAdapter(opts) {
|
|
21040
|
+
async startAdapter(opts, tools) {
|
|
20077
21041
|
const adapter = this.makeAdapter(this.runner, this.processDeps);
|
|
20078
21042
|
if (!adapter) throw new Error(`No planner adapter is available for "${this.runner}".`);
|
|
21043
|
+
this.toolsAttached = false;
|
|
21044
|
+
if (tools && this.mcpServer && adapter.mcpAttached) {
|
|
21045
|
+
if (await this.startWithTools(adapter, opts, tools, this.mcpServer)) return adapter;
|
|
21046
|
+
return this.startAdapter(opts);
|
|
21047
|
+
}
|
|
20079
21048
|
await adapter.start(opts);
|
|
20080
21049
|
this.adapter = adapter;
|
|
20081
21050
|
return adapter;
|
|
20082
21051
|
}
|
|
21052
|
+
/** Spawn with the Ordewell server injected. False, with nothing left running, when it did not connect. */
|
|
21053
|
+
async startWithTools(adapter, opts, tools, server) {
|
|
21054
|
+
let credential;
|
|
21055
|
+
try {
|
|
21056
|
+
credential = await server.issuePlannerToken({ sessionId: tools.offer.sessionId }, tools.offer.handler);
|
|
21057
|
+
} catch {
|
|
21058
|
+
return false;
|
|
21059
|
+
}
|
|
21060
|
+
try {
|
|
21061
|
+
await adapter.start({ ...opts, systemPrompt: tools.systemPrompt, mcp: mcpClientConfig(credential) });
|
|
21062
|
+
if (await adapter.mcpAttached()) {
|
|
21063
|
+
this.adapter = adapter;
|
|
21064
|
+
this.plannerToken = credential.token;
|
|
21065
|
+
this.toolsAttached = true;
|
|
21066
|
+
return true;
|
|
21067
|
+
}
|
|
21068
|
+
} catch (err) {
|
|
21069
|
+
adapter.dispose();
|
|
21070
|
+
server.revoke(credential.token);
|
|
21071
|
+
throw err;
|
|
21072
|
+
}
|
|
21073
|
+
adapter.dispose();
|
|
21074
|
+
server.revoke(credential.token);
|
|
21075
|
+
return false;
|
|
21076
|
+
}
|
|
21077
|
+
/** Kill the planner process, and with it the token it was spawned with. */
|
|
21078
|
+
disposeAdapter() {
|
|
21079
|
+
this.adapter?.dispose();
|
|
21080
|
+
this.adapter = null;
|
|
21081
|
+
if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
|
|
21082
|
+
this.plannerToken = null;
|
|
21083
|
+
}
|
|
20083
21084
|
/**
|
|
20084
21085
|
* The live agent process, restarted from its own session id if it died
|
|
20085
21086
|
* between turns. Resume is a hint: when it fails, the caller's next
|
|
@@ -20090,7 +21091,7 @@ var CliAgentAiService = class {
|
|
|
20090
21091
|
if (this.adapter) return this.adapter;
|
|
20091
21092
|
const conversation = this.conversation;
|
|
20092
21093
|
if (!conversation) throw new Error("No active planner conversation.");
|
|
20093
|
-
await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
|
|
21094
|
+
await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
|
|
20094
21095
|
return this.adapter;
|
|
20095
21096
|
}
|
|
20096
21097
|
plannerModel() {
|
|
@@ -20109,7 +21110,9 @@ var CliAgentAiService = class {
|
|
|
20109
21110
|
const previous = this.adapter;
|
|
20110
21111
|
const previousConversation = this.conversation;
|
|
20111
21112
|
const previousSessionId = this.lastNativeSessionId;
|
|
21113
|
+
const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
|
|
20112
21114
|
this.adapter = null;
|
|
21115
|
+
this.plannerToken = null;
|
|
20113
21116
|
const startOptions = {
|
|
20114
21117
|
kind: "planner",
|
|
20115
21118
|
cwd: this.workspaceRoot(),
|
|
@@ -20139,6 +21142,8 @@ var CliAgentAiService = class {
|
|
|
20139
21142
|
} finally {
|
|
20140
21143
|
oneShotAdapter?.dispose();
|
|
20141
21144
|
this.adapter = previous;
|
|
21145
|
+
this.plannerToken = previousTools.token;
|
|
21146
|
+
this.toolsAttached = previousTools.attached;
|
|
20142
21147
|
this.conversation = previousConversation;
|
|
20143
21148
|
this.lastNativeSessionId = previousSessionId;
|
|
20144
21149
|
}
|
|
@@ -20234,6 +21239,173 @@ function createAiService(config, deps) {
|
|
|
20234
21239
|
return new OpenAiService(config);
|
|
20235
21240
|
}
|
|
20236
21241
|
|
|
21242
|
+
// src/services/plannerTools.ts
|
|
21243
|
+
function runnersOf(tasks) {
|
|
21244
|
+
return [...new Set(flattenTasks(tasks).filter((t) => t.type !== "user").map((t) => t.assignedRunner))];
|
|
21245
|
+
}
|
|
21246
|
+
function plannerToolHandler(host) {
|
|
21247
|
+
return {
|
|
21248
|
+
async listRunners() {
|
|
21249
|
+
const catalog = await host.liveCatalog();
|
|
21250
|
+
return answer({
|
|
21251
|
+
autonomy: autonomyLevelLabel(catalog.autonomousDefault),
|
|
21252
|
+
runners: catalog.runners.map((id) => {
|
|
21253
|
+
const modes = catalog.modes[id] ?? [];
|
|
21254
|
+
const offered = [...filteredBuildModes(modes, catalog.autonomousDefault), ...modes.filter((m) => m.id === "plan")];
|
|
21255
|
+
return {
|
|
21256
|
+
id,
|
|
21257
|
+
defaultMode: resolveDefaultMode(modes, catalog.autonomousDefault) ?? null,
|
|
21258
|
+
modes: offered.map(({ id: modeId2, label, description }) => ({ id: modeId2, label, description }))
|
|
21259
|
+
};
|
|
21260
|
+
})
|
|
21261
|
+
});
|
|
21262
|
+
},
|
|
21263
|
+
async listModels({ runner }) {
|
|
21264
|
+
const catalog = await host.liveCatalog();
|
|
21265
|
+
if (!catalog.runners.includes(runner)) return notEnabled(runner, catalog.runners);
|
|
21266
|
+
return answer({
|
|
21267
|
+
runner,
|
|
21268
|
+
models: (catalog.models[runner] ?? []).map((m) => ({
|
|
21269
|
+
modelId: m.modelId,
|
|
21270
|
+
modelLabel: m.modelLabel,
|
|
21271
|
+
variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
|
|
21272
|
+
}))
|
|
21273
|
+
});
|
|
21274
|
+
},
|
|
21275
|
+
async submitPlan({ tasks }) {
|
|
21276
|
+
const catalog = await host.liveCatalog();
|
|
21277
|
+
const result = validatePlanTasks({ tasks }, catalog.runners, catalog.modes, catalog.autonomousDefault);
|
|
21278
|
+
if (!result.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: result.errors, enabledRunners: catalog.runners }) };
|
|
21279
|
+
const runners = runnersOf(result.tasks);
|
|
21280
|
+
const coerced = coercions(result.tasks, host.coerce(result.tasks, runners));
|
|
21281
|
+
if (!host.submitPlan(result.tasks, runners)) {
|
|
21282
|
+
return { isError: true, text: JSON.stringify({ ok: false, errors: [{ field: "tasks", message: "No planning turn is open to take the plan." }] }) };
|
|
21283
|
+
}
|
|
21284
|
+
return answer({
|
|
21285
|
+
ok: true,
|
|
21286
|
+
tasks: result.tasks.length,
|
|
21287
|
+
coerced,
|
|
21288
|
+
next: "The plan is committed when this reply ends. Tell the user briefly what you submitted; do not repeat the plan as JSON."
|
|
21289
|
+
});
|
|
21290
|
+
},
|
|
21291
|
+
async taskQuery({ tasks, fields, catalog }) {
|
|
21292
|
+
const query = { tasks: (tasks ?? []).map(String), fields: fields ?? [...TASK_QUERY_TASK_FIELDS], catalog: catalog === true };
|
|
21293
|
+
return read(host, taskQuerySignature(query), async () => ({
|
|
21294
|
+
tasks: query.tasks.map((ref) => taskDetail(host.tasks(), ref, query.fields ?? [])),
|
|
21295
|
+
...query.catalog ? { catalog: catalogDetail(await host.liveCatalog()) } : {}
|
|
21296
|
+
}));
|
|
21297
|
+
},
|
|
21298
|
+
async taskOutput({ task: ref, lines: lines2, since }) {
|
|
21299
|
+
const task = findTask(host.tasks(), String(ref));
|
|
21300
|
+
if (!task) return { isError: true, text: JSON.stringify({ ok: false, error: `No task matches "${ref}" in the current plan.` }) };
|
|
21301
|
+
const maxLines = Math.min(lines2 ?? OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX);
|
|
21302
|
+
return read(host, JSON.stringify(["output", task.id, maxLines, since ?? null]), async () => {
|
|
21303
|
+
const head = { task: { id: task.id, order: task.order, title: task.title, status: task.status } };
|
|
21304
|
+
const tail = host.liveOutput(task.id, { maxLines, sinceOffset: since });
|
|
21305
|
+
if (tail?.running) {
|
|
21306
|
+
const fitted = fitTail(tail.text, OUTPUT_TAIL_MAX_CHARS);
|
|
21307
|
+
return { ...head, running: true, output: fitted.text, nextOffset: tail.nextOffset, ...fitted.trimmed ? { trimmedToFit: true } : {} };
|
|
21308
|
+
}
|
|
21309
|
+
return {
|
|
21310
|
+
...head,
|
|
21311
|
+
running: false,
|
|
21312
|
+
reason: "This task is not running, so there is no live output: its outcome is below.",
|
|
21313
|
+
verdict: verdictDetail(task.verdict),
|
|
21314
|
+
outputSummary: outputSummaryDetail(task),
|
|
21315
|
+
lastAttempt: host.lastAttempt(task.id)
|
|
21316
|
+
};
|
|
21317
|
+
});
|
|
21318
|
+
},
|
|
21319
|
+
async editPlan({ ops }) {
|
|
21320
|
+
const outcome = await host.editPlan(ops);
|
|
21321
|
+
if (!outcome.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: outcome.errors.map(opError) }) };
|
|
21322
|
+
return answer({
|
|
21323
|
+
ok: true,
|
|
21324
|
+
summary: outcome.summary,
|
|
21325
|
+
queued: outcome.queued,
|
|
21326
|
+
next: outcome.queued ? "A task you named is running, so the edit is parked and nothing was checked: it is applied between task batches, when the user's message comes back to you. Tell the user it is queued." : "The edit is applied when this reply ends, and the user is shown what changed. Tell them briefly; do not repeat it as JSON."
|
|
21327
|
+
});
|
|
21328
|
+
}
|
|
21329
|
+
};
|
|
21330
|
+
}
|
|
21331
|
+
function opError(message) {
|
|
21332
|
+
const match = /^op (\d+) \((\w+)\): ([\s\S]*)$/.exec(message);
|
|
21333
|
+
return match ? { op: Number(match[1]), kind: match[2], message: match[3] } : { message };
|
|
21334
|
+
}
|
|
21335
|
+
async function read(host, signature, answerRead) {
|
|
21336
|
+
const result = await host.read(signature, answerRead);
|
|
21337
|
+
if (result.status === "refused") {
|
|
21338
|
+
return { isError: true, text: JSON.stringify({ ok: false, error: "You have used every read this message allows. Nothing was read: answer the user in prose, or call edit_plan for the change you came to make." }) };
|
|
21339
|
+
}
|
|
21340
|
+
return answer({ ...result.value, ...result.landNow ? { note: TASK_READ_ANSWER_OR_EDIT } : {} });
|
|
21341
|
+
}
|
|
21342
|
+
function verdictDetail(verdict) {
|
|
21343
|
+
if (!verdict) return null;
|
|
21344
|
+
return {
|
|
21345
|
+
outcome: verdict.outcome,
|
|
21346
|
+
reason: verdict.reason,
|
|
21347
|
+
checks: verdict.checks.map((c) => ({ name: c.name, result: c.skipped ? "skipped" : c.passed ? "pass" : "fail" }))
|
|
21348
|
+
};
|
|
21349
|
+
}
|
|
21350
|
+
function outputSummaryDetail({ outputSummary }) {
|
|
21351
|
+
return outputSummary ? { reviewReason: outputSummary.reviewReason, ...outputSummary.logTail ? { logTail: outputSummary.logTail } : {} } : null;
|
|
21352
|
+
}
|
|
21353
|
+
function taskDetail(plan, ref, fields) {
|
|
21354
|
+
const task = findTask(plan, ref);
|
|
21355
|
+
if (!task) return { ref, error: "no task matches this reference in the current plan." };
|
|
21356
|
+
const wants = (field) => fields.includes(field);
|
|
21357
|
+
return {
|
|
21358
|
+
id: task.id,
|
|
21359
|
+
order: task.order,
|
|
21360
|
+
title: task.title,
|
|
21361
|
+
status: task.status,
|
|
21362
|
+
type: task.type === "user" ? "user" : "ai",
|
|
21363
|
+
...wants("description") ? { description: task.description || null } : {},
|
|
21364
|
+
...wants("prompt") ? { prompt: task.prompt || null } : {},
|
|
21365
|
+
...wants("userSteps") ? { userSteps: task.userSteps?.length ? task.userSteps.map((s) => ({ order: s.order, instruction: s.instruction, completed: s.completed === true })) : null } : {},
|
|
21366
|
+
...wants("verdict") ? { verdict: verdictDetail(task.verdict) } : {},
|
|
21367
|
+
...wants("outputSummary") ? { outputSummary: outputSummaryDetail(task) } : {},
|
|
21368
|
+
...wants("userStoriesCovered") ? { userStoriesCovered: task.userStoriesCovered?.length ? task.userStoriesCovered : null } : {}
|
|
21369
|
+
};
|
|
21370
|
+
}
|
|
21371
|
+
function catalogDetail(catalog) {
|
|
21372
|
+
return {
|
|
21373
|
+
runners: catalog.runners.map((id) => {
|
|
21374
|
+
const modes = catalog.modes[id] ?? [];
|
|
21375
|
+
const defaultMode = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
21376
|
+
return {
|
|
21377
|
+
id,
|
|
21378
|
+
models: (catalog.models[id] ?? []).map((m) => ({
|
|
21379
|
+
modelId: m.modelId,
|
|
21380
|
+
modelLabel: m.modelLabel,
|
|
21381
|
+
variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
|
|
21382
|
+
})),
|
|
21383
|
+
modes: modes.map((m) => ({ id: m.id, label: m.label, description: m.description, default: m.id === defaultMode }))
|
|
21384
|
+
};
|
|
21385
|
+
})
|
|
21386
|
+
};
|
|
21387
|
+
}
|
|
21388
|
+
function answer(body) {
|
|
21389
|
+
return { text: JSON.stringify(body) };
|
|
21390
|
+
}
|
|
21391
|
+
function notEnabled(runner, enabled) {
|
|
21392
|
+
return { isError: true, text: `Runner "${runner}" is not enabled. Enabled runners: ${enabled.join(", ") || "(none)"}.` };
|
|
21393
|
+
}
|
|
21394
|
+
function coercions(submitted, landed) {
|
|
21395
|
+
return submitted.flatMap((task, i) => {
|
|
21396
|
+
const after = landed[i];
|
|
21397
|
+
const out = [];
|
|
21398
|
+
if (after.assignedRunner !== task.assignedRunner) {
|
|
21399
|
+
out.push({ taskId: task.id, field: "assignedRunner", from: task.assignedRunner, to: after.assignedRunner });
|
|
21400
|
+
}
|
|
21401
|
+
const [fromModel, toModel] = [task.assignedModel?.modelId ?? null, after.assignedModel?.modelId ?? null];
|
|
21402
|
+
if (fromModel !== toModel) out.push({ taskId: task.id, field: "assignedModel", from: fromModel, to: toModel });
|
|
21403
|
+
const [fromEffort, toEffort] = [task.assignedModel?.thinkingEffort ?? null, after.assignedModel?.thinkingEffort ?? null];
|
|
21404
|
+
if (fromEffort !== toEffort) out.push({ taskId: task.id, field: "thinkingEffort", from: fromEffort, to: toEffort });
|
|
21405
|
+
return out;
|
|
21406
|
+
});
|
|
21407
|
+
}
|
|
21408
|
+
|
|
20237
21409
|
// src/services/PlannerConversation.ts
|
|
20238
21410
|
var import_uuid7 = require("uuid");
|
|
20239
21411
|
|
|
@@ -20250,8 +21422,8 @@ function summaryRequest(planLines) {
|
|
|
20250
21422
|
...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
|
|
20251
21423
|
].join("\n");
|
|
20252
21424
|
}
|
|
20253
|
-
function extractSummary(
|
|
20254
|
-
const matches = [...
|
|
21425
|
+
function extractSummary(reply2) {
|
|
21426
|
+
const matches = [...reply2.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
|
|
20255
21427
|
const summary = matches.at(-1)?.[1].trim();
|
|
20256
21428
|
return summary || null;
|
|
20257
21429
|
}
|
|
@@ -20270,6 +21442,7 @@ function keptTail(transcript) {
|
|
|
20270
21442
|
|
|
20271
21443
|
// src/services/PlannerConversation.ts
|
|
20272
21444
|
var CATALOG_MODEL_CAP = 100;
|
|
21445
|
+
var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
|
|
20273
21446
|
var MAX_TASK_QUERIES = 3;
|
|
20274
21447
|
var MAX_TASK_QUERIES_HARD = 6;
|
|
20275
21448
|
function freshReadBudget() {
|
|
@@ -20299,7 +21472,7 @@ var PlannerConversation = class {
|
|
|
20299
21472
|
savedInBackground = 0;
|
|
20300
21473
|
turnsInFlight = 0;
|
|
20301
21474
|
compacting = false;
|
|
20302
|
-
|
|
21475
|
+
openTurn = null;
|
|
20303
21476
|
get transcript() {
|
|
20304
21477
|
return this.host.plan()?.conversationHistory ?? [];
|
|
20305
21478
|
}
|
|
@@ -20309,7 +21482,48 @@ var PlannerConversation = class {
|
|
|
20309
21482
|
}
|
|
20310
21483
|
/** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
|
|
20311
21484
|
get currentTurnId() {
|
|
20312
|
-
return this.
|
|
21485
|
+
return this.openTurn?.stream.turnId;
|
|
21486
|
+
}
|
|
21487
|
+
/**
|
|
21488
|
+
* Hand the open turn a plan (already validated) or a task edit. The turn
|
|
21489
|
+
* settles with it exactly as with the same plan or ops parsed out of its
|
|
21490
|
+
* reply, and it wins over any envelope that reply carries. A later
|
|
21491
|
+
* submission replaces an earlier one. False when no user turn is open.
|
|
21492
|
+
*/
|
|
21493
|
+
submit(submission) {
|
|
21494
|
+
if (!this.openTurn) return false;
|
|
21495
|
+
this.openTurn.submitted = submission;
|
|
21496
|
+
return true;
|
|
21497
|
+
}
|
|
21498
|
+
/**
|
|
21499
|
+
* The edit the open turn already holds, for a further edit in the same reply
|
|
21500
|
+
* to join rather than replace. Empty when it holds nothing; null when it holds
|
|
21501
|
+
* a whole plan, which an edit cannot be layered on.
|
|
21502
|
+
*/
|
|
21503
|
+
pendingOps() {
|
|
21504
|
+
const held = this.openTurn?.submitted;
|
|
21505
|
+
if (!held) return [];
|
|
21506
|
+
return held.kind === "task_ops" ? held.ops : null;
|
|
21507
|
+
}
|
|
21508
|
+
/** Whether the turn would park this edit until a batch boundary instead of applying it as it settles. */
|
|
21509
|
+
editWouldQueue(ops) {
|
|
21510
|
+
return this.editTouchesLiveWork({ kind: "task_ops", ops, text: "", researchLog: [] });
|
|
21511
|
+
}
|
|
21512
|
+
/**
|
|
21513
|
+
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
21514
|
+
* as an envelope read, so a planner cannot read more by switching channels:
|
|
21515
|
+
* past the soft limit, or on a repeated question, the answer still comes but
|
|
21516
|
+
* is told to land the turn; at the hard limit it is refused. `signature`
|
|
21517
|
+
* names the question, for the repeat check.
|
|
21518
|
+
*/
|
|
21519
|
+
async read(signature, answer2) {
|
|
21520
|
+
const reads = this.openTurn?.reads;
|
|
21521
|
+
if (!reads) return { status: "answered", value: await answer2(), landNow: false };
|
|
21522
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD) return { status: "refused" };
|
|
21523
|
+
const landNow = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
21524
|
+
reads.seen.add(signature);
|
|
21525
|
+
reads.answered++;
|
|
21526
|
+
return { status: "answered", value: await answer2(), landNow };
|
|
20313
21527
|
}
|
|
20314
21528
|
/** Whether the model still holds this conversation in memory. */
|
|
20315
21529
|
get isActive() {
|
|
@@ -20597,15 +21811,16 @@ ${message}` : message;
|
|
|
20597
21811
|
async userTurn(prompt, signal, run) {
|
|
20598
21812
|
const turnId = (0, import_uuid7.v4)();
|
|
20599
21813
|
const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
|
|
20600
|
-
|
|
21814
|
+
const turn = { stream, signal, reads: freshReadBudget() };
|
|
21815
|
+
this.openTurn = turn;
|
|
20601
21816
|
this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
|
|
20602
21817
|
let outcome = "error";
|
|
20603
21818
|
try {
|
|
20604
|
-
const settled = await this.inTurn(() => run(
|
|
21819
|
+
const settled = await this.inTurn(() => run(turn));
|
|
20605
21820
|
outcome = settled.outcome;
|
|
20606
21821
|
return settled.plan;
|
|
20607
21822
|
} finally {
|
|
20608
|
-
if (this.
|
|
21823
|
+
if (this.openTurn === turn) this.openTurn = null;
|
|
20609
21824
|
this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
|
|
20610
21825
|
}
|
|
20611
21826
|
}
|
|
@@ -20656,10 +21871,11 @@ ${message}` : message;
|
|
|
20656
21871
|
* ops JSON still gets its two corrective retries; charging it for the read
|
|
20657
21872
|
* would cost it the chance to fix the edit.
|
|
20658
21873
|
*/
|
|
20659
|
-
async drainTaskQueries(turn,
|
|
21874
|
+
async drainTaskQueries(turn, userTurn) {
|
|
21875
|
+
const { reads, signal, stream } = userTurn;
|
|
20660
21876
|
const ai = this.host.aiService();
|
|
20661
21877
|
const carried = [];
|
|
20662
|
-
let current = turn;
|
|
21878
|
+
let current = this.claimSubmission(turn, userTurn);
|
|
20663
21879
|
while (current.kind === "task_query") {
|
|
20664
21880
|
carried.push(...current.researchLog);
|
|
20665
21881
|
if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
|
|
@@ -20673,17 +21889,30 @@ ${message}` : message;
|
|
|
20673
21889
|
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
20674
21890
|
reads.seen.add(signature);
|
|
20675
21891
|
reads.answered++;
|
|
20676
|
-
const
|
|
20677
|
-
current = await ai.continueConversation(
|
|
20678
|
-
insist ? `${
|
|
21892
|
+
const answer2 = this.taskQueryAnswer(current.query);
|
|
21893
|
+
current = this.claimSubmission(await ai.continueConversation(
|
|
21894
|
+
insist ? `${answer2}
|
|
20679
21895
|
|
|
20680
|
-
${TASK_QUERY_ANSWER_OR_OPS}` :
|
|
21896
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
|
|
20681
21897
|
stream.sink(),
|
|
20682
21898
|
signal
|
|
20683
|
-
);
|
|
21899
|
+
), userTurn);
|
|
20684
21900
|
}
|
|
20685
21901
|
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
20686
21902
|
}
|
|
21903
|
+
/**
|
|
21904
|
+
* The backend's answer, with what was handed in while it ran standing in
|
|
21905
|
+
* for whatever its reply carried. Taken once: a corrective re-send after a
|
|
21906
|
+
* rejected edit must not settle on the same submission again. A stopped
|
|
21907
|
+
* turn commits nothing, the same as a stopped reply with a plan in it.
|
|
21908
|
+
*/
|
|
21909
|
+
claimSubmission(turn, userTurn) {
|
|
21910
|
+
const submitted = userTurn.submitted;
|
|
21911
|
+
userTurn.submitted = void 0;
|
|
21912
|
+
if (!submitted || userTurn.signal?.aborted) return turn;
|
|
21913
|
+
const { text: text2, researchLog } = turn;
|
|
21914
|
+
return submitted.kind === "plan" ? { kind: "plan", tasks: submitted.tasks, text: text2, researchLog } : { kind: "task_ops", ops: submitted.ops, text: text2, researchLog };
|
|
21915
|
+
}
|
|
20687
21916
|
/**
|
|
20688
21917
|
* Render one read out of live state. Never persisted to the transcript: the
|
|
20689
21918
|
* detail is context for the planner's next reply, and re-sending it on every
|
|
@@ -20727,7 +21956,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
20727
21956
|
return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
|
|
20728
21957
|
},
|
|
20729
21958
|
maxRepairs: 2,
|
|
20730
|
-
onExhausted: ({ reply, errors }) => invalidOps(errors,
|
|
21959
|
+
onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
|
|
20731
21960
|
});
|
|
20732
21961
|
if ("plan" in settled) {
|
|
20733
21962
|
await this.host.afterEdit();
|
|
@@ -20790,10 +22019,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
20790
22019
|
*
|
|
20791
22020
|
* The host's catalog is allowlist-filtered, so a restricted allowlist stays a
|
|
20792
22021
|
* hard bound on every turn, and reads the plan's runners live, so a runner
|
|
20793
|
-
* admitted mid-session by a retarget is shown like every other.
|
|
22022
|
+
* admitted mid-session by a retarget is shown like every other. A planner
|
|
22023
|
+
* with Ordewell's tools gets a reminder to read the catalog instead.
|
|
20794
22024
|
*/
|
|
20795
22025
|
catalogBlock() {
|
|
20796
22026
|
if (!this.host.plan()) return null;
|
|
22027
|
+
if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
|
|
20797
22028
|
const { runners, models, modes, autonomousDefault } = this.host.catalog();
|
|
20798
22029
|
const modelLines = runners.map((runner) => {
|
|
20799
22030
|
const list = models[runner] ?? [];
|
|
@@ -20826,19 +22057,20 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
20826
22057
|
*/
|
|
20827
22058
|
planContextBlock() {
|
|
20828
22059
|
const tasks = this.host.tasks();
|
|
20829
|
-
const
|
|
20830
|
-
if (!
|
|
22060
|
+
const lines2 = this.currentPlanLines();
|
|
22061
|
+
if (!lines2) return null;
|
|
22062
|
+
const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
|
|
20831
22063
|
const locked = tasks.filter((t) => t.status === "in_progress");
|
|
20832
22064
|
const execNote = this.host.hasLiveWork() ? `
|
|
20833
22065
|
Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
|
|
20834
22066
|
return [
|
|
20835
22067
|
"<current_plan>",
|
|
20836
|
-
...
|
|
22068
|
+
...lines2,
|
|
20837
22069
|
"</current_plan>",
|
|
20838
22070
|
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
20839
22071
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
20840
|
-
TASK_QUERY_REMINDER,
|
|
20841
|
-
...taskOpsProtocol(execNote)
|
|
22072
|
+
tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
|
|
22073
|
+
...taskOpsProtocol(execNote, tools)
|
|
20842
22074
|
].filter(Boolean).join("\n");
|
|
20843
22075
|
}
|
|
20844
22076
|
/** One line per task with stable references — null until the plan has tasks. */
|
|
@@ -21114,6 +22346,7 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
21114
22346
|
}
|
|
21115
22347
|
|
|
21116
22348
|
// src/services/SkillsService.ts
|
|
22349
|
+
var import_crypto6 = require("crypto");
|
|
21117
22350
|
var fs9 = __toESM(require("fs"));
|
|
21118
22351
|
var path19 = __toESM(require("path"));
|
|
21119
22352
|
|
|
@@ -21131,6 +22364,11 @@ function builtinSkillsDir() {
|
|
|
21131
22364
|
// src/services/SkillsService.ts
|
|
21132
22365
|
var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
|
|
21133
22366
|
var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
|
|
22367
|
+
var PRIOR_BUILTIN_HASHES = {
|
|
22368
|
+
grilling: ["d5d4cb8589fbb4a0f3296bae15033f2297ef682aaf5615a3dee258451f96e5c3"],
|
|
22369
|
+
"improve-codebase-architecture": ["b5154b5e3baca9a5f244634453e83706d742011c31163648c2b8395a2524dd9c"]
|
|
22370
|
+
};
|
|
22371
|
+
var SEED_MANIFEST = ".seeded.json";
|
|
21134
22372
|
function parseSkillFile(filePath, name, source) {
|
|
21135
22373
|
const raw = fs9.readFileSync(filePath, "utf8");
|
|
21136
22374
|
const frontmatter = {};
|
|
@@ -21170,6 +22408,13 @@ function copyDirSync2(src, dest) {
|
|
|
21170
22408
|
else fs9.copyFileSync(srcPath, destPath);
|
|
21171
22409
|
}
|
|
21172
22410
|
}
|
|
22411
|
+
function fileHash(file) {
|
|
22412
|
+
try {
|
|
22413
|
+
return (0, import_crypto6.createHash)("sha256").update(fs9.readFileSync(file)).digest("hex");
|
|
22414
|
+
} catch {
|
|
22415
|
+
return void 0;
|
|
22416
|
+
}
|
|
22417
|
+
}
|
|
21173
22418
|
function readDir(dir, source) {
|
|
21174
22419
|
if (!fs9.existsSync(dir)) return [];
|
|
21175
22420
|
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
@@ -21194,21 +22439,59 @@ var SkillsService = class {
|
|
|
21194
22439
|
globalDir() {
|
|
21195
22440
|
return path19.join(globalDataDir(), "skills");
|
|
21196
22441
|
}
|
|
22442
|
+
readSeedManifest() {
|
|
22443
|
+
try {
|
|
22444
|
+
const parsed = JSON.parse(fs9.readFileSync(path19.join(this.globalDir(), SEED_MANIFEST), "utf8"));
|
|
22445
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
22446
|
+
return Object.fromEntries(
|
|
22447
|
+
Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
|
|
22448
|
+
);
|
|
22449
|
+
}
|
|
22450
|
+
} catch {
|
|
22451
|
+
}
|
|
22452
|
+
return {};
|
|
22453
|
+
}
|
|
22454
|
+
recordSeed(name, hash) {
|
|
22455
|
+
const manifest = this.readSeedManifest();
|
|
22456
|
+
if (manifest[name] === hash) return;
|
|
22457
|
+
manifest[name] = hash;
|
|
22458
|
+
try {
|
|
22459
|
+
fs9.mkdirSync(this.globalDir(), { recursive: true });
|
|
22460
|
+
fs9.writeFileSync(path19.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
22461
|
+
} catch (err) {
|
|
22462
|
+
console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
|
|
22463
|
+
}
|
|
22464
|
+
}
|
|
22465
|
+
/** A seed the user never edited: safe to replace with a newer built-in. */
|
|
22466
|
+
isUntouchedSeed(name, currentHash) {
|
|
22467
|
+
if (currentHash === void 0) return false;
|
|
22468
|
+
return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
|
|
22469
|
+
}
|
|
21197
22470
|
seed(name) {
|
|
21198
22471
|
const dest = path19.join(this.globalDir(), name);
|
|
21199
|
-
|
|
22472
|
+
const exists = fs9.existsSync(dest);
|
|
21200
22473
|
let srcDir;
|
|
21201
22474
|
try {
|
|
21202
22475
|
srcDir = path19.join(builtinSkillsDir(), name);
|
|
21203
22476
|
} catch (err) {
|
|
21204
|
-
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
22477
|
+
if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
21205
22478
|
return false;
|
|
21206
22479
|
}
|
|
21207
22480
|
if (!fs9.existsSync(srcDir)) {
|
|
21208
|
-
console.warn(`Built-in skill not found in package: ${name}`);
|
|
22481
|
+
if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
|
|
21209
22482
|
return false;
|
|
21210
22483
|
}
|
|
22484
|
+
const srcHash = fileHash(path19.join(srcDir, "SKILL.md"));
|
|
22485
|
+
if (exists) {
|
|
22486
|
+
const currentHash = fileHash(path19.join(dest, "SKILL.md"));
|
|
22487
|
+
if (currentHash !== void 0 && currentHash === srcHash) {
|
|
22488
|
+
this.recordSeed(name, currentHash);
|
|
22489
|
+
return false;
|
|
22490
|
+
}
|
|
22491
|
+
if (!this.isUntouchedSeed(name, currentHash)) return false;
|
|
22492
|
+
}
|
|
21211
22493
|
copyDirSync2(srcDir, dest);
|
|
22494
|
+
if (srcHash) this.recordSeed(name, srcHash);
|
|
21212
22495
|
return true;
|
|
21213
22496
|
}
|
|
21214
22497
|
seedBuiltinSkill(name) {
|
|
@@ -21564,9 +22847,9 @@ var fs10 = __toESM(require("fs"));
|
|
|
21564
22847
|
var path20 = __toESM(require("path"));
|
|
21565
22848
|
|
|
21566
22849
|
// src/utils/sessionId.ts
|
|
21567
|
-
var
|
|
22850
|
+
var import_crypto7 = require("crypto");
|
|
21568
22851
|
function mintSessionId() {
|
|
21569
|
-
return `session-${(0,
|
|
22852
|
+
return `session-${(0, import_crypto7.randomBytes)(16).toString("hex")}`;
|
|
21570
22853
|
}
|
|
21571
22854
|
|
|
21572
22855
|
// src/utils/sessionStore.ts
|
|
@@ -21903,7 +23186,12 @@ function lastAttemptDigest(location, taskId) {
|
|
|
21903
23186
|
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
21904
23187
|
}
|
|
21905
23188
|
function sessionRuntimeSettings(settings) {
|
|
21906
|
-
return {
|
|
23189
|
+
return {
|
|
23190
|
+
...plannerRuntimeToggles(settings),
|
|
23191
|
+
modelAllowlist: settings.modelAllowlist,
|
|
23192
|
+
runnerTransport: settings.runnerTransport,
|
|
23193
|
+
enabledRunners: settings.enabledRunners
|
|
23194
|
+
};
|
|
21907
23195
|
}
|
|
21908
23196
|
var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
|
|
21909
23197
|
function resolveSkillInvocation(text2, skillsService) {
|
|
@@ -21927,14 +23215,14 @@ function resolveSkillInvocation(text2, skillsService) {
|
|
|
21927
23215
|
return `${lead}${skill.content}${punct}`;
|
|
21928
23216
|
});
|
|
21929
23217
|
}
|
|
21930
|
-
function liveAiService(config, workspaceRoot) {
|
|
23218
|
+
function liveAiService(config, workspaceRoot, mcpServer) {
|
|
21931
23219
|
let live = null;
|
|
21932
23220
|
let liveProvider = null;
|
|
21933
23221
|
return () => {
|
|
21934
23222
|
const provider = config.aiProvider;
|
|
21935
23223
|
if (live && liveProvider === provider) return live;
|
|
21936
23224
|
live?.reset();
|
|
21937
|
-
live = createAiService(config, { workspaceRoot });
|
|
23225
|
+
live = createAiService(config, { workspaceRoot, mcpServer });
|
|
21938
23226
|
liveProvider = provider;
|
|
21939
23227
|
return live;
|
|
21940
23228
|
};
|
|
@@ -21944,7 +23232,7 @@ function isPlannerApproval(request) {
|
|
|
21944
23232
|
}
|
|
21945
23233
|
function createSession(deps) {
|
|
21946
23234
|
const pinnedAiService = deps.aiService;
|
|
21947
|
-
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
|
|
23235
|
+
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
|
|
21948
23236
|
const store = new PlanStore();
|
|
21949
23237
|
const taskLogs = new TaskLogRecorder({
|
|
21950
23238
|
broadcast: deps.broadcast,
|
|
@@ -22074,6 +23362,16 @@ var Session = class {
|
|
|
22074
23362
|
currentSessionId;
|
|
22075
23363
|
skillsService;
|
|
22076
23364
|
conversation;
|
|
23365
|
+
plannerTools = plannerToolHandler({
|
|
23366
|
+
liveCatalog: () => this.liveCatalog(),
|
|
23367
|
+
coerce: (tasks, runners) => coerceAssignments(tasks, this.allowlist(), runners, this.models()),
|
|
23368
|
+
submitPlan: (tasks, runners) => this.submitPlanFromTool(tasks, runners),
|
|
23369
|
+
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
23370
|
+
tasks: () => this.store.planTasks,
|
|
23371
|
+
read: (signature, answer2) => this.conversation.read(signature, answer2),
|
|
23372
|
+
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
23373
|
+
lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId)
|
|
23374
|
+
});
|
|
22077
23375
|
constructor(parts) {
|
|
22078
23376
|
this.config = parts.config;
|
|
22079
23377
|
this.registry = parts.registry;
|
|
@@ -22102,17 +23400,7 @@ var Session = class {
|
|
|
22102
23400
|
aiService: () => this.aiService(),
|
|
22103
23401
|
onProgress: (p) => this.events.progress(p),
|
|
22104
23402
|
opening: (runners) => this.conversationOpening(runners),
|
|
22105
|
-
catalog: () =>
|
|
22106
|
-
const runners = this.plan?.runners ?? [];
|
|
22107
|
-
return {
|
|
22108
|
-
runners,
|
|
22109
|
-
// Allowlist-filtered: neither the per-turn block nor a read may offer
|
|
22110
|
-
// a model the planner is forbidden to assign.
|
|
22111
|
-
models: filterModelsForPrompt(this.models(), this.allowlist()),
|
|
22112
|
-
modes: this.runnerModesFor(runners),
|
|
22113
|
-
autonomousDefault: this.config.autonomousMode
|
|
22114
|
-
};
|
|
22115
|
-
},
|
|
23403
|
+
catalog: () => this.catalogOf(this.plan?.runners ?? []),
|
|
22116
23404
|
tasks: () => this.store.planTasks,
|
|
22117
23405
|
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
22118
23406
|
hasLiveWork: () => this.hasLiveWork,
|
|
@@ -22136,8 +23424,8 @@ var Session = class {
|
|
|
22136
23424
|
* server's HTTP route, the VS Code webview, the TUI prompt — so the decision
|
|
22137
23425
|
* path is identical regardless of who is looking.
|
|
22138
23426
|
*/
|
|
22139
|
-
resolveApproval(id,
|
|
22140
|
-
return this.approvals.resolve(id,
|
|
23427
|
+
resolveApproval(id, answer2) {
|
|
23428
|
+
return this.approvals.resolve(id, answer2);
|
|
22141
23429
|
}
|
|
22142
23430
|
/** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
|
|
22143
23431
|
outstandingApprovals() {
|
|
@@ -22257,6 +23545,27 @@ var Session = class {
|
|
|
22257
23545
|
this.modelsCache = {};
|
|
22258
23546
|
this.remintSessionId();
|
|
22259
23547
|
}
|
|
23548
|
+
/**
|
|
23549
|
+
* What a planner may assign right now: every enabled runner, its modes, and
|
|
23550
|
+
* its allowlisted models — read as of this call, so a runner enabled or an
|
|
23551
|
+
* allowlist edited since planning started is in it (#69). Discovers a
|
|
23552
|
+
* runner's models the first time it is asked for.
|
|
23553
|
+
*/
|
|
23554
|
+
async liveCatalog() {
|
|
23555
|
+
const runners = this.enabledRunners();
|
|
23556
|
+
this.modelsCache = { ...this.modelsCache, ...await this.modelResolver.modelsForRunners(runners) };
|
|
23557
|
+
return this.catalogOf(runners);
|
|
23558
|
+
}
|
|
23559
|
+
catalogOf(runners) {
|
|
23560
|
+
return {
|
|
23561
|
+
runners,
|
|
23562
|
+
// Allowlist-filtered: neither the per-turn block nor a read may offer
|
|
23563
|
+
// a model the planner is forbidden to assign.
|
|
23564
|
+
models: filterModelsForPrompt(this.models(), this.allowlist()),
|
|
23565
|
+
modes: this.runnerModesFor(runners),
|
|
23566
|
+
autonomousDefault: this.config.autonomousMode
|
|
23567
|
+
};
|
|
23568
|
+
}
|
|
22260
23569
|
runnerModesFor(runners) {
|
|
22261
23570
|
return runnerModesFrom(this.registry, runners);
|
|
22262
23571
|
}
|
|
@@ -22268,13 +23577,17 @@ var Session = class {
|
|
|
22268
23577
|
* allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
22269
23578
|
* allowlist itself, the same way `coerceAssignments` does.
|
|
22270
23579
|
*/
|
|
22271
|
-
editCatalog() {
|
|
23580
|
+
editCatalog(runners = this.plan?.runners ?? []) {
|
|
22272
23581
|
return {
|
|
22273
23582
|
modelsByRunner: this.models(),
|
|
22274
|
-
runnerModes: this.runnerModesFor(
|
|
23583
|
+
runnerModes: this.runnerModesFor(runners),
|
|
22275
23584
|
perRunnerAllowlist: this.allowlist()
|
|
22276
23585
|
};
|
|
22277
23586
|
}
|
|
23587
|
+
/** The runners enabled right now — a toggle made since the session was built counts (#69). */
|
|
23588
|
+
enabledRunners() {
|
|
23589
|
+
return this.settingsFn().enabledRunners ?? this.config.enabledRunners;
|
|
23590
|
+
}
|
|
22278
23591
|
/**
|
|
22279
23592
|
* The allowlist in force right now. Unset means no restriction — falling
|
|
22280
23593
|
* back to the one planning started under would keep a restriction the user
|
|
@@ -22376,7 +23689,7 @@ var Session = class {
|
|
|
22376
23689
|
this.goal = goal;
|
|
22377
23690
|
this.remintSessionId();
|
|
22378
23691
|
this.beginFreshPlan();
|
|
22379
|
-
const enabled = this.
|
|
23692
|
+
const enabled = this.enabledRunners();
|
|
22380
23693
|
const chosenRunners = runners.filter((r) => enabled.includes(r));
|
|
22381
23694
|
if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
|
|
22382
23695
|
const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
|
|
@@ -22420,7 +23733,7 @@ var Session = class {
|
|
|
22420
23733
|
this.goal = this.resolveSkillInvocation(goal);
|
|
22421
23734
|
this.remintSessionId();
|
|
22422
23735
|
this.beginFreshPlan();
|
|
22423
|
-
const enabled = this.
|
|
23736
|
+
const enabled = this.enabledRunners();
|
|
22424
23737
|
const chosenRunners = runners.filter((r) => enabled.includes(r));
|
|
22425
23738
|
if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
|
|
22426
23739
|
const opening = await this.conversationOpening(chosenRunners);
|
|
@@ -22498,6 +23811,49 @@ var Session = class {
|
|
|
22498
23811
|
rewindTargets() {
|
|
22499
23812
|
return this.conversation.rewindTargets();
|
|
22500
23813
|
}
|
|
23814
|
+
/**
|
|
23815
|
+
* Hand the planner turn in flight a validated plan or a task edit; the turn
|
|
23816
|
+
* commits it as it would the same JSON in the planner's reply. False when
|
|
23817
|
+
* no turn is open.
|
|
23818
|
+
*/
|
|
23819
|
+
submitToTurn(submission) {
|
|
23820
|
+
return this.conversation.submit(submission);
|
|
23821
|
+
}
|
|
23822
|
+
/**
|
|
23823
|
+
* A plan the planner submitted through its tool, already checked against the
|
|
23824
|
+
* live catalog. Its runners join `plan.runners` here, so a runner enabled
|
|
23825
|
+
* since planning started is not snapped back by the commit (#69).
|
|
23826
|
+
*/
|
|
23827
|
+
submitPlanFromTool(tasks, runners) {
|
|
23828
|
+
if (!this.plan || !this.conversation.submit({ kind: "plan", tasks })) return false;
|
|
23829
|
+
for (const runner of runners) this.admitRunner(runner, this.modelsCache[runner] ?? []);
|
|
23830
|
+
return true;
|
|
23831
|
+
}
|
|
23832
|
+
/**
|
|
23833
|
+
* A task edit the planner made through its tool. Checked against the live
|
|
23834
|
+
* catalog, then handed to the open turn, which commits it as it would the
|
|
23835
|
+
* same ops in a taskOps reply — including parking it behind a running batch,
|
|
23836
|
+
* which the envelope does before any check, so a queued edit is not checked
|
|
23837
|
+
* here either. Edits made in one reply join one batch.
|
|
23838
|
+
*/
|
|
23839
|
+
async editPlanFromTool(ops) {
|
|
23840
|
+
const refuse = (message) => ({ ok: false, errors: [message] });
|
|
23841
|
+
const catalog = await this.liveCatalog();
|
|
23842
|
+
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
23843
|
+
const held = this.conversation.pendingOps();
|
|
23844
|
+
if (!held) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
|
|
23845
|
+
const batch = [...held, ...ops];
|
|
23846
|
+
const queued = this.conversation.editWouldQueue(batch);
|
|
23847
|
+
let summary = [];
|
|
23848
|
+
if (!queued) {
|
|
23849
|
+
const result = applyTaskOps(this.store.planTasks, batch, catalog.runners, this.editCatalog(catalog.runners));
|
|
23850
|
+
if (!result.ok) return { ok: false, errors: result.errors };
|
|
23851
|
+
summary = result.summary;
|
|
23852
|
+
for (const runner of runnersOf(result.tasks)) this.admitRunner(runner, this.modelsCache[runner] ?? []);
|
|
23853
|
+
}
|
|
23854
|
+
if (!this.conversation.submit({ kind: "task_ops", ops: batch })) return refuse("No planning turn is open to take the edit.");
|
|
23855
|
+
return { ok: true, summary, queued };
|
|
23856
|
+
}
|
|
22501
23857
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
22502
23858
|
get isConversationActive() {
|
|
22503
23859
|
return this.conversation.isActive;
|
|
@@ -22543,7 +23899,8 @@ var Session = class {
|
|
|
22543
23899
|
// usage line can show context fill (#49). Unknown stays absent.
|
|
22544
23900
|
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
22545
23901
|
fs: this.fsAdapter,
|
|
22546
|
-
fetcher: this.fetcher
|
|
23902
|
+
fetcher: this.fetcher,
|
|
23903
|
+
plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
|
|
22547
23904
|
};
|
|
22548
23905
|
}
|
|
22549
23906
|
/**
|
|
@@ -22661,7 +24018,7 @@ var Session = class {
|
|
|
22661
24018
|
])
|
|
22662
24019
|
);
|
|
22663
24020
|
try {
|
|
22664
|
-
const modelsByRunner = await this.modelResolver.modelsForRunners(this.
|
|
24021
|
+
const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
|
|
22665
24022
|
const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
|
|
22666
24023
|
const { modelAllowlist } = this.settingsFn();
|
|
22667
24024
|
const result = await this.planner.modifyDuringExecution({
|
|
@@ -23008,7 +24365,7 @@ var Session = class {
|
|
|
23008
24365
|
if (!this.plan) throw new Error("No active plan state");
|
|
23009
24366
|
const check = canMergeTasks(this.store.planTasks, taskIds);
|
|
23010
24367
|
if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
|
|
23011
|
-
const prompt = buildMergePrompt(taskIds, this.store.planTasks);
|
|
24368
|
+
const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
|
|
23012
24369
|
return this.continueConversation(prompt, options);
|
|
23013
24370
|
}
|
|
23014
24371
|
/**
|
|
@@ -23020,7 +24377,7 @@ var Session = class {
|
|
|
23020
24377
|
if (!this.plan) throw new Error("No active plan state");
|
|
23021
24378
|
const check = canSplitTask(this.store.planTasks, taskId);
|
|
23022
24379
|
if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
|
|
23023
|
-
const prompt = buildSplitPrompt(taskId, this.store.planTasks);
|
|
24380
|
+
const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
|
|
23024
24381
|
return this.continueConversation(prompt, options);
|
|
23025
24382
|
}
|
|
23026
24383
|
loadPlan(plan, goal, workspace, opts) {
|
|
@@ -23060,41 +24417,6 @@ var Session = class {
|
|
|
23060
24417
|
}
|
|
23061
24418
|
};
|
|
23062
24419
|
|
|
23063
|
-
// src/utils/privateFile.ts
|
|
23064
|
-
var import_fs2 = require("fs");
|
|
23065
|
-
var import_path = require("path");
|
|
23066
|
-
var import_crypto4 = require("crypto");
|
|
23067
|
-
var DIR_MODE = 448;
|
|
23068
|
-
var FILE_MODE = 384;
|
|
23069
|
-
var POSIX_MODES = process.platform !== "win32";
|
|
23070
|
-
function ensurePrivateDir(dir) {
|
|
23071
|
-
if (POSIX_MODES) {
|
|
23072
|
-
(0, import_fs2.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
|
|
23073
|
-
return;
|
|
23074
|
-
}
|
|
23075
|
-
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
23076
|
-
}
|
|
23077
|
-
function writePrivateFile(filePath, content) {
|
|
23078
|
-
const dir = (0, import_path.dirname)(filePath);
|
|
23079
|
-
ensurePrivateDir(dir);
|
|
23080
|
-
const tempPath = (0, import_path.join)(dir, `.${(0, import_path.basename)(filePath)}.${(0, import_crypto4.randomBytes)(8).toString("hex")}.tmp`);
|
|
23081
|
-
const fd = (0, import_fs2.openSync)(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
|
|
23082
|
-
try {
|
|
23083
|
-
(0, import_fs2.writeSync)(fd, content);
|
|
23084
|
-
} finally {
|
|
23085
|
-
(0, import_fs2.closeSync)(fd);
|
|
23086
|
-
}
|
|
23087
|
-
try {
|
|
23088
|
-
(0, import_fs2.renameSync)(tempPath, filePath);
|
|
23089
|
-
} catch (err) {
|
|
23090
|
-
try {
|
|
23091
|
-
(0, import_fs2.unlinkSync)(tempPath);
|
|
23092
|
-
} catch {
|
|
23093
|
-
}
|
|
23094
|
-
throw err;
|
|
23095
|
-
}
|
|
23096
|
-
}
|
|
23097
|
-
|
|
23098
24420
|
// src/conversation/reduce.ts
|
|
23099
24421
|
var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
|
|
23100
24422
|
function isUnsettledSegment(block, turnId) {
|
|
@@ -23776,9 +25098,9 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
23776
25098
|
// src/services/TmuxRunner.ts
|
|
23777
25099
|
var import_child_process11 = require("child_process");
|
|
23778
25100
|
var import_util5 = require("util");
|
|
23779
|
-
var
|
|
23780
|
-
var
|
|
23781
|
-
var
|
|
25101
|
+
var import_os2 = require("os");
|
|
25102
|
+
var import_path5 = require("path");
|
|
25103
|
+
var import_fs5 = require("fs");
|
|
23782
25104
|
|
|
23783
25105
|
// src/utils/tmux.ts
|
|
23784
25106
|
var import_child_process10 = require("child_process");
|
|
@@ -23876,7 +25198,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
23876
25198
|
* lets the user keep poking at a finished task's terminal.
|
|
23877
25199
|
*/
|
|
23878
25200
|
async start(command, args, cwd, env) {
|
|
23879
|
-
(0,
|
|
25201
|
+
(0, import_fs5.writeFileSync)(this.logPath, "");
|
|
23880
25202
|
const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
|
|
23881
25203
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
23882
25204
|
const prefix = envAssignments ? `env ${envAssignments} ` : "";
|
|
@@ -23899,7 +25221,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
23899
25221
|
readLog() {
|
|
23900
25222
|
let content;
|
|
23901
25223
|
try {
|
|
23902
|
-
content = (0,
|
|
25224
|
+
content = (0, import_fs5.existsSync)(this.logPath) ? (0, import_fs5.readFileSync)(this.logPath, "utf8") : "";
|
|
23903
25225
|
} catch {
|
|
23904
25226
|
return false;
|
|
23905
25227
|
}
|
|
@@ -23955,7 +25277,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
23955
25277
|
this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
|
|
23956
25278
|
});
|
|
23957
25279
|
try {
|
|
23958
|
-
(0,
|
|
25280
|
+
(0, import_fs5.unlinkSync)(this.logPath);
|
|
23959
25281
|
} catch {
|
|
23960
25282
|
}
|
|
23961
25283
|
this.baseHandleExit(code);
|
|
@@ -23992,7 +25314,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23992
25314
|
this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
|
|
23993
25315
|
this.resolvePath = deps.resolvePath ?? augmentedPath;
|
|
23994
25316
|
this.pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
23995
|
-
this.logDir = deps.logDir ?? (0,
|
|
25317
|
+
this.logDir = deps.logDir ?? (0, import_os2.tmpdir)();
|
|
23996
25318
|
this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
|
|
23997
25319
|
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
23998
25320
|
}
|
|
@@ -24112,7 +25434,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
24112
25434
|
windowName,
|
|
24113
25435
|
this.execFileImpl,
|
|
24114
25436
|
this.pollIntervalMs,
|
|
24115
|
-
(0,
|
|
25437
|
+
(0, import_path5.join)(this.logDir, `${id}.log`),
|
|
24116
25438
|
this.socket
|
|
24117
25439
|
);
|
|
24118
25440
|
invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
|
|
@@ -24140,9 +25462,9 @@ var KEY_ARGS = {
|
|
|
24140
25462
|
};
|
|
24141
25463
|
var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
|
|
24142
25464
|
function toolLine(name, args) {
|
|
24143
|
-
const { tool } = mapAgentTool(name);
|
|
24144
|
-
const normalized = normalizeAgentArgs(
|
|
24145
|
-
const preferred = KEY_ARGS[
|
|
25465
|
+
const { tool: tool2 } = mapAgentTool(name);
|
|
25466
|
+
const normalized = normalizeAgentArgs(tool2, args);
|
|
25467
|
+
const preferred = KEY_ARGS[tool2];
|
|
24146
25468
|
for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
|
|
24147
25469
|
const value = normalized[key];
|
|
24148
25470
|
if (typeof value !== "string") continue;
|
|
@@ -24205,6 +25527,7 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
24205
25527
|
constructor(id, taskId, launch) {
|
|
24206
25528
|
super(id, taskId);
|
|
24207
25529
|
this.launch = launch;
|
|
25530
|
+
this.startOptions = launch.startOptions;
|
|
24208
25531
|
this.text = new PlainTextChannel((text2) => {
|
|
24209
25532
|
this.output += text2;
|
|
24210
25533
|
this.outputEmitter.emit("output", text2);
|
|
@@ -24233,9 +25556,20 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
24233
25556
|
* requests — while an approval must be answerable by id across the session.
|
|
24234
25557
|
*/
|
|
24235
25558
|
permissions = /* @__PURE__ */ new Map();
|
|
25559
|
+
/** The launch's start options, with this attempt's server once its token is issued. */
|
|
25560
|
+
startOptions;
|
|
25561
|
+
/** This attempt's MCP token, until the session ends (ADR-0022, A2). */
|
|
25562
|
+
toolToken = null;
|
|
25563
|
+
checkpointHandler = null;
|
|
24236
25564
|
/** Start the runner and send the task's prompt as its first turn. */
|
|
24237
25565
|
async start(prompt) {
|
|
24238
|
-
await this.
|
|
25566
|
+
await this.issueTools();
|
|
25567
|
+
try {
|
|
25568
|
+
await this.startAdapter(this.startOptions);
|
|
25569
|
+
} catch (err) {
|
|
25570
|
+
this.revokeTools();
|
|
25571
|
+
throw err;
|
|
25572
|
+
}
|
|
24239
25573
|
this.state = "working";
|
|
24240
25574
|
setImmediate(() => {
|
|
24241
25575
|
if (!this.exited) this.deliver(prompt);
|
|
@@ -24244,6 +25578,49 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
24244
25578
|
getOutput() {
|
|
24245
25579
|
return this.output;
|
|
24246
25580
|
}
|
|
25581
|
+
onTaskComplete(listener) {
|
|
25582
|
+
this.structuredEmitter.on("taskComplete", listener);
|
|
25583
|
+
}
|
|
25584
|
+
onToolCheckpoint(handler) {
|
|
25585
|
+
this.checkpointHandler = handler;
|
|
25586
|
+
}
|
|
25587
|
+
/**
|
|
25588
|
+
* A server that cannot start costs the task its tools, not its run: the
|
|
25589
|
+
* prompt still teaches the marker (ADR-0022, S2).
|
|
25590
|
+
*/
|
|
25591
|
+
async issueTools() {
|
|
25592
|
+
const tools = this.launch.tools;
|
|
25593
|
+
if (!tools) return;
|
|
25594
|
+
try {
|
|
25595
|
+
const credential = await tools.server.issueTaskToken(tools.scope, {
|
|
25596
|
+
taskComplete: async (report) => {
|
|
25597
|
+
this.structuredEmitter.emit("taskComplete", report);
|
|
25598
|
+
return { text: "Recorded. End your turn now." };
|
|
25599
|
+
},
|
|
25600
|
+
checkpoint: async ({ question }, { signal }) => {
|
|
25601
|
+
if (!this.checkpointHandler) return { text: "checkpoint is not available in this session.", isError: true };
|
|
25602
|
+
return checkpointReply(await this.checkpointHandler(question, signal));
|
|
25603
|
+
}
|
|
25604
|
+
});
|
|
25605
|
+
if (this.exited) {
|
|
25606
|
+
tools.server.revoke(credential.token);
|
|
25607
|
+
return;
|
|
25608
|
+
}
|
|
25609
|
+
this.toolToken = credential.token;
|
|
25610
|
+
this.startOptions = { ...this.startOptions, mcp: mcpClientConfig(credential) };
|
|
25611
|
+
} catch (err) {
|
|
25612
|
+
console.error(`[structured] No Ordewell tools for task ${this.taskId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
25613
|
+
}
|
|
25614
|
+
}
|
|
25615
|
+
revokeTools() {
|
|
25616
|
+
if (this.toolToken === null) return;
|
|
25617
|
+
this.launch.tools?.server.revoke(this.toolToken);
|
|
25618
|
+
this.toolToken = null;
|
|
25619
|
+
}
|
|
25620
|
+
baseHandleExit(code) {
|
|
25621
|
+
this.revokeTools();
|
|
25622
|
+
super.baseHandleExit(code);
|
|
25623
|
+
}
|
|
24247
25624
|
/** A reply typed at the task — a checkpoint answer, most often — is a user message. */
|
|
24248
25625
|
write(text2) {
|
|
24249
25626
|
const message = text2.trim();
|
|
@@ -24341,12 +25718,12 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
24341
25718
|
* session id after an abort.
|
|
24342
25719
|
*/
|
|
24343
25720
|
async restartInterrupted(turn) {
|
|
24344
|
-
const resumeSessionId = this.nativeSessionId() ?? this.
|
|
25721
|
+
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
24345
25722
|
this.generation += 1;
|
|
24346
25723
|
turn.abort.abort();
|
|
24347
25724
|
if (this.adapter) this.withdrawPermissions(this.adapter);
|
|
24348
25725
|
try {
|
|
24349
|
-
await this.startAdapter({ ...this.
|
|
25726
|
+
await this.startAdapter({ ...this.startOptions, resumeSessionId });
|
|
24350
25727
|
} catch (err) {
|
|
24351
25728
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
24352
25729
|
this.endTurn(turn, "interrupted");
|
|
@@ -24484,11 +25861,13 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
24484
25861
|
processDeps;
|
|
24485
25862
|
createAdapter;
|
|
24486
25863
|
interruptGraceMs;
|
|
25864
|
+
mcp;
|
|
24487
25865
|
constructor(deps = {}) {
|
|
24488
25866
|
super();
|
|
24489
25867
|
this.processDeps = deps.process ?? {};
|
|
24490
25868
|
this.createAdapter = deps.createAdapter ?? createTaskAdapter;
|
|
24491
25869
|
this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
|
|
25870
|
+
this.mcp = deps.mcp ?? sharedMcpServer();
|
|
24492
25871
|
}
|
|
24493
25872
|
async spawn(opts) {
|
|
24494
25873
|
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
@@ -24515,7 +25894,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
24515
25894
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
24516
25895
|
resumeSessionId: opts.resumeSessionId
|
|
24517
25896
|
},
|
|
24518
|
-
interruptGraceMs: this.interruptGraceMs
|
|
25897
|
+
interruptGraceMs: this.interruptGraceMs,
|
|
25898
|
+
...takesOrdewellTools(opts.runner) ? { tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } } } : {}
|
|
24519
25899
|
});
|
|
24520
25900
|
console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
|
|
24521
25901
|
this.registerSession(id, session);
|
|
@@ -24529,73 +25909,12 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
24529
25909
|
}
|
|
24530
25910
|
};
|
|
24531
25911
|
|
|
24532
|
-
// src/utils/daemonToken.ts
|
|
24533
|
-
var import_crypto5 = require("crypto");
|
|
24534
|
-
var import_fs4 = require("fs");
|
|
24535
|
-
var import_path3 = require("path");
|
|
24536
|
-
var BEARER_PREFIX = "bearer ";
|
|
24537
|
-
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
24538
|
-
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
24539
|
-
function configDir() {
|
|
24540
|
-
return globalDataDir();
|
|
24541
|
-
}
|
|
24542
|
-
function daemonTokenPath(port) {
|
|
24543
|
-
return (0, import_path3.join)(configDir(), `server-${port}.token`);
|
|
24544
|
-
}
|
|
24545
|
-
function mintDaemonToken(port) {
|
|
24546
|
-
const token = (0, import_crypto5.randomBytes)(32).toString("base64url");
|
|
24547
|
-
const file = daemonTokenPath(port);
|
|
24548
|
-
writePrivateFile(file, token);
|
|
24549
|
-
return { token, file };
|
|
24550
|
-
}
|
|
24551
|
-
function readDaemonToken(port) {
|
|
24552
|
-
try {
|
|
24553
|
-
const token = (0, import_fs4.readFileSync)(daemonTokenPath(port), "utf8").trim();
|
|
24554
|
-
return token === "" ? void 0 : token;
|
|
24555
|
-
} catch {
|
|
24556
|
-
return void 0;
|
|
24557
|
-
}
|
|
24558
|
-
}
|
|
24559
|
-
function clearDaemonToken(port) {
|
|
24560
|
-
try {
|
|
24561
|
-
(0, import_fs4.unlinkSync)(daemonTokenPath(port));
|
|
24562
|
-
} catch {
|
|
24563
|
-
}
|
|
24564
|
-
}
|
|
24565
|
-
function bearerHeaderValue(token) {
|
|
24566
|
-
return `Bearer ${token}`;
|
|
24567
|
-
}
|
|
24568
|
-
function tokenSubprotocols(token) {
|
|
24569
|
-
return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
|
|
24570
|
-
}
|
|
24571
|
-
function extractPresentedToken(carriers) {
|
|
24572
|
-
const authorization = carriers.authorization;
|
|
24573
|
-
if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
|
|
24574
|
-
const value = authorization.slice(BEARER_PREFIX.length).trim();
|
|
24575
|
-
if (value !== "") return value;
|
|
24576
|
-
}
|
|
24577
|
-
for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
|
|
24578
|
-
const value = offered.trim();
|
|
24579
|
-
if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
|
|
24580
|
-
const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
|
|
24581
|
-
if (token !== "") return token;
|
|
24582
|
-
}
|
|
24583
|
-
return void 0;
|
|
24584
|
-
}
|
|
24585
|
-
function tokensMatch(presented, expected) {
|
|
24586
|
-
if (presented === void 0) return false;
|
|
24587
|
-
const a = Buffer.from(presented, "utf8");
|
|
24588
|
-
const b = Buffer.from(expected, "utf8");
|
|
24589
|
-
if (a.length !== b.length) return false;
|
|
24590
|
-
return (0, import_crypto5.timingSafeEqual)(a, b);
|
|
24591
|
-
}
|
|
24592
|
-
|
|
24593
25912
|
// src/plugins/RunnerRegistry.ts
|
|
24594
25913
|
var fs13 = __toESM(require("fs"));
|
|
24595
25914
|
var path24 = __toESM(require("path"));
|
|
24596
25915
|
var os6 = __toESM(require("os"));
|
|
24597
25916
|
var import_child_process13 = require("child_process");
|
|
24598
|
-
var
|
|
25917
|
+
var import_crypto8 = require("crypto");
|
|
24599
25918
|
|
|
24600
25919
|
// src/plugins/FsPluginStore.ts
|
|
24601
25920
|
var fs12 = __toESM(require("fs"));
|
|
@@ -24619,8 +25938,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
|
|
|
24619
25938
|
assertPlainPluginName(name);
|
|
24620
25939
|
const root = path22.resolve(pluginsDir);
|
|
24621
25940
|
const dest = path22.resolve(root, name);
|
|
24622
|
-
const
|
|
24623
|
-
if (
|
|
25941
|
+
const relative6 = path22.relative(root, dest);
|
|
25942
|
+
if (relative6 !== name || relative6 === "" || path22.isAbsolute(relative6)) {
|
|
24624
25943
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
24625
25944
|
}
|
|
24626
25945
|
return dest;
|
|
@@ -25234,7 +26553,7 @@ var RunnerRegistry = class {
|
|
|
25234
26553
|
}
|
|
25235
26554
|
installFromGit(url) {
|
|
25236
26555
|
const validated = assertInstallablePluginUrl(url);
|
|
25237
|
-
const tmpDir = path24.join(os6.tmpdir(), `ordewell-plugin-${(0,
|
|
26556
|
+
const tmpDir = path24.join(os6.tmpdir(), `ordewell-plugin-${(0, import_crypto8.randomBytes)(12).toString("hex")}`);
|
|
25238
26557
|
this.store.ensureDir(tmpDir);
|
|
25239
26558
|
try {
|
|
25240
26559
|
try {
|
|
@@ -25654,11 +26973,15 @@ var PlannerModelMemory = class {
|
|
|
25654
26973
|
NO_TURN,
|
|
25655
26974
|
OPENCODE_MANIFEST,
|
|
25656
26975
|
ORCHESTRATOR_SHORTCUTS,
|
|
26976
|
+
ORDEWELL_MCP_PATH,
|
|
26977
|
+
ORDEWELL_MCP_SERVER_NAME,
|
|
25657
26978
|
ORDEWELL_SETTABLE_ENV,
|
|
25658
26979
|
OUTPUT_LINES_DEFAULT,
|
|
25659
26980
|
OUTPUT_LINES_MAX,
|
|
25660
26981
|
OpenAiService,
|
|
25661
26982
|
OpenCodeAdapter,
|
|
26983
|
+
OrdewellMcpServer,
|
|
26984
|
+
PLANNER_TOOLS,
|
|
25662
26985
|
PLAN_ENVELOPE_KEY,
|
|
25663
26986
|
PLUGIN_NAME_PATTERN,
|
|
25664
26987
|
PROVIDER_CREDENTIAL_ENV,
|
|
@@ -25695,6 +27018,7 @@ var PlannerModelMemory = class {
|
|
|
25695
27018
|
TASK_QUERY_FIELDS,
|
|
25696
27019
|
TASK_QUERY_PROTOCOL,
|
|
25697
27020
|
TASK_QUERY_REMINDER,
|
|
27021
|
+
TASK_TOOLS,
|
|
25698
27022
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
25699
27023
|
TaskControlError,
|
|
25700
27024
|
TaskLogRecorder,
|
|
@@ -25778,10 +27102,14 @@ var PlannerModelMemory = class {
|
|
|
25778
27102
|
daemonTokenPath,
|
|
25779
27103
|
defaultLogger,
|
|
25780
27104
|
definitionPattern,
|
|
27105
|
+
defuseMarkers,
|
|
25781
27106
|
deleteSession,
|
|
25782
27107
|
dependencyCandidates,
|
|
25783
27108
|
dependentsOf,
|
|
25784
27109
|
describeMergeResult,
|
|
27110
|
+
diffRows,
|
|
27111
|
+
diffStat,
|
|
27112
|
+
diffSummary,
|
|
25785
27113
|
digestTaskLog,
|
|
25786
27114
|
discoverGeminiModels,
|
|
25787
27115
|
drainNext,
|
|
@@ -25844,6 +27172,7 @@ var PlannerModelMemory = class {
|
|
|
25844
27172
|
loadState,
|
|
25845
27173
|
looksLikePlanAttempt,
|
|
25846
27174
|
mapAgentTool,
|
|
27175
|
+
mcpClientConfig,
|
|
25847
27176
|
migrateLegacyPlan,
|
|
25848
27177
|
migrateOldConfigDir,
|
|
25849
27178
|
migratePlanIsolation,
|
|
@@ -25859,6 +27188,7 @@ var PlannerModelMemory = class {
|
|
|
25859
27188
|
opensWithJsonObject,
|
|
25860
27189
|
outputLines,
|
|
25861
27190
|
outputPreview,
|
|
27191
|
+
ownerOnlyConfigFile,
|
|
25862
27192
|
parseAutonomyLevel,
|
|
25863
27193
|
parseMaxParallel,
|
|
25864
27194
|
parsePartialPlan,
|
|
@@ -25917,6 +27247,7 @@ var PlannerModelMemory = class {
|
|
|
25917
27247
|
serializeTaskStatus,
|
|
25918
27248
|
sessionDataDir,
|
|
25919
27249
|
sessionRuntimeSettings,
|
|
27250
|
+
sharedMcpServer,
|
|
25920
27251
|
stateExists,
|
|
25921
27252
|
stopTurn,
|
|
25922
27253
|
stripAnsi,
|
|
@@ -25950,6 +27281,7 @@ var PlannerModelMemory = class {
|
|
|
25950
27281
|
usageLine,
|
|
25951
27282
|
validateModifiedPlan,
|
|
25952
27283
|
validatePlanModification,
|
|
27284
|
+
validatePlanTasks,
|
|
25953
27285
|
warningsText,
|
|
25954
27286
|
wellKnownBinDirs,
|
|
25955
27287
|
windowsCommandLine,
|