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