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