@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.mjs
CHANGED
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
resolveDefaultMode,
|
|
15
15
|
resolveTaskMode,
|
|
16
16
|
runnerModesFrom,
|
|
17
|
-
validatePlanModification
|
|
18
|
-
|
|
17
|
+
validatePlanModification,
|
|
18
|
+
validatePlanTasks
|
|
19
|
+
} from "./chunk-J3YSTPCE.mjs";
|
|
19
20
|
import {
|
|
20
21
|
CHECKPOINT_TRUNCATE_LENGTH,
|
|
21
22
|
EMPTY_CONVERSATION,
|
|
@@ -37,6 +38,9 @@ import {
|
|
|
37
38
|
continuability,
|
|
38
39
|
dependencyCandidates,
|
|
39
40
|
dependentsOf,
|
|
41
|
+
diffRows,
|
|
42
|
+
diffStat,
|
|
43
|
+
diffSummary,
|
|
40
44
|
drainNext,
|
|
41
45
|
effectiveAllowlist,
|
|
42
46
|
executionSummary,
|
|
@@ -76,7 +80,7 @@ import {
|
|
|
76
80
|
unsendLatest,
|
|
77
81
|
usageLine,
|
|
78
82
|
validateTaskEdit
|
|
79
|
-
} from "./chunk-
|
|
83
|
+
} from "./chunk-LSGY7AIZ.mjs";
|
|
80
84
|
import {
|
|
81
85
|
PLAN_ENVELOPE_KEY,
|
|
82
86
|
PlanParseError,
|
|
@@ -105,7 +109,7 @@ import {
|
|
|
105
109
|
updateTaskInPlan,
|
|
106
110
|
validateModifiedPlan,
|
|
107
111
|
warningsText
|
|
108
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-YU4UKKKI.mjs";
|
|
109
113
|
import {
|
|
110
114
|
resolveOrderLabel,
|
|
111
115
|
taskOrderLabel
|
|
@@ -266,9 +270,9 @@ var SEARCH_EXCLUSIONS = [
|
|
|
266
270
|
];
|
|
267
271
|
|
|
268
272
|
// src/interfaces/IApproval.ts
|
|
269
|
-
function toApprovalDecision(
|
|
270
|
-
if (typeof
|
|
271
|
-
return
|
|
273
|
+
function toApprovalDecision(answer2) {
|
|
274
|
+
if (typeof answer2 === "boolean") return { decision: answer2 ? "allow" : "deny" };
|
|
275
|
+
return answer2;
|
|
272
276
|
}
|
|
273
277
|
function isGranted(decision) {
|
|
274
278
|
return decision.decision !== "deny";
|
|
@@ -2345,7 +2349,10 @@ var WRAPPER_FAMILY = {
|
|
|
2345
2349
|
// The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
|
|
2346
2350
|
// name arrives as its first argument.
|
|
2347
2351
|
busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
|
|
2348
|
-
command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
|
|
2352
|
+
command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] },
|
|
2353
|
+
// Runs the named shell builtin, so `builtin eval …` is an `eval`. Classified
|
|
2354
|
+
// on its own name it only asked, and a grant for `builtin echo` covered it.
|
|
2355
|
+
builtin: {}
|
|
2349
2356
|
};
|
|
2350
2357
|
var XARGS_SPEC = {
|
|
2351
2358
|
valueFlags: [
|
|
@@ -3075,6 +3082,9 @@ function refusalFor(seg) {
|
|
|
3075
3082
|
if (seg.binary === "source" || seg.binary === ".") {
|
|
3076
3083
|
return `"${seg.binary}" runs a file's contents as commands, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
|
|
3077
3084
|
}
|
|
3085
|
+
if (seg.binary === "enable" && seg.args.some((a) => /^-[a-zA-Z]*f/.test(a))) {
|
|
3086
|
+
return `"enable -f" loads a shared library into the shell, which runs its code. Use the read-only research tools, or describe it as a task.`;
|
|
3087
|
+
}
|
|
3078
3088
|
if (SHELL_KEYWORDS.includes(seg.binary)) {
|
|
3079
3089
|
return `"${seg.binary}" is a shell keyword or compound-command opener. This classifier only inspects the command that runs first, and a keyword hides the real one from it. Run the inner command directly, or describe it as a task.`;
|
|
3080
3090
|
}
|
|
@@ -3185,6 +3195,9 @@ function classifyCommand(command, opts = {}) {
|
|
|
3185
3195
|
}
|
|
3186
3196
|
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
|
|
3187
3197
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
3198
|
+
if (nonAuto.some((u) => u.seg.binary === "cd" || u.seg.binary !== "command" && SHELL_STATE_COMMANDS.has(u.seg.binary))) {
|
|
3199
|
+
return { tier: "ask", scope: trimmed };
|
|
3200
|
+
}
|
|
3188
3201
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3189
3202
|
return { tier: "ask", scope };
|
|
3190
3203
|
}
|
|
@@ -3805,12 +3818,12 @@ var ApprovalPolicy = class {
|
|
|
3805
3818
|
if (this.generation === gen) this.inFlight.delete(openKey);
|
|
3806
3819
|
});
|
|
3807
3820
|
this.inFlight.set(openKey, pending);
|
|
3808
|
-
const
|
|
3821
|
+
const answer2 = await pending;
|
|
3809
3822
|
if (this.generation === gen) {
|
|
3810
|
-
if (
|
|
3823
|
+
if (answer2) open.forEach((s) => this.granted.add(s));
|
|
3811
3824
|
else this.refused.add(key);
|
|
3812
3825
|
}
|
|
3813
|
-
return decide(
|
|
3826
|
+
return decide(answer2, "asked");
|
|
3814
3827
|
}
|
|
3815
3828
|
/** Scopes the user has granted this session — for display and for persistence. */
|
|
3816
3829
|
grantedScopes() {
|
|
@@ -3871,10 +3884,10 @@ var PendingApprovals = class {
|
|
|
3871
3884
|
* settled. "Allow for this task" on a request that did not offer it is a
|
|
3872
3885
|
* plain allow: no grant is made that the requester never proposed.
|
|
3873
3886
|
*/
|
|
3874
|
-
resolve(id,
|
|
3887
|
+
resolve(id, answer2) {
|
|
3875
3888
|
const entry = this.entries.get(id);
|
|
3876
3889
|
if (!entry) return false;
|
|
3877
|
-
const decision = toApprovalDecision(
|
|
3890
|
+
const decision = toApprovalDecision(answer2);
|
|
3878
3891
|
entry.settle(decision.decision === "allowForTask" && !entry.pending.request.allowForTask ? { decision: "allow" } : decision);
|
|
3879
3892
|
return true;
|
|
3880
3893
|
}
|
|
@@ -4112,9 +4125,9 @@ function applyHeadLimit(stdout, headLimit) {
|
|
|
4112
4125
|
}
|
|
4113
4126
|
function formatSearchOutput(capped, root, opts) {
|
|
4114
4127
|
if (capped.rows.length === 0) return opts.emptyMessage;
|
|
4115
|
-
const
|
|
4116
|
-
const prefix = root ===
|
|
4117
|
-
const body = prefix ===
|
|
4128
|
+
const sep3 = opts.sep ?? path3.sep;
|
|
4129
|
+
const prefix = root === sep3 || root.endsWith(sep3) ? root : `${root}${sep3}`;
|
|
4130
|
+
const body = prefix === sep3 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
|
|
4118
4131
|
if (!capped.truncated) return body;
|
|
4119
4132
|
const hint = opts.hint ? ` ${opts.hint}` : "";
|
|
4120
4133
|
return `${body}
|
|
@@ -5069,28 +5082,28 @@ function posixBinDirs() {
|
|
|
5069
5082
|
];
|
|
5070
5083
|
}
|
|
5071
5084
|
function windowsBinDirs(env, home) {
|
|
5072
|
-
const
|
|
5073
|
-
const under = (base, ...parts) => base ? [
|
|
5085
|
+
const join26 = (...parts) => path4.win32.join(...parts);
|
|
5086
|
+
const under = (base, ...parts) => base ? [join26(base, ...parts)] : [];
|
|
5074
5087
|
const appData = env.APPDATA;
|
|
5075
5088
|
const localAppData = env.LOCALAPPDATA;
|
|
5076
5089
|
const programData = env.ProgramData;
|
|
5077
5090
|
return [
|
|
5078
5091
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5092
|
+
join26(home, ".local", "bin"),
|
|
5093
|
+
join26(home, ".opencode", "bin"),
|
|
5094
|
+
join26(home, ".claude", "bin"),
|
|
5095
|
+
join26(home, ".codex", "bin"),
|
|
5083
5096
|
...under(localAppData, "Programs", "claude"),
|
|
5084
5097
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
5085
5098
|
// literal spelling is kept as a fallback for a host that does not export
|
|
5086
5099
|
// APPDATA (a service account, a stripped CI container).
|
|
5087
5100
|
...under(appData, "npm"),
|
|
5088
|
-
|
|
5101
|
+
join26(home, "AppData", "Roaming", "npm"),
|
|
5089
5102
|
...under(localAppData, "pnpm"),
|
|
5090
5103
|
...under(localAppData, "Yarn", "bin"),
|
|
5091
|
-
|
|
5104
|
+
join26(home, ".bun", "bin"),
|
|
5092
5105
|
// Windows package managers.
|
|
5093
|
-
|
|
5106
|
+
join26(home, "scoop", "shims"),
|
|
5094
5107
|
...under(env.SCOOP, "shims"),
|
|
5095
5108
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
5096
5109
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -5100,7 +5113,7 @@ function windowsBinDirs(env, home) {
|
|
|
5100
5113
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
5101
5114
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
5102
5115
|
...under(localAppData, "Volta", "bin"),
|
|
5103
|
-
|
|
5116
|
+
join26(home, ".volta", "bin")
|
|
5104
5117
|
];
|
|
5105
5118
|
}
|
|
5106
5119
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -5588,18 +5601,18 @@ var GitWorktreeIsolation = class {
|
|
|
5588
5601
|
*/
|
|
5589
5602
|
sharedPaths(workspaceRoot, isolated) {
|
|
5590
5603
|
if (isolated.includes(SELF_REPO)) return [];
|
|
5591
|
-
const
|
|
5604
|
+
const shared2 = [];
|
|
5592
5605
|
const walk = (rel) => {
|
|
5593
5606
|
for (const name of listDir(path8.join(workspaceRoot, rel))) {
|
|
5594
5607
|
if (!rel && (name === STATE_DIR || name === ".git")) continue;
|
|
5595
5608
|
const child = rel ? `${rel}/${name}` : name;
|
|
5596
5609
|
if (isolated.includes(child)) continue;
|
|
5597
5610
|
if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
|
|
5598
|
-
else if (resolves(path8.join(workspaceRoot, child)))
|
|
5611
|
+
else if (resolves(path8.join(workspaceRoot, child))) shared2.push(child);
|
|
5599
5612
|
}
|
|
5600
5613
|
};
|
|
5601
5614
|
walk("");
|
|
5602
|
-
return
|
|
5615
|
+
return shared2;
|
|
5603
5616
|
}
|
|
5604
5617
|
prepare(task, run) {
|
|
5605
5618
|
return this.admin(run.workspaceRoot, async () => {
|
|
@@ -6427,10 +6440,12 @@ var TASK_QUERY_FIELDS = [
|
|
|
6427
6440
|
"userStoriesCovered",
|
|
6428
6441
|
"output"
|
|
6429
6442
|
];
|
|
6443
|
+
var TASK_QUERY_TASK_FIELDS = TASK_QUERY_FIELDS.filter((f) => f !== "output");
|
|
6430
6444
|
var OUTPUT_LINES_DEFAULT = 80;
|
|
6431
6445
|
var OUTPUT_LINES_MAX = 400;
|
|
6432
6446
|
var TASK_QUERY_ANSWER_MAX_CHARS = 2e4;
|
|
6433
6447
|
var ANSWER_TAIL_RESERVE = 4e3;
|
|
6448
|
+
var OUTPUT_TAIL_MAX_CHARS = TASK_QUERY_ANSWER_MAX_CHARS - ANSWER_TAIL_RESERVE;
|
|
6434
6449
|
function textHasTaskQuery(text) {
|
|
6435
6450
|
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
6436
6451
|
}
|
|
@@ -6506,6 +6521,7 @@ function taskQuerySignature(query) {
|
|
|
6506
6521
|
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog, query.outputLines ?? null, query.outputSince ?? null]);
|
|
6507
6522
|
}
|
|
6508
6523
|
var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
|
|
6524
|
+
var TASK_READ_ANSWER_OR_EDIT = "You have now read everything you asked for. Do not read again this turn: answer the user in prose, or call edit_plan for the change you came to make.";
|
|
6509
6525
|
var TASK_QUERY_PROTOCOL = [
|
|
6510
6526
|
"READING A TASK BEFORE YOU EDIT IT:",
|
|
6511
6527
|
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
|
|
@@ -6517,7 +6533,16 @@ var TASK_QUERY_PROTOCOL = [
|
|
|
6517
6533
|
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
6518
6534
|
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
6519
6535
|
];
|
|
6536
|
+
var TASK_READ_TOOLS_PROTOCOL = [
|
|
6537
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
6538
|
+
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. Read with these tools; they change nothing:",
|
|
6539
|
+
'- task_query: "tasks" are one or more task references ("<id or #order>"). Ask for everything you need in ONE call. "fields" is optional (omit it to get description, prompt, userSteps, verdict, outputSummary and userStoriesCovered). "catalog": true also returns every runner with its models, thinking-effort variants and modes.',
|
|
6540
|
+
`- task_output: the recent output of a task that is running right now \u2014 the way to diagnose a task that looks stuck mid-execution. "lines" (default 80, at most 400) sets how many; pass an answer's "nextOffset" back as "since" to read only what follows. A task that is not running answers with its verdict, output summary and a digest of its last attempt.`,
|
|
6541
|
+
"Three reads per user message; after that every answer also tells you to land the turn, and after six they are refused. Do not ask the same question twice.",
|
|
6542
|
+
"Once a plan exists, change it with edit_plan \u2014 every turn shows you its operations. Never reply with taskOps or taskQuery JSON."
|
|
6543
|
+
];
|
|
6520
6544
|
var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
|
|
6545
|
+
var TASK_READ_TOOLS_REMINDER = "- To READ a task in full (prompt, user steps, verdict, user stories), a running task's output, or the whole model/mode catalog before editing, call task_query or task_output \u2014 they change nothing \u2014 and then call edit_plan with your ops.";
|
|
6521
6546
|
function findTask(tasks, ref) {
|
|
6522
6547
|
const byId = tasks.find((t) => t.id === ref);
|
|
6523
6548
|
if (byId) return byId;
|
|
@@ -6726,7 +6751,7 @@ function verificationModeBlock() {
|
|
|
6726
6751
|
].join("\n");
|
|
6727
6752
|
}
|
|
6728
6753
|
function researchPhaseBlock(harnessMode) {
|
|
6729
|
-
const
|
|
6754
|
+
const shared2 = [
|
|
6730
6755
|
"RESEARCH PHASE:",
|
|
6731
6756
|
"- Start by reading README.md and any agent config files (AGENTS.md, CLAUDE.md)"
|
|
6732
6757
|
];
|
|
@@ -6740,7 +6765,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
6740
6765
|
"- Read key source files to understand architecture"
|
|
6741
6766
|
];
|
|
6742
6767
|
return [
|
|
6743
|
-
...
|
|
6768
|
+
...shared2,
|
|
6744
6769
|
...toolSpecific,
|
|
6745
6770
|
"- Synthesize tool findings into your messages. The user does not see raw tool output; only your synthesis reaches them.",
|
|
6746
6771
|
"- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
|
|
@@ -6802,28 +6827,38 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
|
|
|
6802
6827
|
verificationBlock
|
|
6803
6828
|
);
|
|
6804
6829
|
}
|
|
6830
|
+
var PLANNER_TOOLS_CATALOG = [
|
|
6831
|
+
"RUNNERS, MODELS AND MODES:",
|
|
6832
|
+
"Which runners, models and modes you may assign can change while you plan: the user may enable a runner or edit the model allowlist at any time. Read them just before you submit, never from memory:",
|
|
6833
|
+
"1. Call list_runners: the enabled runners, each with its task modes and default mode.",
|
|
6834
|
+
"2. Call list_models for each runner you will assign: its allowed models, with their thinking-effort variants.",
|
|
6835
|
+
`3. Call submit_plan with the whole plan. "assignedRunner" must be a runner list_runners returned, "assignedModel.modelId" a modelId list_models returned for that runner, "taskMode" one of that runner's modes.`,
|
|
6836
|
+
'4. If submit_plan returns errors, fix exactly what each one names and call it again in the same reply. If it reports "coerced" changes, tell the user what was changed.',
|
|
6837
|
+
""
|
|
6838
|
+
];
|
|
6805
6839
|
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, variant, verificationBlock) {
|
|
6840
|
+
const tools = variant.plannerTools ?? false;
|
|
6806
6841
|
return [
|
|
6807
|
-
|
|
6842
|
+
`You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans ${tools ? "through the submit_plan tool" : "as JSON"}. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.`,
|
|
6808
6843
|
"",
|
|
6809
6844
|
"WORKFLOW:",
|
|
6810
6845
|
"1. Explore the workspace with tools to understand the codebase.",
|
|
6811
6846
|
"2. Ask clarifying questions grounded in your findings. Reference actual files. When the goal is vague, or a decision would materially change the outcome (storage engine, library choice, scope, API shape), ask the user BEFORE planning \u2014 never silently assume. Clear, fully-specified goals need no questions.",
|
|
6812
6847
|
"3. When you have enough context, produce a prose outline \u2014 a short list describing each vertical slice in order.",
|
|
6813
|
-
'4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
6848
|
+
tools ? "4. After the user confirms the outline, call list_runners and list_models, then submit the final task plan with submit_plan." : '4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
6814
6849
|
"",
|
|
6815
6850
|
researchPhaseBlock(variant.harness ?? false),
|
|
6816
6851
|
verificationBlock,
|
|
6817
6852
|
"",
|
|
6818
6853
|
"OUTLINE PHASE:",
|
|
6819
|
-
|
|
6854
|
+
`- When you are ready to propose a plan, first show a prose outline. DO NOT jump straight to ${tools ? "submit_plan" : "JSON"}.`,
|
|
6820
6855
|
"- Format: a numbered list describing each vertical tracer-bullet slice, with the files/layers each slice touches.",
|
|
6821
6856
|
'- Example: "1. Set up database schema (src/db/schema.ts) \u2014 creates tables for users and sessions\n2. Add auth middleware (src/auth/middleware.ts) \u2014 validates JWTs on protected routes\n3. Build login page (src/ui/Login.tsx) \u2014 form component with validation"',
|
|
6822
6857
|
"- The user may reply with adjustments. Revise the outline and re-present it.",
|
|
6823
|
-
"- When the user confirms the outline, emit the task plan JSON.",
|
|
6858
|
+
tools ? "- When the user confirms the outline, submit the plan with submit_plan." : "- When the user confirms the outline, emit the task plan JSON.",
|
|
6824
6859
|
"",
|
|
6825
6860
|
"PLAN FORMAT:",
|
|
6826
|
-
"Generate a task plan using this JSON format:",
|
|
6861
|
+
tools ? 'submit_plan takes {"tasks": [...]}, every task in this shape:' : "Generate a task plan using this JSON format:",
|
|
6827
6862
|
"",
|
|
6828
6863
|
"{",
|
|
6829
6864
|
' "tasks": [',
|
|
@@ -6837,8 +6872,8 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
6837
6872
|
' "prompt": "Detailed instructions for the AI assistant (ai tasks only)",',
|
|
6838
6873
|
' "userSteps": [{ "order": 1, "instruction": "Step description", "completed": false }],',
|
|
6839
6874
|
` "assignedModel": { "modelId": "model-id", "modelLabel": "Model Label", "thinkingEffort": "a variant id from that model's variants list \u2014 omit if the model has none" },`,
|
|
6840
|
-
' "assignedRunner": "' + runners.join("|") + '",',
|
|
6841
|
-
' "taskMode": "' + modeExamples + '",',
|
|
6875
|
+
' "assignedRunner": "' + (tools ? "a runner id from list_runners" : runners.join("|")) + '",',
|
|
6876
|
+
' "taskMode": "' + (tools ? "one of that runner's modes from list_runners" : modeExamples) + '",',
|
|
6842
6877
|
' "autonomy": "AFK|HITL",',
|
|
6843
6878
|
' "sliceType": "AFK|HITL",',
|
|
6844
6879
|
' "ops": true,',
|
|
@@ -6870,8 +6905,13 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
6870
6905
|
...opsTaskSection(true),
|
|
6871
6906
|
"",
|
|
6872
6907
|
"RULES:",
|
|
6873
|
-
|
|
6874
|
-
|
|
6908
|
+
...tools ? [
|
|
6909
|
+
"- Submit the plan ONLY through submit_plan. Never write it as JSON in your reply.",
|
|
6910
|
+
"- Show the outline BEFORE submitting. Do not skip the outline."
|
|
6911
|
+
] : [
|
|
6912
|
+
"- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
|
|
6913
|
+
"- Emit the outline BEFORE the JSON. Do not skip the outline."
|
|
6914
|
+
],
|
|
6875
6915
|
"- Reference specific files and patterns from your research in task prompts.",
|
|
6876
6916
|
...variant.isolatedExecution ? [] : [OVERLAP_AVOIDANCE_RULE],
|
|
6877
6917
|
"",
|
|
@@ -6879,23 +6919,19 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
6879
6919
|
"- When suggesting libraries, frameworks, or patterns, first verify they already exist in the codebase. NEVER assume a library is available just because it is well-known. Check package.json, imports, or surrounding files first.",
|
|
6880
6920
|
"- Follow the existing code style, naming conventions, and architectural patterns of the project.",
|
|
6881
6921
|
"",
|
|
6882
|
-
"MODEL ASSIGNMENT:",
|
|
6883
|
-
"Available models:",
|
|
6884
|
-
modelsJson,
|
|
6885
|
-
"",
|
|
6886
|
-
runnerInstruction(runners),
|
|
6922
|
+
...tools ? PLANNER_TOOLS_CATALOG : ["MODEL ASSIGNMENT:", "Available models:", modelsJson, "", runnerInstruction(runners)],
|
|
6887
6923
|
"MODEL SELECTION GUIDELINES:",
|
|
6888
6924
|
"- Use stronger models for complex refactoring, architecture changes, security-critical code.",
|
|
6889
6925
|
"- Use weaker/faster models for simple file operations, test generation, config changes, documentation.",
|
|
6890
|
-
`- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list above (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
|
|
6926
|
+
`- Assign thinkingEffort based on task complexity, choosing ONLY from the assigned model's "variants" list ${tools ? "from list_models" : "above"} (e.g. low for simple tasks, high for complex ones). Omit thinkingEffort if the model has no variants.`,
|
|
6891
6927
|
"",
|
|
6892
6928
|
"TASK MODE:",
|
|
6893
|
-
modeGuide,
|
|
6929
|
+
tools ? `Use each runner's default mode from list_runners unless the task needs a different mode it lists. Avoid "plan" (read-only) unless the user asked for analysis only.` : modeGuide,
|
|
6894
6930
|
"",
|
|
6895
|
-
//
|
|
6896
|
-
//
|
|
6897
|
-
//
|
|
6898
|
-
...TASK_QUERY_PROTOCOL,
|
|
6931
|
+
// The envelope is shared by both variants on purpose: a harness planner
|
|
6932
|
+
// Ordewell could not hand tools to speaks the same protocol as an
|
|
6933
|
+
// API-backed one (ADR-0009). One it did hand them to reads through them.
|
|
6934
|
+
...tools ? TASK_READ_TOOLS_PROTOCOL : TASK_QUERY_PROTOCOL,
|
|
6899
6935
|
"",
|
|
6900
6936
|
context ? `PROJECT CONTEXT:
|
|
6901
6937
|
${context}
|
|
@@ -7198,7 +7234,7 @@ function buildModifyDuringExecutionPrompt(executionLog, pendingTasks, userMessag
|
|
|
7198
7234
|
sections.push('Return the COMPLETE modified pending tasks as a JSON object with a single "tasks" array. Include all pending tasks, not just the modified ones.\nDo NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object.');
|
|
7199
7235
|
return sections.join("\n");
|
|
7200
7236
|
}
|
|
7201
|
-
function buildMergePrompt(taskIds, tasks) {
|
|
7237
|
+
function buildMergePrompt(taskIds, tasks, tools = false) {
|
|
7202
7238
|
const idSet = new Set(taskIds);
|
|
7203
7239
|
const selected = tasks.filter((t) => idSet.has(t.id)).sort((a, b) => a.order - b.order);
|
|
7204
7240
|
const refs = selected.map((t) => `#${t.order} "${t.title}" (id=${t.id})`).join(", ");
|
|
@@ -7206,22 +7242,22 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
7206
7242
|
`Merge these tasks into ONE combined task: ${refs}.`,
|
|
7207
7243
|
"Write a clear combined title, description, and prompt that cover all of their work.",
|
|
7208
7244
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
7209
|
-
|
|
7210
|
-
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
7211
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
7245
|
+
`Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. Prefer the strongest model if the merged work is complex.`,
|
|
7246
|
+
tools ? 'Call edit_plan with a single "merge" op:' : 'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
7247
|
+
` {"${tools ? "ops" : "taskOps"}":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
7212
7248
|
`If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
|
|
7213
7249
|
].join("\n");
|
|
7214
7250
|
}
|
|
7215
|
-
function buildSplitPrompt(taskId, tasks) {
|
|
7251
|
+
function buildSplitPrompt(taskId, tasks, tools = false) {
|
|
7216
7252
|
const task = tasks.find((t) => t.id === taskId);
|
|
7217
7253
|
if (!task) return `Split task ${taskId} into smaller tasks.`;
|
|
7218
7254
|
return [
|
|
7219
7255
|
`Split task #${task.order} "${task.title}" (id=${task.id}) into multiple smaller tasks that together accomplish the same work.`,
|
|
7220
7256
|
"Decompose it into a sensible ordered sequence. Write a clear title, description, and prompt for each part.",
|
|
7221
7257
|
"The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
|
|
7222
|
-
|
|
7223
|
-
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
7224
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
7258
|
+
`Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models> above"}. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).`,
|
|
7259
|
+
tools ? 'Call edit_plan with a single "split" op:' : 'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
7260
|
+
` {"${tools ? "ops" : "taskOps"}":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
7225
7261
|
`If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
|
|
7226
7262
|
].join("\n");
|
|
7227
7263
|
}
|
|
@@ -7275,15 +7311,15 @@ ${task.prompt ?? task.description}`;
|
|
|
7275
7311
|
|
|
7276
7312
|
// src/services/PlanRepair.ts
|
|
7277
7313
|
async function repairLoop(opts) {
|
|
7278
|
-
let
|
|
7314
|
+
let reply2 = await opts.first();
|
|
7279
7315
|
for (let repairs = 0; ; repairs++) {
|
|
7280
|
-
const verdict = await opts.interpret(
|
|
7316
|
+
const verdict = await opts.interpret(reply2);
|
|
7281
7317
|
if ("done" in verdict) return verdict.done;
|
|
7282
7318
|
if (repairs >= opts.maxRepairs) {
|
|
7283
|
-
return opts.onExhausted({ reply, errors: verdict.retry.errors, cause: verdict.retry.cause });
|
|
7319
|
+
return opts.onExhausted({ reply: reply2, errors: verdict.retry.errors, cause: verdict.retry.cause });
|
|
7284
7320
|
}
|
|
7285
7321
|
const { corrective } = verdict.retry;
|
|
7286
|
-
|
|
7322
|
+
reply2 = await opts.resend(typeof corrective === "function" ? corrective() : corrective);
|
|
7287
7323
|
}
|
|
7288
7324
|
}
|
|
7289
7325
|
var JSON_REPAIR_INSTRUCTION = "Your previous response could not be parsed as JSON. Re-send ONLY the JSON object \u2014 no prose before or after, no explanation, no markdown code fences.";
|
|
@@ -7399,34 +7435,34 @@ async function settleReply(opts) {
|
|
|
7399
7435
|
}
|
|
7400
7436
|
if (attempt.failure !== void 0) return { done: message(attempt.failure) };
|
|
7401
7437
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
7402
|
-
const
|
|
7403
|
-
switch (
|
|
7438
|
+
const reply2 = classifyPlannerReply(attempt.text, opts.classify);
|
|
7439
|
+
switch (reply2.kind) {
|
|
7404
7440
|
case "plan":
|
|
7405
|
-
return { done: { kind: "plan", tasks:
|
|
7441
|
+
return { done: { kind: "plan", tasks: reply2.tasks, text: said(attempt), researchLog } };
|
|
7406
7442
|
case "task_ops":
|
|
7407
|
-
return { done: { kind: "task_ops", ops:
|
|
7443
|
+
return { done: { kind: "task_ops", ops: reply2.ops, text: said(attempt), researchLog } };
|
|
7408
7444
|
// A read is answered by the conversation, which owns the plan and the
|
|
7409
7445
|
// catalog, so it leaves here the way a plan or an edit does.
|
|
7410
7446
|
case "task_query":
|
|
7411
|
-
return { done: { kind: "task_query", query:
|
|
7447
|
+
return { done: { kind: "task_query", query: reply2.query, text: said(attempt), researchLog } };
|
|
7412
7448
|
case "prose":
|
|
7413
7449
|
return { done: asProse(attempt) };
|
|
7414
7450
|
}
|
|
7415
7451
|
if (opts.signal?.aborted) return { done: asProse(attempt) };
|
|
7416
|
-
const errors = [
|
|
7417
|
-
switch (
|
|
7452
|
+
const errors = [reply2.error.message];
|
|
7453
|
+
switch (reply2.kind) {
|
|
7418
7454
|
case "broken_task_ops":
|
|
7419
|
-
return { retry: { errors, corrective: reEmitTaskOpsPrompt(
|
|
7455
|
+
return { retry: { errors, corrective: reEmitTaskOpsPrompt(reply2.error.message), cause: reply2.error } };
|
|
7420
7456
|
case "broken_task_query":
|
|
7421
|
-
return { retry: { errors, corrective: reEmitTaskQueryPrompt(
|
|
7457
|
+
return { retry: { errors, corrective: reEmitTaskQueryPrompt(reply2.error.message), cause: reply2.error } };
|
|
7422
7458
|
case "broken_plan": {
|
|
7423
|
-
const corrective =
|
|
7424
|
-
return { retry: { errors, corrective, cause:
|
|
7459
|
+
const corrective = reply2.error.truncated || attempt.cutOff ? () => truncatedPlanReEmitPrompt((opts.compactHistory?.() ?? 0) > 0) : reEmitPlanPrompt(reply2.error.message);
|
|
7460
|
+
return { retry: { errors, corrective, cause: reply2.error } };
|
|
7425
7461
|
}
|
|
7426
7462
|
}
|
|
7427
7463
|
},
|
|
7428
7464
|
maxRepairs: MAX_JSON_REPAIRS,
|
|
7429
|
-
onExhausted: ({ reply }) => asProse(
|
|
7465
|
+
onExhausted: ({ reply: reply2 }) => asProse(reply2)
|
|
7430
7466
|
});
|
|
7431
7467
|
}
|
|
7432
7468
|
var said = (attempt) => attempt.fullText ?? attempt.text;
|
|
@@ -7653,8 +7689,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
|
|
|
7653
7689
|
const blocks = [{ tool: null, lines: [] }];
|
|
7654
7690
|
for (const line of resultsText.split("\n")) {
|
|
7655
7691
|
const m = headerRe.exec(line);
|
|
7656
|
-
const
|
|
7657
|
-
if (
|
|
7692
|
+
const tool2 = m && knownNames.has(m[1]) ? m[1] : line === "[LLM response]" ? "llm_response" : null;
|
|
7693
|
+
if (tool2) blocks.push({ tool: tool2, lines: [line] });
|
|
7658
7694
|
else blocks[blocks.length - 1].lines.push(line);
|
|
7659
7695
|
}
|
|
7660
7696
|
const texts = blocks.map((b) => b.lines.join("\n"));
|
|
@@ -8147,9 +8183,9 @@ ${llmOutput}
|
|
|
8147
8183
|
return { tasks: null, researchLog, researchResults };
|
|
8148
8184
|
}
|
|
8149
8185
|
const thinking = turn.reasoning ? turn.reasoning.slice(-200) : turn.text.slice(-200);
|
|
8150
|
-
const
|
|
8151
|
-
if (
|
|
8152
|
-
return { tasks:
|
|
8186
|
+
const reply2 = classifyPlannerReply(turn.text, { runners, runnerModes, autonomousDefault });
|
|
8187
|
+
if (reply2.kind === "plan") {
|
|
8188
|
+
return { tasks: reply2.tasks, researchLog, researchResults };
|
|
8153
8189
|
}
|
|
8154
8190
|
if (!turn.hasToolCalls) break;
|
|
8155
8191
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
@@ -8868,7 +8904,7 @@ ${collected.aiflowContext}
|
|
|
8868
8904
|
};
|
|
8869
8905
|
|
|
8870
8906
|
// src/services/TaskOrchestrator.ts
|
|
8871
|
-
import * as
|
|
8907
|
+
import * as path14 from "path";
|
|
8872
8908
|
|
|
8873
8909
|
// src/services/promptAugment.ts
|
|
8874
8910
|
var MAX_TAIL_CHARS = 500;
|
|
@@ -8986,10 +9022,14 @@ function renderPreviousAttempt(output) {
|
|
|
8986
9022
|
tail
|
|
8987
9023
|
].join("\n");
|
|
8988
9024
|
}
|
|
8989
|
-
function renderCompletionMarker(task) {
|
|
9025
|
+
function renderCompletionMarker(task, completionTool = false) {
|
|
9026
|
+
const howto = `Build it by writing \`<<<ORDEWELL_\` immediately followed by \`DONE_${task.completionMarker}>>>\` \u2014 joined into a single unbroken token, with no space, quote, or any other character between the two parts.`;
|
|
9027
|
+
if (!completionTool) return `
|
|
9028
|
+
|
|
9029
|
+
When you have fully completed this task, print one final line containing only the completion marker. ${howto}`;
|
|
8990
9030
|
return `
|
|
8991
9031
|
|
|
8992
|
-
When you have fully completed this task,
|
|
9032
|
+
When you have fully completed this task, call the \`task_complete\` tool with status \`done\` and a summary of what you did; the tasks that depend on this one are given that summary. If you cannot complete it, call \`task_complete\` with status \`blocked\` or \`failed\` and the reason instead, and print no marker. After a \`done\` call, or if the tool is not available to you, also print one final line containing only the completion marker. ${howto}`;
|
|
8993
9033
|
}
|
|
8994
9034
|
function renderTddInstruction() {
|
|
8995
9035
|
return [
|
|
@@ -9010,7 +9050,21 @@ function renderTddInstruction() {
|
|
|
9010
9050
|
].join("\n");
|
|
9011
9051
|
}
|
|
9012
9052
|
var CHECKPOINT_MARKER_HOWTO = "Build it by writing `<<<ORDEWELL_` immediately followed by `CHECKPOINT:` \u2014 no space, quote, or any other character between those two parts \u2014 then a brief summary of what you are about to do and why human input is needed, closed with `>>>`";
|
|
9013
|
-
function renderCheckpointInstruction() {
|
|
9053
|
+
function renderCheckpointInstruction(checkpointTool = false) {
|
|
9054
|
+
if (checkpointTool) {
|
|
9055
|
+
return [
|
|
9056
|
+
"## Human-in-the-loop checkpoints",
|
|
9057
|
+
"",
|
|
9058
|
+
"When you reach a decision point that requires human judgment \u2014 before destructive",
|
|
9059
|
+
"operations, after major design decisions, or when multiple viable paths exist \u2014 pause",
|
|
9060
|
+
"and request input:",
|
|
9061
|
+
"",
|
|
9062
|
+
"1. Call the `checkpoint` tool with a brief summary of what you are about to do and why human input is needed. The call waits for the human, and its result is their answer",
|
|
9063
|
+
"2. If the result is `continue`: proceed with the action you described",
|
|
9064
|
+
"3. If the result starts with `rejected:`: the rest is why. Adjust your approach and call `checkpoint` again if needed",
|
|
9065
|
+
`4. If the \`checkpoint\` tool is not available to you, or the call fails, fall back to the marker: print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}`
|
|
9066
|
+
].join("\n");
|
|
9067
|
+
}
|
|
9014
9068
|
return [
|
|
9015
9069
|
"## Human-in-the-loop checkpoints",
|
|
9016
9070
|
"",
|
|
@@ -9042,9 +9096,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
9042
9096
|
blocks.push(renderTddInstruction());
|
|
9043
9097
|
}
|
|
9044
9098
|
if (isHitlTask(task)) {
|
|
9045
|
-
blocks.push(renderCheckpointInstruction());
|
|
9099
|
+
blocks.push(renderCheckpointInstruction(opts?.completionTool));
|
|
9046
9100
|
}
|
|
9047
|
-
const marker2 = renderCompletionMarker(task);
|
|
9101
|
+
const marker2 = renderCompletionMarker(task, opts?.completionTool);
|
|
9048
9102
|
if (blocks.length === 0) return basePrompt + marker2;
|
|
9049
9103
|
return `${blocks.join("\n\n")}
|
|
9050
9104
|
|
|
@@ -9055,12 +9109,12 @@ function composeContinuationPrompt(task, message, opts = {}) {
|
|
|
9055
9109
|
opts.ops ? "(Ordewell) You are continuing this task in the same session, in the same checkout. What your earlier attempt did outside the repository was not undone: check what already exists before acting again." : "(Ordewell) You are continuing this task in the same session. Your working directory was recreated from the integration branch: work from your earlier attempt is there only if it landed, so check the files before relying on them."
|
|
9056
9110
|
];
|
|
9057
9111
|
if (isHitlTask(task)) {
|
|
9058
|
-
reminder.push(`If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
|
|
9112
|
+
reminder.push(opts.completionTool ? `If you reach a decision that needs human judgment, call the \`checkpoint\` tool: its result is \`continue\` or \`rejected: <why>\`. Only if the tool is not available to you, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.` : `If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
|
|
9059
9113
|
}
|
|
9060
9114
|
return `${message.trim()}
|
|
9061
9115
|
|
|
9062
9116
|
---
|
|
9063
|
-
${reminder.join("\n\n")}${renderCompletionMarker(task)}`;
|
|
9117
|
+
${reminder.join("\n\n")}${renderCompletionMarker(task, opts.completionTool)}`;
|
|
9064
9118
|
}
|
|
9065
9119
|
|
|
9066
9120
|
// src/services/VerdictEngine.ts
|
|
@@ -9078,6 +9132,13 @@ var VerdictEngine = class {
|
|
|
9078
9132
|
pausedSessions = /* @__PURE__ */ new Map();
|
|
9079
9133
|
listeners = [];
|
|
9080
9134
|
checkpointListeners = [];
|
|
9135
|
+
withdrawnListeners = [];
|
|
9136
|
+
/**
|
|
9137
|
+
* The open `checkpoint` tool call per task (ADR-0022, V5): settling it is how
|
|
9138
|
+
* an answer reaches a runner that asked through the tool, where the marker's
|
|
9139
|
+
* answer is typed into the session instead.
|
|
9140
|
+
*/
|
|
9141
|
+
toolCheckpoints = /* @__PURE__ */ new Map();
|
|
9081
9142
|
idleListeners = [];
|
|
9082
9143
|
/**
|
|
9083
9144
|
* Per-task generation. Replaced on every watch(), clear() and verdict.
|
|
@@ -9108,6 +9169,9 @@ var VerdictEngine = class {
|
|
|
9108
9169
|
onCheckpoint(listener) {
|
|
9109
9170
|
this.checkpointListeners.push(listener);
|
|
9110
9171
|
}
|
|
9172
|
+
onCheckpointWithdrawn(listener) {
|
|
9173
|
+
this.withdrawnListeners.push(listener);
|
|
9174
|
+
}
|
|
9111
9175
|
onIdleChange(listener) {
|
|
9112
9176
|
this.idleListeners.push(listener);
|
|
9113
9177
|
}
|
|
@@ -9169,6 +9233,11 @@ ${line}
|
|
|
9169
9233
|
}
|
|
9170
9234
|
approveCheckpoint(taskId) {
|
|
9171
9235
|
this.resumeIdle(taskId);
|
|
9236
|
+
const toolCall = this.toolCheckpoints.get(taskId);
|
|
9237
|
+
if (toolCall) {
|
|
9238
|
+
toolCall({ kind: "continue" });
|
|
9239
|
+
return;
|
|
9240
|
+
}
|
|
9172
9241
|
const session = this.pausedSessions.get(taskId);
|
|
9173
9242
|
if (session) {
|
|
9174
9243
|
session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
|
|
@@ -9177,6 +9246,11 @@ ${line}
|
|
|
9177
9246
|
}
|
|
9178
9247
|
rejectCheckpoint(taskId, reason) {
|
|
9179
9248
|
this.resumeIdle(taskId);
|
|
9249
|
+
const toolCall = this.toolCheckpoints.get(taskId);
|
|
9250
|
+
if (toolCall) {
|
|
9251
|
+
toolCall({ kind: "rejected", reason });
|
|
9252
|
+
return;
|
|
9253
|
+
}
|
|
9180
9254
|
const session = this.pausedSessions.get(taskId);
|
|
9181
9255
|
if (session) {
|
|
9182
9256
|
session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
|
|
@@ -9187,7 +9261,10 @@ ${line}
|
|
|
9187
9261
|
* Attach to a spawned session: scan the output tail for the task's completion
|
|
9188
9262
|
* marker (delivering a verdict immediately while leaving interactive sessions
|
|
9189
9263
|
* open), scan for checkpoint markers, and on exit produce a failed verdict
|
|
9190
|
-
* when the marker was never observed.
|
|
9264
|
+
* when the marker was never observed. A structured session's `task_complete`
|
|
9265
|
+
* call is evidence too (ADR-0022, V2): whichever signal comes first decides.
|
|
9266
|
+
*
|
|
9267
|
+
* Returns the attempt's generation, what {@link signalComplete} is checked against.
|
|
9191
9268
|
*/
|
|
9192
9269
|
watch(task, session) {
|
|
9193
9270
|
const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
|
|
@@ -9217,6 +9294,8 @@ ${line}
|
|
|
9217
9294
|
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
9218
9295
|
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
9219
9296
|
});
|
|
9297
|
+
session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
|
|
9298
|
+
session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
|
|
9220
9299
|
}
|
|
9221
9300
|
session.onExit((exitCode) => {
|
|
9222
9301
|
if (this.generations.get(task.id) !== gen) return;
|
|
@@ -9225,6 +9304,52 @@ ${line}
|
|
|
9225
9304
|
const verdict = this.decide(task, exitCode);
|
|
9226
9305
|
for (const l of this.listeners) l(task.id, verdict);
|
|
9227
9306
|
});
|
|
9307
|
+
return gen;
|
|
9308
|
+
}
|
|
9309
|
+
/**
|
|
9310
|
+
* The runner's own `task_complete` call (ADR-0022, V1/V3): settles the
|
|
9311
|
+
* attempt exactly as the marker does, unless that attempt is no longer the
|
|
9312
|
+
* task's current one or another signal already settled it.
|
|
9313
|
+
*/
|
|
9314
|
+
signalComplete(taskId, generation, report) {
|
|
9315
|
+
if (this.generations.get(taskId) !== generation) return;
|
|
9316
|
+
this.forget(taskId);
|
|
9317
|
+
this.bumpGeneration(taskId);
|
|
9318
|
+
const verdict = reportedVerdict(report);
|
|
9319
|
+
for (const l of this.listeners) l(taskId, verdict);
|
|
9320
|
+
}
|
|
9321
|
+
/**
|
|
9322
|
+
* The runner's `checkpoint` call (ADR-0022, V5): raised through the same
|
|
9323
|
+
* listeners as the marker, so the task waits on the user the same way, and
|
|
9324
|
+
* settled by {@link approveCheckpoint} or {@link rejectCheckpoint}. It is
|
|
9325
|
+
* withdrawn, never left hanging, once the attempt is over or the call goes.
|
|
9326
|
+
*/
|
|
9327
|
+
raiseCheckpoint(taskId, generation, question, signal) {
|
|
9328
|
+
if (this.generations.get(taskId) !== generation || signal.aborted) {
|
|
9329
|
+
return Promise.resolve({ kind: "withdrawn", why: "this attempt has ended." });
|
|
9330
|
+
}
|
|
9331
|
+
if (this.toolCheckpoints.has(taskId)) {
|
|
9332
|
+
return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
|
|
9333
|
+
}
|
|
9334
|
+
return new Promise((resolve7) => {
|
|
9335
|
+
const settle = (answer2) => {
|
|
9336
|
+
signal.removeEventListener("abort", onAbort);
|
|
9337
|
+
if (this.toolCheckpoints.get(taskId) === settle) this.toolCheckpoints.delete(taskId);
|
|
9338
|
+
resolve7(answer2);
|
|
9339
|
+
};
|
|
9340
|
+
const onAbort = () => {
|
|
9341
|
+
settle({ kind: "withdrawn", why: "the call was cancelled." });
|
|
9342
|
+
if (this.generations.get(taskId) !== generation) return;
|
|
9343
|
+
this.resumeIdle(taskId);
|
|
9344
|
+
for (const l of this.withdrawnListeners) l(taskId);
|
|
9345
|
+
};
|
|
9346
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9347
|
+
this.toolCheckpoints.set(taskId, settle);
|
|
9348
|
+
for (const l of this.checkpointListeners) l(taskId, question.trim());
|
|
9349
|
+
});
|
|
9350
|
+
}
|
|
9351
|
+
withdrawToolCheckpoint(taskId) {
|
|
9352
|
+
this.toolCheckpoints.get(taskId)?.({ kind: "withdrawn", why: "this attempt has ended." });
|
|
9228
9353
|
}
|
|
9229
9354
|
approvalOpened(taskId, approvalId) {
|
|
9230
9355
|
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
@@ -9258,6 +9383,7 @@ ${line}
|
|
|
9258
9383
|
this.markerTails.delete(taskId);
|
|
9259
9384
|
this.checkpointCarry.delete(taskId);
|
|
9260
9385
|
this.pausedSessions.delete(taskId);
|
|
9386
|
+
this.withdrawToolCheckpoint(taskId);
|
|
9261
9387
|
this.idlePaused.delete(taskId);
|
|
9262
9388
|
this.openApprovals.delete(taskId);
|
|
9263
9389
|
this.clearIdle(taskId);
|
|
@@ -9293,6 +9419,7 @@ ${line}
|
|
|
9293
9419
|
this.markerTails.clear();
|
|
9294
9420
|
this.checkpointCarry.clear();
|
|
9295
9421
|
this.pausedSessions.clear();
|
|
9422
|
+
for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
|
|
9296
9423
|
for (const timer of this.idleTimers.values()) clearTimeout(timer);
|
|
9297
9424
|
this.idleTimers.clear();
|
|
9298
9425
|
this.idleSince.clear();
|
|
@@ -9346,6 +9473,28 @@ ${line}
|
|
|
9346
9473
|
};
|
|
9347
9474
|
}
|
|
9348
9475
|
};
|
|
9476
|
+
function reportedVerdict(report) {
|
|
9477
|
+
const checks = [
|
|
9478
|
+
{
|
|
9479
|
+
name: "task_complete",
|
|
9480
|
+
passed: report.status === "done",
|
|
9481
|
+
skipped: false,
|
|
9482
|
+
detail: `the runner called task_complete with status "${report.status}"`
|
|
9483
|
+
},
|
|
9484
|
+
{
|
|
9485
|
+
name: "completion_marker",
|
|
9486
|
+
passed: report.status === "done",
|
|
9487
|
+
skipped: true,
|
|
9488
|
+
detail: "bypassed \u2014 the runner reported through task_complete"
|
|
9489
|
+
}
|
|
9490
|
+
];
|
|
9491
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9492
|
+
if (report.status === "done") {
|
|
9493
|
+
return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt };
|
|
9494
|
+
}
|
|
9495
|
+
const why = report.reason?.trim() || "no reason given";
|
|
9496
|
+
return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt };
|
|
9497
|
+
}
|
|
9349
9498
|
|
|
9350
9499
|
// src/services/transcriptCapture.ts
|
|
9351
9500
|
import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync4 } from "fs";
|
|
@@ -9509,10 +9658,10 @@ async function fromDirenv(cwd, deps) {
|
|
|
9509
9658
|
}
|
|
9510
9659
|
return { env, blocked: null };
|
|
9511
9660
|
}
|
|
9512
|
-
function findUp(start,
|
|
9661
|
+
function findUp(start, relative6, deps) {
|
|
9513
9662
|
let dir = path9.resolve(start);
|
|
9514
9663
|
for (; ; ) {
|
|
9515
|
-
const candidate = path9.join(dir,
|
|
9664
|
+
const candidate = path9.join(dir, relative6);
|
|
9516
9665
|
if (deps.readFile(candidate) !== null) return candidate;
|
|
9517
9666
|
const parent = path9.dirname(dir);
|
|
9518
9667
|
if (parent === dir) return null;
|
|
@@ -9761,6 +9910,11 @@ var BufferedTaskOutputSource = class {
|
|
|
9761
9910
|
session.onExit(() => {
|
|
9762
9911
|
capture.exited = true;
|
|
9763
9912
|
});
|
|
9913
|
+
if (isStructuredSession(session)) {
|
|
9914
|
+
session.onTaskComplete(({ summary }) => {
|
|
9915
|
+
if (capture.attached && summary.trim()) capture.reported = summary.trim();
|
|
9916
|
+
});
|
|
9917
|
+
}
|
|
9764
9918
|
}
|
|
9765
9919
|
detach(taskId) {
|
|
9766
9920
|
const capture = this.captures.get(taskId);
|
|
@@ -9771,6 +9925,8 @@ var BufferedTaskOutputSource = class {
|
|
|
9771
9925
|
this.captures.clear();
|
|
9772
9926
|
}
|
|
9773
9927
|
async finalText(attempt, doneToken) {
|
|
9928
|
+
const reported = this.captures.get(attempt.taskId)?.reported;
|
|
9929
|
+
if (reported) return defuseMarkers(reported);
|
|
9774
9930
|
if (attempt.cwd && attempt.completionMarker) {
|
|
9775
9931
|
const transcript = await this.transcripts.finalAssistantText({
|
|
9776
9932
|
runner: attempt.runner,
|
|
@@ -10610,8 +10766,8 @@ var IsolationRunController = class {
|
|
|
10610
10766
|
this.resolvers = {};
|
|
10611
10767
|
this.reportedCopies.clear();
|
|
10612
10768
|
this.listener.changed();
|
|
10613
|
-
const
|
|
10614
|
-
if (
|
|
10769
|
+
const shared2 = sharedPathsNotice(this.run);
|
|
10770
|
+
if (shared2) this.tell("info", shared2);
|
|
10615
10771
|
}
|
|
10616
10772
|
reportCopies(copied) {
|
|
10617
10773
|
const fresh = copied.filter((p) => !this.reportedCopies.has(p));
|
|
@@ -11783,6 +11939,502 @@ ${tail}` : ""}`;
|
|
|
11783
11939
|
}
|
|
11784
11940
|
};
|
|
11785
11941
|
|
|
11942
|
+
// src/services/harness/fileDiff.ts
|
|
11943
|
+
function markedLines(text, mark) {
|
|
11944
|
+
if (!text) return text;
|
|
11945
|
+
const body = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
11946
|
+
return `${body.split("\n").map((line) => `${mark}${line}`).join("\n")}${body === text ? "" : "\n"}`;
|
|
11947
|
+
}
|
|
11948
|
+
var HUNK_HEADER = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/;
|
|
11949
|
+
function hunksOf(diff) {
|
|
11950
|
+
const kept = [];
|
|
11951
|
+
let oldLeft = 0;
|
|
11952
|
+
let newLeft = 0;
|
|
11953
|
+
for (const line of diff.split("\n")) {
|
|
11954
|
+
if (oldLeft > 0 || newLeft > 0 || line.startsWith("\\")) {
|
|
11955
|
+
kept.push(line);
|
|
11956
|
+
if (line.startsWith("\\")) continue;
|
|
11957
|
+
if (!line.startsWith("+")) oldLeft -= 1;
|
|
11958
|
+
if (!line.startsWith("-")) newLeft -= 1;
|
|
11959
|
+
continue;
|
|
11960
|
+
}
|
|
11961
|
+
const header = HUNK_HEADER.exec(line);
|
|
11962
|
+
if (!header) continue;
|
|
11963
|
+
kept.push(line);
|
|
11964
|
+
oldLeft = header[1] === void 0 ? 1 : Number(header[1]);
|
|
11965
|
+
newLeft = header[2] === void 0 ? 1 : Number(header[2]);
|
|
11966
|
+
}
|
|
11967
|
+
return kept.length ? `${kept.join("\n")}${diff.endsWith("\n") ? "\n" : ""}` : diff;
|
|
11968
|
+
}
|
|
11969
|
+
function range(start, lines) {
|
|
11970
|
+
return lines === 1 ? `${start}` : `${start},${lines}`;
|
|
11971
|
+
}
|
|
11972
|
+
function structuredPatchText(patch) {
|
|
11973
|
+
if (!Array.isArray(patch)) return "";
|
|
11974
|
+
let text = "";
|
|
11975
|
+
for (const hunk of patch) {
|
|
11976
|
+
if (!hunk || typeof hunk !== "object") return "";
|
|
11977
|
+
const { oldStart, oldLines, newStart, newLines, lines } = hunk;
|
|
11978
|
+
if (typeof oldStart !== "number" || typeof oldLines !== "number" || typeof newStart !== "number" || typeof newLines !== "number") return "";
|
|
11979
|
+
if (!Array.isArray(lines) || !lines.every((line) => typeof line === "string")) return "";
|
|
11980
|
+
text += `@@ -${range(oldStart, oldLines)} +${range(newStart, newLines)} @@
|
|
11981
|
+
${lines.map((line) => `${line}
|
|
11982
|
+
`).join("")}`;
|
|
11983
|
+
}
|
|
11984
|
+
return text;
|
|
11985
|
+
}
|
|
11986
|
+
|
|
11987
|
+
// src/services/mcp/OrdewellMcpServer.ts
|
|
11988
|
+
import { createServer } from "http";
|
|
11989
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
11990
|
+
import { Server as McpProtocolServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11991
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11992
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
11993
|
+
|
|
11994
|
+
// src/utils/daemonToken.ts
|
|
11995
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
11996
|
+
import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
11997
|
+
import { join as join13 } from "path";
|
|
11998
|
+
|
|
11999
|
+
// src/utils/globalDataDir.ts
|
|
12000
|
+
import * as fs7 from "fs";
|
|
12001
|
+
import * as path13 from "path";
|
|
12002
|
+
import * as os5 from "os";
|
|
12003
|
+
function globalDataDir() {
|
|
12004
|
+
return path13.join(os5.homedir(), ".ordewell");
|
|
12005
|
+
}
|
|
12006
|
+
function copyDirSync(src, dest) {
|
|
12007
|
+
fs7.mkdirSync(dest, { recursive: true });
|
|
12008
|
+
const entries = fs7.readdirSync(src, { withFileTypes: true });
|
|
12009
|
+
for (const entry of entries) {
|
|
12010
|
+
const srcPath = path13.join(src, entry.name);
|
|
12011
|
+
const destPath = path13.join(dest, entry.name);
|
|
12012
|
+
if (entry.isDirectory()) {
|
|
12013
|
+
copyDirSync(srcPath, destPath);
|
|
12014
|
+
} else {
|
|
12015
|
+
fs7.copyFileSync(srcPath, destPath);
|
|
12016
|
+
}
|
|
12017
|
+
}
|
|
12018
|
+
}
|
|
12019
|
+
var migrated = false;
|
|
12020
|
+
function migrateOldConfigDir() {
|
|
12021
|
+
if (migrated) return;
|
|
12022
|
+
migrated = true;
|
|
12023
|
+
const oldDir = path13.join(os5.homedir(), ".config", "ordewell");
|
|
12024
|
+
if (!fs7.existsSync(oldDir)) return;
|
|
12025
|
+
const newDir = globalDataDir();
|
|
12026
|
+
const liftFile = (name) => {
|
|
12027
|
+
const oldFile = path13.join(oldDir, name);
|
|
12028
|
+
const newFile = path13.join(newDir, name);
|
|
12029
|
+
if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
|
|
12030
|
+
fs7.mkdirSync(newDir, { recursive: true });
|
|
12031
|
+
fs7.copyFileSync(oldFile, newFile);
|
|
12032
|
+
}
|
|
12033
|
+
};
|
|
12034
|
+
liftFile("settings.json");
|
|
12035
|
+
liftFile(".env");
|
|
12036
|
+
const oldPlugins = path13.join(oldDir, "plugins");
|
|
12037
|
+
const newPlugins = path13.join(newDir, "plugins");
|
|
12038
|
+
if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
|
|
12039
|
+
copyDirSync(oldPlugins, newPlugins);
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
|
|
12043
|
+
// src/utils/privateFile.ts
|
|
12044
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, renameSync, unlinkSync, writeSync } from "fs";
|
|
12045
|
+
import { basename as basename2, dirname as dirname5, join as join12 } from "path";
|
|
12046
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
12047
|
+
var DIR_MODE = 448;
|
|
12048
|
+
var FILE_MODE = 384;
|
|
12049
|
+
var POSIX_MODES = process.platform !== "win32";
|
|
12050
|
+
function ensurePrivateDir(dir) {
|
|
12051
|
+
if (POSIX_MODES) {
|
|
12052
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
|
|
12053
|
+
return;
|
|
12054
|
+
}
|
|
12055
|
+
mkdirSync6(dir, { recursive: true });
|
|
12056
|
+
}
|
|
12057
|
+
function writePrivateFile(filePath, content) {
|
|
12058
|
+
const dir = dirname5(filePath);
|
|
12059
|
+
ensurePrivateDir(dir);
|
|
12060
|
+
const tempPath = join12(dir, `.${basename2(filePath)}.${randomBytes2(8).toString("hex")}.tmp`);
|
|
12061
|
+
const fd = openSync(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
|
|
12062
|
+
try {
|
|
12063
|
+
writeSync(fd, content);
|
|
12064
|
+
} finally {
|
|
12065
|
+
closeSync(fd);
|
|
12066
|
+
}
|
|
12067
|
+
try {
|
|
12068
|
+
renameSync(tempPath, filePath);
|
|
12069
|
+
} catch (err) {
|
|
12070
|
+
try {
|
|
12071
|
+
unlinkSync(tempPath);
|
|
12072
|
+
} catch {
|
|
12073
|
+
}
|
|
12074
|
+
throw err;
|
|
12075
|
+
}
|
|
12076
|
+
}
|
|
12077
|
+
|
|
12078
|
+
// src/utils/daemonToken.ts
|
|
12079
|
+
var BEARER_PREFIX = "bearer ";
|
|
12080
|
+
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
12081
|
+
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
12082
|
+
function configDir() {
|
|
12083
|
+
return globalDataDir();
|
|
12084
|
+
}
|
|
12085
|
+
function daemonTokenPath(port) {
|
|
12086
|
+
return join13(configDir(), `server-${port}.token`);
|
|
12087
|
+
}
|
|
12088
|
+
function mintDaemonToken(port) {
|
|
12089
|
+
const token = randomBytes3(32).toString("base64url");
|
|
12090
|
+
const file = daemonTokenPath(port);
|
|
12091
|
+
writePrivateFile(file, token);
|
|
12092
|
+
return { token, file };
|
|
12093
|
+
}
|
|
12094
|
+
function readDaemonToken(port) {
|
|
12095
|
+
try {
|
|
12096
|
+
const token = readFileSync4(daemonTokenPath(port), "utf8").trim();
|
|
12097
|
+
return token === "" ? void 0 : token;
|
|
12098
|
+
} catch {
|
|
12099
|
+
return void 0;
|
|
12100
|
+
}
|
|
12101
|
+
}
|
|
12102
|
+
function clearDaemonToken(port) {
|
|
12103
|
+
try {
|
|
12104
|
+
unlinkSync2(daemonTokenPath(port));
|
|
12105
|
+
} catch {
|
|
12106
|
+
}
|
|
12107
|
+
}
|
|
12108
|
+
function bearerHeaderValue(token) {
|
|
12109
|
+
return `Bearer ${token}`;
|
|
12110
|
+
}
|
|
12111
|
+
function tokenSubprotocols(token) {
|
|
12112
|
+
return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
|
|
12113
|
+
}
|
|
12114
|
+
function extractPresentedToken(carriers) {
|
|
12115
|
+
const authorization = carriers.authorization;
|
|
12116
|
+
if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
|
|
12117
|
+
const value = authorization.slice(BEARER_PREFIX.length).trim();
|
|
12118
|
+
if (value !== "") return value;
|
|
12119
|
+
}
|
|
12120
|
+
for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
|
|
12121
|
+
const value = offered.trim();
|
|
12122
|
+
if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
|
|
12123
|
+
const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
|
|
12124
|
+
if (token !== "") return token;
|
|
12125
|
+
}
|
|
12126
|
+
return void 0;
|
|
12127
|
+
}
|
|
12128
|
+
function tokensMatch(presented, expected) {
|
|
12129
|
+
if (presented === void 0) return false;
|
|
12130
|
+
const a = Buffer.from(presented, "utf8");
|
|
12131
|
+
const b = Buffer.from(expected, "utf8");
|
|
12132
|
+
if (a.length !== b.length) return false;
|
|
12133
|
+
return timingSafeEqual(a, b);
|
|
12134
|
+
}
|
|
12135
|
+
|
|
12136
|
+
// src/services/mcp/tools.ts
|
|
12137
|
+
import { z } from "zod/v4";
|
|
12138
|
+
var taskCompleteInput = z.object({
|
|
12139
|
+
status: z.enum(["done", "blocked", "failed"]).describe("'done' only when the task is fully complete. 'blocked' or 'failed' end the attempt without passing."),
|
|
12140
|
+
summary: z.string().describe("What was done, handed to the tasks that depend on this one."),
|
|
12141
|
+
reason: z.string().optional().describe("Why the task is blocked or failed. Expected for anything but 'done'.")
|
|
12142
|
+
});
|
|
12143
|
+
var checkpointInput = z.object({
|
|
12144
|
+
question: z.string().min(1).describe("The question for the user. The call returns their answer.")
|
|
12145
|
+
});
|
|
12146
|
+
var listRunnersInput = z.object({});
|
|
12147
|
+
var listModelsInput = z.object({
|
|
12148
|
+
runner: z.string().min(1).describe("A runner id from list_runners.")
|
|
12149
|
+
});
|
|
12150
|
+
var taskRef = z.union([z.string().min(1), z.number().int().min(0)]).describe('A task id, "#order", a bare order, or a title.');
|
|
12151
|
+
var modelAssignment = z.looseObject({
|
|
12152
|
+
modelId: z.string(),
|
|
12153
|
+
modelLabel: z.string().optional(),
|
|
12154
|
+
thinkingEffort: z.string().optional().describe("A variant id from that model's variants list.")
|
|
12155
|
+
});
|
|
12156
|
+
var planTask = z.looseObject({
|
|
12157
|
+
id: z.string().optional(),
|
|
12158
|
+
order: z.number().optional(),
|
|
12159
|
+
title: z.string(),
|
|
12160
|
+
description: z.string().optional(),
|
|
12161
|
+
type: z.enum(["ai", "user"]).optional(),
|
|
12162
|
+
dependencies: z.array(z.string()).optional(),
|
|
12163
|
+
prompt: z.string().optional(),
|
|
12164
|
+
userSteps: z.array(z.looseObject({ order: z.number().optional(), instruction: z.string() })).optional(),
|
|
12165
|
+
assignedRunner: z.string().optional(),
|
|
12166
|
+
assignedModel: modelAssignment.optional(),
|
|
12167
|
+
taskMode: z.string().optional(),
|
|
12168
|
+
autonomy: z.enum(["AFK", "HITL"]).optional(),
|
|
12169
|
+
sliceType: z.enum(["AFK", "HITL"]).optional(),
|
|
12170
|
+
ops: z.boolean().optional(),
|
|
12171
|
+
userStoriesCovered: z.array(z.string()).optional(),
|
|
12172
|
+
subtasks: z.array(z.record(z.string(), z.unknown())).optional()
|
|
12173
|
+
});
|
|
12174
|
+
var submitPlanInput = z.object({
|
|
12175
|
+
tasks: z.array(planTask).min(1).describe("The whole plan, in order.")
|
|
12176
|
+
});
|
|
12177
|
+
var taskFields = z.record(z.string(), z.unknown());
|
|
12178
|
+
var handle = z.string().optional();
|
|
12179
|
+
var taskOp = z.discriminatedUnion("op", [
|
|
12180
|
+
z.object({ op: z.literal("update"), taskId: taskRef, changes: taskFields }),
|
|
12181
|
+
z.object({ op: z.literal("add"), task: taskFields, handle }),
|
|
12182
|
+
z.object({ op: z.literal("remove"), taskId: taskRef }),
|
|
12183
|
+
z.object({ op: z.literal("reorder"), taskIds: z.array(taskRef) }),
|
|
12184
|
+
z.object({ op: z.literal("merge"), taskIds: z.array(taskRef), merged: taskFields, handle }),
|
|
12185
|
+
z.object({ op: z.literal("split"), taskId: taskRef, parts: z.array(taskFields), handle }),
|
|
12186
|
+
z.object({ op: z.literal("rearm"), taskId: taskRef, changes: taskFields.optional() })
|
|
12187
|
+
]);
|
|
12188
|
+
var editPlanInput = z.object({
|
|
12189
|
+
ops: z.array(taskOp).min(1).describe("Applied as one batch; every ref resolves against the plan before any op runs.")
|
|
12190
|
+
});
|
|
12191
|
+
var taskQueryInput = z.object({
|
|
12192
|
+
tasks: z.array(taskRef).optional(),
|
|
12193
|
+
fields: z.array(z.enum(TASK_QUERY_TASK_FIELDS)).optional().describe("Omit to read every field. A task's output is read with task_output."),
|
|
12194
|
+
catalog: z.boolean().optional().describe("Also return every runner with its models, thinking-effort variants and modes.")
|
|
12195
|
+
}).refine((q) => (q.tasks?.length ?? 0) > 0 || q.catalog === true, { message: "Name at least one task, or set catalog: true." });
|
|
12196
|
+
var taskOutputInput = z.object({
|
|
12197
|
+
task: taskRef,
|
|
12198
|
+
lines: z.number().int().min(1).optional().describe(`How many lines to return; the default is 80 and anything over ${OUTPUT_LINES_MAX} is cut to ${OUTPUT_LINES_MAX}.`),
|
|
12199
|
+
since: z.number().int().min(0).optional().describe("A previous answer's nextOffset: return only what came after it.")
|
|
12200
|
+
});
|
|
12201
|
+
function checkpointReply(answer2) {
|
|
12202
|
+
switch (answer2.kind) {
|
|
12203
|
+
case "continue":
|
|
12204
|
+
return { text: "continue" };
|
|
12205
|
+
case "rejected":
|
|
12206
|
+
return { text: `rejected: ${answer2.reason}` };
|
|
12207
|
+
case "withdrawn":
|
|
12208
|
+
return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
|
|
12209
|
+
}
|
|
12210
|
+
}
|
|
12211
|
+
function tool(name, description, input, pick, annotations) {
|
|
12212
|
+
return {
|
|
12213
|
+
name,
|
|
12214
|
+
description,
|
|
12215
|
+
inputSchema: { ...z.toJSONSchema(input, { io: "input" }), type: "object" },
|
|
12216
|
+
...annotations ? { annotations } : {},
|
|
12217
|
+
async call(handler, args, context) {
|
|
12218
|
+
const run = pick(handler);
|
|
12219
|
+
if (!run) return { text: `${name} is not available in this session.`, isError: true };
|
|
12220
|
+
const parsed = input.safeParse(args ?? {});
|
|
12221
|
+
if (!parsed.success) return { text: `Invalid ${name} arguments: ${z.prettifyError(parsed.error)}`, isError: true };
|
|
12222
|
+
return run(parsed.data, context);
|
|
12223
|
+
}
|
|
12224
|
+
};
|
|
12225
|
+
}
|
|
12226
|
+
var TASK_TOOLS = [
|
|
12227
|
+
tool(
|
|
12228
|
+
"task_complete",
|
|
12229
|
+
"Report that this task has ended, and how. Call it once, as your last action.",
|
|
12230
|
+
taskCompleteInput,
|
|
12231
|
+
(h) => h.taskComplete?.bind(h)
|
|
12232
|
+
),
|
|
12233
|
+
tool(
|
|
12234
|
+
"checkpoint",
|
|
12235
|
+
"Ask the user a question and wait for the answer, which is this call's result.",
|
|
12236
|
+
checkpointInput,
|
|
12237
|
+
(h) => h.checkpoint?.bind(h)
|
|
12238
|
+
)
|
|
12239
|
+
];
|
|
12240
|
+
var PLANNER_READ_ONLY = { readOnlyHint: true };
|
|
12241
|
+
var PLANNER_TOOLS = [
|
|
12242
|
+
tool(
|
|
12243
|
+
"list_runners",
|
|
12244
|
+
"List the runners enabled right now, each with its modes and default mode. Call it just before submit_plan.",
|
|
12245
|
+
listRunnersInput,
|
|
12246
|
+
(h) => h.listRunners?.bind(h),
|
|
12247
|
+
PLANNER_READ_ONLY
|
|
12248
|
+
),
|
|
12249
|
+
tool(
|
|
12250
|
+
"list_models",
|
|
12251
|
+
"List the models a runner may use right now, with labels and thinking-effort variants.",
|
|
12252
|
+
listModelsInput,
|
|
12253
|
+
(h) => h.listModels?.bind(h),
|
|
12254
|
+
PLANNER_READ_ONLY
|
|
12255
|
+
),
|
|
12256
|
+
tool(
|
|
12257
|
+
"submit_plan",
|
|
12258
|
+
"Submit the whole plan. It is checked against the live runners and models; an error names the task, the field and what would be accepted.",
|
|
12259
|
+
submitPlanInput,
|
|
12260
|
+
(h) => h.submitPlan?.bind(h),
|
|
12261
|
+
PLANNER_READ_ONLY
|
|
12262
|
+
),
|
|
12263
|
+
tool(
|
|
12264
|
+
"edit_plan",
|
|
12265
|
+
"Change the current plan with task operations: update, add, remove, reorder, merge, split or rearm.",
|
|
12266
|
+
editPlanInput,
|
|
12267
|
+
(h) => h.editPlan?.bind(h),
|
|
12268
|
+
PLANNER_READ_ONLY
|
|
12269
|
+
),
|
|
12270
|
+
tool(
|
|
12271
|
+
"task_query",
|
|
12272
|
+
"Read the long fields of plan tasks that the plan summary leaves out (prompt, user steps, verdict, output summary), and optionally the live runner catalog. Read a task before you rewrite it.",
|
|
12273
|
+
taskQueryInput,
|
|
12274
|
+
(h) => h.taskQuery?.bind(h),
|
|
12275
|
+
PLANNER_READ_ONLY
|
|
12276
|
+
),
|
|
12277
|
+
tool(
|
|
12278
|
+
"task_output",
|
|
12279
|
+
"Read the recent output of a running task, to check what its runner is doing; paged by offset. A task that is not running answers with its verdict, output summary and a digest of its last attempt.",
|
|
12280
|
+
taskOutputInput,
|
|
12281
|
+
(h) => h.taskOutput?.bind(h),
|
|
12282
|
+
PLANNER_READ_ONLY
|
|
12283
|
+
)
|
|
12284
|
+
];
|
|
12285
|
+
|
|
12286
|
+
// src/services/mcp/OrdewellMcpServer.ts
|
|
12287
|
+
var ORDEWELL_MCP_PATH = "/mcp";
|
|
12288
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
12289
|
+
var OrdewellMcpServer = class {
|
|
12290
|
+
grants = /* @__PURE__ */ new Map();
|
|
12291
|
+
http;
|
|
12292
|
+
listening;
|
|
12293
|
+
boundUrl;
|
|
12294
|
+
boundHost;
|
|
12295
|
+
heartbeatMs;
|
|
12296
|
+
constructor(options = {}) {
|
|
12297
|
+
this.heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
12298
|
+
}
|
|
12299
|
+
/** The listener's URL, once the first token has started it. */
|
|
12300
|
+
get url() {
|
|
12301
|
+
return this.boundUrl;
|
|
12302
|
+
}
|
|
12303
|
+
issueTaskToken(scope, handler = {}) {
|
|
12304
|
+
return this.issue({ role: "task", scope: { ...scope }, handler, revoked: new AbortController() });
|
|
12305
|
+
}
|
|
12306
|
+
issuePlannerToken(scope, handler = {}) {
|
|
12307
|
+
return this.issue({ role: "planner", scope: { ...scope }, handler, revoked: new AbortController() });
|
|
12308
|
+
}
|
|
12309
|
+
/** Refuse every later request carrying `token`, and abort the calls it has in flight. */
|
|
12310
|
+
revoke(token) {
|
|
12311
|
+
const grant = this.grants.get(token);
|
|
12312
|
+
if (!grant) return;
|
|
12313
|
+
this.grants.delete(token);
|
|
12314
|
+
grant.revoked.abort();
|
|
12315
|
+
}
|
|
12316
|
+
async dispose() {
|
|
12317
|
+
for (const token of [...this.grants.keys()]) this.revoke(token);
|
|
12318
|
+
const http = this.http;
|
|
12319
|
+
this.http = void 0;
|
|
12320
|
+
this.listening = void 0;
|
|
12321
|
+
this.boundUrl = void 0;
|
|
12322
|
+
this.boundHost = void 0;
|
|
12323
|
+
if (!http) return;
|
|
12324
|
+
http.closeAllConnections();
|
|
12325
|
+
await new Promise((resolve7) => http.close(() => resolve7()));
|
|
12326
|
+
}
|
|
12327
|
+
async issue(grant) {
|
|
12328
|
+
const url = await this.start();
|
|
12329
|
+
const token = randomBytes4(32).toString("base64url");
|
|
12330
|
+
this.grants.set(token, grant);
|
|
12331
|
+
return { url, token };
|
|
12332
|
+
}
|
|
12333
|
+
start() {
|
|
12334
|
+
this.listening ??= new Promise((resolve7, reject) => {
|
|
12335
|
+
const http = createServer((req, res) => {
|
|
12336
|
+
this.handle(req, res).catch(() => {
|
|
12337
|
+
if (!res.headersSent) reply(res, 500, "Internal error");
|
|
12338
|
+
else res.destroy();
|
|
12339
|
+
});
|
|
12340
|
+
});
|
|
12341
|
+
http.once("error", (err) => {
|
|
12342
|
+
this.listening = void 0;
|
|
12343
|
+
reject(err);
|
|
12344
|
+
});
|
|
12345
|
+
http.listen(0, "127.0.0.1", () => {
|
|
12346
|
+
const address = http.address();
|
|
12347
|
+
if (address === null || typeof address === "string") {
|
|
12348
|
+
reject(new Error("Ordewell MCP server bound to no TCP port"));
|
|
12349
|
+
return;
|
|
12350
|
+
}
|
|
12351
|
+
this.boundHost = `127.0.0.1:${address.port}`;
|
|
12352
|
+
this.boundUrl = `http://${this.boundHost}${ORDEWELL_MCP_PATH}`;
|
|
12353
|
+
resolve7(this.boundUrl);
|
|
12354
|
+
});
|
|
12355
|
+
http.unref();
|
|
12356
|
+
this.http = http;
|
|
12357
|
+
});
|
|
12358
|
+
return this.listening;
|
|
12359
|
+
}
|
|
12360
|
+
async handle(req, res) {
|
|
12361
|
+
if (req.headers.host !== this.boundHost || req.headers.origin !== void 0) {
|
|
12362
|
+
reply(res, 403, "Forbidden");
|
|
12363
|
+
return;
|
|
12364
|
+
}
|
|
12365
|
+
const token = extractPresentedToken({ authorization: req.headers.authorization });
|
|
12366
|
+
const grant = token === void 0 ? void 0 : this.grants.get(token);
|
|
12367
|
+
if (!grant) {
|
|
12368
|
+
reply(res, 401, "Unauthorized");
|
|
12369
|
+
return;
|
|
12370
|
+
}
|
|
12371
|
+
if (new URL(req.url ?? "/", `http://${this.boundHost}`).pathname !== ORDEWELL_MCP_PATH) {
|
|
12372
|
+
reply(res, 404, "Not found");
|
|
12373
|
+
return;
|
|
12374
|
+
}
|
|
12375
|
+
const mcp = new McpProtocolServer({ name: "ordewell", version: "1" }, { capabilities: { tools: {} } });
|
|
12376
|
+
if (grant.role === "task") serveTools(mcp, TASK_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
12377
|
+
else serveTools(mcp, PLANNER_TOOLS, grant.handler, grant.revoked.signal, this.heartbeatMs);
|
|
12378
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
12379
|
+
res.on("close", () => {
|
|
12380
|
+
void transport.close();
|
|
12381
|
+
void mcp.close();
|
|
12382
|
+
});
|
|
12383
|
+
await mcp.connect(transport);
|
|
12384
|
+
await transport.handleRequest(req, res);
|
|
12385
|
+
}
|
|
12386
|
+
};
|
|
12387
|
+
function serveTools(mcp, tools, handler, revoked, heartbeatMs) {
|
|
12388
|
+
mcp.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
12389
|
+
tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, ...annotations ? { annotations } : {} }))
|
|
12390
|
+
}));
|
|
12391
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
12392
|
+
const tool2 = tools.find((t) => t.name === request.params.name);
|
|
12393
|
+
if (!tool2) throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
|
|
12394
|
+
const progressToken = request.params._meta?.progressToken;
|
|
12395
|
+
let beats = 0;
|
|
12396
|
+
const heartbeat = progressToken === void 0 ? void 0 : setInterval(() => {
|
|
12397
|
+
beats += 1;
|
|
12398
|
+
extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress: beats } }).catch(() => void 0);
|
|
12399
|
+
}, heartbeatMs);
|
|
12400
|
+
try {
|
|
12401
|
+
const result = await tool2.call(handler, request.params.arguments, { signal: AbortSignal.any([revoked, extra.signal]) });
|
|
12402
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError ?? false };
|
|
12403
|
+
} catch (err) {
|
|
12404
|
+
return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
|
|
12405
|
+
} finally {
|
|
12406
|
+
clearInterval(heartbeat);
|
|
12407
|
+
}
|
|
12408
|
+
});
|
|
12409
|
+
}
|
|
12410
|
+
function reply(res, status, message) {
|
|
12411
|
+
res.writeHead(status, { "content-type": "text/plain" }).end(message);
|
|
12412
|
+
}
|
|
12413
|
+
var shared;
|
|
12414
|
+
function sharedMcpServer() {
|
|
12415
|
+
shared ??= new OrdewellMcpServer();
|
|
12416
|
+
return shared;
|
|
12417
|
+
}
|
|
12418
|
+
|
|
12419
|
+
// src/services/mcp/clientConfig.ts
|
|
12420
|
+
import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
12421
|
+
import { tmpdir } from "os";
|
|
12422
|
+
import { join as join14 } from "path";
|
|
12423
|
+
var ORDEWELL_MCP_SERVER_NAME = "ordewell";
|
|
12424
|
+
function mcpClientConfig(credential) {
|
|
12425
|
+
return {
|
|
12426
|
+
name: ORDEWELL_MCP_SERVER_NAME,
|
|
12427
|
+
url: credential.url,
|
|
12428
|
+
headers: { Authorization: bearerHeaderValue(credential.token) }
|
|
12429
|
+
};
|
|
12430
|
+
}
|
|
12431
|
+
function ownerOnlyConfigFile(fileName, contents) {
|
|
12432
|
+
const dir = mkdtempSync(join14(tmpdir(), "ordewell-mcp-"));
|
|
12433
|
+
const path26 = join14(dir, fileName);
|
|
12434
|
+
writeFileSync3(path26, contents, { mode: 384 });
|
|
12435
|
+
return { path: path26, remove: () => rmSync2(dir, { recursive: true, force: true }) };
|
|
12436
|
+
}
|
|
12437
|
+
|
|
11786
12438
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
11787
12439
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
11788
12440
|
var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
|
|
@@ -11797,7 +12449,10 @@ var PROTOCOL_ARGS = [
|
|
|
11797
12449
|
];
|
|
11798
12450
|
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
11799
12451
|
var FOLLOW_ON_TURN_GRACE_MS = 5e3;
|
|
12452
|
+
var MCP_ATTACH_TIMEOUT_MS = 1e4;
|
|
12453
|
+
var MCP_STATUS_POLL_MS = 100;
|
|
11800
12454
|
var DEFAULT_DENIAL = "Denied in Ordewell. Continue without it, or say what you need.";
|
|
12455
|
+
var ORDEWELL_TASK_TOOLS = ["task_complete", "checkpoint"];
|
|
11801
12456
|
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
11802
12457
|
function usageRecord(usage, model, subagentId) {
|
|
11803
12458
|
const record = {
|
|
@@ -11830,10 +12485,16 @@ function flattenContent(content) {
|
|
|
11830
12485
|
}
|
|
11831
12486
|
return "";
|
|
11832
12487
|
}
|
|
12488
|
+
function editDiff(result) {
|
|
12489
|
+
if (!result || typeof result !== "object") return "";
|
|
12490
|
+
const { type, content, structuredPatch } = result;
|
|
12491
|
+
if (type === "create" && typeof content === "string") return markedLines(content, "+");
|
|
12492
|
+
return structuredPatchText(structuredPatch);
|
|
12493
|
+
}
|
|
11833
12494
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
11834
12495
|
agentId = "claude-code";
|
|
11835
|
-
|
|
11836
|
-
/**
|
|
12496
|
+
controlCount = 0;
|
|
12497
|
+
/** Control requests sent and not yet answered, by request id. */
|
|
11837
12498
|
pendingControl = /* @__PURE__ */ new Map();
|
|
11838
12499
|
/** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
|
|
11839
12500
|
interruptRequested = false;
|
|
@@ -11872,6 +12533,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11872
12533
|
requestedMode = null;
|
|
11873
12534
|
/** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
|
|
11874
12535
|
pendingResume = null;
|
|
12536
|
+
/** The `--mcp-config` file of the running process, which holds its token; removed with the process. */
|
|
12537
|
+
mcpConfig = null;
|
|
12538
|
+
/** `mcp__ordewell__`, once this process was given the server. */
|
|
12539
|
+
ordewellToolPrefix = null;
|
|
11875
12540
|
spawnSpec(opts) {
|
|
11876
12541
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
11877
12542
|
const args = [
|
|
@@ -11884,6 +12549,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11884
12549
|
"--append-system-prompt",
|
|
11885
12550
|
opts.systemPrompt
|
|
11886
12551
|
];
|
|
12552
|
+
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, PLANNER_TOOLS.map((t) => t.name)));
|
|
11887
12553
|
if (opts.model) args.push("--model", opts.model);
|
|
11888
12554
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
11889
12555
|
if (opts.resumeSessionId) {
|
|
@@ -11919,30 +12585,78 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11919
12585
|
this.reportedCostUsd = void 0;
|
|
11920
12586
|
this.pendingResume = opts.resumeSessionId;
|
|
11921
12587
|
}
|
|
12588
|
+
if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, ORDEWELL_TASK_TOOLS));
|
|
11922
12589
|
return { command: "claude", args };
|
|
11923
12590
|
}
|
|
12591
|
+
/**
|
|
12592
|
+
* `--mcp-config` takes a path as well as inline JSON; the path keeps the
|
|
12593
|
+
* token out of the argv every local user can list (ADR-0022, A5). The file
|
|
12594
|
+
* lives exactly as long as the process that reads it. `alwaysLoad` because
|
|
12595
|
+
* the CLI otherwise defers MCP tools behind its tool search, and a model
|
|
12596
|
+
* that has to look an Ordewell tool up first tends to go on without it.
|
|
12597
|
+
*/
|
|
12598
|
+
ordewellServerArgs(mcp, tools) {
|
|
12599
|
+
this.removeMcpConfig();
|
|
12600
|
+
const config = { mcpServers: { [mcp.name]: { type: "http", url: mcp.url, headers: mcp.headers, alwaysLoad: true } } };
|
|
12601
|
+
const file = ownerOnlyConfigFile("mcp.json", JSON.stringify(config));
|
|
12602
|
+
this.mcpConfig = file;
|
|
12603
|
+
void this.processEnded.then(() => file.remove());
|
|
12604
|
+
const prefix = `mcp__${mcp.name}__`;
|
|
12605
|
+
this.ordewellToolPrefix = prefix;
|
|
12606
|
+
return ["--mcp-config", file.path, "--allowedTools", tools.map((tool2) => `${prefix}${tool2}`).join(",")];
|
|
12607
|
+
}
|
|
12608
|
+
removeMcpConfig() {
|
|
12609
|
+
this.mcpConfig?.remove();
|
|
12610
|
+
this.mcpConfig = null;
|
|
12611
|
+
}
|
|
12612
|
+
async start(opts) {
|
|
12613
|
+
try {
|
|
12614
|
+
await super.start(opts);
|
|
12615
|
+
} catch (err) {
|
|
12616
|
+
this.removeMcpConfig();
|
|
12617
|
+
throw err;
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
/** Asks the CLI itself, which reports `pending` until its connection attempt settles. */
|
|
12621
|
+
async mcpAttached() {
|
|
12622
|
+
if (!this.mcpConfig) return false;
|
|
12623
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS;
|
|
12624
|
+
while (Date.now() < deadline) {
|
|
12625
|
+
const answer2 = await this.control({ subtype: "mcp_status" }, deadline - Date.now());
|
|
12626
|
+
const status = answer2?.response?.mcpServers?.find((s) => s.name === ORDEWELL_MCP_SERVER_NAME)?.status;
|
|
12627
|
+
if (status === "connected") return true;
|
|
12628
|
+
if (status !== "pending") return false;
|
|
12629
|
+
await new Promise((resolve7) => setTimeout(resolve7, MCP_STATUS_POLL_MS));
|
|
12630
|
+
}
|
|
12631
|
+
return false;
|
|
12632
|
+
}
|
|
11924
12633
|
/**
|
|
11925
12634
|
* Claude Code's soft interrupt: the turn stops, the process and its session
|
|
11926
12635
|
* stay. The CLI acknowledges on the control channel, then closes the turn
|
|
11927
12636
|
* with an `error_during_execution` result, which {@link handleLine} reports
|
|
11928
12637
|
* as an interrupted `turn_end`.
|
|
11929
12638
|
*/
|
|
11930
|
-
interrupt(timeoutMs) {
|
|
11931
|
-
if (!this.process) return
|
|
11932
|
-
this.interruptCount += 1;
|
|
11933
|
-
const requestId = `ordewell-interrupt-${this.interruptCount}`;
|
|
12639
|
+
async interrupt(timeoutMs) {
|
|
12640
|
+
if (!this.process) return false;
|
|
11934
12641
|
this.interruptRequested = true;
|
|
12642
|
+
return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
|
|
12643
|
+
}
|
|
12644
|
+
/** Send a control request and wait for its answer; null when none came in time or the process ended. */
|
|
12645
|
+
control(request, timeoutMs) {
|
|
12646
|
+
if (!this.process) return Promise.resolve(null);
|
|
12647
|
+
this.controlCount += 1;
|
|
12648
|
+
const requestId = `ordewell-${request.subtype}-${this.controlCount}`;
|
|
11935
12649
|
return new Promise((resolve7) => {
|
|
11936
|
-
const settle = (
|
|
12650
|
+
const settle = (response) => {
|
|
11937
12651
|
if (!this.pendingControl.delete(requestId)) return;
|
|
11938
12652
|
clearTimeout(timer);
|
|
11939
|
-
resolve7(
|
|
12653
|
+
resolve7(response);
|
|
11940
12654
|
};
|
|
11941
|
-
const timer = setTimeout(() => settle(
|
|
12655
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
11942
12656
|
timer.unref?.();
|
|
11943
12657
|
this.pendingControl.set(requestId, settle);
|
|
11944
|
-
void this.processEnded.then(() => settle(
|
|
11945
|
-
this.writeLine({ type: "control_request", request_id: requestId, request
|
|
12658
|
+
void this.processEnded.then(() => settle(null));
|
|
12659
|
+
this.writeLine({ type: "control_request", request_id: requestId, request });
|
|
11946
12660
|
});
|
|
11947
12661
|
}
|
|
11948
12662
|
/**
|
|
@@ -11977,6 +12691,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11977
12691
|
}
|
|
11978
12692
|
dispose() {
|
|
11979
12693
|
this.clearFollowOnTimer();
|
|
12694
|
+
this.removeMcpConfig();
|
|
11980
12695
|
super.dispose();
|
|
11981
12696
|
}
|
|
11982
12697
|
clearFollowOnTimer() {
|
|
@@ -12027,12 +12742,18 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
12027
12742
|
emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
|
|
12028
12743
|
return;
|
|
12029
12744
|
}
|
|
12745
|
+
const name = msg.request.tool_name ?? "unknown";
|
|
12746
|
+
if (this.ordewellToolPrefix && name.startsWith(this.ordewellToolPrefix)) {
|
|
12747
|
+
this.writeLine({ type: "control_response", response: { subtype: "success", request_id: id, response: { behavior: "allow", updatedInput: input } } });
|
|
12748
|
+
emit({ type: "permission_request", id, name, detail: JSON.stringify(input), input, decided: { decision: "allow" } });
|
|
12749
|
+
return;
|
|
12750
|
+
}
|
|
12030
12751
|
const suggestions = msg.request.permission_suggestions ?? [];
|
|
12031
12752
|
this.openPermissions.set(id, { input, suggestions });
|
|
12032
12753
|
emit({
|
|
12033
12754
|
type: "permission_request",
|
|
12034
12755
|
id,
|
|
12035
|
-
name
|
|
12756
|
+
name,
|
|
12036
12757
|
detail: JSON.stringify(input),
|
|
12037
12758
|
input,
|
|
12038
12759
|
suggestions,
|
|
@@ -12048,7 +12769,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
12048
12769
|
}
|
|
12049
12770
|
case "control_response": {
|
|
12050
12771
|
const requestId = msg.response?.request_id;
|
|
12051
|
-
if (requestId) this.pendingControl.get(requestId)?.(msg.response
|
|
12772
|
+
if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
|
|
12052
12773
|
return;
|
|
12053
12774
|
}
|
|
12054
12775
|
case "assistant":
|
|
@@ -12076,6 +12797,7 @@ ${block.text}` : block.text });
|
|
|
12076
12797
|
if (block.type !== "tool_result") continue;
|
|
12077
12798
|
const id = block.tool_use_id ?? "";
|
|
12078
12799
|
const output = flattenContent(block.content);
|
|
12800
|
+
const diff = block.is_error === true ? "" : editDiff(msg.tool_use_result);
|
|
12079
12801
|
const subagent = subagentId ? void 0 : this.openSubagents.get(id);
|
|
12080
12802
|
if (!subagentId && output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
12081
12803
|
emit({ type: "background_agent", id });
|
|
@@ -12083,7 +12805,7 @@ ${block.text}` : block.text });
|
|
|
12083
12805
|
} else if (subagent) {
|
|
12084
12806
|
this.finishForegroundSubagent(id, msg.tool_use_result, output, block.is_error === true, emit);
|
|
12085
12807
|
}
|
|
12086
|
-
emit({ type: "tool_result", id, name: "", output, success: block.is_error !== true, subagentId });
|
|
12808
|
+
emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
|
|
12087
12809
|
}
|
|
12088
12810
|
return;
|
|
12089
12811
|
case "system":
|
|
@@ -12219,6 +12941,9 @@ ${block.text}` : block.text });
|
|
|
12219
12941
|
}
|
|
12220
12942
|
};
|
|
12221
12943
|
|
|
12944
|
+
// src/services/harness/CodexAdapter.ts
|
|
12945
|
+
import { isAbsolute as isAbsolute4, relative as relative4, sep as sep2 } from "path";
|
|
12946
|
+
|
|
12222
12947
|
// src/services/harness/codexSandbox.ts
|
|
12223
12948
|
var PROBE_TIMEOUT_MS = 1e4;
|
|
12224
12949
|
var USERNS_FAILURE = /bwrap|bubblewrap|user namespace|RTM_NEWADDR/i;
|
|
@@ -12278,6 +13003,17 @@ function runProbe(deps, cwd, env, flags) {
|
|
|
12278
13003
|
|
|
12279
13004
|
// src/services/harness/CodexAdapter.ts
|
|
12280
13005
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
13006
|
+
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
13007
|
+
var CHANGE_NAMES = { add: "Add", update: "Update", delete: "Delete" };
|
|
13008
|
+
function changeName(change) {
|
|
13009
|
+
return CHANGE_NAMES[change.kind?.type ?? ""] ?? "file_change";
|
|
13010
|
+
}
|
|
13011
|
+
function changeDiff(change) {
|
|
13012
|
+
const diff = change.diff ?? "";
|
|
13013
|
+
if (change.kind?.type === "add") return markedLines(diff, "+");
|
|
13014
|
+
if (change.kind?.type === "delete") return markedLines(diff, "-");
|
|
13015
|
+
return diff;
|
|
13016
|
+
}
|
|
12281
13017
|
function subagentOutcome(status) {
|
|
12282
13018
|
switch (status) {
|
|
12283
13019
|
case "completed":
|
|
@@ -12311,7 +13047,29 @@ var DECLINE_RESULTS = {
|
|
|
12311
13047
|
execCommandApproval: { decision: "denied" },
|
|
12312
13048
|
applyPatchApproval: { decision: "denied" }
|
|
12313
13049
|
};
|
|
12314
|
-
|
|
13050
|
+
function shownToolName(item) {
|
|
13051
|
+
const tool2 = item.tool ?? "mcp_tool";
|
|
13052
|
+
return item.server === ORDEWELL_MCP_SERVER_NAME ? `mcp__${ORDEWELL_MCP_SERVER_NAME}__${tool2}` : tool2;
|
|
13053
|
+
}
|
|
13054
|
+
function mcpHeaderEnv(mcp) {
|
|
13055
|
+
return Object.fromEntries(Object.keys(mcp.headers).map((header, i) => [header, `ORDEWELL_MCP_TOKEN_${i}`]));
|
|
13056
|
+
}
|
|
13057
|
+
function ordewellServerConfig(mcp) {
|
|
13058
|
+
return {
|
|
13059
|
+
url: mcp.url,
|
|
13060
|
+
env_http_headers: mcpHeaderEnv(mcp),
|
|
13061
|
+
default_tools_approval_mode: "approve"
|
|
13062
|
+
};
|
|
13063
|
+
}
|
|
13064
|
+
var TASK_TOOLS_NOTE = [
|
|
13065
|
+
"This task has two tools from the `ordewell` MCP server: `task_complete` and `checkpoint`.",
|
|
13066
|
+
"They are not in your tool list up front. Find them with the tool discovery you have (the `exec` tool's `ALL_TOOLS` list) and call them by their full names, `mcp__ordewell__task_complete` and `mcp__ordewell__checkpoint`.",
|
|
13067
|
+
"Look for them before you finish; the task is reported complete through `task_complete`."
|
|
13068
|
+
].join(" ");
|
|
13069
|
+
function isOrdewellElicitation(method, params) {
|
|
13070
|
+
return method === "mcpServer/elicitation/request" && params.serverName === ORDEWELL_MCP_SERVER_NAME;
|
|
13071
|
+
}
|
|
13072
|
+
var REVIEW_DECISIONS = { allow: "accept", allowForTask: "acceptForSession", deny: "decline" };
|
|
12315
13073
|
var TASK_APPROVALS = {
|
|
12316
13074
|
"item/commandExecution/requestApproval": {
|
|
12317
13075
|
name: "shell",
|
|
@@ -12385,11 +13143,16 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
12385
13143
|
* names only the item, and a person deciding needs to see the files.
|
|
12386
13144
|
*/
|
|
12387
13145
|
fileChangePaths = /* @__PURE__ */ new Map();
|
|
13146
|
+
/** The row each announced file of an in-flight `fileChange` item is shown under, by the file's shown path. */
|
|
13147
|
+
fileChangeRows = /* @__PURE__ */ new Map();
|
|
12388
13148
|
/** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
|
|
12389
13149
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
12390
13150
|
resumeAttempted = false;
|
|
12391
13151
|
resumeFallbackSent = false;
|
|
12392
13152
|
sandbox = "default";
|
|
13153
|
+
/** The Ordewell server's startup state, as Codex last reported it for this thread. */
|
|
13154
|
+
mcpStartup = null;
|
|
13155
|
+
mcpStartupSettled = null;
|
|
12393
13156
|
/**
|
|
12394
13157
|
* Subagent threads this session has spawned, keyed by the child thread id.
|
|
12395
13158
|
* Codex runs a subagent in its own thread and replays both threads' events on
|
|
@@ -12398,7 +13161,9 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
12398
13161
|
subagents = /* @__PURE__ */ new Map();
|
|
12399
13162
|
spawnSpec(opts) {
|
|
12400
13163
|
this.startOpts = opts;
|
|
12401
|
-
|
|
13164
|
+
const headers = opts.mcp ? mcpHeaderEnv(opts.mcp) : {};
|
|
13165
|
+
const env = Object.fromEntries(Object.entries(headers).map(([header, name]) => [name, opts.mcp.headers[header]]));
|
|
13166
|
+
return { command: "codex", args: ["app-server"], ...opts.mcp ? { env } : {} };
|
|
12402
13167
|
}
|
|
12403
13168
|
/**
|
|
12404
13169
|
* `initialize`, then `thread/start`, both before the first user message. A
|
|
@@ -12465,7 +13230,11 @@ ${this.exitMessage()}`);
|
|
|
12465
13230
|
cwd: opts?.cwd,
|
|
12466
13231
|
...opts?.model ? { model: opts.model } : {}
|
|
12467
13232
|
};
|
|
12468
|
-
const
|
|
13233
|
+
const config = {
|
|
13234
|
+
...this.sandbox === "legacy-landlock" ? { features: { use_legacy_landlock: true } } : {},
|
|
13235
|
+
...opts?.mcp ? { mcp_servers: { [opts.mcp.name]: ordewellServerConfig(opts.mcp) } } : {}
|
|
13236
|
+
};
|
|
13237
|
+
const threadConfig = Object.keys(config).length ? { config } : {};
|
|
12469
13238
|
if (opts?.kind === "task") {
|
|
12470
13239
|
const { approvalPolicy, approvalsReviewer } = opts.flags.modeSettings;
|
|
12471
13240
|
return {
|
|
@@ -12473,14 +13242,15 @@ ${this.exitMessage()}`);
|
|
|
12473
13242
|
sandbox: opts.flags.permissionMode,
|
|
12474
13243
|
...approvalPolicy ? { approvalPolicy } : {},
|
|
12475
13244
|
...approvalsReviewer ? { approvalsReviewer } : {},
|
|
12476
|
-
...
|
|
13245
|
+
...opts.mcp ? { developerInstructions: TASK_TOOLS_NOTE } : {},
|
|
13246
|
+
...threadConfig
|
|
12477
13247
|
};
|
|
12478
13248
|
}
|
|
12479
13249
|
return {
|
|
12480
13250
|
...common,
|
|
12481
13251
|
sandbox: "read-only",
|
|
12482
13252
|
approvalPolicy: "never",
|
|
12483
|
-
...
|
|
13253
|
+
...threadConfig,
|
|
12484
13254
|
// `developerInstructions` layers on top of Codex's own base prompt, the
|
|
12485
13255
|
// way Claude Code's `--append-system-prompt` does. `baseInstructions`
|
|
12486
13256
|
// replaces it — which takes Codex's description of its own tools with
|
|
@@ -12489,6 +13259,32 @@ ${this.exitMessage()}`);
|
|
|
12489
13259
|
developerInstructions: opts?.systemPrompt
|
|
12490
13260
|
};
|
|
12491
13261
|
}
|
|
13262
|
+
/**
|
|
13263
|
+
* Codex's own startup report for the server, which it sends once the thread
|
|
13264
|
+
* has tried to connect (ADR-0022, S4). A report that came before this was
|
|
13265
|
+
* asked is kept, so asking late still gets the answer.
|
|
13266
|
+
*/
|
|
13267
|
+
async mcpAttached() {
|
|
13268
|
+
if (!this.startOpts?.mcp || !this.process) return false;
|
|
13269
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS2;
|
|
13270
|
+
while (this.mcpStartup === null || this.mcpStartup === "starting") {
|
|
13271
|
+
const left = deadline - Date.now();
|
|
13272
|
+
if (left <= 0) return false;
|
|
13273
|
+
const ended = await Promise.race([
|
|
13274
|
+
new Promise((resolve7) => {
|
|
13275
|
+
this.mcpStartupSettled = () => resolve7(false);
|
|
13276
|
+
}),
|
|
13277
|
+
this.processEnded.then(() => true),
|
|
13278
|
+
new Promise((resolve7) => {
|
|
13279
|
+
const t = setTimeout(() => resolve7(false), left);
|
|
13280
|
+
t.unref?.();
|
|
13281
|
+
})
|
|
13282
|
+
]);
|
|
13283
|
+
this.mcpStartupSettled = null;
|
|
13284
|
+
if (ended) return false;
|
|
13285
|
+
}
|
|
13286
|
+
return this.mcpStartup === "ready";
|
|
13287
|
+
}
|
|
12492
13288
|
effort() {
|
|
12493
13289
|
const opts = this.startOpts;
|
|
12494
13290
|
return opts?.kind === "task" ? opts.flags.effort : opts?.effort;
|
|
@@ -12660,6 +13456,13 @@ ${this.exitMessage()}`);
|
|
|
12660
13456
|
if (this.openPermissions.delete(id)) emit({ type: "permission_cancelled", id });
|
|
12661
13457
|
return;
|
|
12662
13458
|
}
|
|
13459
|
+
case "mcpServer/startupStatus/updated": {
|
|
13460
|
+
const startup = msg.params;
|
|
13461
|
+
if (startup?.name !== ORDEWELL_MCP_SERVER_NAME || !startup.status) return;
|
|
13462
|
+
this.mcpStartup = startup.status;
|
|
13463
|
+
this.mcpStartupSettled?.();
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
12663
13466
|
case "thread/tokenUsage/updated":
|
|
12664
13467
|
this.emitUsage(msg.params, emit);
|
|
12665
13468
|
return;
|
|
@@ -12798,6 +13601,10 @@ ${this.exitMessage()}`);
|
|
|
12798
13601
|
const method = msg.method;
|
|
12799
13602
|
const params = msg.params ?? {};
|
|
12800
13603
|
const approval = TASK_APPROVALS[method];
|
|
13604
|
+
if (isOrdewellElicitation(method, params)) {
|
|
13605
|
+
this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
|
|
13606
|
+
return;
|
|
13607
|
+
}
|
|
12801
13608
|
if (approval && (method !== "mcpServer/elicitation/request" || isYesNoElicitation(params))) {
|
|
12802
13609
|
const id = String(msg.id);
|
|
12803
13610
|
const input = this.approvalInput(method, params);
|
|
@@ -12838,6 +13645,10 @@ ${this.exitMessage()}`);
|
|
|
12838
13645
|
*/
|
|
12839
13646
|
answerServerRequest(msg, emit) {
|
|
12840
13647
|
const method = msg.method;
|
|
13648
|
+
if (isOrdewellElicitation(method, msg.params ?? {})) {
|
|
13649
|
+
this.writeLine({ jsonrpc: "2.0", id: msg.id, result: { action: "accept", content: {} } });
|
|
13650
|
+
return;
|
|
13651
|
+
}
|
|
12841
13652
|
const result = DECLINE_RESULTS[method];
|
|
12842
13653
|
if (result) {
|
|
12843
13654
|
this.writeLine({ jsonrpc: "2.0", id: msg.id, result });
|
|
@@ -12864,7 +13675,7 @@ ${this.exitMessage()}`);
|
|
|
12864
13675
|
emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
|
|
12865
13676
|
return;
|
|
12866
13677
|
case "mcpToolCall":
|
|
12867
|
-
emit({ type: "tool_call", id: item.id, name: item
|
|
13678
|
+
emit({ type: "tool_call", id: item.id, name: shownToolName(item), args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
12868
13679
|
return;
|
|
12869
13680
|
case "dynamicToolCall":
|
|
12870
13681
|
emit({ type: "tool_call", id: item.id, name: item.tool ?? "tool", args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
@@ -12876,7 +13687,7 @@ ${this.exitMessage()}`);
|
|
|
12876
13687
|
const paths = (item.changes ?? []).map((change) => change.path).filter((path26) => !!path26);
|
|
12877
13688
|
this.fileChangePaths.set(item.id, paths);
|
|
12878
13689
|
if (this.startOpts?.kind === "task") {
|
|
12879
|
-
|
|
13690
|
+
for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
|
|
12880
13691
|
}
|
|
12881
13692
|
return;
|
|
12882
13693
|
}
|
|
@@ -12933,7 +13744,7 @@ ${item.text}` : item.text });
|
|
|
12933
13744
|
emit({
|
|
12934
13745
|
type: "tool_result",
|
|
12935
13746
|
id,
|
|
12936
|
-
name: item.tool ?? "tool",
|
|
13747
|
+
name: item.type === "mcpToolCall" ? shownToolName(item) : item.tool ?? "tool",
|
|
12937
13748
|
output: item.error ?? flattenText(item.result) ?? "",
|
|
12938
13749
|
success: item.status !== "error" && item.success !== false && !item.error,
|
|
12939
13750
|
...subagentId ? { subagentId } : {}
|
|
@@ -12947,8 +13758,11 @@ ${item.text}` : item.text });
|
|
|
12947
13758
|
case "fileChange":
|
|
12948
13759
|
this.fileChangePaths.delete(id);
|
|
12949
13760
|
if (this.startOpts?.kind === "task") {
|
|
12950
|
-
const
|
|
12951
|
-
|
|
13761
|
+
for (const change of item.changes ?? []) {
|
|
13762
|
+
const row = this.announceFileChange(id, change, emit, subagentId);
|
|
13763
|
+
emit({ type: "tool_result", id: row, name: changeName(change), output: changeDiff(change), success: item.status === "completed", ...subagentId ? { subagentId } : {} });
|
|
13764
|
+
}
|
|
13765
|
+
this.fileChangeRows.delete(id);
|
|
12952
13766
|
return;
|
|
12953
13767
|
}
|
|
12954
13768
|
emit({ type: "tool_result", id, name: "file_change", output: JSON.stringify(item), success: false, ...subagentId ? { subagentId } : {} });
|
|
@@ -12957,6 +13771,34 @@ ${item.text}` : item.text });
|
|
|
12957
13771
|
return;
|
|
12958
13772
|
}
|
|
12959
13773
|
}
|
|
13774
|
+
/**
|
|
13775
|
+
* One row per changed file: a patch is several files, and one row naming
|
|
13776
|
+
* them all reads as a single cut-off path. The first file keeps the item's
|
|
13777
|
+
* id, which an approval for the patch points at. Announcing a file twice
|
|
13778
|
+
* returns the row it already has, so a change first seen completed still
|
|
13779
|
+
* gets its call.
|
|
13780
|
+
*/
|
|
13781
|
+
announceFileChange(itemId, change, emit, subagentId) {
|
|
13782
|
+
const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
|
|
13783
|
+
this.fileChangeRows.set(itemId, rows);
|
|
13784
|
+
const path26 = this.shownChangePath(change);
|
|
13785
|
+
const known = rows.get(path26);
|
|
13786
|
+
if (known) return known;
|
|
13787
|
+
const row = rows.size === 0 ? itemId : `${itemId}:${path26}`;
|
|
13788
|
+
rows.set(path26, row);
|
|
13789
|
+
emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path26 }, ...subagentId ? { subagentId } : {} });
|
|
13790
|
+
return row;
|
|
13791
|
+
}
|
|
13792
|
+
/** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
|
|
13793
|
+
shownChangePath(change) {
|
|
13794
|
+
const shown = (path26) => {
|
|
13795
|
+
const cwd = this.startOpts?.cwd;
|
|
13796
|
+
const rel = cwd ? relative4(cwd, path26) : path26;
|
|
13797
|
+
return rel && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute4(rel) ? rel : path26;
|
|
13798
|
+
};
|
|
13799
|
+
const from = shown(change.path ?? "");
|
|
13800
|
+
return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
|
|
13801
|
+
}
|
|
12960
13802
|
/**
|
|
12961
13803
|
* A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
|
|
12962
13804
|
* subagent. The call itself is planner-level tool activity; the lifecycle it
|
|
@@ -12996,7 +13838,45 @@ ${item.text}` : item.text });
|
|
|
12996
13838
|
};
|
|
12997
13839
|
|
|
12998
13840
|
// src/services/harness/OpenCodeAdapter.ts
|
|
12999
|
-
import { randomBytes as
|
|
13841
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
13842
|
+
|
|
13843
|
+
// src/services/harness/openCodeOrdewell.ts
|
|
13844
|
+
function ordewellToolPrefix(mcp) {
|
|
13845
|
+
return `${mcp.name}_`;
|
|
13846
|
+
}
|
|
13847
|
+
function isOrdewellTool(mcp, permission) {
|
|
13848
|
+
return !!mcp && !!permission && permission.startsWith(ordewellToolPrefix(mcp));
|
|
13849
|
+
}
|
|
13850
|
+
function isRecord(value) {
|
|
13851
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13852
|
+
}
|
|
13853
|
+
function deepMerge(base, over) {
|
|
13854
|
+
const merged = { ...base };
|
|
13855
|
+
for (const [key, value] of Object.entries(over)) {
|
|
13856
|
+
const current = merged[key];
|
|
13857
|
+
merged[key] = isRecord(current) && isRecord(value) ? deepMerge(current, value) : value;
|
|
13858
|
+
}
|
|
13859
|
+
return merged;
|
|
13860
|
+
}
|
|
13861
|
+
function mergeOrdewellConfig(existing, mcp) {
|
|
13862
|
+
let base = {};
|
|
13863
|
+
if (existing?.trim()) {
|
|
13864
|
+
let parsed;
|
|
13865
|
+
try {
|
|
13866
|
+
parsed = JSON.parse(existing);
|
|
13867
|
+
} catch {
|
|
13868
|
+
return null;
|
|
13869
|
+
}
|
|
13870
|
+
if (!isRecord(parsed)) return null;
|
|
13871
|
+
base = parsed;
|
|
13872
|
+
}
|
|
13873
|
+
const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
|
|
13874
|
+
const ours = {
|
|
13875
|
+
mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } },
|
|
13876
|
+
permission: { [`${ordewellToolPrefix(mcp)}*`]: "allow" }
|
|
13877
|
+
};
|
|
13878
|
+
return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
|
|
13879
|
+
}
|
|
13000
13880
|
|
|
13001
13881
|
// src/services/harness/OpenCodeV2.ts
|
|
13002
13882
|
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
@@ -13079,12 +13959,16 @@ var OpenCodeV2 = class {
|
|
|
13079
13959
|
const created = await this.host.json("POST", "/api/session", {
|
|
13080
13960
|
agent: this.agent,
|
|
13081
13961
|
...this.model ? { model: this.model } : {},
|
|
13082
|
-
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
13962
|
+
permissions: [...this.role === "planner" ? PLANNER_RULES : TASK_RULES, ...this.ordewellRules()]
|
|
13083
13963
|
});
|
|
13084
13964
|
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
13085
13965
|
this.sessionId = created.data.id;
|
|
13086
13966
|
await this.applyInstructions();
|
|
13087
13967
|
}
|
|
13968
|
+
/** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
|
|
13969
|
+
ordewellRules() {
|
|
13970
|
+
return this.opts.mcp ? [{ action: `${ordewellToolPrefix(this.opts.mcp)}*`, resource: "*", effect: "allow" }] : [];
|
|
13971
|
+
}
|
|
13088
13972
|
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
13089
13973
|
async applyInstructions() {
|
|
13090
13974
|
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
@@ -13222,8 +14106,8 @@ var OpenCodeV2 = class {
|
|
|
13222
14106
|
});
|
|
13223
14107
|
return true;
|
|
13224
14108
|
}
|
|
13225
|
-
async replyPermission(id, sessionId,
|
|
13226
|
-
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`,
|
|
14109
|
+
async replyPermission(id, sessionId, reply2) {
|
|
14110
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply2);
|
|
13227
14111
|
}
|
|
13228
14112
|
async openStream(state, turn, onEvent, onActivity) {
|
|
13229
14113
|
const streamAbort = new AbortController();
|
|
@@ -13463,6 +14347,11 @@ var OpenCodeV2 = class {
|
|
|
13463
14347
|
const request = this.permissionRequest(ask);
|
|
13464
14348
|
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13465
14349
|
state.seen.add(`perm:${request.id}`);
|
|
14350
|
+
if (isOrdewellTool(this.opts.mcp, request.name)) {
|
|
14351
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
14352
|
+
});
|
|
14353
|
+
return;
|
|
14354
|
+
}
|
|
13466
14355
|
onEvent(request);
|
|
13467
14356
|
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
13468
14357
|
});
|
|
@@ -13478,7 +14367,7 @@ var OpenCodeV2 = class {
|
|
|
13478
14367
|
const request = this.permissionRequest(ask);
|
|
13479
14368
|
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13480
14369
|
state.seen.add(`perm:${request.id}`);
|
|
13481
|
-
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
14370
|
+
if (isOrdewellTool(this.opts.mcp, request.name) || this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
13482
14371
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
13483
14372
|
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
13484
14373
|
});
|
|
@@ -13497,6 +14386,8 @@ var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
|
13497
14386
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
13498
14387
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
13499
14388
|
var SERVER_USERNAME = "opencode";
|
|
14389
|
+
var MCP_ATTACH_TIMEOUT_MS3 = 1e4;
|
|
14390
|
+
var MCP_STATUS_POLL_MS2 = 200;
|
|
13500
14391
|
var DISABLED_TOOLS = {
|
|
13501
14392
|
question: false,
|
|
13502
14393
|
edit: false,
|
|
@@ -13562,6 +14453,12 @@ function unwrapFileToolOutput(output) {
|
|
|
13562
14453
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
13563
14454
|
return inner ? inner[1] : output;
|
|
13564
14455
|
}
|
|
14456
|
+
function editDiff2(tool2, state) {
|
|
14457
|
+
const { diff, exists } = state?.metadata ?? {};
|
|
14458
|
+
if (typeof diff === "string" && diff) return hunksOf(diff);
|
|
14459
|
+
const content = state?.input?.content;
|
|
14460
|
+
return tool2 === "write" && exists === false && typeof content === "string" ? markedLines(content, "+") : "";
|
|
14461
|
+
}
|
|
13565
14462
|
function delay2(ms, signal) {
|
|
13566
14463
|
return new Promise((resolve7) => {
|
|
13567
14464
|
const timer = setTimeout(resolve7, ms);
|
|
@@ -13614,6 +14511,8 @@ var OpenCodeAdapter = class {
|
|
|
13614
14511
|
openPermissions = /* @__PURE__ */ new Map();
|
|
13615
14512
|
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
13616
14513
|
v2 = null;
|
|
14514
|
+
/** The Ordewell server this process was configured with; null when none was given or its config could not be merged. */
|
|
14515
|
+
ordewell = null;
|
|
13617
14516
|
async start(opts) {
|
|
13618
14517
|
if (opts.kind === "task") this.task = opts;
|
|
13619
14518
|
else this.planner = opts;
|
|
@@ -13628,9 +14527,19 @@ var OpenCodeAdapter = class {
|
|
|
13628
14527
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
13629
14528
|
}
|
|
13630
14529
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
13631
|
-
const password =
|
|
14530
|
+
const password = randomBytes5(24).toString("base64url");
|
|
14531
|
+
const ordewellConfig = opts.mcp ? mergeOrdewellConfig(workspaceEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT, opts.mcp) : null;
|
|
14532
|
+
if (opts.mcp && ordewellConfig === null) {
|
|
14533
|
+
console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
|
|
14534
|
+
}
|
|
14535
|
+
this.ordewell = ordewellConfig === null ? null : opts.mcp ?? null;
|
|
13632
14536
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
13633
|
-
env: runnerEnv(PATH, {
|
|
14537
|
+
env: runnerEnv(PATH, {
|
|
14538
|
+
...workspaceEnv,
|
|
14539
|
+
...ordewellConfig === null ? {} : { OPENCODE_CONFIG_CONTENT: ordewellConfig },
|
|
14540
|
+
OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
|
|
14541
|
+
OPENCODE_SERVER_PASSWORD: password
|
|
14542
|
+
}),
|
|
13634
14543
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13635
14544
|
cwd: opts.cwd,
|
|
13636
14545
|
windowsVerbatimArguments: launch.verbatim
|
|
@@ -13680,7 +14589,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13680
14589
|
processEnded: this.processEnded,
|
|
13681
14590
|
isExited: () => this.exited,
|
|
13682
14591
|
exitMessage: () => this.exitMessage()
|
|
13683
|
-
}, opts, this.role());
|
|
14592
|
+
}, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
|
|
13684
14593
|
try {
|
|
13685
14594
|
await this.v2.start();
|
|
13686
14595
|
} catch (err) {
|
|
@@ -13733,12 +14642,12 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13733
14642
|
...this.planner?.effort ? { variant: this.planner.effort } : {},
|
|
13734
14643
|
...this.planner?.systemPrompt ? { system: this.planner.systemPrompt } : {}
|
|
13735
14644
|
};
|
|
13736
|
-
const
|
|
14645
|
+
const reply2 = await this.json("POST", `/session/${this.sessionId}/message`, body, signal);
|
|
13737
14646
|
if (signal?.aborted) {
|
|
13738
14647
|
this.dispose();
|
|
13739
14648
|
return;
|
|
13740
14649
|
}
|
|
13741
|
-
this.settle(
|
|
14650
|
+
this.settle(reply2, turn, onEvent);
|
|
13742
14651
|
} catch (err) {
|
|
13743
14652
|
if (signal?.aborted) {
|
|
13744
14653
|
this.dispose();
|
|
@@ -13844,13 +14753,13 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13844
14753
|
break;
|
|
13845
14754
|
}
|
|
13846
14755
|
}
|
|
13847
|
-
for (const
|
|
13848
|
-
const id =
|
|
13849
|
-
if (
|
|
13850
|
-
for (const part of
|
|
14756
|
+
for (const reply2 of messages.slice(start)) {
|
|
14757
|
+
const id = reply2.info?.id;
|
|
14758
|
+
if (reply2.info?.role !== "assistant" || !id) continue;
|
|
14759
|
+
for (const part of reply2.parts ?? []) {
|
|
13851
14760
|
if (part.messageID === id) this.emitPart(part, turn, onEvent);
|
|
13852
14761
|
}
|
|
13853
|
-
this.countUsage(
|
|
14762
|
+
this.countUsage(reply2.info, turn, onEvent);
|
|
13854
14763
|
}
|
|
13855
14764
|
}
|
|
13856
14765
|
/**
|
|
@@ -13960,18 +14869,18 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13960
14869
|
* their text, reasoning and usage — reach Ordewell over the stream or not at
|
|
13961
14870
|
* all.
|
|
13962
14871
|
*/
|
|
13963
|
-
settle(
|
|
13964
|
-
const failure = typeof
|
|
14872
|
+
settle(reply2, turn, onEvent) {
|
|
14873
|
+
const failure = typeof reply2?.error === "string" ? reply2.error : reply2?.error?.message ?? reply2?.info?.error?.data?.message;
|
|
13965
14874
|
if (failure) {
|
|
13966
14875
|
onEvent({ type: "error", message: failure });
|
|
13967
14876
|
return;
|
|
13968
14877
|
}
|
|
13969
|
-
const assistantId =
|
|
13970
|
-
for (const part of
|
|
14878
|
+
const assistantId = reply2?.info?.id;
|
|
14879
|
+
for (const part of reply2?.parts ?? []) {
|
|
13971
14880
|
if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
|
|
13972
14881
|
this.emitPart(part, turn, onEvent);
|
|
13973
14882
|
}
|
|
13974
|
-
if (
|
|
14883
|
+
if (reply2?.info) this.countUsage(reply2.info, turn, onEvent);
|
|
13975
14884
|
if (assistantId) this.lastAssistantId = assistantId;
|
|
13976
14885
|
onEvent({ type: "turn_end" });
|
|
13977
14886
|
}
|
|
@@ -14048,7 +14957,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
14048
14957
|
type: "tool_result",
|
|
14049
14958
|
id: callId,
|
|
14050
14959
|
name,
|
|
14051
|
-
output: unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
|
|
14960
|
+
output: status === "completed" && editDiff2(name, part.state) || unwrapFileToolOutput(part.state?.output ?? part.state?.error ?? ""),
|
|
14052
14961
|
success: status === "completed",
|
|
14053
14962
|
subagentId
|
|
14054
14963
|
});
|
|
@@ -14182,6 +15091,11 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
14182
15091
|
const id = ask.id;
|
|
14183
15092
|
if (!id || seen.has(`perm:${id}`)) return;
|
|
14184
15093
|
seen.add(`perm:${id}`);
|
|
15094
|
+
if (isOrdewellTool(this.ordewell, permissionName(ask))) {
|
|
15095
|
+
void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "once" }).catch(() => {
|
|
15096
|
+
});
|
|
15097
|
+
return;
|
|
15098
|
+
}
|
|
14185
15099
|
onEvent({ type: "permission_request", id, name: permissionName(ask), detail: permissionDetail(ask) });
|
|
14186
15100
|
void this.replyPermission(id, ask.sessionID ?? this.sessionId ?? "", { reply: "reject" }).catch(() => {
|
|
14187
15101
|
});
|
|
@@ -14208,7 +15122,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
14208
15122
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
14209
15123
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
14210
15124
|
};
|
|
14211
|
-
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
15125
|
+
if (isOrdewellTool(this.ordewell, request.name) || this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
14212
15126
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
14213
15127
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
14214
15128
|
});
|
|
@@ -14221,10 +15135,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
14221
15135
|
* `POST /permission/:id/reply` is the current answer. The per-session path
|
|
14222
15136
|
* it replaced is tried only when a server too old to know the new one 404s.
|
|
14223
15137
|
*/
|
|
14224
|
-
async replyPermission(id, sessionId,
|
|
14225
|
-
const response = await this.request("POST", `/permission/${id}/reply`,
|
|
15138
|
+
async replyPermission(id, sessionId, reply2) {
|
|
15139
|
+
const response = await this.request("POST", `/permission/${id}/reply`, reply2);
|
|
14226
15140
|
if (response.status !== 404) return;
|
|
14227
|
-
await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response:
|
|
15141
|
+
await this.request("POST", `/session/${sessionId}/permissions/${id}`, { response: reply2.reply });
|
|
14228
15142
|
}
|
|
14229
15143
|
/**
|
|
14230
15144
|
* Open `/event` for one turn and wait until it is connected. The stream
|
|
@@ -14303,6 +15217,19 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
14303
15217
|
nativeSessionId() {
|
|
14304
15218
|
return this.sessionId;
|
|
14305
15219
|
}
|
|
15220
|
+
/** Asks the server, which lists every MCP server it has with the state of its connection. */
|
|
15221
|
+
async mcpAttached() {
|
|
15222
|
+
if (!this.ordewell || !this.baseUrl) return false;
|
|
15223
|
+
const deadline = Date.now() + MCP_ATTACH_TIMEOUT_MS3;
|
|
15224
|
+
while (Date.now() < deadline && !this.exited) {
|
|
15225
|
+
const servers = await this.json("GET", "/mcp").catch(() => null);
|
|
15226
|
+
const status = servers?.[this.ordewell.name]?.status;
|
|
15227
|
+
if (status === "connected") return true;
|
|
15228
|
+
if (status !== void 0 && status !== "pending") return false;
|
|
15229
|
+
await delay2(MCP_STATUS_POLL_MS2);
|
|
15230
|
+
}
|
|
15231
|
+
return false;
|
|
15232
|
+
}
|
|
14306
15233
|
dispose() {
|
|
14307
15234
|
if (this.disposed) return;
|
|
14308
15235
|
this.disposed = true;
|
|
@@ -14325,9 +15252,13 @@ var TASK_MODE_ADAPTERS = {
|
|
|
14325
15252
|
codex: (deps) => new CodexAdapter(deps),
|
|
14326
15253
|
opencode: (deps) => new OpenCodeAdapter(deps)
|
|
14327
15254
|
};
|
|
15255
|
+
var ORDEWELL_TOOL_RUNNERS = /* @__PURE__ */ new Set(["claude-code", "codex", "opencode"]);
|
|
14328
15256
|
function supportsTaskMode(runner) {
|
|
14329
15257
|
return Object.hasOwn(TASK_MODE_ADAPTERS, runner);
|
|
14330
15258
|
}
|
|
15259
|
+
function takesOrdewellTools(runner) {
|
|
15260
|
+
return ORDEWELL_TOOL_RUNNERS.has(runner);
|
|
15261
|
+
}
|
|
14331
15262
|
function createTaskAdapter(runner, deps) {
|
|
14332
15263
|
if (!supportsTaskMode(runner)) throw new TaskModeUnsupportedError(runner);
|
|
14333
15264
|
return TASK_MODE_ADAPTERS[runner](deps);
|
|
@@ -14340,6 +15271,9 @@ function routeTransport(requested, runner, registry) {
|
|
|
14340
15271
|
const name = registry?.get(runner)?.manifest.displayName ?? runner;
|
|
14341
15272
|
return { transport: "terminal", fallback: `no structured connector for ${name} yet` };
|
|
14342
15273
|
}
|
|
15274
|
+
function givesCompletionTool(requested, runner, registry) {
|
|
15275
|
+
return routeTransport(requested, runner, registry).transport === "structured" && takesOrdewellTools(runner);
|
|
15276
|
+
}
|
|
14343
15277
|
var TransportRouter = class {
|
|
14344
15278
|
constructor(runners) {
|
|
14345
15279
|
this.runners = runners;
|
|
@@ -14462,6 +15396,11 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
14462
15396
|
this.emit("onTaskSettled", { taskId });
|
|
14463
15397
|
this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
|
|
14464
15398
|
});
|
|
15399
|
+
this.verifier.onCheckpointWithdrawn((taskId) => {
|
|
15400
|
+
if (!this.atCheckpoint(taskId)) return;
|
|
15401
|
+
this.store.markInProgress(taskId);
|
|
15402
|
+
this.emit("onTaskChanged");
|
|
15403
|
+
});
|
|
14465
15404
|
this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
|
|
14466
15405
|
}
|
|
14467
15406
|
/**
|
|
@@ -14548,7 +15487,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
14548
15487
|
this.notifications.warn(message);
|
|
14549
15488
|
};
|
|
14550
15489
|
if (resolved.blockedEnvrc) {
|
|
14551
|
-
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${
|
|
15490
|
+
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path14.dirname(resolved.blockedEnvrc)} to use them.`);
|
|
14552
15491
|
}
|
|
14553
15492
|
if (resolved.trackedEnvFile) {
|
|
14554
15493
|
warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
|
|
@@ -15379,17 +16318,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
15379
16318
|
this.abandonSpawn(task, attempt);
|
|
15380
16319
|
return false;
|
|
15381
16320
|
}
|
|
15382
|
-
const
|
|
16321
|
+
const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
|
|
16322
|
+
const completionTool = givesCompletionTool(transport, attempt.runner, this.registry);
|
|
16323
|
+
const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message, { ops: attempt.ops, completionTool }) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
|
|
15383
16324
|
planMapEnabled: this.config.planMapEnabled,
|
|
15384
16325
|
// A merge to resolve is not new behaviour to drive test-first.
|
|
15385
16326
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
15386
16327
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
15387
16328
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
15388
|
-
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
16329
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0,
|
|
16330
|
+
completionTool
|
|
15389
16331
|
});
|
|
15390
16332
|
this.lingering.close(task.id);
|
|
15391
16333
|
const env = await this.envForTask(cwd);
|
|
15392
|
-
const transport = attempt.continuation ? "structured" : this.planTransport ?? "terminal";
|
|
15393
16334
|
const session = await this.terminalRunner.spawn({
|
|
15394
16335
|
taskId: task.id,
|
|
15395
16336
|
runner: attempt.runner,
|
|
@@ -15404,7 +16345,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
15404
16345
|
title: task.title,
|
|
15405
16346
|
env,
|
|
15406
16347
|
transport,
|
|
15407
|
-
resumeSessionId: attempt.continuation?.resumeSessionId
|
|
16348
|
+
resumeSessionId: attempt.continuation?.resumeSessionId,
|
|
16349
|
+
attempt: attempt.attempt
|
|
15408
16350
|
});
|
|
15409
16351
|
if (this.attempts.get(task.id) !== attempt) {
|
|
15410
16352
|
this.abandonSpawn(task, attempt, session);
|
|
@@ -15702,52 +16644,6 @@ import { exec as exec3, spawn } from "child_process";
|
|
|
15702
16644
|
import { promisify as promisify3 } from "util";
|
|
15703
16645
|
import * as fsSync3 from "fs";
|
|
15704
16646
|
import * as osMod from "os";
|
|
15705
|
-
|
|
15706
|
-
// src/utils/globalDataDir.ts
|
|
15707
|
-
import * as fs7 from "fs";
|
|
15708
|
-
import * as path14 from "path";
|
|
15709
|
-
import * as os5 from "os";
|
|
15710
|
-
function globalDataDir() {
|
|
15711
|
-
return path14.join(os5.homedir(), ".ordewell");
|
|
15712
|
-
}
|
|
15713
|
-
function copyDirSync(src, dest) {
|
|
15714
|
-
fs7.mkdirSync(dest, { recursive: true });
|
|
15715
|
-
const entries = fs7.readdirSync(src, { withFileTypes: true });
|
|
15716
|
-
for (const entry of entries) {
|
|
15717
|
-
const srcPath = path14.join(src, entry.name);
|
|
15718
|
-
const destPath = path14.join(dest, entry.name);
|
|
15719
|
-
if (entry.isDirectory()) {
|
|
15720
|
-
copyDirSync(srcPath, destPath);
|
|
15721
|
-
} else {
|
|
15722
|
-
fs7.copyFileSync(srcPath, destPath);
|
|
15723
|
-
}
|
|
15724
|
-
}
|
|
15725
|
-
}
|
|
15726
|
-
var migrated = false;
|
|
15727
|
-
function migrateOldConfigDir() {
|
|
15728
|
-
if (migrated) return;
|
|
15729
|
-
migrated = true;
|
|
15730
|
-
const oldDir = path14.join(os5.homedir(), ".config", "ordewell");
|
|
15731
|
-
if (!fs7.existsSync(oldDir)) return;
|
|
15732
|
-
const newDir = globalDataDir();
|
|
15733
|
-
const liftFile = (name) => {
|
|
15734
|
-
const oldFile = path14.join(oldDir, name);
|
|
15735
|
-
const newFile = path14.join(newDir, name);
|
|
15736
|
-
if (fs7.existsSync(oldFile) && !fs7.existsSync(newFile)) {
|
|
15737
|
-
fs7.mkdirSync(newDir, { recursive: true });
|
|
15738
|
-
fs7.copyFileSync(oldFile, newFile);
|
|
15739
|
-
}
|
|
15740
|
-
};
|
|
15741
|
-
liftFile("settings.json");
|
|
15742
|
-
liftFile(".env");
|
|
15743
|
-
const oldPlugins = path14.join(oldDir, "plugins");
|
|
15744
|
-
const newPlugins = path14.join(newDir, "plugins");
|
|
15745
|
-
if (fs7.existsSync(oldPlugins) && !fs7.existsSync(newPlugins)) {
|
|
15746
|
-
copyDirSync(oldPlugins, newPlugins);
|
|
15747
|
-
}
|
|
15748
|
-
}
|
|
15749
|
-
|
|
15750
|
-
// src/services/ModelDiscovery.ts
|
|
15751
16647
|
var execAsync3 = promisify3(exec3);
|
|
15752
16648
|
var defaultExec2 = async (command, options) => {
|
|
15753
16649
|
const PATH = await augmentedPath();
|
|
@@ -16954,6 +17850,7 @@ var CliAgentAiService = class {
|
|
|
16954
17850
|
};
|
|
16955
17851
|
this.makeAdapter = deps.createAdapter ?? defaultAdapter;
|
|
16956
17852
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
17853
|
+
this.mcpServer = deps.mcpServer;
|
|
16957
17854
|
}
|
|
16958
17855
|
config;
|
|
16959
17856
|
runner;
|
|
@@ -16968,6 +17865,11 @@ var CliAgentAiService = class {
|
|
|
16968
17865
|
*/
|
|
16969
17866
|
lastNativeSessionId = null;
|
|
16970
17867
|
conversation = null;
|
|
17868
|
+
mcpServer;
|
|
17869
|
+
/** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
|
|
17870
|
+
plannerToken = null;
|
|
17871
|
+
/** Whether the running planner was spawned with its tools and the CLI reported them connected. */
|
|
17872
|
+
toolsAttached = false;
|
|
16971
17873
|
activeAbort = null;
|
|
16972
17874
|
/**
|
|
16973
17875
|
* Every subagent this conversation has reported starting, and finishing.
|
|
@@ -16990,6 +17892,10 @@ var CliAgentAiService = class {
|
|
|
16990
17892
|
if (!this.conversation) return true;
|
|
16991
17893
|
return this.conversation.startOptions.model === this.plannerModel() && this.conversation.startOptions.effort === this.config.plannerThinkingEffort;
|
|
16992
17894
|
}
|
|
17895
|
+
/** Whether this conversation's planner submits through the Ordewell MCP server rather than the envelopes. */
|
|
17896
|
+
plannerToolsAttached() {
|
|
17897
|
+
return this.toolsAttached;
|
|
17898
|
+
}
|
|
16993
17899
|
/** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
|
|
16994
17900
|
nativeSessionId() {
|
|
16995
17901
|
return this.adapter?.nativeSessionId() ?? null;
|
|
@@ -16997,8 +17903,8 @@ var CliAgentAiService = class {
|
|
|
16997
17903
|
reset() {
|
|
16998
17904
|
this.activeAbort?.abort();
|
|
16999
17905
|
this.activeAbort = null;
|
|
17000
|
-
this.
|
|
17001
|
-
this.
|
|
17906
|
+
this.disposeAdapter();
|
|
17907
|
+
this.toolsAttached = false;
|
|
17002
17908
|
this.lastNativeSessionId = null;
|
|
17003
17909
|
this.conversation = null;
|
|
17004
17910
|
this.subagents.started.clear();
|
|
@@ -17025,9 +17931,23 @@ var CliAgentAiService = class {
|
|
|
17025
17931
|
model: this.plannerModel(),
|
|
17026
17932
|
effort: this.config.plannerThinkingEffort
|
|
17027
17933
|
};
|
|
17028
|
-
|
|
17934
|
+
const tools = req.plannerTools && this.mcpServer ? {
|
|
17935
|
+
offer: req.plannerTools,
|
|
17936
|
+
systemPrompt: buildConversationSystemPrompt(
|
|
17937
|
+
req.goal,
|
|
17938
|
+
contextStr,
|
|
17939
|
+
req.modelsByRunner,
|
|
17940
|
+
req.runners,
|
|
17941
|
+
req.runnerModes,
|
|
17942
|
+
req.autonomousDefault ?? true,
|
|
17943
|
+
req.verificationEnabled ?? false,
|
|
17944
|
+
{ harness: true, isolatedExecution: req.isolatedExecution, plannerTools: true }
|
|
17945
|
+
)
|
|
17946
|
+
} : void 0;
|
|
17947
|
+
await this.startAdapter(startOptions, tools);
|
|
17029
17948
|
this.conversation = {
|
|
17030
17949
|
startOptions,
|
|
17950
|
+
tools,
|
|
17031
17951
|
runners: req.runners,
|
|
17032
17952
|
runnerModes: req.runnerModes,
|
|
17033
17953
|
autonomousDefault: req.autonomousDefault
|
|
@@ -17232,10 +18152,7 @@ var CliAgentAiService = class {
|
|
|
17232
18152
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
17233
18153
|
}
|
|
17234
18154
|
this.lastNativeSessionId = adapter.nativeSessionId() ?? this.lastNativeSessionId;
|
|
17235
|
-
if (error || signal?.aborted)
|
|
17236
|
-
adapter.dispose();
|
|
17237
|
-
this.adapter = null;
|
|
17238
|
-
}
|
|
18155
|
+
if (error || signal?.aborted) this.disposeAdapter();
|
|
17239
18156
|
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
17240
18157
|
}
|
|
17241
18158
|
// --- Process lifecycle ---
|
|
@@ -17244,13 +18161,50 @@ var CliAgentAiService = class {
|
|
|
17244
18161
|
* type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
|
|
17245
18162
|
* mode from here without changing this signature.
|
|
17246
18163
|
*/
|
|
17247
|
-
async startAdapter(opts) {
|
|
18164
|
+
async startAdapter(opts, tools) {
|
|
17248
18165
|
const adapter = this.makeAdapter(this.runner, this.processDeps);
|
|
17249
18166
|
if (!adapter) throw new Error(`No planner adapter is available for "${this.runner}".`);
|
|
18167
|
+
this.toolsAttached = false;
|
|
18168
|
+
if (tools && this.mcpServer && adapter.mcpAttached) {
|
|
18169
|
+
if (await this.startWithTools(adapter, opts, tools, this.mcpServer)) return adapter;
|
|
18170
|
+
return this.startAdapter(opts);
|
|
18171
|
+
}
|
|
17250
18172
|
await adapter.start(opts);
|
|
17251
18173
|
this.adapter = adapter;
|
|
17252
18174
|
return adapter;
|
|
17253
18175
|
}
|
|
18176
|
+
/** Spawn with the Ordewell server injected. False, with nothing left running, when it did not connect. */
|
|
18177
|
+
async startWithTools(adapter, opts, tools, server) {
|
|
18178
|
+
let credential;
|
|
18179
|
+
try {
|
|
18180
|
+
credential = await server.issuePlannerToken({ sessionId: tools.offer.sessionId }, tools.offer.handler);
|
|
18181
|
+
} catch {
|
|
18182
|
+
return false;
|
|
18183
|
+
}
|
|
18184
|
+
try {
|
|
18185
|
+
await adapter.start({ ...opts, systemPrompt: tools.systemPrompt, mcp: mcpClientConfig(credential) });
|
|
18186
|
+
if (await adapter.mcpAttached()) {
|
|
18187
|
+
this.adapter = adapter;
|
|
18188
|
+
this.plannerToken = credential.token;
|
|
18189
|
+
this.toolsAttached = true;
|
|
18190
|
+
return true;
|
|
18191
|
+
}
|
|
18192
|
+
} catch (err) {
|
|
18193
|
+
adapter.dispose();
|
|
18194
|
+
server.revoke(credential.token);
|
|
18195
|
+
throw err;
|
|
18196
|
+
}
|
|
18197
|
+
adapter.dispose();
|
|
18198
|
+
server.revoke(credential.token);
|
|
18199
|
+
return false;
|
|
18200
|
+
}
|
|
18201
|
+
/** Kill the planner process, and with it the token it was spawned with. */
|
|
18202
|
+
disposeAdapter() {
|
|
18203
|
+
this.adapter?.dispose();
|
|
18204
|
+
this.adapter = null;
|
|
18205
|
+
if (this.plannerToken) this.mcpServer?.revoke(this.plannerToken);
|
|
18206
|
+
this.plannerToken = null;
|
|
18207
|
+
}
|
|
17254
18208
|
/**
|
|
17255
18209
|
* The live agent process, restarted from its own session id if it died
|
|
17256
18210
|
* between turns. Resume is a hint: when it fails, the caller's next
|
|
@@ -17261,7 +18215,7 @@ var CliAgentAiService = class {
|
|
|
17261
18215
|
if (this.adapter) return this.adapter;
|
|
17262
18216
|
const conversation = this.conversation;
|
|
17263
18217
|
if (!conversation) throw new Error("No active planner conversation.");
|
|
17264
|
-
await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 });
|
|
18218
|
+
await this.startAdapter({ ...conversation.startOptions, resumeSessionId: this.lastNativeSessionId ?? void 0 }, conversation.tools);
|
|
17265
18219
|
return this.adapter;
|
|
17266
18220
|
}
|
|
17267
18221
|
plannerModel() {
|
|
@@ -17280,7 +18234,9 @@ var CliAgentAiService = class {
|
|
|
17280
18234
|
const previous = this.adapter;
|
|
17281
18235
|
const previousConversation = this.conversation;
|
|
17282
18236
|
const previousSessionId = this.lastNativeSessionId;
|
|
18237
|
+
const previousTools = { token: this.plannerToken, attached: this.toolsAttached };
|
|
17283
18238
|
this.adapter = null;
|
|
18239
|
+
this.plannerToken = null;
|
|
17284
18240
|
const startOptions = {
|
|
17285
18241
|
kind: "planner",
|
|
17286
18242
|
cwd: this.workspaceRoot(),
|
|
@@ -17310,6 +18266,8 @@ var CliAgentAiService = class {
|
|
|
17310
18266
|
} finally {
|
|
17311
18267
|
oneShotAdapter?.dispose();
|
|
17312
18268
|
this.adapter = previous;
|
|
18269
|
+
this.plannerToken = previousTools.token;
|
|
18270
|
+
this.toolsAttached = previousTools.attached;
|
|
17313
18271
|
this.conversation = previousConversation;
|
|
17314
18272
|
this.lastNativeSessionId = previousSessionId;
|
|
17315
18273
|
}
|
|
@@ -17405,6 +18363,173 @@ function createAiService(config, deps) {
|
|
|
17405
18363
|
return new OpenAiService(config);
|
|
17406
18364
|
}
|
|
17407
18365
|
|
|
18366
|
+
// src/services/plannerTools.ts
|
|
18367
|
+
function runnersOf(tasks) {
|
|
18368
|
+
return [...new Set(flattenTasks(tasks).filter((t) => t.type !== "user").map((t) => t.assignedRunner))];
|
|
18369
|
+
}
|
|
18370
|
+
function plannerToolHandler(host) {
|
|
18371
|
+
return {
|
|
18372
|
+
async listRunners() {
|
|
18373
|
+
const catalog = await host.liveCatalog();
|
|
18374
|
+
return answer({
|
|
18375
|
+
autonomy: autonomyLevelLabel(catalog.autonomousDefault),
|
|
18376
|
+
runners: catalog.runners.map((id) => {
|
|
18377
|
+
const modes = catalog.modes[id] ?? [];
|
|
18378
|
+
const offered = [...filteredBuildModes(modes, catalog.autonomousDefault), ...modes.filter((m) => m.id === "plan")];
|
|
18379
|
+
return {
|
|
18380
|
+
id,
|
|
18381
|
+
defaultMode: resolveDefaultMode(modes, catalog.autonomousDefault) ?? null,
|
|
18382
|
+
modes: offered.map(({ id: modeId2, label, description }) => ({ id: modeId2, label, description }))
|
|
18383
|
+
};
|
|
18384
|
+
})
|
|
18385
|
+
});
|
|
18386
|
+
},
|
|
18387
|
+
async listModels({ runner }) {
|
|
18388
|
+
const catalog = await host.liveCatalog();
|
|
18389
|
+
if (!catalog.runners.includes(runner)) return notEnabled(runner, catalog.runners);
|
|
18390
|
+
return answer({
|
|
18391
|
+
runner,
|
|
18392
|
+
models: (catalog.models[runner] ?? []).map((m) => ({
|
|
18393
|
+
modelId: m.modelId,
|
|
18394
|
+
modelLabel: m.modelLabel,
|
|
18395
|
+
variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
|
|
18396
|
+
}))
|
|
18397
|
+
});
|
|
18398
|
+
},
|
|
18399
|
+
async submitPlan({ tasks }) {
|
|
18400
|
+
const catalog = await host.liveCatalog();
|
|
18401
|
+
const result = validatePlanTasks({ tasks }, catalog.runners, catalog.modes, catalog.autonomousDefault);
|
|
18402
|
+
if (!result.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: result.errors, enabledRunners: catalog.runners }) };
|
|
18403
|
+
const runners = runnersOf(result.tasks);
|
|
18404
|
+
const coerced = coercions(result.tasks, host.coerce(result.tasks, runners));
|
|
18405
|
+
if (!host.submitPlan(result.tasks, runners)) {
|
|
18406
|
+
return { isError: true, text: JSON.stringify({ ok: false, errors: [{ field: "tasks", message: "No planning turn is open to take the plan." }] }) };
|
|
18407
|
+
}
|
|
18408
|
+
return answer({
|
|
18409
|
+
ok: true,
|
|
18410
|
+
tasks: result.tasks.length,
|
|
18411
|
+
coerced,
|
|
18412
|
+
next: "The plan is committed when this reply ends. Tell the user briefly what you submitted; do not repeat the plan as JSON."
|
|
18413
|
+
});
|
|
18414
|
+
},
|
|
18415
|
+
async taskQuery({ tasks, fields, catalog }) {
|
|
18416
|
+
const query = { tasks: (tasks ?? []).map(String), fields: fields ?? [...TASK_QUERY_TASK_FIELDS], catalog: catalog === true };
|
|
18417
|
+
return read(host, taskQuerySignature(query), async () => ({
|
|
18418
|
+
tasks: query.tasks.map((ref) => taskDetail(host.tasks(), ref, query.fields ?? [])),
|
|
18419
|
+
...query.catalog ? { catalog: catalogDetail(await host.liveCatalog()) } : {}
|
|
18420
|
+
}));
|
|
18421
|
+
},
|
|
18422
|
+
async taskOutput({ task: ref, lines, since }) {
|
|
18423
|
+
const task = findTask(host.tasks(), String(ref));
|
|
18424
|
+
if (!task) return { isError: true, text: JSON.stringify({ ok: false, error: `No task matches "${ref}" in the current plan.` }) };
|
|
18425
|
+
const maxLines = Math.min(lines ?? OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX);
|
|
18426
|
+
return read(host, JSON.stringify(["output", task.id, maxLines, since ?? null]), async () => {
|
|
18427
|
+
const head = { task: { id: task.id, order: task.order, title: task.title, status: task.status } };
|
|
18428
|
+
const tail = host.liveOutput(task.id, { maxLines, sinceOffset: since });
|
|
18429
|
+
if (tail?.running) {
|
|
18430
|
+
const fitted = fitTail(tail.text, OUTPUT_TAIL_MAX_CHARS);
|
|
18431
|
+
return { ...head, running: true, output: fitted.text, nextOffset: tail.nextOffset, ...fitted.trimmed ? { trimmedToFit: true } : {} };
|
|
18432
|
+
}
|
|
18433
|
+
return {
|
|
18434
|
+
...head,
|
|
18435
|
+
running: false,
|
|
18436
|
+
reason: "This task is not running, so there is no live output: its outcome is below.",
|
|
18437
|
+
verdict: verdictDetail(task.verdict),
|
|
18438
|
+
outputSummary: outputSummaryDetail(task),
|
|
18439
|
+
lastAttempt: host.lastAttempt(task.id)
|
|
18440
|
+
};
|
|
18441
|
+
});
|
|
18442
|
+
},
|
|
18443
|
+
async editPlan({ ops }) {
|
|
18444
|
+
const outcome = await host.editPlan(ops);
|
|
18445
|
+
if (!outcome.ok) return { isError: true, text: JSON.stringify({ ok: false, errors: outcome.errors.map(opError) }) };
|
|
18446
|
+
return answer({
|
|
18447
|
+
ok: true,
|
|
18448
|
+
summary: outcome.summary,
|
|
18449
|
+
queued: outcome.queued,
|
|
18450
|
+
next: outcome.queued ? "A task you named is running, so the edit is parked and nothing was checked: it is applied between task batches, when the user's message comes back to you. Tell the user it is queued." : "The edit is applied when this reply ends, and the user is shown what changed. Tell them briefly; do not repeat it as JSON."
|
|
18451
|
+
});
|
|
18452
|
+
}
|
|
18453
|
+
};
|
|
18454
|
+
}
|
|
18455
|
+
function opError(message) {
|
|
18456
|
+
const match = /^op (\d+) \((\w+)\): ([\s\S]*)$/.exec(message);
|
|
18457
|
+
return match ? { op: Number(match[1]), kind: match[2], message: match[3] } : { message };
|
|
18458
|
+
}
|
|
18459
|
+
async function read(host, signature, answerRead) {
|
|
18460
|
+
const result = await host.read(signature, answerRead);
|
|
18461
|
+
if (result.status === "refused") {
|
|
18462
|
+
return { isError: true, text: JSON.stringify({ ok: false, error: "You have used every read this message allows. Nothing was read: answer the user in prose, or call edit_plan for the change you came to make." }) };
|
|
18463
|
+
}
|
|
18464
|
+
return answer({ ...result.value, ...result.landNow ? { note: TASK_READ_ANSWER_OR_EDIT } : {} });
|
|
18465
|
+
}
|
|
18466
|
+
function verdictDetail(verdict) {
|
|
18467
|
+
if (!verdict) return null;
|
|
18468
|
+
return {
|
|
18469
|
+
outcome: verdict.outcome,
|
|
18470
|
+
reason: verdict.reason,
|
|
18471
|
+
checks: verdict.checks.map((c) => ({ name: c.name, result: c.skipped ? "skipped" : c.passed ? "pass" : "fail" }))
|
|
18472
|
+
};
|
|
18473
|
+
}
|
|
18474
|
+
function outputSummaryDetail({ outputSummary }) {
|
|
18475
|
+
return outputSummary ? { reviewReason: outputSummary.reviewReason, ...outputSummary.logTail ? { logTail: outputSummary.logTail } : {} } : null;
|
|
18476
|
+
}
|
|
18477
|
+
function taskDetail(plan, ref, fields) {
|
|
18478
|
+
const task = findTask(plan, ref);
|
|
18479
|
+
if (!task) return { ref, error: "no task matches this reference in the current plan." };
|
|
18480
|
+
const wants = (field) => fields.includes(field);
|
|
18481
|
+
return {
|
|
18482
|
+
id: task.id,
|
|
18483
|
+
order: task.order,
|
|
18484
|
+
title: task.title,
|
|
18485
|
+
status: task.status,
|
|
18486
|
+
type: task.type === "user" ? "user" : "ai",
|
|
18487
|
+
...wants("description") ? { description: task.description || null } : {},
|
|
18488
|
+
...wants("prompt") ? { prompt: task.prompt || null } : {},
|
|
18489
|
+
...wants("userSteps") ? { userSteps: task.userSteps?.length ? task.userSteps.map((s) => ({ order: s.order, instruction: s.instruction, completed: s.completed === true })) : null } : {},
|
|
18490
|
+
...wants("verdict") ? { verdict: verdictDetail(task.verdict) } : {},
|
|
18491
|
+
...wants("outputSummary") ? { outputSummary: outputSummaryDetail(task) } : {},
|
|
18492
|
+
...wants("userStoriesCovered") ? { userStoriesCovered: task.userStoriesCovered?.length ? task.userStoriesCovered : null } : {}
|
|
18493
|
+
};
|
|
18494
|
+
}
|
|
18495
|
+
function catalogDetail(catalog) {
|
|
18496
|
+
return {
|
|
18497
|
+
runners: catalog.runners.map((id) => {
|
|
18498
|
+
const modes = catalog.modes[id] ?? [];
|
|
18499
|
+
const defaultMode = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
18500
|
+
return {
|
|
18501
|
+
id,
|
|
18502
|
+
models: (catalog.models[id] ?? []).map((m) => ({
|
|
18503
|
+
modelId: m.modelId,
|
|
18504
|
+
modelLabel: m.modelLabel,
|
|
18505
|
+
variants: (m.variants ?? []).map((v) => ({ id: v.id, label: v.label }))
|
|
18506
|
+
})),
|
|
18507
|
+
modes: modes.map((m) => ({ id: m.id, label: m.label, description: m.description, default: m.id === defaultMode }))
|
|
18508
|
+
};
|
|
18509
|
+
})
|
|
18510
|
+
};
|
|
18511
|
+
}
|
|
18512
|
+
function answer(body) {
|
|
18513
|
+
return { text: JSON.stringify(body) };
|
|
18514
|
+
}
|
|
18515
|
+
function notEnabled(runner, enabled) {
|
|
18516
|
+
return { isError: true, text: `Runner "${runner}" is not enabled. Enabled runners: ${enabled.join(", ") || "(none)"}.` };
|
|
18517
|
+
}
|
|
18518
|
+
function coercions(submitted, landed) {
|
|
18519
|
+
return submitted.flatMap((task, i) => {
|
|
18520
|
+
const after = landed[i];
|
|
18521
|
+
const out = [];
|
|
18522
|
+
if (after.assignedRunner !== task.assignedRunner) {
|
|
18523
|
+
out.push({ taskId: task.id, field: "assignedRunner", from: task.assignedRunner, to: after.assignedRunner });
|
|
18524
|
+
}
|
|
18525
|
+
const [fromModel, toModel] = [task.assignedModel?.modelId ?? null, after.assignedModel?.modelId ?? null];
|
|
18526
|
+
if (fromModel !== toModel) out.push({ taskId: task.id, field: "assignedModel", from: fromModel, to: toModel });
|
|
18527
|
+
const [fromEffort, toEffort] = [task.assignedModel?.thinkingEffort ?? null, after.assignedModel?.thinkingEffort ?? null];
|
|
18528
|
+
if (fromEffort !== toEffort) out.push({ taskId: task.id, field: "thinkingEffort", from: fromEffort, to: toEffort });
|
|
18529
|
+
return out;
|
|
18530
|
+
});
|
|
18531
|
+
}
|
|
18532
|
+
|
|
17408
18533
|
// src/services/PlannerConversation.ts
|
|
17409
18534
|
import { v4 as uuidv45 } from "uuid";
|
|
17410
18535
|
|
|
@@ -17421,8 +18546,8 @@ function summaryRequest(planLines) {
|
|
|
17421
18546
|
...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
|
|
17422
18547
|
].join("\n");
|
|
17423
18548
|
}
|
|
17424
|
-
function extractSummary(
|
|
17425
|
-
const matches = [...
|
|
18549
|
+
function extractSummary(reply2) {
|
|
18550
|
+
const matches = [...reply2.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
|
|
17426
18551
|
const summary = matches.at(-1)?.[1].trim();
|
|
17427
18552
|
return summary || null;
|
|
17428
18553
|
}
|
|
@@ -17441,6 +18566,7 @@ function keptTail(transcript) {
|
|
|
17441
18566
|
|
|
17442
18567
|
// src/services/PlannerConversation.ts
|
|
17443
18568
|
var CATALOG_MODEL_CAP = 100;
|
|
18569
|
+
var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
|
|
17444
18570
|
var MAX_TASK_QUERIES = 3;
|
|
17445
18571
|
var MAX_TASK_QUERIES_HARD = 6;
|
|
17446
18572
|
function freshReadBudget() {
|
|
@@ -17470,7 +18596,7 @@ var PlannerConversation = class {
|
|
|
17470
18596
|
savedInBackground = 0;
|
|
17471
18597
|
turnsInFlight = 0;
|
|
17472
18598
|
compacting = false;
|
|
17473
|
-
|
|
18599
|
+
openTurn = null;
|
|
17474
18600
|
get transcript() {
|
|
17475
18601
|
return this.host.plan()?.conversationHistory ?? [];
|
|
17476
18602
|
}
|
|
@@ -17480,7 +18606,48 @@ var PlannerConversation = class {
|
|
|
17480
18606
|
}
|
|
17481
18607
|
/** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
|
|
17482
18608
|
get currentTurnId() {
|
|
17483
|
-
return this.
|
|
18609
|
+
return this.openTurn?.stream.turnId;
|
|
18610
|
+
}
|
|
18611
|
+
/**
|
|
18612
|
+
* Hand the open turn a plan (already validated) or a task edit. The turn
|
|
18613
|
+
* settles with it exactly as with the same plan or ops parsed out of its
|
|
18614
|
+
* reply, and it wins over any envelope that reply carries. A later
|
|
18615
|
+
* submission replaces an earlier one. False when no user turn is open.
|
|
18616
|
+
*/
|
|
18617
|
+
submit(submission) {
|
|
18618
|
+
if (!this.openTurn) return false;
|
|
18619
|
+
this.openTurn.submitted = submission;
|
|
18620
|
+
return true;
|
|
18621
|
+
}
|
|
18622
|
+
/**
|
|
18623
|
+
* The edit the open turn already holds, for a further edit in the same reply
|
|
18624
|
+
* to join rather than replace. Empty when it holds nothing; null when it holds
|
|
18625
|
+
* a whole plan, which an edit cannot be layered on.
|
|
18626
|
+
*/
|
|
18627
|
+
pendingOps() {
|
|
18628
|
+
const held = this.openTurn?.submitted;
|
|
18629
|
+
if (!held) return [];
|
|
18630
|
+
return held.kind === "task_ops" ? held.ops : null;
|
|
18631
|
+
}
|
|
18632
|
+
/** Whether the turn would park this edit until a batch boundary instead of applying it as it settles. */
|
|
18633
|
+
editWouldQueue(ops) {
|
|
18634
|
+
return this.editTouchesLiveWork({ kind: "task_ops", ops, text: "", researchLog: [] });
|
|
18635
|
+
}
|
|
18636
|
+
/**
|
|
18637
|
+
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
18638
|
+
* as an envelope read, so a planner cannot read more by switching channels:
|
|
18639
|
+
* past the soft limit, or on a repeated question, the answer still comes but
|
|
18640
|
+
* is told to land the turn; at the hard limit it is refused. `signature`
|
|
18641
|
+
* names the question, for the repeat check.
|
|
18642
|
+
*/
|
|
18643
|
+
async read(signature, answer2) {
|
|
18644
|
+
const reads = this.openTurn?.reads;
|
|
18645
|
+
if (!reads) return { status: "answered", value: await answer2(), landNow: false };
|
|
18646
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD) return { status: "refused" };
|
|
18647
|
+
const landNow = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
18648
|
+
reads.seen.add(signature);
|
|
18649
|
+
reads.answered++;
|
|
18650
|
+
return { status: "answered", value: await answer2(), landNow };
|
|
17484
18651
|
}
|
|
17485
18652
|
/** Whether the model still holds this conversation in memory. */
|
|
17486
18653
|
get isActive() {
|
|
@@ -17768,15 +18935,16 @@ ${message}` : message;
|
|
|
17768
18935
|
async userTurn(prompt, signal, run) {
|
|
17769
18936
|
const turnId = uuidv45();
|
|
17770
18937
|
const stream = new TurnStream(turnId, (p) => this.host.onProgress(p));
|
|
17771
|
-
|
|
18938
|
+
const turn = { stream, signal, reads: freshReadBudget() };
|
|
18939
|
+
this.openTurn = turn;
|
|
17772
18940
|
this.host.broadcast({ type: "planner_turn_started", turnId, prompt });
|
|
17773
18941
|
let outcome = "error";
|
|
17774
18942
|
try {
|
|
17775
|
-
const settled = await this.inTurn(() => run(
|
|
18943
|
+
const settled = await this.inTurn(() => run(turn));
|
|
17776
18944
|
outcome = settled.outcome;
|
|
17777
18945
|
return settled.plan;
|
|
17778
18946
|
} finally {
|
|
17779
|
-
if (this.
|
|
18947
|
+
if (this.openTurn === turn) this.openTurn = null;
|
|
17780
18948
|
this.host.broadcast({ type: "planner_turn_ended", turnId, outcome: signal?.aborted ? "stopped" : outcome });
|
|
17781
18949
|
}
|
|
17782
18950
|
}
|
|
@@ -17827,10 +18995,11 @@ ${message}` : message;
|
|
|
17827
18995
|
* ops JSON still gets its two corrective retries; charging it for the read
|
|
17828
18996
|
* would cost it the chance to fix the edit.
|
|
17829
18997
|
*/
|
|
17830
|
-
async drainTaskQueries(turn,
|
|
18998
|
+
async drainTaskQueries(turn, userTurn) {
|
|
18999
|
+
const { reads, signal, stream } = userTurn;
|
|
17831
19000
|
const ai = this.host.aiService();
|
|
17832
19001
|
const carried = [];
|
|
17833
|
-
let current = turn;
|
|
19002
|
+
let current = this.claimSubmission(turn, userTurn);
|
|
17834
19003
|
while (current.kind === "task_query") {
|
|
17835
19004
|
carried.push(...current.researchLog);
|
|
17836
19005
|
if (reads.answered >= MAX_TASK_QUERIES_HARD || !ai.hasActiveConversation() || signal?.aborted) {
|
|
@@ -17844,17 +19013,30 @@ ${message}` : message;
|
|
|
17844
19013
|
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
17845
19014
|
reads.seen.add(signature);
|
|
17846
19015
|
reads.answered++;
|
|
17847
|
-
const
|
|
17848
|
-
current = await ai.continueConversation(
|
|
17849
|
-
insist ? `${
|
|
19016
|
+
const answer2 = this.taskQueryAnswer(current.query);
|
|
19017
|
+
current = this.claimSubmission(await ai.continueConversation(
|
|
19018
|
+
insist ? `${answer2}
|
|
17850
19019
|
|
|
17851
|
-
${TASK_QUERY_ANSWER_OR_OPS}` :
|
|
19020
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer2,
|
|
17852
19021
|
stream.sink(),
|
|
17853
19022
|
signal
|
|
17854
|
-
);
|
|
19023
|
+
), userTurn);
|
|
17855
19024
|
}
|
|
17856
19025
|
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
17857
19026
|
}
|
|
19027
|
+
/**
|
|
19028
|
+
* The backend's answer, with what was handed in while it ran standing in
|
|
19029
|
+
* for whatever its reply carried. Taken once: a corrective re-send after a
|
|
19030
|
+
* rejected edit must not settle on the same submission again. A stopped
|
|
19031
|
+
* turn commits nothing, the same as a stopped reply with a plan in it.
|
|
19032
|
+
*/
|
|
19033
|
+
claimSubmission(turn, userTurn) {
|
|
19034
|
+
const submitted = userTurn.submitted;
|
|
19035
|
+
userTurn.submitted = void 0;
|
|
19036
|
+
if (!submitted || userTurn.signal?.aborted) return turn;
|
|
19037
|
+
const { text, researchLog } = turn;
|
|
19038
|
+
return submitted.kind === "plan" ? { kind: "plan", tasks: submitted.tasks, text, researchLog } : { kind: "task_ops", ops: submitted.ops, text, researchLog };
|
|
19039
|
+
}
|
|
17858
19040
|
/**
|
|
17859
19041
|
* Render one read out of live state. Never persisted to the transcript: the
|
|
17860
19042
|
* detail is context for the planner's next reply, and re-sending it on every
|
|
@@ -17898,7 +19080,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
17898
19080
|
return { retry: { errors: applied.errors, corrective: taskOpsRejectedPrompt(applied.errors) } };
|
|
17899
19081
|
},
|
|
17900
19082
|
maxRepairs: 2,
|
|
17901
|
-
onExhausted: ({ reply, errors }) => invalidOps(errors,
|
|
19083
|
+
onExhausted: ({ reply: reply2, errors }) => invalidOps(errors, reply2.researchLog)
|
|
17902
19084
|
});
|
|
17903
19085
|
if ("plan" in settled) {
|
|
17904
19086
|
await this.host.afterEdit();
|
|
@@ -17961,10 +19143,12 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
17961
19143
|
*
|
|
17962
19144
|
* The host's catalog is allowlist-filtered, so a restricted allowlist stays a
|
|
17963
19145
|
* hard bound on every turn, and reads the plan's runners live, so a runner
|
|
17964
|
-
* admitted mid-session by a retarget is shown like every other.
|
|
19146
|
+
* admitted mid-session by a retarget is shown like every other. A planner
|
|
19147
|
+
* with Ordewell's tools gets a reminder to read the catalog instead.
|
|
17965
19148
|
*/
|
|
17966
19149
|
catalogBlock() {
|
|
17967
19150
|
if (!this.host.plan()) return null;
|
|
19151
|
+
if (this.host.aiService().plannerToolsAttached?.()) return PLANNER_TOOLS_REMINDER;
|
|
17968
19152
|
const { runners, models, modes, autonomousDefault } = this.host.catalog();
|
|
17969
19153
|
const modelLines = runners.map((runner) => {
|
|
17970
19154
|
const list = models[runner] ?? [];
|
|
@@ -17999,6 +19183,7 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
17999
19183
|
const tasks = this.host.tasks();
|
|
18000
19184
|
const lines = this.currentPlanLines();
|
|
18001
19185
|
if (!lines) return null;
|
|
19186
|
+
const tools = this.host.aiService().plannerToolsAttached?.() ?? false;
|
|
18002
19187
|
const locked = tasks.filter((t) => t.status === "in_progress");
|
|
18003
19188
|
const execNote = this.host.hasLiveWork() ? `
|
|
18004
19189
|
Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
|
|
@@ -18008,8 +19193,8 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
18008
19193
|
"</current_plan>",
|
|
18009
19194
|
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
18010
19195
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
18011
|
-
TASK_QUERY_REMINDER,
|
|
18012
|
-
...taskOpsProtocol(execNote)
|
|
19196
|
+
tools ? TASK_READ_TOOLS_REMINDER : TASK_QUERY_REMINDER,
|
|
19197
|
+
...taskOpsProtocol(execNote, tools)
|
|
18013
19198
|
].filter(Boolean).join("\n");
|
|
18014
19199
|
}
|
|
18015
19200
|
/** One line per task with stable references — null until the plan has tasks. */
|
|
@@ -18285,6 +19470,7 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
18285
19470
|
}
|
|
18286
19471
|
|
|
18287
19472
|
// src/services/SkillsService.ts
|
|
19473
|
+
import { createHash } from "crypto";
|
|
18288
19474
|
import * as fs9 from "fs";
|
|
18289
19475
|
import * as path18 from "path";
|
|
18290
19476
|
|
|
@@ -18302,6 +19488,11 @@ function builtinSkillsDir() {
|
|
|
18302
19488
|
// src/services/SkillsService.ts
|
|
18303
19489
|
var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
|
|
18304
19490
|
var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
|
|
19491
|
+
var PRIOR_BUILTIN_HASHES = {
|
|
19492
|
+
grilling: ["d5d4cb8589fbb4a0f3296bae15033f2297ef682aaf5615a3dee258451f96e5c3"],
|
|
19493
|
+
"improve-codebase-architecture": ["b5154b5e3baca9a5f244634453e83706d742011c31163648c2b8395a2524dd9c"]
|
|
19494
|
+
};
|
|
19495
|
+
var SEED_MANIFEST = ".seeded.json";
|
|
18305
19496
|
function parseSkillFile(filePath, name, source) {
|
|
18306
19497
|
const raw = fs9.readFileSync(filePath, "utf8");
|
|
18307
19498
|
const frontmatter = {};
|
|
@@ -18341,6 +19532,13 @@ function copyDirSync2(src, dest) {
|
|
|
18341
19532
|
else fs9.copyFileSync(srcPath, destPath);
|
|
18342
19533
|
}
|
|
18343
19534
|
}
|
|
19535
|
+
function fileHash(file) {
|
|
19536
|
+
try {
|
|
19537
|
+
return createHash("sha256").update(fs9.readFileSync(file)).digest("hex");
|
|
19538
|
+
} catch {
|
|
19539
|
+
return void 0;
|
|
19540
|
+
}
|
|
19541
|
+
}
|
|
18344
19542
|
function readDir(dir, source) {
|
|
18345
19543
|
if (!fs9.existsSync(dir)) return [];
|
|
18346
19544
|
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
@@ -18365,21 +19563,59 @@ var SkillsService = class {
|
|
|
18365
19563
|
globalDir() {
|
|
18366
19564
|
return path18.join(globalDataDir(), "skills");
|
|
18367
19565
|
}
|
|
19566
|
+
readSeedManifest() {
|
|
19567
|
+
try {
|
|
19568
|
+
const parsed = JSON.parse(fs9.readFileSync(path18.join(this.globalDir(), SEED_MANIFEST), "utf8"));
|
|
19569
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
19570
|
+
return Object.fromEntries(
|
|
19571
|
+
Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
|
|
19572
|
+
);
|
|
19573
|
+
}
|
|
19574
|
+
} catch {
|
|
19575
|
+
}
|
|
19576
|
+
return {};
|
|
19577
|
+
}
|
|
19578
|
+
recordSeed(name, hash) {
|
|
19579
|
+
const manifest = this.readSeedManifest();
|
|
19580
|
+
if (manifest[name] === hash) return;
|
|
19581
|
+
manifest[name] = hash;
|
|
19582
|
+
try {
|
|
19583
|
+
fs9.mkdirSync(this.globalDir(), { recursive: true });
|
|
19584
|
+
fs9.writeFileSync(path18.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
19585
|
+
} catch (err) {
|
|
19586
|
+
console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
|
|
19587
|
+
}
|
|
19588
|
+
}
|
|
19589
|
+
/** A seed the user never edited: safe to replace with a newer built-in. */
|
|
19590
|
+
isUntouchedSeed(name, currentHash) {
|
|
19591
|
+
if (currentHash === void 0) return false;
|
|
19592
|
+
return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
|
|
19593
|
+
}
|
|
18368
19594
|
seed(name) {
|
|
18369
19595
|
const dest = path18.join(this.globalDir(), name);
|
|
18370
|
-
|
|
19596
|
+
const exists = fs9.existsSync(dest);
|
|
18371
19597
|
let srcDir;
|
|
18372
19598
|
try {
|
|
18373
19599
|
srcDir = path18.join(builtinSkillsDir(), name);
|
|
18374
19600
|
} catch (err) {
|
|
18375
|
-
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
19601
|
+
if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
18376
19602
|
return false;
|
|
18377
19603
|
}
|
|
18378
19604
|
if (!fs9.existsSync(srcDir)) {
|
|
18379
|
-
console.warn(`Built-in skill not found in package: ${name}`);
|
|
19605
|
+
if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
|
|
18380
19606
|
return false;
|
|
18381
19607
|
}
|
|
19608
|
+
const srcHash = fileHash(path18.join(srcDir, "SKILL.md"));
|
|
19609
|
+
if (exists) {
|
|
19610
|
+
const currentHash = fileHash(path18.join(dest, "SKILL.md"));
|
|
19611
|
+
if (currentHash !== void 0 && currentHash === srcHash) {
|
|
19612
|
+
this.recordSeed(name, currentHash);
|
|
19613
|
+
return false;
|
|
19614
|
+
}
|
|
19615
|
+
if (!this.isUntouchedSeed(name, currentHash)) return false;
|
|
19616
|
+
}
|
|
18382
19617
|
copyDirSync2(srcDir, dest);
|
|
19618
|
+
if (srcHash) this.recordSeed(name, srcHash);
|
|
18383
19619
|
return true;
|
|
18384
19620
|
}
|
|
18385
19621
|
seedBuiltinSkill(name) {
|
|
@@ -18673,9 +19909,9 @@ import * as fs10 from "fs";
|
|
|
18673
19909
|
import * as path19 from "path";
|
|
18674
19910
|
|
|
18675
19911
|
// src/utils/sessionId.ts
|
|
18676
|
-
import { randomBytes as
|
|
19912
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
18677
19913
|
function mintSessionId() {
|
|
18678
|
-
return `session-${
|
|
19914
|
+
return `session-${randomBytes6(16).toString("hex")}`;
|
|
18679
19915
|
}
|
|
18680
19916
|
|
|
18681
19917
|
// src/utils/sessionStore.ts
|
|
@@ -19012,7 +20248,12 @@ function lastAttemptDigest(location, taskId) {
|
|
|
19012
20248
|
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
19013
20249
|
}
|
|
19014
20250
|
function sessionRuntimeSettings(settings) {
|
|
19015
|
-
return {
|
|
20251
|
+
return {
|
|
20252
|
+
...plannerRuntimeToggles(settings),
|
|
20253
|
+
modelAllowlist: settings.modelAllowlist,
|
|
20254
|
+
runnerTransport: settings.runnerTransport,
|
|
20255
|
+
enabledRunners: settings.enabledRunners
|
|
20256
|
+
};
|
|
19016
20257
|
}
|
|
19017
20258
|
var SKILL_TOKEN = /(^|\s)\/([a-z][a-z0-9_-]*)([,.!?;:]*)(?=\s|$)/gi;
|
|
19018
20259
|
function resolveSkillInvocation(text, skillsService) {
|
|
@@ -19036,14 +20277,14 @@ function resolveSkillInvocation(text, skillsService) {
|
|
|
19036
20277
|
return `${lead}${skill.content}${punct}`;
|
|
19037
20278
|
});
|
|
19038
20279
|
}
|
|
19039
|
-
function liveAiService(config, workspaceRoot) {
|
|
20280
|
+
function liveAiService(config, workspaceRoot, mcpServer) {
|
|
19040
20281
|
let live = null;
|
|
19041
20282
|
let liveProvider = null;
|
|
19042
20283
|
return () => {
|
|
19043
20284
|
const provider = config.aiProvider;
|
|
19044
20285
|
if (live && liveProvider === provider) return live;
|
|
19045
20286
|
live?.reset();
|
|
19046
|
-
live = createAiService(config, { workspaceRoot });
|
|
20287
|
+
live = createAiService(config, { workspaceRoot, mcpServer });
|
|
19047
20288
|
liveProvider = provider;
|
|
19048
20289
|
return live;
|
|
19049
20290
|
};
|
|
@@ -19053,7 +20294,7 @@ function isPlannerApproval(request) {
|
|
|
19053
20294
|
}
|
|
19054
20295
|
function createSession(deps) {
|
|
19055
20296
|
const pinnedAiService = deps.aiService;
|
|
19056
|
-
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot);
|
|
20297
|
+
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, deps.mcpServer ?? sharedMcpServer());
|
|
19057
20298
|
const store = new PlanStore();
|
|
19058
20299
|
const taskLogs = new TaskLogRecorder({
|
|
19059
20300
|
broadcast: deps.broadcast,
|
|
@@ -19183,6 +20424,16 @@ var Session = class {
|
|
|
19183
20424
|
currentSessionId;
|
|
19184
20425
|
skillsService;
|
|
19185
20426
|
conversation;
|
|
20427
|
+
plannerTools = plannerToolHandler({
|
|
20428
|
+
liveCatalog: () => this.liveCatalog(),
|
|
20429
|
+
coerce: (tasks, runners) => coerceAssignments(tasks, this.allowlist(), runners, this.models()),
|
|
20430
|
+
submitPlan: (tasks, runners) => this.submitPlanFromTool(tasks, runners),
|
|
20431
|
+
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
20432
|
+
tasks: () => this.store.planTasks,
|
|
20433
|
+
read: (signature, answer2) => this.conversation.read(signature, answer2),
|
|
20434
|
+
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
20435
|
+
lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId)
|
|
20436
|
+
});
|
|
19186
20437
|
constructor(parts) {
|
|
19187
20438
|
this.config = parts.config;
|
|
19188
20439
|
this.registry = parts.registry;
|
|
@@ -19211,17 +20462,7 @@ var Session = class {
|
|
|
19211
20462
|
aiService: () => this.aiService(),
|
|
19212
20463
|
onProgress: (p) => this.events.progress(p),
|
|
19213
20464
|
opening: (runners) => this.conversationOpening(runners),
|
|
19214
|
-
catalog: () =>
|
|
19215
|
-
const runners = this.plan?.runners ?? [];
|
|
19216
|
-
return {
|
|
19217
|
-
runners,
|
|
19218
|
-
// Allowlist-filtered: neither the per-turn block nor a read may offer
|
|
19219
|
-
// a model the planner is forbidden to assign.
|
|
19220
|
-
models: filterModelsForPrompt(this.models(), this.allowlist()),
|
|
19221
|
-
modes: this.runnerModesFor(runners),
|
|
19222
|
-
autonomousDefault: this.config.autonomousMode
|
|
19223
|
-
};
|
|
19224
|
-
},
|
|
20465
|
+
catalog: () => this.catalogOf(this.plan?.runners ?? []),
|
|
19225
20466
|
tasks: () => this.store.planTasks,
|
|
19226
20467
|
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
19227
20468
|
hasLiveWork: () => this.hasLiveWork,
|
|
@@ -19245,8 +20486,8 @@ var Session = class {
|
|
|
19245
20486
|
* server's HTTP route, the VS Code webview, the TUI prompt — so the decision
|
|
19246
20487
|
* path is identical regardless of who is looking.
|
|
19247
20488
|
*/
|
|
19248
|
-
resolveApproval(id,
|
|
19249
|
-
return this.approvals.resolve(id,
|
|
20489
|
+
resolveApproval(id, answer2) {
|
|
20490
|
+
return this.approvals.resolve(id, answer2);
|
|
19250
20491
|
}
|
|
19251
20492
|
/** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
|
|
19252
20493
|
outstandingApprovals() {
|
|
@@ -19366,6 +20607,27 @@ var Session = class {
|
|
|
19366
20607
|
this.modelsCache = {};
|
|
19367
20608
|
this.remintSessionId();
|
|
19368
20609
|
}
|
|
20610
|
+
/**
|
|
20611
|
+
* What a planner may assign right now: every enabled runner, its modes, and
|
|
20612
|
+
* its allowlisted models — read as of this call, so a runner enabled or an
|
|
20613
|
+
* allowlist edited since planning started is in it (#69). Discovers a
|
|
20614
|
+
* runner's models the first time it is asked for.
|
|
20615
|
+
*/
|
|
20616
|
+
async liveCatalog() {
|
|
20617
|
+
const runners = this.enabledRunners();
|
|
20618
|
+
this.modelsCache = { ...this.modelsCache, ...await this.modelResolver.modelsForRunners(runners) };
|
|
20619
|
+
return this.catalogOf(runners);
|
|
20620
|
+
}
|
|
20621
|
+
catalogOf(runners) {
|
|
20622
|
+
return {
|
|
20623
|
+
runners,
|
|
20624
|
+
// Allowlist-filtered: neither the per-turn block nor a read may offer
|
|
20625
|
+
// a model the planner is forbidden to assign.
|
|
20626
|
+
models: filterModelsForPrompt(this.models(), this.allowlist()),
|
|
20627
|
+
modes: this.runnerModesFor(runners),
|
|
20628
|
+
autonomousDefault: this.config.autonomousMode
|
|
20629
|
+
};
|
|
20630
|
+
}
|
|
19369
20631
|
runnerModesFor(runners) {
|
|
19370
20632
|
return runnerModesFrom(this.registry, runners);
|
|
19371
20633
|
}
|
|
@@ -19377,13 +20639,17 @@ var Session = class {
|
|
|
19377
20639
|
* allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
19378
20640
|
* allowlist itself, the same way `coerceAssignments` does.
|
|
19379
20641
|
*/
|
|
19380
|
-
editCatalog() {
|
|
20642
|
+
editCatalog(runners = this.plan?.runners ?? []) {
|
|
19381
20643
|
return {
|
|
19382
20644
|
modelsByRunner: this.models(),
|
|
19383
|
-
runnerModes: this.runnerModesFor(
|
|
20645
|
+
runnerModes: this.runnerModesFor(runners),
|
|
19384
20646
|
perRunnerAllowlist: this.allowlist()
|
|
19385
20647
|
};
|
|
19386
20648
|
}
|
|
20649
|
+
/** The runners enabled right now — a toggle made since the session was built counts (#69). */
|
|
20650
|
+
enabledRunners() {
|
|
20651
|
+
return this.settingsFn().enabledRunners ?? this.config.enabledRunners;
|
|
20652
|
+
}
|
|
19387
20653
|
/**
|
|
19388
20654
|
* The allowlist in force right now. Unset means no restriction — falling
|
|
19389
20655
|
* back to the one planning started under would keep a restriction the user
|
|
@@ -19485,7 +20751,7 @@ var Session = class {
|
|
|
19485
20751
|
this.goal = goal;
|
|
19486
20752
|
this.remintSessionId();
|
|
19487
20753
|
this.beginFreshPlan();
|
|
19488
|
-
const enabled = this.
|
|
20754
|
+
const enabled = this.enabledRunners();
|
|
19489
20755
|
const chosenRunners = runners.filter((r) => enabled.includes(r));
|
|
19490
20756
|
if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
|
|
19491
20757
|
const { modelsByRunner, runnerModes, settings } = await this.plannerCatalog(chosenRunners);
|
|
@@ -19529,7 +20795,7 @@ var Session = class {
|
|
|
19529
20795
|
this.goal = this.resolveSkillInvocation(goal);
|
|
19530
20796
|
this.remintSessionId();
|
|
19531
20797
|
this.beginFreshPlan();
|
|
19532
|
-
const enabled = this.
|
|
20798
|
+
const enabled = this.enabledRunners();
|
|
19533
20799
|
const chosenRunners = runners.filter((r) => enabled.includes(r));
|
|
19534
20800
|
if (chosenRunners.length === 0) throw new Error("None of the requested runners are enabled");
|
|
19535
20801
|
const opening = await this.conversationOpening(chosenRunners);
|
|
@@ -19607,6 +20873,49 @@ var Session = class {
|
|
|
19607
20873
|
rewindTargets() {
|
|
19608
20874
|
return this.conversation.rewindTargets();
|
|
19609
20875
|
}
|
|
20876
|
+
/**
|
|
20877
|
+
* Hand the planner turn in flight a validated plan or a task edit; the turn
|
|
20878
|
+
* commits it as it would the same JSON in the planner's reply. False when
|
|
20879
|
+
* no turn is open.
|
|
20880
|
+
*/
|
|
20881
|
+
submitToTurn(submission) {
|
|
20882
|
+
return this.conversation.submit(submission);
|
|
20883
|
+
}
|
|
20884
|
+
/**
|
|
20885
|
+
* A plan the planner submitted through its tool, already checked against the
|
|
20886
|
+
* live catalog. Its runners join `plan.runners` here, so a runner enabled
|
|
20887
|
+
* since planning started is not snapped back by the commit (#69).
|
|
20888
|
+
*/
|
|
20889
|
+
submitPlanFromTool(tasks, runners) {
|
|
20890
|
+
if (!this.plan || !this.conversation.submit({ kind: "plan", tasks })) return false;
|
|
20891
|
+
for (const runner of runners) this.admitRunner(runner, this.modelsCache[runner] ?? []);
|
|
20892
|
+
return true;
|
|
20893
|
+
}
|
|
20894
|
+
/**
|
|
20895
|
+
* A task edit the planner made through its tool. Checked against the live
|
|
20896
|
+
* catalog, then handed to the open turn, which commits it as it would the
|
|
20897
|
+
* same ops in a taskOps reply — including parking it behind a running batch,
|
|
20898
|
+
* which the envelope does before any check, so a queued edit is not checked
|
|
20899
|
+
* here either. Edits made in one reply join one batch.
|
|
20900
|
+
*/
|
|
20901
|
+
async editPlanFromTool(ops) {
|
|
20902
|
+
const refuse = (message) => ({ ok: false, errors: [message] });
|
|
20903
|
+
const catalog = await this.liveCatalog();
|
|
20904
|
+
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
20905
|
+
const held = this.conversation.pendingOps();
|
|
20906
|
+
if (!held) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
|
|
20907
|
+
const batch = [...held, ...ops];
|
|
20908
|
+
const queued = this.conversation.editWouldQueue(batch);
|
|
20909
|
+
let summary = [];
|
|
20910
|
+
if (!queued) {
|
|
20911
|
+
const result = applyTaskOps(this.store.planTasks, batch, catalog.runners, this.editCatalog(catalog.runners));
|
|
20912
|
+
if (!result.ok) return { ok: false, errors: result.errors };
|
|
20913
|
+
summary = result.summary;
|
|
20914
|
+
for (const runner of runnersOf(result.tasks)) this.admitRunner(runner, this.modelsCache[runner] ?? []);
|
|
20915
|
+
}
|
|
20916
|
+
if (!this.conversation.submit({ kind: "task_ops", ops: batch })) return refuse("No planning turn is open to take the edit.");
|
|
20917
|
+
return { ok: true, summary, queued };
|
|
20918
|
+
}
|
|
19610
20919
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
19611
20920
|
get isConversationActive() {
|
|
19612
20921
|
return this.conversation.isActive;
|
|
@@ -19652,7 +20961,8 @@ var Session = class {
|
|
|
19652
20961
|
// usage line can show context fill (#49). Unknown stays absent.
|
|
19653
20962
|
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
19654
20963
|
fs: this.fsAdapter,
|
|
19655
|
-
fetcher: this.fetcher
|
|
20964
|
+
fetcher: this.fetcher,
|
|
20965
|
+
plannerTools: { sessionId: this.sessionId, handler: this.plannerTools }
|
|
19656
20966
|
};
|
|
19657
20967
|
}
|
|
19658
20968
|
/**
|
|
@@ -19770,7 +21080,7 @@ var Session = class {
|
|
|
19770
21080
|
])
|
|
19771
21081
|
);
|
|
19772
21082
|
try {
|
|
19773
|
-
const modelsByRunner = await this.modelResolver.modelsForRunners(this.
|
|
21083
|
+
const modelsByRunner = await this.modelResolver.modelsForRunners(this.enabledRunners());
|
|
19774
21084
|
const runnerModes = this.runnerModesFor(this.plan?.runners ?? ["claude-code"]);
|
|
19775
21085
|
const { modelAllowlist } = this.settingsFn();
|
|
19776
21086
|
const result = await this.planner.modifyDuringExecution({
|
|
@@ -20117,7 +21427,7 @@ var Session = class {
|
|
|
20117
21427
|
if (!this.plan) throw new Error("No active plan state");
|
|
20118
21428
|
const check = canMergeTasks(this.store.planTasks, taskIds);
|
|
20119
21429
|
if (!check.ok) throw new Error(check.error ?? "These tasks cannot be merged");
|
|
20120
|
-
const prompt = buildMergePrompt(taskIds, this.store.planTasks);
|
|
21430
|
+
const prompt = buildMergePrompt(taskIds, this.store.planTasks, this.aiService().plannerToolsAttached?.());
|
|
20121
21431
|
return this.continueConversation(prompt, options);
|
|
20122
21432
|
}
|
|
20123
21433
|
/**
|
|
@@ -20129,7 +21439,7 @@ var Session = class {
|
|
|
20129
21439
|
if (!this.plan) throw new Error("No active plan state");
|
|
20130
21440
|
const check = canSplitTask(this.store.planTasks, taskId);
|
|
20131
21441
|
if (!check.ok) throw new Error(check.error ?? "This task cannot be split");
|
|
20132
|
-
const prompt = buildSplitPrompt(taskId, this.store.planTasks);
|
|
21442
|
+
const prompt = buildSplitPrompt(taskId, this.store.planTasks, this.aiService().plannerToolsAttached?.());
|
|
20133
21443
|
return this.continueConversation(prompt, options);
|
|
20134
21444
|
}
|
|
20135
21445
|
loadPlan(plan, goal, workspace, opts) {
|
|
@@ -20169,41 +21479,6 @@ var Session = class {
|
|
|
20169
21479
|
}
|
|
20170
21480
|
};
|
|
20171
21481
|
|
|
20172
|
-
// src/utils/privateFile.ts
|
|
20173
|
-
import { closeSync, mkdirSync as mkdirSync10, openSync, renameSync, unlinkSync as unlinkSync2, writeSync } from "fs";
|
|
20174
|
-
import { basename as basename2, dirname as dirname7, join as join18 } from "path";
|
|
20175
|
-
import { randomBytes as randomBytes4 } from "crypto";
|
|
20176
|
-
var DIR_MODE = 448;
|
|
20177
|
-
var FILE_MODE = 384;
|
|
20178
|
-
var POSIX_MODES = process.platform !== "win32";
|
|
20179
|
-
function ensurePrivateDir(dir) {
|
|
20180
|
-
if (POSIX_MODES) {
|
|
20181
|
-
mkdirSync10(dir, { recursive: true, mode: DIR_MODE });
|
|
20182
|
-
return;
|
|
20183
|
-
}
|
|
20184
|
-
mkdirSync10(dir, { recursive: true });
|
|
20185
|
-
}
|
|
20186
|
-
function writePrivateFile(filePath, content) {
|
|
20187
|
-
const dir = dirname7(filePath);
|
|
20188
|
-
ensurePrivateDir(dir);
|
|
20189
|
-
const tempPath = join18(dir, `.${basename2(filePath)}.${randomBytes4(8).toString("hex")}.tmp`);
|
|
20190
|
-
const fd = openSync(tempPath, "wx", POSIX_MODES ? FILE_MODE : void 0);
|
|
20191
|
-
try {
|
|
20192
|
-
writeSync(fd, content);
|
|
20193
|
-
} finally {
|
|
20194
|
-
closeSync(fd);
|
|
20195
|
-
}
|
|
20196
|
-
try {
|
|
20197
|
-
renameSync(tempPath, filePath);
|
|
20198
|
-
} catch (err) {
|
|
20199
|
-
try {
|
|
20200
|
-
unlinkSync2(tempPath);
|
|
20201
|
-
} catch {
|
|
20202
|
-
}
|
|
20203
|
-
throw err;
|
|
20204
|
-
}
|
|
20205
|
-
}
|
|
20206
|
-
|
|
20207
21482
|
// src/services/buildRunnerArgs.ts
|
|
20208
21483
|
function buildRunnerInvocation(opts) {
|
|
20209
21484
|
const entry = opts.registry.get(opts.runner);
|
|
@@ -20460,9 +21735,9 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
20460
21735
|
// src/services/TmuxRunner.ts
|
|
20461
21736
|
import { execFile as execFile5 } from "child_process";
|
|
20462
21737
|
import { promisify as promisify5 } from "util";
|
|
20463
|
-
import { tmpdir } from "os";
|
|
20464
|
-
import { join as
|
|
20465
|
-
import { existsSync as existsSync11, readFileSync as
|
|
21738
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
21739
|
+
import { join as join21 } from "path";
|
|
21740
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, unlinkSync as unlinkSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
20466
21741
|
|
|
20467
21742
|
// src/utils/tmux.ts
|
|
20468
21743
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -20560,7 +21835,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
20560
21835
|
* lets the user keep poking at a finished task's terminal.
|
|
20561
21836
|
*/
|
|
20562
21837
|
async start(command, args, cwd, env) {
|
|
20563
|
-
|
|
21838
|
+
writeFileSync9(this.logPath, "");
|
|
20564
21839
|
const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
|
|
20565
21840
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
20566
21841
|
const prefix = envAssignments ? `env ${envAssignments} ` : "";
|
|
@@ -20583,7 +21858,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
20583
21858
|
readLog() {
|
|
20584
21859
|
let content;
|
|
20585
21860
|
try {
|
|
20586
|
-
content = existsSync11(this.logPath) ?
|
|
21861
|
+
content = existsSync11(this.logPath) ? readFileSync10(this.logPath, "utf8") : "";
|
|
20587
21862
|
} catch {
|
|
20588
21863
|
return false;
|
|
20589
21864
|
}
|
|
@@ -20639,7 +21914,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
20639
21914
|
this.tmux(["pipe-pane", "-t", this.target]).catch(() => {
|
|
20640
21915
|
});
|
|
20641
21916
|
try {
|
|
20642
|
-
|
|
21917
|
+
unlinkSync4(this.logPath);
|
|
20643
21918
|
} catch {
|
|
20644
21919
|
}
|
|
20645
21920
|
this.baseHandleExit(code);
|
|
@@ -20676,7 +21951,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
20676
21951
|
this.execFileImpl = deps.execFileImpl ?? defaultExecFile3;
|
|
20677
21952
|
this.resolvePath = deps.resolvePath ?? augmentedPath;
|
|
20678
21953
|
this.pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
20679
|
-
this.logDir = deps.logDir ??
|
|
21954
|
+
this.logDir = deps.logDir ?? tmpdir2();
|
|
20680
21955
|
this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
|
|
20681
21956
|
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
20682
21957
|
}
|
|
@@ -20796,7 +22071,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
20796
22071
|
windowName,
|
|
20797
22072
|
this.execFileImpl,
|
|
20798
22073
|
this.pollIntervalMs,
|
|
20799
|
-
|
|
22074
|
+
join21(this.logDir, `${id}.log`),
|
|
20800
22075
|
this.socket
|
|
20801
22076
|
);
|
|
20802
22077
|
invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
|
|
@@ -20824,9 +22099,9 @@ var KEY_ARGS = {
|
|
|
20824
22099
|
};
|
|
20825
22100
|
var FALLBACK_KEY_ARGS = ["file_path", "path", "notebook_path", "description", "pattern", "command", "url", "query", "prompt"];
|
|
20826
22101
|
function toolLine(name, args) {
|
|
20827
|
-
const { tool } = mapAgentTool(name);
|
|
20828
|
-
const normalized = normalizeAgentArgs(
|
|
20829
|
-
const preferred = KEY_ARGS[
|
|
22102
|
+
const { tool: tool2 } = mapAgentTool(name);
|
|
22103
|
+
const normalized = normalizeAgentArgs(tool2, args);
|
|
22104
|
+
const preferred = KEY_ARGS[tool2];
|
|
20830
22105
|
for (const key of preferred ? [preferred, ...FALLBACK_KEY_ARGS] : FALLBACK_KEY_ARGS) {
|
|
20831
22106
|
const value = normalized[key];
|
|
20832
22107
|
if (typeof value !== "string") continue;
|
|
@@ -20889,6 +22164,7 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
20889
22164
|
constructor(id, taskId, launch) {
|
|
20890
22165
|
super(id, taskId);
|
|
20891
22166
|
this.launch = launch;
|
|
22167
|
+
this.startOptions = launch.startOptions;
|
|
20892
22168
|
this.text = new PlainTextChannel((text) => {
|
|
20893
22169
|
this.output += text;
|
|
20894
22170
|
this.outputEmitter.emit("output", text);
|
|
@@ -20917,9 +22193,20 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
20917
22193
|
* requests — while an approval must be answerable by id across the session.
|
|
20918
22194
|
*/
|
|
20919
22195
|
permissions = /* @__PURE__ */ new Map();
|
|
22196
|
+
/** The launch's start options, with this attempt's server once its token is issued. */
|
|
22197
|
+
startOptions;
|
|
22198
|
+
/** This attempt's MCP token, until the session ends (ADR-0022, A2). */
|
|
22199
|
+
toolToken = null;
|
|
22200
|
+
checkpointHandler = null;
|
|
20920
22201
|
/** Start the runner and send the task's prompt as its first turn. */
|
|
20921
22202
|
async start(prompt) {
|
|
20922
|
-
await this.
|
|
22203
|
+
await this.issueTools();
|
|
22204
|
+
try {
|
|
22205
|
+
await this.startAdapter(this.startOptions);
|
|
22206
|
+
} catch (err) {
|
|
22207
|
+
this.revokeTools();
|
|
22208
|
+
throw err;
|
|
22209
|
+
}
|
|
20923
22210
|
this.state = "working";
|
|
20924
22211
|
setImmediate(() => {
|
|
20925
22212
|
if (!this.exited) this.deliver(prompt);
|
|
@@ -20928,6 +22215,49 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
20928
22215
|
getOutput() {
|
|
20929
22216
|
return this.output;
|
|
20930
22217
|
}
|
|
22218
|
+
onTaskComplete(listener) {
|
|
22219
|
+
this.structuredEmitter.on("taskComplete", listener);
|
|
22220
|
+
}
|
|
22221
|
+
onToolCheckpoint(handler) {
|
|
22222
|
+
this.checkpointHandler = handler;
|
|
22223
|
+
}
|
|
22224
|
+
/**
|
|
22225
|
+
* A server that cannot start costs the task its tools, not its run: the
|
|
22226
|
+
* prompt still teaches the marker (ADR-0022, S2).
|
|
22227
|
+
*/
|
|
22228
|
+
async issueTools() {
|
|
22229
|
+
const tools = this.launch.tools;
|
|
22230
|
+
if (!tools) return;
|
|
22231
|
+
try {
|
|
22232
|
+
const credential = await tools.server.issueTaskToken(tools.scope, {
|
|
22233
|
+
taskComplete: async (report) => {
|
|
22234
|
+
this.structuredEmitter.emit("taskComplete", report);
|
|
22235
|
+
return { text: "Recorded. End your turn now." };
|
|
22236
|
+
},
|
|
22237
|
+
checkpoint: async ({ question }, { signal }) => {
|
|
22238
|
+
if (!this.checkpointHandler) return { text: "checkpoint is not available in this session.", isError: true };
|
|
22239
|
+
return checkpointReply(await this.checkpointHandler(question, signal));
|
|
22240
|
+
}
|
|
22241
|
+
});
|
|
22242
|
+
if (this.exited) {
|
|
22243
|
+
tools.server.revoke(credential.token);
|
|
22244
|
+
return;
|
|
22245
|
+
}
|
|
22246
|
+
this.toolToken = credential.token;
|
|
22247
|
+
this.startOptions = { ...this.startOptions, mcp: mcpClientConfig(credential) };
|
|
22248
|
+
} catch (err) {
|
|
22249
|
+
console.error(`[structured] No Ordewell tools for task ${this.taskId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
22250
|
+
}
|
|
22251
|
+
}
|
|
22252
|
+
revokeTools() {
|
|
22253
|
+
if (this.toolToken === null) return;
|
|
22254
|
+
this.launch.tools?.server.revoke(this.toolToken);
|
|
22255
|
+
this.toolToken = null;
|
|
22256
|
+
}
|
|
22257
|
+
baseHandleExit(code) {
|
|
22258
|
+
this.revokeTools();
|
|
22259
|
+
super.baseHandleExit(code);
|
|
22260
|
+
}
|
|
20931
22261
|
/** A reply typed at the task — a checkpoint answer, most often — is a user message. */
|
|
20932
22262
|
write(text) {
|
|
20933
22263
|
const message = text.trim();
|
|
@@ -21025,12 +22355,12 @@ var StructuredSession = class extends AbstractTerminalSession {
|
|
|
21025
22355
|
* session id after an abort.
|
|
21026
22356
|
*/
|
|
21027
22357
|
async restartInterrupted(turn) {
|
|
21028
|
-
const resumeSessionId = this.nativeSessionId() ?? this.
|
|
22358
|
+
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
21029
22359
|
this.generation += 1;
|
|
21030
22360
|
turn.abort.abort();
|
|
21031
22361
|
if (this.adapter) this.withdrawPermissions(this.adapter);
|
|
21032
22362
|
try {
|
|
21033
|
-
await this.startAdapter({ ...this.
|
|
22363
|
+
await this.startAdapter({ ...this.startOptions, resumeSessionId });
|
|
21034
22364
|
} catch (err) {
|
|
21035
22365
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
21036
22366
|
this.endTurn(turn, "interrupted");
|
|
@@ -21168,11 +22498,13 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
21168
22498
|
processDeps;
|
|
21169
22499
|
createAdapter;
|
|
21170
22500
|
interruptGraceMs;
|
|
22501
|
+
mcp;
|
|
21171
22502
|
constructor(deps = {}) {
|
|
21172
22503
|
super();
|
|
21173
22504
|
this.processDeps = deps.process ?? {};
|
|
21174
22505
|
this.createAdapter = deps.createAdapter ?? createTaskAdapter;
|
|
21175
22506
|
this.interruptGraceMs = deps.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
|
|
22507
|
+
this.mcp = deps.mcp ?? sharedMcpServer();
|
|
21176
22508
|
}
|
|
21177
22509
|
async spawn(opts) {
|
|
21178
22510
|
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
@@ -21199,7 +22531,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
21199
22531
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
21200
22532
|
resumeSessionId: opts.resumeSessionId
|
|
21201
22533
|
},
|
|
21202
|
-
interruptGraceMs: this.interruptGraceMs
|
|
22534
|
+
interruptGraceMs: this.interruptGraceMs,
|
|
22535
|
+
...takesOrdewellTools(opts.runner) ? { tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } } } : {}
|
|
21203
22536
|
});
|
|
21204
22537
|
console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
|
|
21205
22538
|
this.registerSession(id, session);
|
|
@@ -21213,73 +22546,12 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
21213
22546
|
}
|
|
21214
22547
|
};
|
|
21215
22548
|
|
|
21216
|
-
// src/utils/daemonToken.ts
|
|
21217
|
-
import { randomBytes as randomBytes5, timingSafeEqual } from "crypto";
|
|
21218
|
-
import { readFileSync as readFileSync10, unlinkSync as unlinkSync4 } from "fs";
|
|
21219
|
-
import { join as join20 } from "path";
|
|
21220
|
-
var BEARER_PREFIX = "bearer ";
|
|
21221
|
-
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
21222
|
-
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
21223
|
-
function configDir() {
|
|
21224
|
-
return globalDataDir();
|
|
21225
|
-
}
|
|
21226
|
-
function daemonTokenPath(port) {
|
|
21227
|
-
return join20(configDir(), `server-${port}.token`);
|
|
21228
|
-
}
|
|
21229
|
-
function mintDaemonToken(port) {
|
|
21230
|
-
const token = randomBytes5(32).toString("base64url");
|
|
21231
|
-
const file = daemonTokenPath(port);
|
|
21232
|
-
writePrivateFile(file, token);
|
|
21233
|
-
return { token, file };
|
|
21234
|
-
}
|
|
21235
|
-
function readDaemonToken(port) {
|
|
21236
|
-
try {
|
|
21237
|
-
const token = readFileSync10(daemonTokenPath(port), "utf8").trim();
|
|
21238
|
-
return token === "" ? void 0 : token;
|
|
21239
|
-
} catch {
|
|
21240
|
-
return void 0;
|
|
21241
|
-
}
|
|
21242
|
-
}
|
|
21243
|
-
function clearDaemonToken(port) {
|
|
21244
|
-
try {
|
|
21245
|
-
unlinkSync4(daemonTokenPath(port));
|
|
21246
|
-
} catch {
|
|
21247
|
-
}
|
|
21248
|
-
}
|
|
21249
|
-
function bearerHeaderValue(token) {
|
|
21250
|
-
return `Bearer ${token}`;
|
|
21251
|
-
}
|
|
21252
|
-
function tokenSubprotocols(token) {
|
|
21253
|
-
return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
|
|
21254
|
-
}
|
|
21255
|
-
function extractPresentedToken(carriers) {
|
|
21256
|
-
const authorization = carriers.authorization;
|
|
21257
|
-
if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
|
|
21258
|
-
const value = authorization.slice(BEARER_PREFIX.length).trim();
|
|
21259
|
-
if (value !== "") return value;
|
|
21260
|
-
}
|
|
21261
|
-
for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
|
|
21262
|
-
const value = offered.trim();
|
|
21263
|
-
if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
|
|
21264
|
-
const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
|
|
21265
|
-
if (token !== "") return token;
|
|
21266
|
-
}
|
|
21267
|
-
return void 0;
|
|
21268
|
-
}
|
|
21269
|
-
function tokensMatch(presented, expected) {
|
|
21270
|
-
if (presented === void 0) return false;
|
|
21271
|
-
const a = Buffer.from(presented, "utf8");
|
|
21272
|
-
const b = Buffer.from(expected, "utf8");
|
|
21273
|
-
if (a.length !== b.length) return false;
|
|
21274
|
-
return timingSafeEqual(a, b);
|
|
21275
|
-
}
|
|
21276
|
-
|
|
21277
22549
|
// src/plugins/RunnerRegistry.ts
|
|
21278
22550
|
import * as fs13 from "fs";
|
|
21279
22551
|
import * as path23 from "path";
|
|
21280
22552
|
import * as os6 from "os";
|
|
21281
22553
|
import { execFileSync } from "child_process";
|
|
21282
|
-
import { randomBytes as
|
|
22554
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
21283
22555
|
|
|
21284
22556
|
// src/plugins/FsPluginStore.ts
|
|
21285
22557
|
import * as fs12 from "fs";
|
|
@@ -21303,8 +22575,8 @@ function resolvePluginInstallDir(pluginsDir, name) {
|
|
|
21303
22575
|
assertPlainPluginName(name);
|
|
21304
22576
|
const root = path21.resolve(pluginsDir);
|
|
21305
22577
|
const dest = path21.resolve(root, name);
|
|
21306
|
-
const
|
|
21307
|
-
if (
|
|
22578
|
+
const relative6 = path21.relative(root, dest);
|
|
22579
|
+
if (relative6 !== name || relative6 === "" || path21.isAbsolute(relative6)) {
|
|
21308
22580
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
21309
22581
|
}
|
|
21310
22582
|
return dest;
|
|
@@ -21918,7 +23190,7 @@ var RunnerRegistry = class {
|
|
|
21918
23190
|
}
|
|
21919
23191
|
installFromGit(url) {
|
|
21920
23192
|
const validated = assertInstallablePluginUrl(url);
|
|
21921
|
-
const tmpDir = path23.join(os6.tmpdir(), `ordewell-plugin-${
|
|
23193
|
+
const tmpDir = path23.join(os6.tmpdir(), `ordewell-plugin-${randomBytes7(12).toString("hex")}`);
|
|
21922
23194
|
this.store.ensureDir(tmpDir);
|
|
21923
23195
|
try {
|
|
21924
23196
|
try {
|
|
@@ -22337,11 +23609,15 @@ export {
|
|
|
22337
23609
|
NO_TURN,
|
|
22338
23610
|
OPENCODE_MANIFEST,
|
|
22339
23611
|
ORCHESTRATOR_SHORTCUTS,
|
|
23612
|
+
ORDEWELL_MCP_PATH,
|
|
23613
|
+
ORDEWELL_MCP_SERVER_NAME,
|
|
22340
23614
|
ORDEWELL_SETTABLE_ENV,
|
|
22341
23615
|
OUTPUT_LINES_DEFAULT,
|
|
22342
23616
|
OUTPUT_LINES_MAX,
|
|
22343
23617
|
OpenAiService,
|
|
22344
23618
|
OpenCodeAdapter,
|
|
23619
|
+
OrdewellMcpServer,
|
|
23620
|
+
PLANNER_TOOLS,
|
|
22345
23621
|
PLAN_ENVELOPE_KEY,
|
|
22346
23622
|
PLUGIN_NAME_PATTERN,
|
|
22347
23623
|
PROVIDER_CREDENTIAL_ENV,
|
|
@@ -22378,6 +23654,7 @@ export {
|
|
|
22378
23654
|
TASK_QUERY_FIELDS,
|
|
22379
23655
|
TASK_QUERY_PROTOCOL,
|
|
22380
23656
|
TASK_QUERY_REMINDER,
|
|
23657
|
+
TASK_TOOLS,
|
|
22381
23658
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
22382
23659
|
TaskControlError,
|
|
22383
23660
|
TaskLogRecorder,
|
|
@@ -22461,10 +23738,14 @@ export {
|
|
|
22461
23738
|
daemonTokenPath,
|
|
22462
23739
|
defaultLogger,
|
|
22463
23740
|
definitionPattern,
|
|
23741
|
+
defuseMarkers,
|
|
22464
23742
|
deleteSession,
|
|
22465
23743
|
dependencyCandidates,
|
|
22466
23744
|
dependentsOf,
|
|
22467
23745
|
describeMergeResult,
|
|
23746
|
+
diffRows,
|
|
23747
|
+
diffStat,
|
|
23748
|
+
diffSummary,
|
|
22468
23749
|
digestTaskLog,
|
|
22469
23750
|
discoverGeminiModels,
|
|
22470
23751
|
drainNext,
|
|
@@ -22527,6 +23808,7 @@ export {
|
|
|
22527
23808
|
loadState,
|
|
22528
23809
|
looksLikePlanAttempt,
|
|
22529
23810
|
mapAgentTool,
|
|
23811
|
+
mcpClientConfig,
|
|
22530
23812
|
migrateLegacyPlan,
|
|
22531
23813
|
migrateOldConfigDir,
|
|
22532
23814
|
migratePlanIsolation,
|
|
@@ -22542,6 +23824,7 @@ export {
|
|
|
22542
23824
|
opensWithJsonObject,
|
|
22543
23825
|
outputLines,
|
|
22544
23826
|
outputPreview,
|
|
23827
|
+
ownerOnlyConfigFile,
|
|
22545
23828
|
parseAutonomyLevel,
|
|
22546
23829
|
parseMaxParallel,
|
|
22547
23830
|
parsePartialPlan,
|
|
@@ -22600,6 +23883,7 @@ export {
|
|
|
22600
23883
|
serializeTaskStatus,
|
|
22601
23884
|
sessionDataDir,
|
|
22602
23885
|
sessionRuntimeSettings,
|
|
23886
|
+
sharedMcpServer,
|
|
22603
23887
|
stateExists,
|
|
22604
23888
|
stopTurn,
|
|
22605
23889
|
stripAnsi,
|
|
@@ -22633,6 +23917,7 @@ export {
|
|
|
22633
23917
|
usageLine,
|
|
22634
23918
|
validateModifiedPlan,
|
|
22635
23919
|
validatePlanModification,
|
|
23920
|
+
validatePlanTasks,
|
|
22636
23921
|
warningsText,
|
|
22637
23922
|
wellKnownBinDirs,
|
|
22638
23923
|
windowsCommandLine,
|