@ordewell/core 0.4.5 → 0.4.7
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/dist/{ITerminalRunner-D7DwHYSg.d.ts → ITerminalRunner-Cpyzyrnl.d.ts} +7 -1
- package/dist/{ITerminalRunner-YMVsbfem.d.mts → ITerminalRunner-fynILAiR.d.mts} +7 -1
- package/dist/index.d.mts +232 -32
- package/dist/index.d.ts +232 -32
- package/dist/index.js +606 -289
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +601 -291
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ __export(index_exports, {
|
|
|
51
51
|
DENY_ALL: () => DENY_ALL,
|
|
52
52
|
EmbeddedNewlineError: () => EmbeddedNewlineError,
|
|
53
53
|
EnvConfig: () => EnvConfig,
|
|
54
|
+
ExecutableNotFoundError: () => ExecutableNotFoundError,
|
|
54
55
|
FsPluginStore: () => FsPluginStore,
|
|
55
56
|
GIT_READONLY_SUBCOMMANDS: () => GIT_READONLY_SUBCOMMANDS,
|
|
56
57
|
GREP_DEFAULT_HEAD_LIMIT: () => GREP_DEFAULT_HEAD_LIMIT,
|
|
@@ -71,14 +72,17 @@ __export(index_exports, {
|
|
|
71
72
|
PROVIDER_PRIORITY: () => PROVIDER_PRIORITY,
|
|
72
73
|
PROVIDER_SHORT_LABEL: () => PROVIDER_SHORT_LABEL,
|
|
73
74
|
PendingApprovals: () => PendingApprovals,
|
|
75
|
+
PlanEditError: () => PlanEditError,
|
|
74
76
|
PlanParseError: () => PlanParseError,
|
|
75
77
|
PlanStore: () => PlanStore,
|
|
76
78
|
Planner: () => Planner,
|
|
79
|
+
PlannerModelMemory: () => PlannerModelMemory,
|
|
77
80
|
REFUSED_COMMANDS: () => REFUSED_COMMANDS,
|
|
78
81
|
RunnerInstallation: () => RunnerInstallation,
|
|
79
82
|
RunnerRegistry: () => RunnerRegistry,
|
|
80
83
|
SEARCH_EXCLUSIONS: () => SEARCH_EXCLUSIONS,
|
|
81
84
|
STATE_DIR: () => STATE_DIR,
|
|
85
|
+
STOPPED_TOOL_RESULT: () => STOPPED_TOOL_RESULT,
|
|
82
86
|
SYMBOL_LANGUAGES: () => SYMBOL_LANGUAGES,
|
|
83
87
|
Session: () => Session,
|
|
84
88
|
SettingsService: () => SettingsService,
|
|
@@ -89,9 +93,11 @@ __export(index_exports, {
|
|
|
89
93
|
TmuxRunner: () => TmuxRunner,
|
|
90
94
|
VerdictEngine: () => VerdictEngine,
|
|
91
95
|
WINDOWS_MAX_COMMAND_LINE: () => WINDOWS_MAX_COMMAND_LINE,
|
|
96
|
+
WorkspaceNotFoundError: () => WorkspaceNotFoundError,
|
|
92
97
|
addTaskToPlan: () => addTaskToPlan,
|
|
93
98
|
applyHeadLimit: () => applyHeadLimit,
|
|
94
99
|
applyTaskOps: () => applyTaskOps,
|
|
100
|
+
assertWorkspaceExists: () => assertWorkspaceExists,
|
|
95
101
|
augmentPromptWithPriorOutputs: () => augmentPromptWithPriorOutputs,
|
|
96
102
|
augmentedPath: () => augmentedPath,
|
|
97
103
|
buildConversationSystemPrompt: () => buildConversationSystemPrompt,
|
|
@@ -165,6 +171,7 @@ __export(index_exports, {
|
|
|
165
171
|
hasTmux: () => hasTmux,
|
|
166
172
|
includeGlobFor: () => includeGlobFor,
|
|
167
173
|
isCliProvider: () => isCliProvider,
|
|
174
|
+
isExecutableResolved: () => isExecutableResolved,
|
|
168
175
|
isOpenAiProvider: () => isOpenAiProvider,
|
|
169
176
|
killTree: () => killTree,
|
|
170
177
|
knownModelId: () => knownModelId,
|
|
@@ -819,6 +826,16 @@ function dialectFor(dialect) {
|
|
|
819
826
|
const resolved = dialect ?? (process.platform === "win32" ? "cmd" : "posix");
|
|
820
827
|
return resolved === "cmd" ? CMD_DIALECT : POSIX_DIALECT;
|
|
821
828
|
}
|
|
829
|
+
function isDevNullTarget(target) {
|
|
830
|
+
if (!target.startsWith("/")) return false;
|
|
831
|
+
const resolved = [];
|
|
832
|
+
for (const seg of target.split("/")) {
|
|
833
|
+
if (seg === "" || seg === ".") continue;
|
|
834
|
+
if (seg === "..") resolved.pop();
|
|
835
|
+
else resolved.push(seg);
|
|
836
|
+
}
|
|
837
|
+
return `/${resolved.join("/")}` === "/dev/null";
|
|
838
|
+
}
|
|
822
839
|
function matchParen(s, open) {
|
|
823
840
|
let depth = 0;
|
|
824
841
|
for (let i = open; i < s.length; i++) {
|
|
@@ -829,7 +846,7 @@ function matchParen(s, open) {
|
|
|
829
846
|
}
|
|
830
847
|
function lex(command, nested, dialect) {
|
|
831
848
|
const segments = [];
|
|
832
|
-
let
|
|
849
|
+
let unsafeRedirect;
|
|
833
850
|
let processSubstitution = false;
|
|
834
851
|
let unbalanced = false;
|
|
835
852
|
let tokens = [];
|
|
@@ -838,7 +855,19 @@ function lex(command, nested, dialect) {
|
|
|
838
855
|
let started = false;
|
|
839
856
|
let piped = false;
|
|
840
857
|
let quote = "";
|
|
841
|
-
|
|
858
|
+
let redirectTargetMode = false;
|
|
859
|
+
let redirectOperator = "";
|
|
860
|
+
const endToken = (hardBoundary = true) => {
|
|
861
|
+
if (redirectTargetMode) {
|
|
862
|
+
if (!started && !hardBoundary) return;
|
|
863
|
+
if (!unsafeRedirect && !isDevNullTarget(current)) {
|
|
864
|
+
unsafeRedirect = { operator: redirectOperator, target: current };
|
|
865
|
+
}
|
|
866
|
+
redirectTargetMode = false;
|
|
867
|
+
current = "";
|
|
868
|
+
started = false;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
842
871
|
if (started) tokens.push(current);
|
|
843
872
|
current = "";
|
|
844
873
|
started = false;
|
|
@@ -926,9 +955,34 @@ function lex(command, nested, dialect) {
|
|
|
926
955
|
i += 2;
|
|
927
956
|
continue;
|
|
928
957
|
}
|
|
929
|
-
|
|
958
|
+
let fd = "";
|
|
959
|
+
if (started && /^[0-9]+$/.test(current)) {
|
|
960
|
+
fd = current;
|
|
961
|
+
current = "";
|
|
962
|
+
started = false;
|
|
963
|
+
} else {
|
|
964
|
+
endToken();
|
|
965
|
+
}
|
|
966
|
+
const doubled = command[i + 1] === c;
|
|
967
|
+
const opLen = doubled ? 2 : 1;
|
|
968
|
+
if (c === ">") {
|
|
969
|
+
const dup = /^&([0-9]+)(?![0-9])/.exec(command.slice(i + opLen));
|
|
970
|
+
if (dup) {
|
|
971
|
+
i += opLen + dup[0].length;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
redirectTargetMode = true;
|
|
975
|
+
redirectOperator = fd + c.repeat(opLen);
|
|
976
|
+
}
|
|
977
|
+
i += opLen;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (c === "&" && command[i + 1] === ">") {
|
|
930
981
|
endToken();
|
|
931
|
-
|
|
982
|
+
const doubled = command[i + 2] === ">";
|
|
983
|
+
redirectTargetMode = true;
|
|
984
|
+
redirectOperator = doubled ? "&>>" : "&>";
|
|
985
|
+
i += doubled ? 3 : 2;
|
|
932
986
|
continue;
|
|
933
987
|
}
|
|
934
988
|
if (c === "(" || c === ")") {
|
|
@@ -948,7 +1002,7 @@ function lex(command, nested, dialect) {
|
|
|
948
1002
|
continue;
|
|
949
1003
|
}
|
|
950
1004
|
if (/\s/.test(c)) {
|
|
951
|
-
endToken();
|
|
1005
|
+
endToken(false);
|
|
952
1006
|
i++;
|
|
953
1007
|
continue;
|
|
954
1008
|
}
|
|
@@ -958,7 +1012,7 @@ function lex(command, nested, dialect) {
|
|
|
958
1012
|
}
|
|
959
1013
|
if (quote !== "") unbalanced = true;
|
|
960
1014
|
endSegment(false);
|
|
961
|
-
return { segments,
|
|
1015
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
962
1016
|
}
|
|
963
1017
|
function binaryName(token, dialect) {
|
|
964
1018
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -978,7 +1032,7 @@ function lexAll(command, dialect) {
|
|
|
978
1032
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
979
1033
|
const inner = lex(queue.shift(), queue, dialect);
|
|
980
1034
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
981
|
-
all.
|
|
1035
|
+
all.unsafeRedirect ??= inner.unsafeRedirect;
|
|
982
1036
|
all.processSubstitution ||= inner.processSubstitution;
|
|
983
1037
|
all.unbalanced ||= inner.unbalanced;
|
|
984
1038
|
}
|
|
@@ -1027,8 +1081,9 @@ function refusalFor(seg) {
|
|
|
1027
1081
|
if (seg.binary === "git") {
|
|
1028
1082
|
const sub = seg.args.find((a) => !a.startsWith("-"));
|
|
1029
1083
|
if (sub === "branch" || sub === "tag") {
|
|
1030
|
-
|
|
1031
|
-
|
|
1084
|
+
const flag = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a));
|
|
1085
|
+
if (flag) {
|
|
1086
|
+
return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
1032
1087
|
}
|
|
1033
1088
|
}
|
|
1034
1089
|
}
|
|
@@ -1038,8 +1093,9 @@ function refusalFor(seg) {
|
|
|
1038
1093
|
return `find with "${flag}" runs an inner command this classifier cannot inspect. Use the read-only research tools on find's output, or describe the change as a task.`;
|
|
1039
1094
|
}
|
|
1040
1095
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1096
|
+
const sedInPlace = seg.binary === "sed" ? seg.args.find((a) => a === "-i" || a === "--in-place" || /^-i./.test(a)) : void 0;
|
|
1097
|
+
if (sedInPlace) {
|
|
1098
|
+
return `"sed ${sedInPlace}" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
1043
1099
|
}
|
|
1044
1100
|
if (seg.binary === "awk" && seg.args.some((a) => a === "inplace")) {
|
|
1045
1101
|
return `"awk -i inplace" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
@@ -1058,7 +1114,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
1058
1114
|
const trimmed = command.trim();
|
|
1059
1115
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
1060
1116
|
const dialect = dialectFor(opts.dialect);
|
|
1061
|
-
const { segments,
|
|
1117
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
1062
1118
|
if (unbalanced) {
|
|
1063
1119
|
return {
|
|
1064
1120
|
tier: "refuse",
|
|
@@ -1073,11 +1129,12 @@ function classifyCommand(command, opts = {}) {
|
|
|
1073
1129
|
reason: "Process substitution runs another command this classifier cannot inspect. Run the command on its own and read its output, or describe it as a task."
|
|
1074
1130
|
};
|
|
1075
1131
|
}
|
|
1076
|
-
if (
|
|
1132
|
+
if (unsafeRedirect) {
|
|
1133
|
+
const written = unsafeRedirect.target ? `"${unsafeRedirect.operator} ${unsafeRedirect.target}"` : `"${unsafeRedirect.operator}"`;
|
|
1077
1134
|
return {
|
|
1078
1135
|
tier: "refuse",
|
|
1079
1136
|
scope: "",
|
|
1080
|
-
reason:
|
|
1137
|
+
reason: `${written} writes to a file. You are a read-only planner \u2014 read the output instead, or describe the write as a task.`
|
|
1081
1138
|
};
|
|
1082
1139
|
}
|
|
1083
1140
|
if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
|
|
@@ -1397,7 +1454,7 @@ ${refs.output.trim()}`);
|
|
|
1397
1454
|
* anything else asks once and is remembered, and writes are refused outright
|
|
1398
1455
|
* because a planner that mutates the workspace has stopped being a planner.
|
|
1399
1456
|
*/
|
|
1400
|
-
async bash(command) {
|
|
1457
|
+
async bash(command, signal) {
|
|
1401
1458
|
const { tier, scope, reason } = classifyCommand(command, { dialect: this.researchShell.dialect });
|
|
1402
1459
|
if (tier === "refuse") {
|
|
1403
1460
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
@@ -1420,7 +1477,7 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
1420
1477
|
};
|
|
1421
1478
|
}
|
|
1422
1479
|
}
|
|
1423
|
-
const outcome = await this.execBashImpl(command);
|
|
1480
|
+
const outcome = await this.execBashImpl(command, signal);
|
|
1424
1481
|
if (outcome.success) return outcome;
|
|
1425
1482
|
const warning = researchShellWarning(this.researchShell);
|
|
1426
1483
|
return warning ? { ...outcome, output: `${outcome.output}
|
|
@@ -1429,6 +1486,82 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
1429
1486
|
}
|
|
1430
1487
|
};
|
|
1431
1488
|
|
|
1489
|
+
// src/services/executeTool.ts
|
|
1490
|
+
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
1491
|
+
function num(value) {
|
|
1492
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1493
|
+
}
|
|
1494
|
+
function bool(value) {
|
|
1495
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1496
|
+
}
|
|
1497
|
+
function str(value) {
|
|
1498
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1499
|
+
}
|
|
1500
|
+
function grepOptions(args) {
|
|
1501
|
+
const mode = str(args.output_mode ?? args.outputMode);
|
|
1502
|
+
return {
|
|
1503
|
+
include: str(args.include),
|
|
1504
|
+
path: str(args.path),
|
|
1505
|
+
outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
|
|
1506
|
+
contextBefore: num(args.context_before ?? args.contextBefore),
|
|
1507
|
+
contextAfter: num(args.context_after ?? args.contextAfter),
|
|
1508
|
+
literal: bool(args.literal),
|
|
1509
|
+
caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
|
|
1510
|
+
headLimit: num(args.head_limit ?? args.headLimit)
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
1514
|
+
async function executeTool(name, args, fs10, fetcher, signal) {
|
|
1515
|
+
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
1516
|
+
switch (name) {
|
|
1517
|
+
case "read_file": {
|
|
1518
|
+
const offset = num(args.offset);
|
|
1519
|
+
const limit = num(args.limit);
|
|
1520
|
+
const maxBytes = num(args.maxBytes);
|
|
1521
|
+
return fs10.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
1522
|
+
}
|
|
1523
|
+
case "read_files": {
|
|
1524
|
+
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
1525
|
+
return fs10.readFiles(paths);
|
|
1526
|
+
}
|
|
1527
|
+
case "glob":
|
|
1528
|
+
return fs10.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
1529
|
+
case "grep":
|
|
1530
|
+
return fs10.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
1531
|
+
case "find_symbol":
|
|
1532
|
+
return fs10.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
1533
|
+
case "list_dir":
|
|
1534
|
+
return fs10.listDir(String(args.path ?? ""), num(args.depth));
|
|
1535
|
+
case "bash":
|
|
1536
|
+
return fs10.bash(String(args.command ?? ""), signal);
|
|
1537
|
+
case "fetch": {
|
|
1538
|
+
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
1539
|
+
const url = String(args.url ?? "");
|
|
1540
|
+
const allowed = await fetcher.confirm(url);
|
|
1541
|
+
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
1542
|
+
return fetcher.fetch(url);
|
|
1543
|
+
}
|
|
1544
|
+
case "web_search": {
|
|
1545
|
+
if (!fetcher?.search) {
|
|
1546
|
+
return { success: false, output: "Web search is not available in this environment. If you already know a documentation URL, use the fetch tool instead.", truncated: false };
|
|
1547
|
+
}
|
|
1548
|
+
const query = String(args.query ?? "").trim();
|
|
1549
|
+
if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
|
|
1550
|
+
return fetcher.search(query);
|
|
1551
|
+
}
|
|
1552
|
+
// Budget models hallucinate two kinds of tools: plan-committing ones
|
|
1553
|
+
// (create_task, submit_plan) and implementation ones (create_file,
|
|
1554
|
+
// write_file). Steer both back: you are a planner, tasks run later,
|
|
1555
|
+
// and the commit channel is raw JSON in the reply text.
|
|
1556
|
+
default:
|
|
1557
|
+
return {
|
|
1558
|
+
success: false,
|
|
1559
|
+
output: `Unknown tool: ${name}. You are a PLANNER \u2014 you never create or edit files yourself; the coding agents executing the plan will do that later. Your tools are read-only research tools: read_file, read_files, glob, grep, find_symbol, list_dir, bash, fetch, web_search. To commit the final plan, output it as a raw JSON object with a "tasks" array directly in your reply text (no tool call).`,
|
|
1560
|
+
truncated: false
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1432
1565
|
// src/services/ApprovalPolicy.ts
|
|
1433
1566
|
function scopeMatches(pattern, scope) {
|
|
1434
1567
|
if (pattern === scope) return true;
|
|
@@ -4315,8 +4448,8 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
4315
4448
|
|
|
4316
4449
|
// src/services/ContextCollector.ts
|
|
4317
4450
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
4318
|
-
async function collectResearchContext(
|
|
4319
|
-
const collector = new ContextCollector(
|
|
4451
|
+
async function collectResearchContext(fs10, runners) {
|
|
4452
|
+
const collector = new ContextCollector(fs10);
|
|
4320
4453
|
let contextStr = "";
|
|
4321
4454
|
let firstContext = null;
|
|
4322
4455
|
for (const r of runners) {
|
|
@@ -4343,8 +4476,8 @@ ${firstContext.dirStructure}
|
|
|
4343
4476
|
return contextStr;
|
|
4344
4477
|
}
|
|
4345
4478
|
var ContextCollector = class {
|
|
4346
|
-
constructor(
|
|
4347
|
-
this.fs =
|
|
4479
|
+
constructor(fs10, registry) {
|
|
4480
|
+
this.fs = fs10;
|
|
4348
4481
|
this.registry = registry;
|
|
4349
4482
|
}
|
|
4350
4483
|
fs;
|
|
@@ -4380,80 +4513,6 @@ var ContextCollector = class {
|
|
|
4380
4513
|
}
|
|
4381
4514
|
};
|
|
4382
4515
|
|
|
4383
|
-
// src/services/executeTool.ts
|
|
4384
|
-
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
4385
|
-
function num(value) {
|
|
4386
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4387
|
-
}
|
|
4388
|
-
function bool(value) {
|
|
4389
|
-
return typeof value === "boolean" ? value : void 0;
|
|
4390
|
-
}
|
|
4391
|
-
function str(value) {
|
|
4392
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4393
|
-
}
|
|
4394
|
-
function grepOptions(args) {
|
|
4395
|
-
const mode = str(args.output_mode ?? args.outputMode);
|
|
4396
|
-
return {
|
|
4397
|
-
include: str(args.include),
|
|
4398
|
-
path: str(args.path),
|
|
4399
|
-
outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
|
|
4400
|
-
contextBefore: num(args.context_before ?? args.contextBefore),
|
|
4401
|
-
contextAfter: num(args.context_after ?? args.contextAfter),
|
|
4402
|
-
literal: bool(args.literal),
|
|
4403
|
-
caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
|
|
4404
|
-
headLimit: num(args.head_limit ?? args.headLimit)
|
|
4405
|
-
};
|
|
4406
|
-
}
|
|
4407
|
-
async function executeTool(name, args, fs9, fetcher) {
|
|
4408
|
-
switch (name) {
|
|
4409
|
-
case "read_file": {
|
|
4410
|
-
const offset = num(args.offset);
|
|
4411
|
-
const limit = num(args.limit);
|
|
4412
|
-
const maxBytes = num(args.maxBytes);
|
|
4413
|
-
return fs9.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
4414
|
-
}
|
|
4415
|
-
case "read_files": {
|
|
4416
|
-
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
4417
|
-
return fs9.readFiles(paths);
|
|
4418
|
-
}
|
|
4419
|
-
case "glob":
|
|
4420
|
-
return fs9.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
4421
|
-
case "grep":
|
|
4422
|
-
return fs9.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
4423
|
-
case "find_symbol":
|
|
4424
|
-
return fs9.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
4425
|
-
case "list_dir":
|
|
4426
|
-
return fs9.listDir(String(args.path ?? ""), num(args.depth));
|
|
4427
|
-
case "bash":
|
|
4428
|
-
return fs9.bash(String(args.command ?? ""));
|
|
4429
|
-
case "fetch": {
|
|
4430
|
-
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
4431
|
-
const url = String(args.url ?? "");
|
|
4432
|
-
const allowed = await fetcher.confirm(url);
|
|
4433
|
-
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
4434
|
-
return fetcher.fetch(url);
|
|
4435
|
-
}
|
|
4436
|
-
case "web_search": {
|
|
4437
|
-
if (!fetcher?.search) {
|
|
4438
|
-
return { success: false, output: "Web search is not available in this environment. If you already know a documentation URL, use the fetch tool instead.", truncated: false };
|
|
4439
|
-
}
|
|
4440
|
-
const query = String(args.query ?? "").trim();
|
|
4441
|
-
if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
|
|
4442
|
-
return fetcher.search(query);
|
|
4443
|
-
}
|
|
4444
|
-
// Budget models hallucinate two kinds of tools: plan-committing ones
|
|
4445
|
-
// (create_task, submit_plan) and implementation ones (create_file,
|
|
4446
|
-
// write_file). Steer both back: you are a planner, tasks run later,
|
|
4447
|
-
// and the commit channel is raw JSON in the reply text.
|
|
4448
|
-
default:
|
|
4449
|
-
return {
|
|
4450
|
-
success: false,
|
|
4451
|
-
output: `Unknown tool: ${name}. You are a PLANNER \u2014 you never create or edit files yourself; the coding agents executing the plan will do that later. Your tools are read-only research tools: read_file, read_files, glob, grep, find_symbol, list_dir, bash, fetch, web_search. To commit the final plan, output it as a raw JSON object with a "tasks" array directly in your reply text (no tool call).`,
|
|
4452
|
-
truncated: false
|
|
4453
|
-
};
|
|
4454
|
-
}
|
|
4455
|
-
}
|
|
4456
|
-
|
|
4457
4516
|
// src/services/researchStepSummary.ts
|
|
4458
4517
|
function ellipsize(text, max) {
|
|
4459
4518
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
@@ -4535,23 +4594,23 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
4535
4594
|
function refused(output) {
|
|
4536
4595
|
return { success: false, output, truncated: false };
|
|
4537
4596
|
}
|
|
4538
|
-
function nonPromptingFs(
|
|
4539
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
4597
|
+
function nonPromptingFs(fs10) {
|
|
4598
|
+
const withinWorkspace = (p) => resolveWithin(fs10.getWorkspaceRoot(), p ?? ".").inside;
|
|
4540
4599
|
const outsideRefusal = (p) => refused(
|
|
4541
4600
|
`Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
|
|
4542
4601
|
);
|
|
4543
4602
|
return {
|
|
4544
|
-
getWorkspaceRoot: () =>
|
|
4545
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
4603
|
+
getWorkspaceRoot: () => fs10.getWorkspaceRoot(),
|
|
4604
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs10.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
4546
4605
|
readFiles: (paths) => {
|
|
4547
4606
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
4548
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
4607
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs10.readFiles(paths);
|
|
4549
4608
|
},
|
|
4550
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4551
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4552
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
4553
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
4554
|
-
bash: async (command) => {
|
|
4609
|
+
glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4610
|
+
grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4611
|
+
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs10.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
4612
|
+
listDir: (p, depth) => withinWorkspace(p) ? fs10.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
4613
|
+
bash: async (command, signal) => {
|
|
4555
4614
|
const dialect = resolveResearchShell().dialect;
|
|
4556
4615
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
4557
4616
|
if (tier !== "auto") {
|
|
@@ -4561,13 +4620,13 @@ function nonPromptingFs(fs9) {
|
|
|
4561
4620
|
}
|
|
4562
4621
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
4563
4622
|
if (escaping) return outsideRefusal(escaping);
|
|
4564
|
-
return
|
|
4623
|
+
return fs10.bash(command, signal);
|
|
4565
4624
|
}
|
|
4566
4625
|
};
|
|
4567
4626
|
}
|
|
4568
4627
|
async function runLoop(prompt, deps) {
|
|
4569
4628
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
4570
|
-
const
|
|
4629
|
+
const fs10 = nonPromptingFs(deps.fs);
|
|
4571
4630
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
4572
4631
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
4573
4632
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -4584,7 +4643,7 @@ ${turn.text}`;
|
|
|
4584
4643
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
4585
4644
|
const toolArgs = JSON.stringify(tc.args);
|
|
4586
4645
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
4587
|
-
const res = await executeTool(tc.name, args,
|
|
4646
|
+
const res = await executeTool(tc.name, args, fs10, void 0, deps.signal);
|
|
4588
4647
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
4589
4648
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
4590
4649
|
const stepEntry = {
|
|
@@ -4699,7 +4758,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
4699
4758
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
4700
4759
|
* never fail a plan or a turn.
|
|
4701
4760
|
*/
|
|
4702
|
-
async executeSpawnAgent(args,
|
|
4761
|
+
async executeSpawnAgent(args, fs10, onProgress, signal, subagentId) {
|
|
4703
4762
|
if (!this.researchSubagentsEnabled) {
|
|
4704
4763
|
return { success: false, output: "spawn_research_agent is not available in this session. Continue researching sequentially with your other tools (read_file, glob, grep, list_dir, bash).", truncated: false };
|
|
4705
4764
|
}
|
|
@@ -4713,20 +4772,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
4713
4772
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
4714
4773
|
return chat;
|
|
4715
4774
|
},
|
|
4716
|
-
fs:
|
|
4775
|
+
fs: fs10,
|
|
4717
4776
|
signal,
|
|
4718
4777
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
4719
4778
|
});
|
|
4720
4779
|
}
|
|
4721
4780
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
4722
|
-
static async collectResearchContext(
|
|
4723
|
-
return collectResearchContext(
|
|
4781
|
+
static async collectResearchContext(fs10, runners) {
|
|
4782
|
+
return collectResearchContext(fs10, runners);
|
|
4724
4783
|
}
|
|
4725
4784
|
/**
|
|
4726
4785
|
* Execute one round of tool calls and report each through onProgress.
|
|
4727
4786
|
* Returns the results to feed back plus the log entries produced.
|
|
4728
4787
|
*/
|
|
4729
|
-
async executeToolCalls(toolCalls,
|
|
4788
|
+
async executeToolCalls(toolCalls, fs10, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
4730
4789
|
const toolResults = [];
|
|
4731
4790
|
const logEntries = [];
|
|
4732
4791
|
let resultsText = "";
|
|
@@ -4745,18 +4804,18 @@ var BaseAiService = class _BaseAiService {
|
|
|
4745
4804
|
const subagentId = (0, import_uuid2.v4)();
|
|
4746
4805
|
spawnSubagentIds.set(i, subagentId);
|
|
4747
4806
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
4748
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
4807
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
|
|
4749
4808
|
}) : Promise.resolve(),
|
|
4750
4809
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
4751
4810
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
4752
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
4811
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs10, fetcher, signal));
|
|
4753
4812
|
}) : Promise.resolve()
|
|
4754
4813
|
]);
|
|
4755
4814
|
for (const [index, tc] of toolCalls.entries()) {
|
|
4756
4815
|
const toolArgs = normalizedArgs[index];
|
|
4757
4816
|
const preResult = precomputed.get(index);
|
|
4758
4817
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
4759
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
4818
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs10, fetcher, signal);
|
|
4760
4819
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
4761
4820
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
4762
4821
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -4942,7 +5001,7 @@ ${llmOutput}
|
|
|
4942
5001
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
4943
5002
|
* exhausted without a valid plan.
|
|
4944
5003
|
*/
|
|
4945
|
-
async runResearchLoop(chat, firstMessage,
|
|
5004
|
+
async runResearchLoop(chat, firstMessage, fs10, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
4946
5005
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
4947
5006
|
if (!initialLog && userGoal) {
|
|
4948
5007
|
researchLog.push({
|
|
@@ -4969,7 +5028,7 @@ ${llmOutput}
|
|
|
4969
5028
|
if (!turn.hasToolCalls) break;
|
|
4970
5029
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
4971
5030
|
turn.toolCalls,
|
|
4972
|
-
|
|
5031
|
+
fs10,
|
|
4973
5032
|
onProgress,
|
|
4974
5033
|
fetcher,
|
|
4975
5034
|
step,
|
|
@@ -5205,11 +5264,11 @@ ${repairHint}` }] : parts;
|
|
|
5205
5264
|
return this.consumePlanStream(result, onToken);
|
|
5206
5265
|
}
|
|
5207
5266
|
// --- IAiService implementation ---
|
|
5208
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5267
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5209
5268
|
this.ensureInit();
|
|
5210
5269
|
const { autonomousDefault } = modes;
|
|
5211
5270
|
const genModel = this.getPlanningModel();
|
|
5212
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
5271
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
5213
5272
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5214
5273
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
5215
5274
|
|
|
@@ -5220,17 +5279,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
5220
5279
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
5221
5280
|
});
|
|
5222
5281
|
const researchChat = new GeminiResearchChat(chat);
|
|
5223
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
5282
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
5224
5283
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5225
5284
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5226
5285
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5227
5286
|
}
|
|
5228
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
5287
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
5229
5288
|
this.ensureInit();
|
|
5230
5289
|
const { autonomousDefault } = modes;
|
|
5231
5290
|
let ctx = "";
|
|
5232
|
-
if (
|
|
5233
|
-
const collected = await new ContextCollector(
|
|
5291
|
+
if (fs10) {
|
|
5292
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
5234
5293
|
if (collected.aiflowContext) ctx = `
|
|
5235
5294
|
<aiflow_context>
|
|
5236
5295
|
${collected.aiflowContext}
|
|
@@ -5246,12 +5305,12 @@ ${collected.aiflowContext}
|
|
|
5246
5305
|
autonomousDefault
|
|
5247
5306
|
);
|
|
5248
5307
|
}
|
|
5249
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5308
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
5250
5309
|
this.ensureInit();
|
|
5251
5310
|
const { autonomousDefault } = modes;
|
|
5252
5311
|
let aiflowContext;
|
|
5253
|
-
if (
|
|
5254
|
-
const collected = await new ContextCollector(
|
|
5312
|
+
if (fs10) {
|
|
5313
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5255
5314
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5256
5315
|
}
|
|
5257
5316
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -5491,12 +5550,12 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
5491
5550
|
}
|
|
5492
5551
|
}
|
|
5493
5552
|
// --- IAiService implementation ---
|
|
5494
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5553
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5495
5554
|
this.ensureInit();
|
|
5496
5555
|
const client = this.getClient();
|
|
5497
5556
|
const { autonomousDefault, researchSubagents: researchSubagentsEnabled } = modes;
|
|
5498
5557
|
this.researchSubagentsEnabled = researchSubagentsEnabled;
|
|
5499
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
5558
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
5500
5559
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5501
5560
|
const userText = `User goal: ${userDescription}`;
|
|
5502
5561
|
const firstMessage = `${buildResearchToolsPrompt(researchSubagentsEnabled)}
|
|
@@ -5514,17 +5573,17 @@ ${userText}`;
|
|
|
5514
5573
|
toOpenAiTools(researchSubagentsEnabled),
|
|
5515
5574
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
5516
5575
|
);
|
|
5517
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
5576
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
5518
5577
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5519
5578
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5520
5579
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5521
5580
|
}
|
|
5522
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
5581
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5523
5582
|
this.ensureInit();
|
|
5524
5583
|
const { autonomousDefault } = modes;
|
|
5525
5584
|
let ctx = "";
|
|
5526
|
-
if (
|
|
5527
|
-
const collected = await new ContextCollector(
|
|
5585
|
+
if (fs10) {
|
|
5586
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
5528
5587
|
if (collected.aiflowContext) ctx = `
|
|
5529
5588
|
<aiflow_context>
|
|
5530
5589
|
${collected.aiflowContext}
|
|
@@ -5540,12 +5599,12 @@ ${collected.aiflowContext}
|
|
|
5540
5599
|
autonomousDefault
|
|
5541
5600
|
);
|
|
5542
5601
|
}
|
|
5543
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5602
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5544
5603
|
this.ensureInit();
|
|
5545
5604
|
const { autonomousDefault } = modes;
|
|
5546
5605
|
let aiflowContext;
|
|
5547
|
-
if (
|
|
5548
|
-
const collected = await new ContextCollector(
|
|
5606
|
+
if (fs10) {
|
|
5607
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5549
5608
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5550
5609
|
}
|
|
5551
5610
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -6163,6 +6222,7 @@ var PlanStore = class {
|
|
|
6163
6222
|
}
|
|
6164
6223
|
remove(taskId) {
|
|
6165
6224
|
if (!this._taskMap.has(taskId)) return;
|
|
6225
|
+
this.unblockDependents(taskId);
|
|
6166
6226
|
this._planTasks = removeTaskFromPlan(this._planTasks, taskId);
|
|
6167
6227
|
this.rebuild();
|
|
6168
6228
|
this.pruneTerminalSets();
|
|
@@ -6217,9 +6277,7 @@ ${taskB.description}`,
|
|
|
6217
6277
|
const userIds = /* @__PURE__ */ new Set([taskIdA, taskIdB]);
|
|
6218
6278
|
const sanitized = this._planTasks.filter((t) => !userIds.has(t.id)).map((t) => ({
|
|
6219
6279
|
...t,
|
|
6220
|
-
dependencies: t.dependencies.map(
|
|
6221
|
-
(depId) => depId === taskIdA || depId === taskIdB ? mergedId : depId
|
|
6222
|
-
)
|
|
6280
|
+
dependencies: [...new Set(t.dependencies.map((depId) => userIds.has(depId) ? mergedId : depId))]
|
|
6223
6281
|
}));
|
|
6224
6282
|
this._planTasks = renumberTasks([...sanitized, merged]);
|
|
6225
6283
|
this.rebuild();
|
|
@@ -6231,11 +6289,12 @@ ${taskB.description}`,
|
|
|
6231
6289
|
const original = this._taskMap.get(taskId);
|
|
6232
6290
|
if (!original) throw new Error(`Task ${taskId} not found`);
|
|
6233
6291
|
if (!newTaskSpecs.length) throw new Error("Must provide at least one new task spec");
|
|
6234
|
-
const
|
|
6235
|
-
|
|
6292
|
+
const finalized = [];
|
|
6293
|
+
newTaskSpecs.forEach((spec, i) => {
|
|
6294
|
+
finalized.push(createTask({
|
|
6236
6295
|
...spec,
|
|
6237
6296
|
order: original.order + i,
|
|
6238
|
-
dependencies: i === 0 ? [...original.dependencies] : [
|
|
6297
|
+
dependencies: i === 0 ? [...original.dependencies] : [finalized[i - 1].id],
|
|
6239
6298
|
type: spec.type ?? original.type,
|
|
6240
6299
|
assignedRunner: spec.assignedRunner ?? original.assignedRunner,
|
|
6241
6300
|
assignedModel: spec.assignedModel ?? original.assignedModel,
|
|
@@ -6243,17 +6302,7 @@ ${taskB.description}`,
|
|
|
6243
6302
|
autonomy: spec.autonomy ?? original.autonomy,
|
|
6244
6303
|
sliceType: spec.sliceType ?? original.sliceType,
|
|
6245
6304
|
userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered
|
|
6246
|
-
})
|
|
6247
|
-
);
|
|
6248
|
-
const newTaskIds = /* @__PURE__ */ new Map();
|
|
6249
|
-
newTasks.forEach((t, i) => newTaskIds.set(i, t.id));
|
|
6250
|
-
const finalized = newTasks.map((t) => {
|
|
6251
|
-
const deps = t.dependencies.map((depId) => {
|
|
6252
|
-
const idx = newTaskSpecs.findIndex((s) => (s.id ?? "") === depId);
|
|
6253
|
-
if (idx >= 0) return newTaskIds.get(idx) ?? depId;
|
|
6254
|
-
return depId;
|
|
6255
|
-
});
|
|
6256
|
-
return { ...t, dependencies: deps };
|
|
6305
|
+
}));
|
|
6257
6306
|
});
|
|
6258
6307
|
const tailId = finalized[finalized.length - 1].id;
|
|
6259
6308
|
const updatedOthers = this._planTasks.filter((t) => t.id !== taskId).map((t) => ({
|
|
@@ -6498,6 +6547,15 @@ var TaskOrchestrator = class {
|
|
|
6498
6547
|
get isRunning() {
|
|
6499
6548
|
return this.running || this.startingTaskIds.size > 0 || this.activeTaskSessions.size > 0;
|
|
6500
6549
|
}
|
|
6550
|
+
/**
|
|
6551
|
+
* A runner is executing *right now*. Narrower than {@link isRunning}, which
|
|
6552
|
+
* also covers the armed-but-idle scheduler `tick()` deliberately leaves
|
|
6553
|
+
* behind when a plan is paused on a user task, a checkpoint or a hold —
|
|
6554
|
+
* nothing is executing then, so nothing is reading the plan mid-mutation.
|
|
6555
|
+
*/
|
|
6556
|
+
get hasLiveWork() {
|
|
6557
|
+
return this.activeTaskSessions.size > 0 || this.startingTaskIds.size > 0;
|
|
6558
|
+
}
|
|
6501
6559
|
get isReviewApproved() {
|
|
6502
6560
|
return this.reviewApproved;
|
|
6503
6561
|
}
|
|
@@ -6544,18 +6602,31 @@ var TaskOrchestrator = class {
|
|
|
6544
6602
|
this.retryCounts.clear();
|
|
6545
6603
|
this.onHold.clear();
|
|
6546
6604
|
}
|
|
6605
|
+
/**
|
|
6606
|
+
* Adopt an edited plan without letting go of the run in progress. Everything
|
|
6607
|
+
* the scheduler owns — live sessions, holds, retry counts, the verifier and
|
|
6608
|
+
* the review approval — survives, because `loadPlan` clears all of it and a
|
|
6609
|
+
* mid-run edit is not a new run. Only the tasks change.
|
|
6610
|
+
*
|
|
6611
|
+
* The ids with live sessions are defended: one dropped from the edited plan is
|
|
6612
|
+
* carried over (its verdict still has to land somewhere), and one whose status
|
|
6613
|
+
* the snapshot predates is put back to `in_progress` — adopting the snapshot's
|
|
6614
|
+
* "pending" would offer the scheduler work a runner is already doing.
|
|
6615
|
+
*/
|
|
6547
6616
|
reconcilePlan(newTasks, planRunners = ["claude-code"]) {
|
|
6617
|
+
const adopted = [...newTasks];
|
|
6548
6618
|
for (const taskId of this.activeTaskSessions.keys()) {
|
|
6549
6619
|
const task = this.store.get(taskId);
|
|
6550
6620
|
if (!task) continue;
|
|
6551
|
-
const
|
|
6552
|
-
if (
|
|
6553
|
-
|
|
6554
|
-
} else if (
|
|
6555
|
-
console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${
|
|
6621
|
+
const at = adopted.findIndex((t) => t.id === taskId);
|
|
6622
|
+
if (at < 0) {
|
|
6623
|
+
adopted.push({ ...task });
|
|
6624
|
+
} else if (adopted[at].status !== "in_progress") {
|
|
6625
|
+
console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${adopted[at].status}" in modified plan, using orchestrator truth`);
|
|
6626
|
+
adopted[at] = { ...adopted[at], status: "in_progress" };
|
|
6556
6627
|
}
|
|
6557
6628
|
}
|
|
6558
|
-
this.store.load(
|
|
6629
|
+
this.store.load(adopted, planRunners);
|
|
6559
6630
|
this.planStatus = "running";
|
|
6560
6631
|
}
|
|
6561
6632
|
async start() {
|
|
@@ -6676,6 +6747,21 @@ ${output.slice(-3e3)}`);
|
|
|
6676
6747
|
this.emit("onTaskChanged");
|
|
6677
6748
|
await this.tick();
|
|
6678
6749
|
}
|
|
6750
|
+
/**
|
|
6751
|
+
* Let go of a task that is leaving the plan. A live runner is cancelled
|
|
6752
|
+
* through {@link cancelTask} so the verifier generation is bumped before the
|
|
6753
|
+
* process dies; a spawn still in flight is dropped from `startingTaskIds`,
|
|
6754
|
+
* which is what makes {@link startTask} kill the session it is about to
|
|
6755
|
+
* receive. The id's remaining bookkeeping goes too — a hold or retry count
|
|
6756
|
+
* kept for a task that no longer exists would be inherited by nothing and
|
|
6757
|
+
* leaves `isRunning` reading true forever.
|
|
6758
|
+
*/
|
|
6759
|
+
async releaseTask(taskId) {
|
|
6760
|
+
this.startingTaskIds.delete(taskId);
|
|
6761
|
+
if (this.activeTaskSessions.has(taskId)) await this.cancelTask(taskId);
|
|
6762
|
+
this.onHold.delete(taskId);
|
|
6763
|
+
this.retryCounts.delete(taskId);
|
|
6764
|
+
}
|
|
6679
6765
|
async markTaskComplete(taskId) {
|
|
6680
6766
|
const task = this.store.get(taskId);
|
|
6681
6767
|
if (!task || task.status === "completed") return;
|
|
@@ -7135,28 +7221,28 @@ function posixBinDirs() {
|
|
|
7135
7221
|
];
|
|
7136
7222
|
}
|
|
7137
7223
|
function windowsBinDirs(env, home) {
|
|
7138
|
-
const
|
|
7139
|
-
const under = (base, ...parts) => base ? [
|
|
7224
|
+
const join14 = (...parts) => path5.win32.join(...parts);
|
|
7225
|
+
const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
|
|
7140
7226
|
const appData = env.APPDATA;
|
|
7141
7227
|
const localAppData = env.LOCALAPPDATA;
|
|
7142
7228
|
const programData = env.ProgramData;
|
|
7143
7229
|
return [
|
|
7144
7230
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7231
|
+
join14(home, ".local", "bin"),
|
|
7232
|
+
join14(home, ".opencode", "bin"),
|
|
7233
|
+
join14(home, ".claude", "bin"),
|
|
7234
|
+
join14(home, ".codex", "bin"),
|
|
7149
7235
|
...under(localAppData, "Programs", "claude"),
|
|
7150
7236
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
7151
7237
|
// literal spelling is kept as a fallback for a host that does not export
|
|
7152
7238
|
// APPDATA (a service account, a stripped CI container).
|
|
7153
7239
|
...under(appData, "npm"),
|
|
7154
|
-
|
|
7240
|
+
join14(home, "AppData", "Roaming", "npm"),
|
|
7155
7241
|
...under(localAppData, "pnpm"),
|
|
7156
7242
|
...under(localAppData, "Yarn", "bin"),
|
|
7157
|
-
|
|
7243
|
+
join14(home, ".bun", "bin"),
|
|
7158
7244
|
// Windows package managers.
|
|
7159
|
-
|
|
7245
|
+
join14(home, "scoop", "shims"),
|
|
7160
7246
|
...under(env.SCOOP, "shims"),
|
|
7161
7247
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
7162
7248
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -7166,7 +7252,7 @@ function windowsBinDirs(env, home) {
|
|
|
7166
7252
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
7167
7253
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
7168
7254
|
...under(localAppData, "Volta", "bin"),
|
|
7169
|
-
|
|
7255
|
+
join14(home, ".volta", "bin")
|
|
7170
7256
|
];
|
|
7171
7257
|
}
|
|
7172
7258
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -7289,6 +7375,26 @@ Install the native executable \u2014 Claude Code's Windows installer, Codex's re
|
|
|
7289
7375
|
function hasLineBreak(parts) {
|
|
7290
7376
|
return parts.some((part) => /[\r\n]/.test(part));
|
|
7291
7377
|
}
|
|
7378
|
+
var ExecutableNotFoundError = class extends Error {
|
|
7379
|
+
constructor(command, searchedPath) {
|
|
7380
|
+
super(`Could not find "${command}" on PATH.
|
|
7381
|
+
|
|
7382
|
+
Searched: ${searchedPath || "(empty PATH)"}`);
|
|
7383
|
+
this.command = command;
|
|
7384
|
+
this.searchedPath = searchedPath;
|
|
7385
|
+
this.name = "ExecutableNotFoundError";
|
|
7386
|
+
}
|
|
7387
|
+
command;
|
|
7388
|
+
searchedPath;
|
|
7389
|
+
};
|
|
7390
|
+
function isResolvableOnPosixPath(command, PATH, exists) {
|
|
7391
|
+
if (command.includes("/")) return exists(command);
|
|
7392
|
+
return PATH.split(path6.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path6.join(dir, command)));
|
|
7393
|
+
}
|
|
7394
|
+
function isExecutableResolved(command, plan, PATH, deps = {}) {
|
|
7395
|
+
if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
|
|
7396
|
+
return isResolvableOnPosixPath(command, PATH, deps.exists ?? defaultExists2);
|
|
7397
|
+
}
|
|
7292
7398
|
function defaultExists2(candidate) {
|
|
7293
7399
|
try {
|
|
7294
7400
|
return fsSync2.statSync(candidate).isFile();
|
|
@@ -8048,6 +8154,10 @@ var ModelDiscovery = class {
|
|
|
8048
8154
|
clear() {
|
|
8049
8155
|
this.cache.clear();
|
|
8050
8156
|
}
|
|
8157
|
+
/** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
|
|
8158
|
+
getCached(runner) {
|
|
8159
|
+
return this.cache.get(runner);
|
|
8160
|
+
}
|
|
8051
8161
|
};
|
|
8052
8162
|
|
|
8053
8163
|
// src/services/ModelCatalog.ts
|
|
@@ -8366,6 +8476,17 @@ var ModelResolver = class {
|
|
|
8366
8476
|
refreshRunnerModels() {
|
|
8367
8477
|
this.discovery.clear();
|
|
8368
8478
|
}
|
|
8479
|
+
/**
|
|
8480
|
+
* Synchronous reads of whatever the last `modelsForRunners`/`pickerOptions`
|
|
8481
|
+
* call already cached — never trigger discovery or a fetch. For callers
|
|
8482
|
+
* (e.g. a settings write) that need a catalog without going async.
|
|
8483
|
+
*/
|
|
8484
|
+
getCachedRunnerModels(runner) {
|
|
8485
|
+
return this.discovery.getCached(runner) ?? [];
|
|
8486
|
+
}
|
|
8487
|
+
getCachedPickerOptions() {
|
|
8488
|
+
return this.cachedPickerOptions ?? [];
|
|
8489
|
+
}
|
|
8369
8490
|
invalidate() {
|
|
8370
8491
|
this.discovery.clear();
|
|
8371
8492
|
this.cachedPickerOptions = null;
|
|
@@ -8493,6 +8614,28 @@ var RunnerInstallation = class {
|
|
|
8493
8614
|
// src/services/harness/CliAgentAiService.ts
|
|
8494
8615
|
var import_child_process5 = require("child_process");
|
|
8495
8616
|
|
|
8617
|
+
// src/utils/workspace.ts
|
|
8618
|
+
var fs3 = __toESM(require("fs"));
|
|
8619
|
+
var WorkspaceNotFoundError = class extends Error {
|
|
8620
|
+
constructor(workspace) {
|
|
8621
|
+
super(`Workspace "${workspace}" does not exist.`);
|
|
8622
|
+
this.workspace = workspace;
|
|
8623
|
+
this.name = "WorkspaceNotFoundError";
|
|
8624
|
+
}
|
|
8625
|
+
workspace;
|
|
8626
|
+
};
|
|
8627
|
+
function defaultIsDirectory(candidate) {
|
|
8628
|
+
try {
|
|
8629
|
+
return fs3.statSync(candidate).isDirectory();
|
|
8630
|
+
} catch {
|
|
8631
|
+
return false;
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8634
|
+
function assertWorkspaceExists(workspace, deps = {}) {
|
|
8635
|
+
const isDirectory = deps.isDirectory ?? defaultIsDirectory;
|
|
8636
|
+
if (!isDirectory(workspace)) throw new WorkspaceNotFoundError(workspace);
|
|
8637
|
+
}
|
|
8638
|
+
|
|
8496
8639
|
// src/services/harness/AgentAdapter.ts
|
|
8497
8640
|
var LineBuffer = class {
|
|
8498
8641
|
buffer = "";
|
|
@@ -8546,6 +8689,7 @@ var StdioAgentAdapter = class {
|
|
|
8546
8689
|
async handshake(_opts) {
|
|
8547
8690
|
}
|
|
8548
8691
|
async start(opts) {
|
|
8692
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
8549
8693
|
this.processEnded = new Promise((resolve3) => {
|
|
8550
8694
|
this.markEnded = resolve3;
|
|
8551
8695
|
});
|
|
@@ -8557,6 +8701,9 @@ var StdioAgentAdapter = class {
|
|
|
8557
8701
|
platform: this.deps.platform,
|
|
8558
8702
|
resolvePath
|
|
8559
8703
|
});
|
|
8704
|
+
if (!isExecutableResolved(spec.command, launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
8705
|
+
throw new ExecutableNotFoundError(spec.command, PATH);
|
|
8706
|
+
}
|
|
8560
8707
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
8561
8708
|
env: this.spawnEnv,
|
|
8562
8709
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9179,12 +9326,16 @@ var OpenCodeAdapter = class {
|
|
|
9179
9326
|
opts = null;
|
|
9180
9327
|
async start(opts) {
|
|
9181
9328
|
this.opts = opts;
|
|
9329
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
9182
9330
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
9183
9331
|
const PATH = await resolvePath();
|
|
9184
9332
|
const launch = await planDirectLaunch("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
|
|
9185
9333
|
platform: this.deps.platform,
|
|
9186
9334
|
resolvePath
|
|
9187
9335
|
});
|
|
9336
|
+
if (!isExecutableResolved("opencode", launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
9337
|
+
throw new ExecutableNotFoundError("opencode", PATH);
|
|
9338
|
+
}
|
|
9188
9339
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
9189
9340
|
env: withPath(process.env, PATH),
|
|
9190
9341
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9498,7 +9649,9 @@ var CliAgentAiService = class {
|
|
|
9498
9649
|
spawn: deps.spawn ?? import_child_process5.spawn,
|
|
9499
9650
|
fetch: deps.fetch ?? globalThis.fetch,
|
|
9500
9651
|
resolvePath: deps.resolvePath,
|
|
9501
|
-
platform: deps.platform
|
|
9652
|
+
platform: deps.platform,
|
|
9653
|
+
isDirectory: deps.isDirectory,
|
|
9654
|
+
exists: deps.exists
|
|
9502
9655
|
};
|
|
9503
9656
|
this.makeAdapter = deps.createAdapter ?? defaultAdapter;
|
|
9504
9657
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
@@ -9843,8 +9996,8 @@ var CliAgentAiService = class {
|
|
|
9843
9996
|
this.lastNativeSessionId = previousSessionId;
|
|
9844
9997
|
}
|
|
9845
9998
|
}
|
|
9846
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
9847
|
-
const contextStr = await collectResearchContext(
|
|
9999
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
10000
|
+
const contextStr = await collectResearchContext(fs10, runners);
|
|
9848
10001
|
const researchLog = [
|
|
9849
10002
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
9850
10003
|
];
|
|
@@ -10214,16 +10367,16 @@ function executionSummary(tasks) {
|
|
|
10214
10367
|
}
|
|
10215
10368
|
|
|
10216
10369
|
// src/utils/sessionStore.ts
|
|
10217
|
-
var
|
|
10370
|
+
var fs5 = __toESM(require("fs"));
|
|
10218
10371
|
var path10 = __toESM(require("path"));
|
|
10219
10372
|
|
|
10220
10373
|
// src/utils/fsHelpers.ts
|
|
10221
|
-
var
|
|
10374
|
+
var fs4 = __toESM(require("fs"));
|
|
10222
10375
|
var path9 = __toESM(require("path"));
|
|
10223
10376
|
var STATE_DIR = ".ordewell";
|
|
10224
10377
|
function ensureDir(dir) {
|
|
10225
|
-
if (!
|
|
10226
|
-
|
|
10378
|
+
if (!fs4.existsSync(dir)) {
|
|
10379
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
10227
10380
|
}
|
|
10228
10381
|
}
|
|
10229
10382
|
function getStateDir(baseDir) {
|
|
@@ -10268,16 +10421,16 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
10268
10421
|
};
|
|
10269
10422
|
const filename = sessionFilename(meta);
|
|
10270
10423
|
const filePath = path10.join(sessionsDir, filename);
|
|
10271
|
-
|
|
10424
|
+
fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
10272
10425
|
return meta;
|
|
10273
10426
|
}
|
|
10274
10427
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
10275
10428
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10276
|
-
if (!
|
|
10277
|
-
const files =
|
|
10429
|
+
if (!fs5.existsSync(sessionsDir)) return [];
|
|
10430
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10278
10431
|
return files.map((f) => {
|
|
10279
10432
|
try {
|
|
10280
|
-
const raw =
|
|
10433
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
10281
10434
|
const parsed = JSON.parse(raw);
|
|
10282
10435
|
if (hasScalarRunner(parsed)) {
|
|
10283
10436
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -10292,10 +10445,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
10292
10445
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
10293
10446
|
}
|
|
10294
10447
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
10295
|
-
const files =
|
|
10448
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10296
10449
|
for (const f of files) {
|
|
10297
10450
|
try {
|
|
10298
|
-
const raw =
|
|
10451
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
10299
10452
|
const session = JSON.parse(raw);
|
|
10300
10453
|
if (session.meta.id === sessionId) return f;
|
|
10301
10454
|
} catch (err) {
|
|
@@ -10306,11 +10459,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
10306
10459
|
}
|
|
10307
10460
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10308
10461
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10309
|
-
if (!
|
|
10462
|
+
if (!fs5.existsSync(sessionsDir)) return null;
|
|
10310
10463
|
try {
|
|
10311
10464
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10312
10465
|
if (file) {
|
|
10313
|
-
const raw =
|
|
10466
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
|
|
10314
10467
|
const parsed = JSON.parse(raw);
|
|
10315
10468
|
if (hasScalarRunner(parsed)) {
|
|
10316
10469
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -10340,11 +10493,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
10340
10493
|
}
|
|
10341
10494
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10342
10495
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10343
|
-
if (!
|
|
10496
|
+
if (!fs5.existsSync(sessionsDir)) return false;
|
|
10344
10497
|
try {
|
|
10345
10498
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10346
10499
|
if (file) {
|
|
10347
|
-
|
|
10500
|
+
fs5.unlinkSync(path10.join(sessionsDir, file));
|
|
10348
10501
|
return true;
|
|
10349
10502
|
}
|
|
10350
10503
|
} catch (err) {
|
|
@@ -10354,6 +10507,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
10354
10507
|
}
|
|
10355
10508
|
|
|
10356
10509
|
// src/services/createSession.ts
|
|
10510
|
+
var PlanEditError = class extends Error {
|
|
10511
|
+
constructor(message) {
|
|
10512
|
+
super(message);
|
|
10513
|
+
this.name = "PlanEditError";
|
|
10514
|
+
}
|
|
10515
|
+
};
|
|
10357
10516
|
function sessionRuntimeSettings(settings) {
|
|
10358
10517
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
|
|
10359
10518
|
}
|
|
@@ -10626,12 +10785,41 @@ var Session = class {
|
|
|
10626
10785
|
get isExecuting() {
|
|
10627
10786
|
return this.orchestrator.isRunning;
|
|
10628
10787
|
}
|
|
10788
|
+
/** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
|
|
10789
|
+
get hasLiveWork() {
|
|
10790
|
+
return this.orchestrator.hasLiveWork;
|
|
10791
|
+
}
|
|
10629
10792
|
get status() {
|
|
10630
10793
|
return this.orchestrator.status;
|
|
10631
10794
|
}
|
|
10632
10795
|
get sessionConfig() {
|
|
10633
10796
|
return this.config;
|
|
10634
10797
|
}
|
|
10798
|
+
/**
|
|
10799
|
+
* Deny every parked approval as soon as a planning turn is aborted, for the
|
|
10800
|
+
* same reason `beginFreshPlan` does it: a prompt raised by a turn nobody is
|
|
10801
|
+
* waiting on has no one left to serve, and denying it unblocks the research
|
|
10802
|
+
* loop instead of stranding it. Without this the request sat out its
|
|
10803
|
+
* five-minute timeout and the loop then carried on as if nothing had
|
|
10804
|
+
* happened — the abort was real, but invisible until long after the user
|
|
10805
|
+
* pressed stop.
|
|
10806
|
+
*
|
|
10807
|
+
* The listener is returned as a disposer rather than left attached: callers
|
|
10808
|
+
* own the signal and may reuse it, and a leaked listener would deny the
|
|
10809
|
+
* *next* turn's prompts the moment that stale signal aborted.
|
|
10810
|
+
*/
|
|
10811
|
+
denyApprovalsOnAbort(signal) {
|
|
10812
|
+
if (!signal) return () => {
|
|
10813
|
+
};
|
|
10814
|
+
if (signal.aborted) {
|
|
10815
|
+
this.approvals.clear();
|
|
10816
|
+
return () => {
|
|
10817
|
+
};
|
|
10818
|
+
}
|
|
10819
|
+
const onAbort = () => this.approvals.clear();
|
|
10820
|
+
signal.addEventListener("abort", onAbort);
|
|
10821
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
10822
|
+
}
|
|
10635
10823
|
async startExecution() {
|
|
10636
10824
|
if (!this.plan || !this.store.planTasks.length) throw new Error("No plan to execute");
|
|
10637
10825
|
this.store.clearLog();
|
|
@@ -10654,19 +10842,25 @@ var Session = class {
|
|
|
10654
10842
|
const settings = this.settingsFn();
|
|
10655
10843
|
const { modelAllowlist } = settings;
|
|
10656
10844
|
const modes = plannerModesFrom(settings, this.config.autonomousMode);
|
|
10657
|
-
const
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10845
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
10846
|
+
let plan;
|
|
10847
|
+
try {
|
|
10848
|
+
plan = await this.planner.generate({
|
|
10849
|
+
goal,
|
|
10850
|
+
runners: chosenRunners,
|
|
10851
|
+
modelsByRunner,
|
|
10852
|
+
runnerModes,
|
|
10853
|
+
autonomousDefault: this.config.autonomousMode,
|
|
10854
|
+
fs: this.fsAdapter,
|
|
10855
|
+
fetcher: this.fetcher,
|
|
10856
|
+
onProgress: (p) => this.translateProgress(p),
|
|
10857
|
+
signal: options?.signal,
|
|
10858
|
+
perRunnerAllowlist: modelAllowlist,
|
|
10859
|
+
modes
|
|
10860
|
+
});
|
|
10861
|
+
} finally {
|
|
10862
|
+
releaseAbort();
|
|
10863
|
+
}
|
|
10670
10864
|
this.plan = plan;
|
|
10671
10865
|
this.orchestrator.loadPlan(plan.tasks, plan.runners);
|
|
10672
10866
|
this.store.resetForRun({ preserveCompleted: false });
|
|
@@ -10703,23 +10897,28 @@ var Session = class {
|
|
|
10703
10897
|
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
|
|
10704
10898
|
conversationHistory: [{ role: "user", content: goal, timestamp: now }]
|
|
10705
10899
|
};
|
|
10706
|
-
const
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10900
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
10901
|
+
try {
|
|
10902
|
+
const turn = await this.aiService.startConversation({
|
|
10903
|
+
goal,
|
|
10904
|
+
runners: chosenRunners,
|
|
10905
|
+
modelsByRunner: filteredModels,
|
|
10906
|
+
runnerModes,
|
|
10907
|
+
autonomousDefault: this.config.autonomousMode,
|
|
10908
|
+
grillMeEnabled: grillMeEnabled ?? false,
|
|
10909
|
+
prdEnabled: prdEnabled ?? false,
|
|
10910
|
+
reviewEnabled: reviewEnabled ?? false,
|
|
10911
|
+
verificationEnabled: verificationEnabled ?? false,
|
|
10912
|
+
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
10913
|
+
fs: this.fsAdapter,
|
|
10914
|
+
fetcher: this.fetcher,
|
|
10915
|
+
onProgress: (p) => this.translateProgress(p),
|
|
10916
|
+
signal: options?.signal
|
|
10917
|
+
});
|
|
10918
|
+
return await this.settleTurn(turn, options);
|
|
10919
|
+
} finally {
|
|
10920
|
+
releaseAbort();
|
|
10921
|
+
}
|
|
10723
10922
|
}
|
|
10724
10923
|
/**
|
|
10725
10924
|
* Every subsequent user reply in the planning conversation — grill-me
|
|
@@ -10737,21 +10936,26 @@ var Session = class {
|
|
|
10737
10936
|
${userMessage}` : userMessage;
|
|
10738
10937
|
const aiService = this.aiService;
|
|
10739
10938
|
const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
this.
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10939
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
10940
|
+
try {
|
|
10941
|
+
let turn = canContinueLive ? await aiService.continueConversation(
|
|
10942
|
+
outgoing,
|
|
10943
|
+
(p) => this.translateProgress(p),
|
|
10944
|
+
options?.signal
|
|
10945
|
+
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
10946
|
+
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
10947
|
+
this.queueMessage(userMessage);
|
|
10948
|
+
this.plan.queuedMessages = this.getQueuedMessages();
|
|
10949
|
+
turn = {
|
|
10950
|
+
kind: "message",
|
|
10951
|
+
text: `Execution is running, so I queued your change \u2014 it will be applied between task batches (${this.queuedCount} queued).`,
|
|
10952
|
+
researchLog: turn.researchLog
|
|
10953
|
+
};
|
|
10954
|
+
}
|
|
10955
|
+
return await this.settleTurn(turn, options);
|
|
10956
|
+
} finally {
|
|
10957
|
+
releaseAbort();
|
|
10753
10958
|
}
|
|
10754
|
-
return this.settleTurn(turn, options);
|
|
10755
10959
|
}
|
|
10756
10960
|
/**
|
|
10757
10961
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
@@ -10790,7 +10994,10 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
10790
10994
|
maxRepairs: 2,
|
|
10791
10995
|
onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
|
|
10792
10996
|
});
|
|
10793
|
-
if ("plan" in settled)
|
|
10997
|
+
if ("plan" in settled) {
|
|
10998
|
+
await this.orchestrator.tick();
|
|
10999
|
+
return settled.plan;
|
|
11000
|
+
}
|
|
10794
11001
|
return this.applyConversationTurn(settled.turn);
|
|
10795
11002
|
}
|
|
10796
11003
|
/**
|
|
@@ -10805,9 +11012,9 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
10805
11012
|
const lines = this.store.planTasks.map(
|
|
10806
11013
|
(t) => `#${t.order} id=${t.id} "${t.title}" [${t.status}] runner:${t.assignedRunner}${t.assignedModel ? ` model:${t.assignedModel.modelId}` : ""} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`
|
|
10807
11014
|
);
|
|
10808
|
-
const
|
|
10809
|
-
const execNote = this.
|
|
10810
|
-
Execution is RUNNING${
|
|
11015
|
+
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
11016
|
+
const execNote = this.hasLiveWork ? `
|
|
11017
|
+
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.` : "";
|
|
10811
11018
|
const modelLines = [];
|
|
10812
11019
|
for (const runner of this.plan.runners) {
|
|
10813
11020
|
const models = this.modelsCache[runner] ?? [];
|
|
@@ -10849,8 +11056,12 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
10849
11056
|
this.plan.researchLog = [...this.plan.researchLog ?? [], ...turn.researchLog];
|
|
10850
11057
|
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
10851
11058
|
const coerced = coerceAssignments(result.tasks, allowlist, this.plan.runners, this.modelsCache);
|
|
10852
|
-
this.orchestrator.
|
|
10853
|
-
|
|
11059
|
+
if (this.orchestrator.isRunning) {
|
|
11060
|
+
this.orchestrator.reconcilePlan(coerced, this.plan.runners);
|
|
11061
|
+
} else {
|
|
11062
|
+
this.orchestrator.loadPlan(coerced, this.plan.runners);
|
|
11063
|
+
this.store.resetForRun();
|
|
11064
|
+
}
|
|
10854
11065
|
this.appendTranscript("assistant", content, now);
|
|
10855
11066
|
return true;
|
|
10856
11067
|
},
|
|
@@ -11086,8 +11297,33 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11086
11297
|
notify();
|
|
11087
11298
|
return this.plan;
|
|
11088
11299
|
}
|
|
11089
|
-
|
|
11090
|
-
|
|
11300
|
+
/**
|
|
11301
|
+
* The direct-edit seam: {@link mutatePlan} plus the reschedule every
|
|
11302
|
+
* structural edit owes an armed scheduler. Nothing else wakes one after a
|
|
11303
|
+
* hand edit — the queue-drain path never runs, because a direct edit never
|
|
11304
|
+
* queues — so a task the edit just unblocked would sit ready and never
|
|
11305
|
+
* start. `tick()` no-ops while the scheduler is idle, so this costs nothing
|
|
11306
|
+
* during plain planning. The planner-driven path re-ticks in
|
|
11307
|
+
* {@link settleTurn} instead; it must not tick twice.
|
|
11308
|
+
*/
|
|
11309
|
+
async editPlan(op, notify) {
|
|
11310
|
+
const plan = this.mutatePlan(op, notify);
|
|
11311
|
+
if (!plan) return null;
|
|
11312
|
+
await this.orchestrator.tick();
|
|
11313
|
+
return plan;
|
|
11314
|
+
}
|
|
11315
|
+
/**
|
|
11316
|
+
* Patch one task's fields. A hand-set dependency list is the one patch that
|
|
11317
|
+
* can leave the plan unschedulable, so it goes through the same
|
|
11318
|
+
* {@link canSetDependencies} guard the planner's pickers use rather than a
|
|
11319
|
+
* second copy of the rule — and throws, so the surface can say why.
|
|
11320
|
+
*/
|
|
11321
|
+
async updateTask(taskId, changes) {
|
|
11322
|
+
if (changes.dependencies) {
|
|
11323
|
+
const check = canSetDependencies(this.store.planTasks, taskId, changes.dependencies);
|
|
11324
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those dependencies are not valid");
|
|
11325
|
+
}
|
|
11326
|
+
return this.editPlan(
|
|
11091
11327
|
() => Boolean(this.store.update(taskId, changes)),
|
|
11092
11328
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
11093
11329
|
);
|
|
@@ -11097,8 +11333,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11097
11333
|
* because a runner change is never a single-field edit: the task's model,
|
|
11098
11334
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
11099
11335
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
11100
|
-
* That needs discovery, which is
|
|
11101
|
-
* than a branch inside the sync `updateTask`.
|
|
11336
|
+
* That needs discovery, which is why it is not a branch inside `updateTask`.
|
|
11102
11337
|
*
|
|
11103
11338
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
11104
11339
|
*/
|
|
@@ -11110,7 +11345,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11110
11345
|
const catalog = await this.catalogFor(runner);
|
|
11111
11346
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner));
|
|
11112
11347
|
if (Object.keys(changes).length === 0) return this.plan;
|
|
11113
|
-
return this.
|
|
11348
|
+
return this.editPlan(() => {
|
|
11114
11349
|
if (!this.store.update(taskId, changes)) return false;
|
|
11115
11350
|
this.admitRunner(runner, catalog.models);
|
|
11116
11351
|
return true;
|
|
@@ -11157,24 +11392,29 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11157
11392
|
if (models.length > 0) this.modelsCache = { ...this.modelsCache, [runner]: models };
|
|
11158
11393
|
}
|
|
11159
11394
|
/**
|
|
11160
|
-
* Replace one task's dependency list
|
|
11161
|
-
*
|
|
11162
|
-
*
|
|
11163
|
-
*
|
|
11164
|
-
* rather than each carrying a copy of the rule. Throws so a surface can say
|
|
11165
|
-
* why the edit was refused.
|
|
11395
|
+
* Replace one task's dependency list — the named entry point the surfaces'
|
|
11396
|
+
* dependency pickers call. The guard itself lives in {@link updateTask}, so
|
|
11397
|
+
* a dependency list arriving as a plain field patch is rejected by the same
|
|
11398
|
+
* rule instead of slipping past it.
|
|
11166
11399
|
*/
|
|
11167
|
-
setTaskDependencies(taskId, dependencies) {
|
|
11400
|
+
async setTaskDependencies(taskId, dependencies) {
|
|
11168
11401
|
if (!this.plan) return null;
|
|
11169
|
-
const check = canSetDependencies(this.store.planTasks, taskId, dependencies);
|
|
11170
|
-
if (!check.ok) throw new Error(check.error ?? "Those dependencies are not valid");
|
|
11171
11402
|
return this.updateTask(taskId, { dependencies });
|
|
11172
11403
|
}
|
|
11173
11404
|
async completeTask(taskId) {
|
|
11174
11405
|
await this.markTaskComplete(taskId);
|
|
11175
11406
|
}
|
|
11176
|
-
|
|
11177
|
-
|
|
11407
|
+
/**
|
|
11408
|
+
* Delete one task. A running task is cancelled first: the plan can drop it
|
|
11409
|
+
* either way, but nothing can reach its runner afterwards — the tmux session
|
|
11410
|
+
* outlives the plan and the orchestrator keeps counting it as active. The
|
|
11411
|
+
* planner-driven path refuses instead (see {@link applyTaskOps}); a user
|
|
11412
|
+
* deleting their own task means it.
|
|
11413
|
+
*/
|
|
11414
|
+
async removeTask(taskId) {
|
|
11415
|
+
if (!this.plan || !this.store.get(taskId)) return null;
|
|
11416
|
+
await this.orchestrator.releaseTask(taskId);
|
|
11417
|
+
return this.editPlan(() => (this.store.remove(taskId), true));
|
|
11178
11418
|
}
|
|
11179
11419
|
/**
|
|
11180
11420
|
* Add one task, filling in whatever the caller left unset. A task with no
|
|
@@ -11194,20 +11434,20 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11194
11434
|
const runner = draft.assignedRunner ?? this.plan.runners[0];
|
|
11195
11435
|
const dependencies = (draft.dependencies ?? []).filter((id) => this.store.get(id));
|
|
11196
11436
|
const catalog = draft.type === "user" ? null : await this.catalogFor(runner);
|
|
11197
|
-
return this.
|
|
11437
|
+
return this.editPlan(() => {
|
|
11198
11438
|
this.store.add({ ...draft, ...catalog ? runnerAssignment(this.allowedCatalog(catalog, runner), draft) : {}, assignedRunner: runner, dependencies });
|
|
11199
11439
|
if (catalog) this.admitRunner(runner, catalog.models);
|
|
11200
11440
|
return true;
|
|
11201
11441
|
});
|
|
11202
11442
|
}
|
|
11203
|
-
mergeTasks(taskIdA, taskIdB) {
|
|
11204
|
-
return this.
|
|
11443
|
+
async mergeTasks(taskIdA, taskIdB) {
|
|
11444
|
+
return this.editPlan(() => (this.store.merge(taskIdA, taskIdB), true));
|
|
11205
11445
|
}
|
|
11206
|
-
mergeMultipleTasks(taskIds) {
|
|
11207
|
-
return this.
|
|
11446
|
+
async mergeMultipleTasks(taskIds) {
|
|
11447
|
+
return this.editPlan(() => (this.store.mergeMultiple(taskIds), true));
|
|
11208
11448
|
}
|
|
11209
|
-
splitTask(taskId, newTasks) {
|
|
11210
|
-
return this.
|
|
11449
|
+
async splitTask(taskId, newTasks) {
|
|
11450
|
+
return this.editPlan(() => (this.store.split(taskId, newTasks), true));
|
|
11211
11451
|
}
|
|
11212
11452
|
/**
|
|
11213
11453
|
* Planner-driven merge: validate compatibility up front, then ask the planner
|
|
@@ -12079,13 +12319,13 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
12079
12319
|
};
|
|
12080
12320
|
|
|
12081
12321
|
// src/plugins/RunnerRegistry.ts
|
|
12082
|
-
var
|
|
12322
|
+
var fs7 = __toESM(require("fs"));
|
|
12083
12323
|
var path12 = __toESM(require("path"));
|
|
12084
12324
|
var os5 = __toESM(require("os"));
|
|
12085
12325
|
var import_child_process9 = require("child_process");
|
|
12086
12326
|
|
|
12087
12327
|
// src/plugins/FsPluginStore.ts
|
|
12088
|
-
var
|
|
12328
|
+
var fs6 = __toESM(require("fs"));
|
|
12089
12329
|
var path11 = __toESM(require("path"));
|
|
12090
12330
|
var os4 = __toESM(require("os"));
|
|
12091
12331
|
function userPluginsDir() {
|
|
@@ -12098,9 +12338,9 @@ function isValidManifest(obj) {
|
|
|
12098
12338
|
}
|
|
12099
12339
|
function loadManifestFromDir(dir) {
|
|
12100
12340
|
const manifestPath = path11.join(dir, "manifest.json");
|
|
12101
|
-
if (!
|
|
12341
|
+
if (!fs6.existsSync(manifestPath)) return null;
|
|
12102
12342
|
try {
|
|
12103
|
-
const raw =
|
|
12343
|
+
const raw = fs6.readFileSync(manifestPath, "utf8");
|
|
12104
12344
|
const parsed = JSON.parse(raw);
|
|
12105
12345
|
if (!isValidManifest(parsed)) return null;
|
|
12106
12346
|
return parsed;
|
|
@@ -12110,29 +12350,29 @@ function loadManifestFromDir(dir) {
|
|
|
12110
12350
|
}
|
|
12111
12351
|
function copyDirSync(src, dest) {
|
|
12112
12352
|
ensureDir(dest);
|
|
12113
|
-
const entries =
|
|
12353
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
12114
12354
|
for (const entry of entries) {
|
|
12115
12355
|
const srcPath = path11.join(src, entry.name);
|
|
12116
12356
|
const destPath = path11.join(dest, entry.name);
|
|
12117
12357
|
if (entry.isDirectory()) {
|
|
12118
12358
|
copyDirSync(srcPath, destPath);
|
|
12119
12359
|
} else {
|
|
12120
|
-
|
|
12360
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
12121
12361
|
}
|
|
12122
12362
|
}
|
|
12123
12363
|
}
|
|
12124
12364
|
function rmDirSync(dir) {
|
|
12125
|
-
if (!
|
|
12126
|
-
const entries =
|
|
12365
|
+
if (!fs6.existsSync(dir)) return;
|
|
12366
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
12127
12367
|
for (const entry of entries) {
|
|
12128
12368
|
const p = path11.join(dir, entry.name);
|
|
12129
12369
|
if (entry.isDirectory()) {
|
|
12130
12370
|
rmDirSync(p);
|
|
12131
12371
|
} else {
|
|
12132
|
-
|
|
12372
|
+
fs6.unlinkSync(p);
|
|
12133
12373
|
}
|
|
12134
12374
|
}
|
|
12135
|
-
|
|
12375
|
+
fs6.rmdirSync(dir);
|
|
12136
12376
|
}
|
|
12137
12377
|
var FsPluginStore = class {
|
|
12138
12378
|
getUserPluginsDir() {
|
|
@@ -12140,10 +12380,10 @@ var FsPluginStore = class {
|
|
|
12140
12380
|
}
|
|
12141
12381
|
listUserPluginDirs() {
|
|
12142
12382
|
const dir = userPluginsDir();
|
|
12143
|
-
if (!
|
|
12383
|
+
if (!fs6.existsSync(dir)) return [];
|
|
12144
12384
|
let entries;
|
|
12145
12385
|
try {
|
|
12146
|
-
entries =
|
|
12386
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
12147
12387
|
} catch {
|
|
12148
12388
|
return [];
|
|
12149
12389
|
}
|
|
@@ -12162,20 +12402,20 @@ var FsPluginStore = class {
|
|
|
12162
12402
|
ensureDir(dir);
|
|
12163
12403
|
}
|
|
12164
12404
|
writeFile(filePath, content) {
|
|
12165
|
-
|
|
12405
|
+
fs6.writeFileSync(filePath, content);
|
|
12166
12406
|
}
|
|
12167
12407
|
readFile(filePath) {
|
|
12168
12408
|
try {
|
|
12169
|
-
return
|
|
12409
|
+
return fs6.readFileSync(filePath, "utf8");
|
|
12170
12410
|
} catch {
|
|
12171
12411
|
return null;
|
|
12172
12412
|
}
|
|
12173
12413
|
}
|
|
12174
12414
|
dirExists(path15) {
|
|
12175
|
-
return
|
|
12415
|
+
return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
|
|
12176
12416
|
}
|
|
12177
12417
|
exists(path15) {
|
|
12178
|
-
return
|
|
12418
|
+
return fs6.existsSync(path15);
|
|
12179
12419
|
}
|
|
12180
12420
|
};
|
|
12181
12421
|
|
|
@@ -12644,20 +12884,20 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
12644
12884
|
};
|
|
12645
12885
|
function fsReaddirSync(dir) {
|
|
12646
12886
|
try {
|
|
12647
|
-
return
|
|
12887
|
+
return fs7.readdirSync(dir);
|
|
12648
12888
|
} catch {
|
|
12649
12889
|
return [];
|
|
12650
12890
|
}
|
|
12651
12891
|
}
|
|
12652
12892
|
function fsChmodSync(fpath, mode) {
|
|
12653
12893
|
try {
|
|
12654
|
-
|
|
12894
|
+
fs7.chmodSync(fpath, mode);
|
|
12655
12895
|
} catch {
|
|
12656
12896
|
}
|
|
12657
12897
|
}
|
|
12658
12898
|
|
|
12659
12899
|
// src/utils/stateStore.ts
|
|
12660
|
-
var
|
|
12900
|
+
var fs8 = __toESM(require("fs"));
|
|
12661
12901
|
var path13 = __toESM(require("path"));
|
|
12662
12902
|
var STATE_FILE = "state.json";
|
|
12663
12903
|
function getStatePath(baseDir) {
|
|
@@ -12667,14 +12907,14 @@ function saveState(plan, baseDir) {
|
|
|
12667
12907
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
12668
12908
|
ensureDir(path13.dirname(statePath));
|
|
12669
12909
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
12670
|
-
|
|
12910
|
+
fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
12671
12911
|
}
|
|
12672
12912
|
function loadState(baseDir, logger = defaultLogger) {
|
|
12673
12913
|
const dir = baseDir ?? process.cwd();
|
|
12674
12914
|
const statePath = getStatePath(dir);
|
|
12675
|
-
if (!
|
|
12915
|
+
if (!fs8.existsSync(statePath)) return null;
|
|
12676
12916
|
try {
|
|
12677
|
-
const raw =
|
|
12917
|
+
const raw = fs8.readFileSync(statePath, "utf-8");
|
|
12678
12918
|
const parsed = JSON.parse(raw);
|
|
12679
12919
|
if (!Array.isArray(parsed.runners)) {
|
|
12680
12920
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -12689,15 +12929,15 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
12689
12929
|
function clearState(baseDir) {
|
|
12690
12930
|
const dir = baseDir ?? process.cwd();
|
|
12691
12931
|
const statePath = getStatePath(dir);
|
|
12692
|
-
if (
|
|
12932
|
+
if (fs8.existsSync(statePath)) fs8.unlinkSync(statePath);
|
|
12693
12933
|
}
|
|
12694
12934
|
function stateExists(baseDir) {
|
|
12695
12935
|
const dir = baseDir ?? process.cwd();
|
|
12696
|
-
return
|
|
12936
|
+
return fs8.existsSync(getStatePath(dir));
|
|
12697
12937
|
}
|
|
12698
12938
|
|
|
12699
12939
|
// src/services/SettingsService.ts
|
|
12700
|
-
var
|
|
12940
|
+
var fs9 = __toESM(require("fs"));
|
|
12701
12941
|
var path14 = __toESM(require("path"));
|
|
12702
12942
|
var os6 = __toESM(require("os"));
|
|
12703
12943
|
var DEFAULTS = {
|
|
@@ -12713,6 +12953,17 @@ function getSettingsPath() {
|
|
|
12713
12953
|
if (override && override.trim()) return override.trim();
|
|
12714
12954
|
return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
|
|
12715
12955
|
}
|
|
12956
|
+
function readPlannerModels(raw) {
|
|
12957
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
12958
|
+
const out = {};
|
|
12959
|
+
for (const [provider, entry] of Object.entries(raw)) {
|
|
12960
|
+
if (!entry || typeof entry !== "object") continue;
|
|
12961
|
+
const { model, effort } = entry;
|
|
12962
|
+
if (typeof model !== "string" || !model.trim()) continue;
|
|
12963
|
+
out[provider] = typeof effort === "string" && effort.trim() ? { model, effort } : { model };
|
|
12964
|
+
}
|
|
12965
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
12966
|
+
}
|
|
12716
12967
|
var SettingsService = class {
|
|
12717
12968
|
filePath;
|
|
12718
12969
|
cache = null;
|
|
@@ -12733,7 +12984,7 @@ var SettingsService = class {
|
|
|
12733
12984
|
}
|
|
12734
12985
|
fileMtimeMs() {
|
|
12735
12986
|
try {
|
|
12736
|
-
return
|
|
12987
|
+
return fs9.statSync(this.filePath).mtimeMs;
|
|
12737
12988
|
} catch {
|
|
12738
12989
|
return null;
|
|
12739
12990
|
}
|
|
@@ -12806,6 +13057,26 @@ var SettingsService = class {
|
|
|
12806
13057
|
}
|
|
12807
13058
|
this.persist();
|
|
12808
13059
|
}
|
|
13060
|
+
getPlannerModel(provider) {
|
|
13061
|
+
return this.getAll().plannerModels?.[provider];
|
|
13062
|
+
}
|
|
13063
|
+
setPlannerModel(provider, entry) {
|
|
13064
|
+
this.getAll();
|
|
13065
|
+
if (entry === void 0) {
|
|
13066
|
+
if (this.cache.plannerModels) {
|
|
13067
|
+
delete this.cache.plannerModels[provider];
|
|
13068
|
+
if (Object.keys(this.cache.plannerModels).length === 0) {
|
|
13069
|
+
this.cache.plannerModels = void 0;
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
13072
|
+
} else {
|
|
13073
|
+
if (!this.cache.plannerModels) {
|
|
13074
|
+
this.cache.plannerModels = {};
|
|
13075
|
+
}
|
|
13076
|
+
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
13077
|
+
}
|
|
13078
|
+
this.persist();
|
|
13079
|
+
}
|
|
12809
13080
|
getEnabledRunners() {
|
|
12810
13081
|
return this.getAll().enabledRunners;
|
|
12811
13082
|
}
|
|
@@ -12816,8 +13087,8 @@ var SettingsService = class {
|
|
|
12816
13087
|
}
|
|
12817
13088
|
load() {
|
|
12818
13089
|
try {
|
|
12819
|
-
if (
|
|
12820
|
-
const raw = JSON.parse(
|
|
13090
|
+
if (fs9.existsSync(this.filePath)) {
|
|
13091
|
+
const raw = JSON.parse(fs9.readFileSync(this.filePath, "utf-8"));
|
|
12821
13092
|
const settings = {
|
|
12822
13093
|
grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
|
|
12823
13094
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
@@ -12831,6 +13102,10 @@ var SettingsService = class {
|
|
|
12831
13102
|
if (raw.modelAllowlist !== void 0) {
|
|
12832
13103
|
settings.modelAllowlist = raw.modelAllowlist;
|
|
12833
13104
|
}
|
|
13105
|
+
const plannerModels = readPlannerModels(raw.plannerModels);
|
|
13106
|
+
if (plannerModels) {
|
|
13107
|
+
settings.plannerModels = plannerModels;
|
|
13108
|
+
}
|
|
12834
13109
|
if (Array.isArray(raw.enabledRunners)) {
|
|
12835
13110
|
settings.enabledRunners = raw.enabledRunners.map(String);
|
|
12836
13111
|
}
|
|
@@ -12842,13 +13117,48 @@ var SettingsService = class {
|
|
|
12842
13117
|
}
|
|
12843
13118
|
persist() {
|
|
12844
13119
|
const dir = path14.dirname(this.filePath);
|
|
12845
|
-
if (!
|
|
12846
|
-
|
|
13120
|
+
if (!fs9.existsSync(dir)) {
|
|
13121
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
12847
13122
|
}
|
|
12848
|
-
|
|
13123
|
+
fs9.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
12849
13124
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
12850
13125
|
}
|
|
12851
13126
|
};
|
|
13127
|
+
|
|
13128
|
+
// src/services/PlannerModelMemory.ts
|
|
13129
|
+
var PlannerModelMemory = class {
|
|
13130
|
+
constructor(store) {
|
|
13131
|
+
this.store = store;
|
|
13132
|
+
}
|
|
13133
|
+
store;
|
|
13134
|
+
/** Record what the user picked for `provider`; a blank model forgets it instead. */
|
|
13135
|
+
remember(provider, model, effort) {
|
|
13136
|
+
const id = model.trim();
|
|
13137
|
+
if (!id) {
|
|
13138
|
+
this.store.setPlannerModel(provider, void 0);
|
|
13139
|
+
return;
|
|
13140
|
+
}
|
|
13141
|
+
const level = effort?.trim();
|
|
13142
|
+
this.store.setPlannerModel(provider, level ? { model: id, effort: level } : { model: id });
|
|
13143
|
+
}
|
|
13144
|
+
/**
|
|
13145
|
+
* What the model and effort should become when `provider` takes over the
|
|
13146
|
+
* planner, given the models it actually serves. The `source` is what the
|
|
13147
|
+
* three surfaces word their notice from — restored, defaulted, or nothing.
|
|
13148
|
+
*/
|
|
13149
|
+
recall(provider, catalog) {
|
|
13150
|
+
const remembered = this.store.getPlannerModel(provider);
|
|
13151
|
+
const match = remembered && catalog.find((c) => c.id === remembered.model);
|
|
13152
|
+
if (remembered && match) {
|
|
13153
|
+
const level = remembered.effort ?? "";
|
|
13154
|
+
const survives = (match.variants ?? []).some((v) => v.id === level);
|
|
13155
|
+
return { model: match.id, effort: survives ? level : "", source: "remembered" };
|
|
13156
|
+
}
|
|
13157
|
+
const first = catalog[0];
|
|
13158
|
+
if (first) return { model: first.id, effort: "", source: "catalog-default" };
|
|
13159
|
+
return { model: "", effort: "", source: "none" };
|
|
13160
|
+
}
|
|
13161
|
+
};
|
|
12852
13162
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12853
13163
|
0 && (module.exports = {
|
|
12854
13164
|
ALL_PROVIDERS,
|
|
@@ -12872,6 +13182,7 @@ var SettingsService = class {
|
|
|
12872
13182
|
DENY_ALL,
|
|
12873
13183
|
EmbeddedNewlineError,
|
|
12874
13184
|
EnvConfig,
|
|
13185
|
+
ExecutableNotFoundError,
|
|
12875
13186
|
FsPluginStore,
|
|
12876
13187
|
GIT_READONLY_SUBCOMMANDS,
|
|
12877
13188
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
@@ -12892,14 +13203,17 @@ var SettingsService = class {
|
|
|
12892
13203
|
PROVIDER_PRIORITY,
|
|
12893
13204
|
PROVIDER_SHORT_LABEL,
|
|
12894
13205
|
PendingApprovals,
|
|
13206
|
+
PlanEditError,
|
|
12895
13207
|
PlanParseError,
|
|
12896
13208
|
PlanStore,
|
|
12897
13209
|
Planner,
|
|
13210
|
+
PlannerModelMemory,
|
|
12898
13211
|
REFUSED_COMMANDS,
|
|
12899
13212
|
RunnerInstallation,
|
|
12900
13213
|
RunnerRegistry,
|
|
12901
13214
|
SEARCH_EXCLUSIONS,
|
|
12902
13215
|
STATE_DIR,
|
|
13216
|
+
STOPPED_TOOL_RESULT,
|
|
12903
13217
|
SYMBOL_LANGUAGES,
|
|
12904
13218
|
Session,
|
|
12905
13219
|
SettingsService,
|
|
@@ -12910,9 +13224,11 @@ var SettingsService = class {
|
|
|
12910
13224
|
TmuxRunner,
|
|
12911
13225
|
VerdictEngine,
|
|
12912
13226
|
WINDOWS_MAX_COMMAND_LINE,
|
|
13227
|
+
WorkspaceNotFoundError,
|
|
12913
13228
|
addTaskToPlan,
|
|
12914
13229
|
applyHeadLimit,
|
|
12915
13230
|
applyTaskOps,
|
|
13231
|
+
assertWorkspaceExists,
|
|
12916
13232
|
augmentPromptWithPriorOutputs,
|
|
12917
13233
|
augmentedPath,
|
|
12918
13234
|
buildConversationSystemPrompt,
|
|
@@ -12986,6 +13302,7 @@ var SettingsService = class {
|
|
|
12986
13302
|
hasTmux,
|
|
12987
13303
|
includeGlobFor,
|
|
12988
13304
|
isCliProvider,
|
|
13305
|
+
isExecutableResolved,
|
|
12989
13306
|
isOpenAiProvider,
|
|
12990
13307
|
killTree,
|
|
12991
13308
|
knownModelId,
|