@ordewell/core 0.4.4 → 0.4.6
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 +254 -40
- package/dist/index.d.ts +254 -40
- package/dist/index.js +668 -306
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +662 -308
- 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,
|
|
@@ -124,6 +130,7 @@ __export(index_exports, {
|
|
|
124
130
|
clearAugmentedPathCache: () => clearAugmentedPathCache,
|
|
125
131
|
clearResearchShellCache: () => clearResearchShellCache,
|
|
126
132
|
clearState: () => clearState,
|
|
133
|
+
clipboardCopyCommand: () => clipboardCopyCommand,
|
|
127
134
|
coerceAssignments: () => coerceAssignments,
|
|
128
135
|
collectDirectDependencyOutputs: () => collectDirectDependencyOutputs,
|
|
129
136
|
collectProviderCredentials: () => collectProviderCredentials,
|
|
@@ -164,6 +171,7 @@ __export(index_exports, {
|
|
|
164
171
|
hasTmux: () => hasTmux,
|
|
165
172
|
includeGlobFor: () => includeGlobFor,
|
|
166
173
|
isCliProvider: () => isCliProvider,
|
|
174
|
+
isExecutableResolved: () => isExecutableResolved,
|
|
167
175
|
isOpenAiProvider: () => isOpenAiProvider,
|
|
168
176
|
killTree: () => killTree,
|
|
169
177
|
knownModelId: () => knownModelId,
|
|
@@ -818,6 +826,16 @@ function dialectFor(dialect) {
|
|
|
818
826
|
const resolved = dialect ?? (process.platform === "win32" ? "cmd" : "posix");
|
|
819
827
|
return resolved === "cmd" ? CMD_DIALECT : POSIX_DIALECT;
|
|
820
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
|
+
}
|
|
821
839
|
function matchParen(s, open) {
|
|
822
840
|
let depth = 0;
|
|
823
841
|
for (let i = open; i < s.length; i++) {
|
|
@@ -828,7 +846,7 @@ function matchParen(s, open) {
|
|
|
828
846
|
}
|
|
829
847
|
function lex(command, nested, dialect) {
|
|
830
848
|
const segments = [];
|
|
831
|
-
let
|
|
849
|
+
let unsafeRedirect;
|
|
832
850
|
let processSubstitution = false;
|
|
833
851
|
let unbalanced = false;
|
|
834
852
|
let tokens = [];
|
|
@@ -837,7 +855,19 @@ function lex(command, nested, dialect) {
|
|
|
837
855
|
let started = false;
|
|
838
856
|
let piped = false;
|
|
839
857
|
let quote = "";
|
|
840
|
-
|
|
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
|
+
}
|
|
841
871
|
if (started) tokens.push(current);
|
|
842
872
|
current = "";
|
|
843
873
|
started = false;
|
|
@@ -925,9 +955,34 @@ function lex(command, nested, dialect) {
|
|
|
925
955
|
i += 2;
|
|
926
956
|
continue;
|
|
927
957
|
}
|
|
928
|
-
|
|
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] === ">") {
|
|
929
981
|
endToken();
|
|
930
|
-
|
|
982
|
+
const doubled = command[i + 2] === ">";
|
|
983
|
+
redirectTargetMode = true;
|
|
984
|
+
redirectOperator = doubled ? "&>>" : "&>";
|
|
985
|
+
i += doubled ? 3 : 2;
|
|
931
986
|
continue;
|
|
932
987
|
}
|
|
933
988
|
if (c === "(" || c === ")") {
|
|
@@ -947,7 +1002,7 @@ function lex(command, nested, dialect) {
|
|
|
947
1002
|
continue;
|
|
948
1003
|
}
|
|
949
1004
|
if (/\s/.test(c)) {
|
|
950
|
-
endToken();
|
|
1005
|
+
endToken(false);
|
|
951
1006
|
i++;
|
|
952
1007
|
continue;
|
|
953
1008
|
}
|
|
@@ -957,7 +1012,7 @@ function lex(command, nested, dialect) {
|
|
|
957
1012
|
}
|
|
958
1013
|
if (quote !== "") unbalanced = true;
|
|
959
1014
|
endSegment(false);
|
|
960
|
-
return { segments,
|
|
1015
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
961
1016
|
}
|
|
962
1017
|
function binaryName(token, dialect) {
|
|
963
1018
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -977,7 +1032,7 @@ function lexAll(command, dialect) {
|
|
|
977
1032
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
978
1033
|
const inner = lex(queue.shift(), queue, dialect);
|
|
979
1034
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
980
|
-
all.
|
|
1035
|
+
all.unsafeRedirect ??= inner.unsafeRedirect;
|
|
981
1036
|
all.processSubstitution ||= inner.processSubstitution;
|
|
982
1037
|
all.unbalanced ||= inner.unbalanced;
|
|
983
1038
|
}
|
|
@@ -1026,8 +1081,9 @@ function refusalFor(seg) {
|
|
|
1026
1081
|
if (seg.binary === "git") {
|
|
1027
1082
|
const sub = seg.args.find((a) => !a.startsWith("-"));
|
|
1028
1083
|
if (sub === "branch" || sub === "tag") {
|
|
1029
|
-
|
|
1030
|
-
|
|
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.`;
|
|
1031
1087
|
}
|
|
1032
1088
|
}
|
|
1033
1089
|
}
|
|
@@ -1037,8 +1093,9 @@ function refusalFor(seg) {
|
|
|
1037
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.`;
|
|
1038
1094
|
}
|
|
1039
1095
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
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.`;
|
|
1042
1099
|
}
|
|
1043
1100
|
if (seg.binary === "awk" && seg.args.some((a) => a === "inplace")) {
|
|
1044
1101
|
return `"awk -i inplace" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
@@ -1057,7 +1114,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
1057
1114
|
const trimmed = command.trim();
|
|
1058
1115
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
1059
1116
|
const dialect = dialectFor(opts.dialect);
|
|
1060
|
-
const { segments,
|
|
1117
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
1061
1118
|
if (unbalanced) {
|
|
1062
1119
|
return {
|
|
1063
1120
|
tier: "refuse",
|
|
@@ -1072,11 +1129,12 @@ function classifyCommand(command, opts = {}) {
|
|
|
1072
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."
|
|
1073
1130
|
};
|
|
1074
1131
|
}
|
|
1075
|
-
if (
|
|
1132
|
+
if (unsafeRedirect) {
|
|
1133
|
+
const written = unsafeRedirect.target ? `"${unsafeRedirect.operator} ${unsafeRedirect.target}"` : `"${unsafeRedirect.operator}"`;
|
|
1076
1134
|
return {
|
|
1077
1135
|
tier: "refuse",
|
|
1078
1136
|
scope: "",
|
|
1079
|
-
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.`
|
|
1080
1138
|
};
|
|
1081
1139
|
}
|
|
1082
1140
|
if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
|
|
@@ -1396,7 +1454,7 @@ ${refs.output.trim()}`);
|
|
|
1396
1454
|
* anything else asks once and is remembered, and writes are refused outright
|
|
1397
1455
|
* because a planner that mutates the workspace has stopped being a planner.
|
|
1398
1456
|
*/
|
|
1399
|
-
async bash(command) {
|
|
1457
|
+
async bash(command, signal) {
|
|
1400
1458
|
const { tier, scope, reason } = classifyCommand(command, { dialect: this.researchShell.dialect });
|
|
1401
1459
|
if (tier === "refuse") {
|
|
1402
1460
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
@@ -1419,7 +1477,7 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
1419
1477
|
};
|
|
1420
1478
|
}
|
|
1421
1479
|
}
|
|
1422
|
-
const outcome = await this.execBashImpl(command);
|
|
1480
|
+
const outcome = await this.execBashImpl(command, signal);
|
|
1423
1481
|
if (outcome.success) return outcome;
|
|
1424
1482
|
const warning = researchShellWarning(this.researchShell);
|
|
1425
1483
|
return warning ? { ...outcome, output: `${outcome.output}
|
|
@@ -1428,6 +1486,82 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
1428
1486
|
}
|
|
1429
1487
|
};
|
|
1430
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
|
+
|
|
1431
1565
|
// src/services/ApprovalPolicy.ts
|
|
1432
1566
|
function scopeMatches(pattern, scope) {
|
|
1433
1567
|
if (pattern === scope) return true;
|
|
@@ -4314,8 +4448,8 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
4314
4448
|
|
|
4315
4449
|
// src/services/ContextCollector.ts
|
|
4316
4450
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
4317
|
-
async function collectResearchContext(
|
|
4318
|
-
const collector = new ContextCollector(
|
|
4451
|
+
async function collectResearchContext(fs10, runners) {
|
|
4452
|
+
const collector = new ContextCollector(fs10);
|
|
4319
4453
|
let contextStr = "";
|
|
4320
4454
|
let firstContext = null;
|
|
4321
4455
|
for (const r of runners) {
|
|
@@ -4342,8 +4476,8 @@ ${firstContext.dirStructure}
|
|
|
4342
4476
|
return contextStr;
|
|
4343
4477
|
}
|
|
4344
4478
|
var ContextCollector = class {
|
|
4345
|
-
constructor(
|
|
4346
|
-
this.fs =
|
|
4479
|
+
constructor(fs10, registry) {
|
|
4480
|
+
this.fs = fs10;
|
|
4347
4481
|
this.registry = registry;
|
|
4348
4482
|
}
|
|
4349
4483
|
fs;
|
|
@@ -4379,80 +4513,6 @@ var ContextCollector = class {
|
|
|
4379
4513
|
}
|
|
4380
4514
|
};
|
|
4381
4515
|
|
|
4382
|
-
// src/services/executeTool.ts
|
|
4383
|
-
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
4384
|
-
function num(value) {
|
|
4385
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4386
|
-
}
|
|
4387
|
-
function bool(value) {
|
|
4388
|
-
return typeof value === "boolean" ? value : void 0;
|
|
4389
|
-
}
|
|
4390
|
-
function str(value) {
|
|
4391
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4392
|
-
}
|
|
4393
|
-
function grepOptions(args) {
|
|
4394
|
-
const mode = str(args.output_mode ?? args.outputMode);
|
|
4395
|
-
return {
|
|
4396
|
-
include: str(args.include),
|
|
4397
|
-
path: str(args.path),
|
|
4398
|
-
outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
|
|
4399
|
-
contextBefore: num(args.context_before ?? args.contextBefore),
|
|
4400
|
-
contextAfter: num(args.context_after ?? args.contextAfter),
|
|
4401
|
-
literal: bool(args.literal),
|
|
4402
|
-
caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
|
|
4403
|
-
headLimit: num(args.head_limit ?? args.headLimit)
|
|
4404
|
-
};
|
|
4405
|
-
}
|
|
4406
|
-
async function executeTool(name, args, fs9, fetcher) {
|
|
4407
|
-
switch (name) {
|
|
4408
|
-
case "read_file": {
|
|
4409
|
-
const offset = num(args.offset);
|
|
4410
|
-
const limit = num(args.limit);
|
|
4411
|
-
const maxBytes = num(args.maxBytes);
|
|
4412
|
-
return fs9.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
4413
|
-
}
|
|
4414
|
-
case "read_files": {
|
|
4415
|
-
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
4416
|
-
return fs9.readFiles(paths);
|
|
4417
|
-
}
|
|
4418
|
-
case "glob":
|
|
4419
|
-
return fs9.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
4420
|
-
case "grep":
|
|
4421
|
-
return fs9.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
4422
|
-
case "find_symbol":
|
|
4423
|
-
return fs9.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
4424
|
-
case "list_dir":
|
|
4425
|
-
return fs9.listDir(String(args.path ?? ""), num(args.depth));
|
|
4426
|
-
case "bash":
|
|
4427
|
-
return fs9.bash(String(args.command ?? ""));
|
|
4428
|
-
case "fetch": {
|
|
4429
|
-
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
4430
|
-
const url = String(args.url ?? "");
|
|
4431
|
-
const allowed = await fetcher.confirm(url);
|
|
4432
|
-
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
4433
|
-
return fetcher.fetch(url);
|
|
4434
|
-
}
|
|
4435
|
-
case "web_search": {
|
|
4436
|
-
if (!fetcher?.search) {
|
|
4437
|
-
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 };
|
|
4438
|
-
}
|
|
4439
|
-
const query = String(args.query ?? "").trim();
|
|
4440
|
-
if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
|
|
4441
|
-
return fetcher.search(query);
|
|
4442
|
-
}
|
|
4443
|
-
// Budget models hallucinate two kinds of tools: plan-committing ones
|
|
4444
|
-
// (create_task, submit_plan) and implementation ones (create_file,
|
|
4445
|
-
// write_file). Steer both back: you are a planner, tasks run later,
|
|
4446
|
-
// and the commit channel is raw JSON in the reply text.
|
|
4447
|
-
default:
|
|
4448
|
-
return {
|
|
4449
|
-
success: false,
|
|
4450
|
-
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).`,
|
|
4451
|
-
truncated: false
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
|
|
4456
4516
|
// src/services/researchStepSummary.ts
|
|
4457
4517
|
function ellipsize(text, max) {
|
|
4458
4518
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
@@ -4534,23 +4594,23 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
4534
4594
|
function refused(output) {
|
|
4535
4595
|
return { success: false, output, truncated: false };
|
|
4536
4596
|
}
|
|
4537
|
-
function nonPromptingFs(
|
|
4538
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
4597
|
+
function nonPromptingFs(fs10) {
|
|
4598
|
+
const withinWorkspace = (p) => resolveWithin(fs10.getWorkspaceRoot(), p ?? ".").inside;
|
|
4539
4599
|
const outsideRefusal = (p) => refused(
|
|
4540
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.`
|
|
4541
4601
|
);
|
|
4542
4602
|
return {
|
|
4543
|
-
getWorkspaceRoot: () =>
|
|
4544
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
4603
|
+
getWorkspaceRoot: () => fs10.getWorkspaceRoot(),
|
|
4604
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs10.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
4545
4605
|
readFiles: (paths) => {
|
|
4546
4606
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
4547
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
4607
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs10.readFiles(paths);
|
|
4548
4608
|
},
|
|
4549
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4550
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
4551
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
4552
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
4553
|
-
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) => {
|
|
4554
4614
|
const dialect = resolveResearchShell().dialect;
|
|
4555
4615
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
4556
4616
|
if (tier !== "auto") {
|
|
@@ -4560,13 +4620,13 @@ function nonPromptingFs(fs9) {
|
|
|
4560
4620
|
}
|
|
4561
4621
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
4562
4622
|
if (escaping) return outsideRefusal(escaping);
|
|
4563
|
-
return
|
|
4623
|
+
return fs10.bash(command, signal);
|
|
4564
4624
|
}
|
|
4565
4625
|
};
|
|
4566
4626
|
}
|
|
4567
4627
|
async function runLoop(prompt, deps) {
|
|
4568
4628
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
4569
|
-
const
|
|
4629
|
+
const fs10 = nonPromptingFs(deps.fs);
|
|
4570
4630
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
4571
4631
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
4572
4632
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -4583,7 +4643,7 @@ ${turn.text}`;
|
|
|
4583
4643
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
4584
4644
|
const toolArgs = JSON.stringify(tc.args);
|
|
4585
4645
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
4586
|
-
const res = await executeTool(tc.name, args,
|
|
4646
|
+
const res = await executeTool(tc.name, args, fs10, void 0, deps.signal);
|
|
4587
4647
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
4588
4648
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
4589
4649
|
const stepEntry = {
|
|
@@ -4698,7 +4758,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
4698
4758
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
4699
4759
|
* never fail a plan or a turn.
|
|
4700
4760
|
*/
|
|
4701
|
-
async executeSpawnAgent(args,
|
|
4761
|
+
async executeSpawnAgent(args, fs10, onProgress, signal, subagentId) {
|
|
4702
4762
|
if (!this.researchSubagentsEnabled) {
|
|
4703
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 };
|
|
4704
4764
|
}
|
|
@@ -4712,20 +4772,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
4712
4772
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
4713
4773
|
return chat;
|
|
4714
4774
|
},
|
|
4715
|
-
fs:
|
|
4775
|
+
fs: fs10,
|
|
4716
4776
|
signal,
|
|
4717
4777
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
4718
4778
|
});
|
|
4719
4779
|
}
|
|
4720
4780
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
4721
|
-
static async collectResearchContext(
|
|
4722
|
-
return collectResearchContext(
|
|
4781
|
+
static async collectResearchContext(fs10, runners) {
|
|
4782
|
+
return collectResearchContext(fs10, runners);
|
|
4723
4783
|
}
|
|
4724
4784
|
/**
|
|
4725
4785
|
* Execute one round of tool calls and report each through onProgress.
|
|
4726
4786
|
* Returns the results to feed back plus the log entries produced.
|
|
4727
4787
|
*/
|
|
4728
|
-
async executeToolCalls(toolCalls,
|
|
4788
|
+
async executeToolCalls(toolCalls, fs10, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
4729
4789
|
const toolResults = [];
|
|
4730
4790
|
const logEntries = [];
|
|
4731
4791
|
let resultsText = "";
|
|
@@ -4744,18 +4804,18 @@ var BaseAiService = class _BaseAiService {
|
|
|
4744
4804
|
const subagentId = (0, import_uuid2.v4)();
|
|
4745
4805
|
spawnSubagentIds.set(i, subagentId);
|
|
4746
4806
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
4747
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
4807
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
|
|
4748
4808
|
}) : Promise.resolve(),
|
|
4749
4809
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
4750
4810
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
4751
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
4811
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs10, fetcher, signal));
|
|
4752
4812
|
}) : Promise.resolve()
|
|
4753
4813
|
]);
|
|
4754
4814
|
for (const [index, tc] of toolCalls.entries()) {
|
|
4755
4815
|
const toolArgs = normalizedArgs[index];
|
|
4756
4816
|
const preResult = precomputed.get(index);
|
|
4757
4817
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
4758
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
4818
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs10, fetcher, signal);
|
|
4759
4819
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
4760
4820
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
4761
4821
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -4941,7 +5001,7 @@ ${llmOutput}
|
|
|
4941
5001
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
4942
5002
|
* exhausted without a valid plan.
|
|
4943
5003
|
*/
|
|
4944
|
-
async runResearchLoop(chat, firstMessage,
|
|
5004
|
+
async runResearchLoop(chat, firstMessage, fs10, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
4945
5005
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
4946
5006
|
if (!initialLog && userGoal) {
|
|
4947
5007
|
researchLog.push({
|
|
@@ -4968,7 +5028,7 @@ ${llmOutput}
|
|
|
4968
5028
|
if (!turn.hasToolCalls) break;
|
|
4969
5029
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
4970
5030
|
turn.toolCalls,
|
|
4971
|
-
|
|
5031
|
+
fs10,
|
|
4972
5032
|
onProgress,
|
|
4973
5033
|
fetcher,
|
|
4974
5034
|
step,
|
|
@@ -5204,11 +5264,11 @@ ${repairHint}` }] : parts;
|
|
|
5204
5264
|
return this.consumePlanStream(result, onToken);
|
|
5205
5265
|
}
|
|
5206
5266
|
// --- IAiService implementation ---
|
|
5207
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5267
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5208
5268
|
this.ensureInit();
|
|
5209
5269
|
const { autonomousDefault } = modes;
|
|
5210
5270
|
const genModel = this.getPlanningModel();
|
|
5211
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
5271
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
5212
5272
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5213
5273
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
5214
5274
|
|
|
@@ -5219,17 +5279,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
5219
5279
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
5220
5280
|
});
|
|
5221
5281
|
const researchChat = new GeminiResearchChat(chat);
|
|
5222
|
-
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);
|
|
5223
5283
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5224
5284
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5225
5285
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5226
5286
|
}
|
|
5227
|
-
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) {
|
|
5228
5288
|
this.ensureInit();
|
|
5229
5289
|
const { autonomousDefault } = modes;
|
|
5230
5290
|
let ctx = "";
|
|
5231
|
-
if (
|
|
5232
|
-
const collected = await new ContextCollector(
|
|
5291
|
+
if (fs10) {
|
|
5292
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
5233
5293
|
if (collected.aiflowContext) ctx = `
|
|
5234
5294
|
<aiflow_context>
|
|
5235
5295
|
${collected.aiflowContext}
|
|
@@ -5245,12 +5305,12 @@ ${collected.aiflowContext}
|
|
|
5245
5305
|
autonomousDefault
|
|
5246
5306
|
);
|
|
5247
5307
|
}
|
|
5248
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5308
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
5249
5309
|
this.ensureInit();
|
|
5250
5310
|
const { autonomousDefault } = modes;
|
|
5251
5311
|
let aiflowContext;
|
|
5252
|
-
if (
|
|
5253
|
-
const collected = await new ContextCollector(
|
|
5312
|
+
if (fs10) {
|
|
5313
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5254
5314
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5255
5315
|
}
|
|
5256
5316
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -5490,12 +5550,12 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
5490
5550
|
}
|
|
5491
5551
|
}
|
|
5492
5552
|
// --- IAiService implementation ---
|
|
5493
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
5553
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5494
5554
|
this.ensureInit();
|
|
5495
5555
|
const client = this.getClient();
|
|
5496
5556
|
const { autonomousDefault, researchSubagents: researchSubagentsEnabled } = modes;
|
|
5497
5557
|
this.researchSubagentsEnabled = researchSubagentsEnabled;
|
|
5498
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
5558
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
5499
5559
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
5500
5560
|
const userText = `User goal: ${userDescription}`;
|
|
5501
5561
|
const firstMessage = `${buildResearchToolsPrompt(researchSubagentsEnabled)}
|
|
@@ -5513,17 +5573,17 @@ ${userText}`;
|
|
|
5513
5573
|
toOpenAiTools(researchSubagentsEnabled),
|
|
5514
5574
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
5515
5575
|
);
|
|
5516
|
-
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);
|
|
5517
5577
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
5518
5578
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
5519
5579
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
5520
5580
|
}
|
|
5521
|
-
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) {
|
|
5522
5582
|
this.ensureInit();
|
|
5523
5583
|
const { autonomousDefault } = modes;
|
|
5524
5584
|
let ctx = "";
|
|
5525
|
-
if (
|
|
5526
|
-
const collected = await new ContextCollector(
|
|
5585
|
+
if (fs10) {
|
|
5586
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
5527
5587
|
if (collected.aiflowContext) ctx = `
|
|
5528
5588
|
<aiflow_context>
|
|
5529
5589
|
${collected.aiflowContext}
|
|
@@ -5539,12 +5599,12 @@ ${collected.aiflowContext}
|
|
|
5539
5599
|
autonomousDefault
|
|
5540
5600
|
);
|
|
5541
5601
|
}
|
|
5542
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
5602
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
5543
5603
|
this.ensureInit();
|
|
5544
5604
|
const { autonomousDefault } = modes;
|
|
5545
5605
|
let aiflowContext;
|
|
5546
|
-
if (
|
|
5547
|
-
const collected = await new ContextCollector(
|
|
5606
|
+
if (fs10) {
|
|
5607
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
5548
5608
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
5549
5609
|
}
|
|
5550
5610
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -6162,6 +6222,7 @@ var PlanStore = class {
|
|
|
6162
6222
|
}
|
|
6163
6223
|
remove(taskId) {
|
|
6164
6224
|
if (!this._taskMap.has(taskId)) return;
|
|
6225
|
+
this.unblockDependents(taskId);
|
|
6165
6226
|
this._planTasks = removeTaskFromPlan(this._planTasks, taskId);
|
|
6166
6227
|
this.rebuild();
|
|
6167
6228
|
this.pruneTerminalSets();
|
|
@@ -6216,9 +6277,7 @@ ${taskB.description}`,
|
|
|
6216
6277
|
const userIds = /* @__PURE__ */ new Set([taskIdA, taskIdB]);
|
|
6217
6278
|
const sanitized = this._planTasks.filter((t) => !userIds.has(t.id)).map((t) => ({
|
|
6218
6279
|
...t,
|
|
6219
|
-
dependencies: t.dependencies.map(
|
|
6220
|
-
(depId) => depId === taskIdA || depId === taskIdB ? mergedId : depId
|
|
6221
|
-
)
|
|
6280
|
+
dependencies: [...new Set(t.dependencies.map((depId) => userIds.has(depId) ? mergedId : depId))]
|
|
6222
6281
|
}));
|
|
6223
6282
|
this._planTasks = renumberTasks([...sanitized, merged]);
|
|
6224
6283
|
this.rebuild();
|
|
@@ -6230,11 +6289,12 @@ ${taskB.description}`,
|
|
|
6230
6289
|
const original = this._taskMap.get(taskId);
|
|
6231
6290
|
if (!original) throw new Error(`Task ${taskId} not found`);
|
|
6232
6291
|
if (!newTaskSpecs.length) throw new Error("Must provide at least one new task spec");
|
|
6233
|
-
const
|
|
6234
|
-
|
|
6292
|
+
const finalized = [];
|
|
6293
|
+
newTaskSpecs.forEach((spec, i) => {
|
|
6294
|
+
finalized.push(createTask({
|
|
6235
6295
|
...spec,
|
|
6236
6296
|
order: original.order + i,
|
|
6237
|
-
dependencies: i === 0 ? [...original.dependencies] : [
|
|
6297
|
+
dependencies: i === 0 ? [...original.dependencies] : [finalized[i - 1].id],
|
|
6238
6298
|
type: spec.type ?? original.type,
|
|
6239
6299
|
assignedRunner: spec.assignedRunner ?? original.assignedRunner,
|
|
6240
6300
|
assignedModel: spec.assignedModel ?? original.assignedModel,
|
|
@@ -6242,17 +6302,7 @@ ${taskB.description}`,
|
|
|
6242
6302
|
autonomy: spec.autonomy ?? original.autonomy,
|
|
6243
6303
|
sliceType: spec.sliceType ?? original.sliceType,
|
|
6244
6304
|
userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered
|
|
6245
|
-
})
|
|
6246
|
-
);
|
|
6247
|
-
const newTaskIds = /* @__PURE__ */ new Map();
|
|
6248
|
-
newTasks.forEach((t, i) => newTaskIds.set(i, t.id));
|
|
6249
|
-
const finalized = newTasks.map((t) => {
|
|
6250
|
-
const deps = t.dependencies.map((depId) => {
|
|
6251
|
-
const idx = newTaskSpecs.findIndex((s) => (s.id ?? "") === depId);
|
|
6252
|
-
if (idx >= 0) return newTaskIds.get(idx) ?? depId;
|
|
6253
|
-
return depId;
|
|
6254
|
-
});
|
|
6255
|
-
return { ...t, dependencies: deps };
|
|
6305
|
+
}));
|
|
6256
6306
|
});
|
|
6257
6307
|
const tailId = finalized[finalized.length - 1].id;
|
|
6258
6308
|
const updatedOthers = this._planTasks.filter((t) => t.id !== taskId).map((t) => ({
|
|
@@ -6497,6 +6547,15 @@ var TaskOrchestrator = class {
|
|
|
6497
6547
|
get isRunning() {
|
|
6498
6548
|
return this.running || this.startingTaskIds.size > 0 || this.activeTaskSessions.size > 0;
|
|
6499
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
|
+
}
|
|
6500
6559
|
get isReviewApproved() {
|
|
6501
6560
|
return this.reviewApproved;
|
|
6502
6561
|
}
|
|
@@ -6543,18 +6602,31 @@ var TaskOrchestrator = class {
|
|
|
6543
6602
|
this.retryCounts.clear();
|
|
6544
6603
|
this.onHold.clear();
|
|
6545
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
|
+
*/
|
|
6546
6616
|
reconcilePlan(newTasks, planRunners = ["claude-code"]) {
|
|
6617
|
+
const adopted = [...newTasks];
|
|
6547
6618
|
for (const taskId of this.activeTaskSessions.keys()) {
|
|
6548
6619
|
const task = this.store.get(taskId);
|
|
6549
6620
|
if (!task) continue;
|
|
6550
|
-
const
|
|
6551
|
-
if (
|
|
6552
|
-
|
|
6553
|
-
} else if (
|
|
6554
|
-
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" };
|
|
6555
6627
|
}
|
|
6556
6628
|
}
|
|
6557
|
-
this.store.load(
|
|
6629
|
+
this.store.load(adopted, planRunners);
|
|
6558
6630
|
this.planStatus = "running";
|
|
6559
6631
|
}
|
|
6560
6632
|
async start() {
|
|
@@ -6675,6 +6747,21 @@ ${output.slice(-3e3)}`);
|
|
|
6675
6747
|
this.emit("onTaskChanged");
|
|
6676
6748
|
await this.tick();
|
|
6677
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
|
+
}
|
|
6678
6765
|
async markTaskComplete(taskId) {
|
|
6679
6766
|
const task = this.store.get(taskId);
|
|
6680
6767
|
if (!task || task.status === "completed") return;
|
|
@@ -7134,28 +7221,28 @@ function posixBinDirs() {
|
|
|
7134
7221
|
];
|
|
7135
7222
|
}
|
|
7136
7223
|
function windowsBinDirs(env, home) {
|
|
7137
|
-
const
|
|
7138
|
-
const under = (base, ...parts) => base ? [
|
|
7224
|
+
const join14 = (...parts) => path5.win32.join(...parts);
|
|
7225
|
+
const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
|
|
7139
7226
|
const appData = env.APPDATA;
|
|
7140
7227
|
const localAppData = env.LOCALAPPDATA;
|
|
7141
7228
|
const programData = env.ProgramData;
|
|
7142
7229
|
return [
|
|
7143
7230
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7231
|
+
join14(home, ".local", "bin"),
|
|
7232
|
+
join14(home, ".opencode", "bin"),
|
|
7233
|
+
join14(home, ".claude", "bin"),
|
|
7234
|
+
join14(home, ".codex", "bin"),
|
|
7148
7235
|
...under(localAppData, "Programs", "claude"),
|
|
7149
7236
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
7150
7237
|
// literal spelling is kept as a fallback for a host that does not export
|
|
7151
7238
|
// APPDATA (a service account, a stripped CI container).
|
|
7152
7239
|
...under(appData, "npm"),
|
|
7153
|
-
|
|
7240
|
+
join14(home, "AppData", "Roaming", "npm"),
|
|
7154
7241
|
...under(localAppData, "pnpm"),
|
|
7155
7242
|
...under(localAppData, "Yarn", "bin"),
|
|
7156
|
-
|
|
7243
|
+
join14(home, ".bun", "bin"),
|
|
7157
7244
|
// Windows package managers.
|
|
7158
|
-
|
|
7245
|
+
join14(home, "scoop", "shims"),
|
|
7159
7246
|
...under(env.SCOOP, "shims"),
|
|
7160
7247
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
7161
7248
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -7165,7 +7252,7 @@ function windowsBinDirs(env, home) {
|
|
|
7165
7252
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
7166
7253
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
7167
7254
|
...under(localAppData, "Volta", "bin"),
|
|
7168
|
-
|
|
7255
|
+
join14(home, ".volta", "bin")
|
|
7169
7256
|
];
|
|
7170
7257
|
}
|
|
7171
7258
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -7288,6 +7375,26 @@ Install the native executable \u2014 Claude Code's Windows installer, Codex's re
|
|
|
7288
7375
|
function hasLineBreak(parts) {
|
|
7289
7376
|
return parts.some((part) => /[\r\n]/.test(part));
|
|
7290
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
|
+
}
|
|
7291
7398
|
function defaultExists2(candidate) {
|
|
7292
7399
|
try {
|
|
7293
7400
|
return fsSync2.statSync(candidate).isFile();
|
|
@@ -8047,6 +8154,10 @@ var ModelDiscovery = class {
|
|
|
8047
8154
|
clear() {
|
|
8048
8155
|
this.cache.clear();
|
|
8049
8156
|
}
|
|
8157
|
+
/** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
|
|
8158
|
+
getCached(runner) {
|
|
8159
|
+
return this.cache.get(runner);
|
|
8160
|
+
}
|
|
8050
8161
|
};
|
|
8051
8162
|
|
|
8052
8163
|
// src/services/ModelCatalog.ts
|
|
@@ -8365,6 +8476,17 @@ var ModelResolver = class {
|
|
|
8365
8476
|
refreshRunnerModels() {
|
|
8366
8477
|
this.discovery.clear();
|
|
8367
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
|
+
}
|
|
8368
8490
|
invalidate() {
|
|
8369
8491
|
this.discovery.clear();
|
|
8370
8492
|
this.cachedPickerOptions = null;
|
|
@@ -8492,6 +8614,28 @@ var RunnerInstallation = class {
|
|
|
8492
8614
|
// src/services/harness/CliAgentAiService.ts
|
|
8493
8615
|
var import_child_process5 = require("child_process");
|
|
8494
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
|
+
|
|
8495
8639
|
// src/services/harness/AgentAdapter.ts
|
|
8496
8640
|
var LineBuffer = class {
|
|
8497
8641
|
buffer = "";
|
|
@@ -8545,6 +8689,7 @@ var StdioAgentAdapter = class {
|
|
|
8545
8689
|
async handshake(_opts) {
|
|
8546
8690
|
}
|
|
8547
8691
|
async start(opts) {
|
|
8692
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
8548
8693
|
this.processEnded = new Promise((resolve3) => {
|
|
8549
8694
|
this.markEnded = resolve3;
|
|
8550
8695
|
});
|
|
@@ -8556,6 +8701,9 @@ var StdioAgentAdapter = class {
|
|
|
8556
8701
|
platform: this.deps.platform,
|
|
8557
8702
|
resolvePath
|
|
8558
8703
|
});
|
|
8704
|
+
if (!isExecutableResolved(spec.command, launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
8705
|
+
throw new ExecutableNotFoundError(spec.command, PATH);
|
|
8706
|
+
}
|
|
8559
8707
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
8560
8708
|
env: this.spawnEnv,
|
|
8561
8709
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9178,12 +9326,16 @@ var OpenCodeAdapter = class {
|
|
|
9178
9326
|
opts = null;
|
|
9179
9327
|
async start(opts) {
|
|
9180
9328
|
this.opts = opts;
|
|
9329
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
9181
9330
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
9182
9331
|
const PATH = await resolvePath();
|
|
9183
9332
|
const launch = await planDirectLaunch("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
|
|
9184
9333
|
platform: this.deps.platform,
|
|
9185
9334
|
resolvePath
|
|
9186
9335
|
});
|
|
9336
|
+
if (!isExecutableResolved("opencode", launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
9337
|
+
throw new ExecutableNotFoundError("opencode", PATH);
|
|
9338
|
+
}
|
|
9187
9339
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
9188
9340
|
env: withPath(process.env, PATH),
|
|
9189
9341
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9497,7 +9649,9 @@ var CliAgentAiService = class {
|
|
|
9497
9649
|
spawn: deps.spawn ?? import_child_process5.spawn,
|
|
9498
9650
|
fetch: deps.fetch ?? globalThis.fetch,
|
|
9499
9651
|
resolvePath: deps.resolvePath,
|
|
9500
|
-
platform: deps.platform
|
|
9652
|
+
platform: deps.platform,
|
|
9653
|
+
isDirectory: deps.isDirectory,
|
|
9654
|
+
exists: deps.exists
|
|
9501
9655
|
};
|
|
9502
9656
|
this.makeAdapter = deps.createAdapter ?? defaultAdapter;
|
|
9503
9657
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
@@ -9842,8 +9996,8 @@ var CliAgentAiService = class {
|
|
|
9842
9996
|
this.lastNativeSessionId = previousSessionId;
|
|
9843
9997
|
}
|
|
9844
9998
|
}
|
|
9845
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
9846
|
-
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);
|
|
9847
10001
|
const researchLog = [
|
|
9848
10002
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
9849
10003
|
];
|
|
@@ -10213,16 +10367,16 @@ function executionSummary(tasks) {
|
|
|
10213
10367
|
}
|
|
10214
10368
|
|
|
10215
10369
|
// src/utils/sessionStore.ts
|
|
10216
|
-
var
|
|
10370
|
+
var fs5 = __toESM(require("fs"));
|
|
10217
10371
|
var path10 = __toESM(require("path"));
|
|
10218
10372
|
|
|
10219
10373
|
// src/utils/fsHelpers.ts
|
|
10220
|
-
var
|
|
10374
|
+
var fs4 = __toESM(require("fs"));
|
|
10221
10375
|
var path9 = __toESM(require("path"));
|
|
10222
10376
|
var STATE_DIR = ".ordewell";
|
|
10223
10377
|
function ensureDir(dir) {
|
|
10224
|
-
if (!
|
|
10225
|
-
|
|
10378
|
+
if (!fs4.existsSync(dir)) {
|
|
10379
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
10226
10380
|
}
|
|
10227
10381
|
}
|
|
10228
10382
|
function getStateDir(baseDir) {
|
|
@@ -10267,16 +10421,16 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
10267
10421
|
};
|
|
10268
10422
|
const filename = sessionFilename(meta);
|
|
10269
10423
|
const filePath = path10.join(sessionsDir, filename);
|
|
10270
|
-
|
|
10424
|
+
fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
10271
10425
|
return meta;
|
|
10272
10426
|
}
|
|
10273
10427
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
10274
10428
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10275
|
-
if (!
|
|
10276
|
-
const files =
|
|
10429
|
+
if (!fs5.existsSync(sessionsDir)) return [];
|
|
10430
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10277
10431
|
return files.map((f) => {
|
|
10278
10432
|
try {
|
|
10279
|
-
const raw =
|
|
10433
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
10280
10434
|
const parsed = JSON.parse(raw);
|
|
10281
10435
|
if (hasScalarRunner(parsed)) {
|
|
10282
10436
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -10291,10 +10445,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
10291
10445
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
10292
10446
|
}
|
|
10293
10447
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
10294
|
-
const files =
|
|
10448
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
10295
10449
|
for (const f of files) {
|
|
10296
10450
|
try {
|
|
10297
|
-
const raw =
|
|
10451
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
10298
10452
|
const session = JSON.parse(raw);
|
|
10299
10453
|
if (session.meta.id === sessionId) return f;
|
|
10300
10454
|
} catch (err) {
|
|
@@ -10305,11 +10459,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
10305
10459
|
}
|
|
10306
10460
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10307
10461
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10308
|
-
if (!
|
|
10462
|
+
if (!fs5.existsSync(sessionsDir)) return null;
|
|
10309
10463
|
try {
|
|
10310
10464
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10311
10465
|
if (file) {
|
|
10312
|
-
const raw =
|
|
10466
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
|
|
10313
10467
|
const parsed = JSON.parse(raw);
|
|
10314
10468
|
if (hasScalarRunner(parsed)) {
|
|
10315
10469
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -10339,11 +10493,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
10339
10493
|
}
|
|
10340
10494
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
10341
10495
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10342
|
-
if (!
|
|
10496
|
+
if (!fs5.existsSync(sessionsDir)) return false;
|
|
10343
10497
|
try {
|
|
10344
10498
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
10345
10499
|
if (file) {
|
|
10346
|
-
|
|
10500
|
+
fs5.unlinkSync(path10.join(sessionsDir, file));
|
|
10347
10501
|
return true;
|
|
10348
10502
|
}
|
|
10349
10503
|
} catch (err) {
|
|
@@ -10353,6 +10507,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
10353
10507
|
}
|
|
10354
10508
|
|
|
10355
10509
|
// src/services/createSession.ts
|
|
10510
|
+
var PlanEditError = class extends Error {
|
|
10511
|
+
constructor(message) {
|
|
10512
|
+
super(message);
|
|
10513
|
+
this.name = "PlanEditError";
|
|
10514
|
+
}
|
|
10515
|
+
};
|
|
10356
10516
|
function sessionRuntimeSettings(settings) {
|
|
10357
10517
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
|
|
10358
10518
|
}
|
|
@@ -10625,12 +10785,41 @@ var Session = class {
|
|
|
10625
10785
|
get isExecuting() {
|
|
10626
10786
|
return this.orchestrator.isRunning;
|
|
10627
10787
|
}
|
|
10788
|
+
/** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
|
|
10789
|
+
get hasLiveWork() {
|
|
10790
|
+
return this.orchestrator.hasLiveWork;
|
|
10791
|
+
}
|
|
10628
10792
|
get status() {
|
|
10629
10793
|
return this.orchestrator.status;
|
|
10630
10794
|
}
|
|
10631
10795
|
get sessionConfig() {
|
|
10632
10796
|
return this.config;
|
|
10633
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
|
+
}
|
|
10634
10823
|
async startExecution() {
|
|
10635
10824
|
if (!this.plan || !this.store.planTasks.length) throw new Error("No plan to execute");
|
|
10636
10825
|
this.store.clearLog();
|
|
@@ -10653,19 +10842,25 @@ var Session = class {
|
|
|
10653
10842
|
const settings = this.settingsFn();
|
|
10654
10843
|
const { modelAllowlist } = settings;
|
|
10655
10844
|
const modes = plannerModesFrom(settings, this.config.autonomousMode);
|
|
10656
|
-
const
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
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
|
+
}
|
|
10669
10864
|
this.plan = plan;
|
|
10670
10865
|
this.orchestrator.loadPlan(plan.tasks, plan.runners);
|
|
10671
10866
|
this.store.resetForRun({ preserveCompleted: false });
|
|
@@ -10702,23 +10897,28 @@ var Session = class {
|
|
|
10702
10897
|
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
|
|
10703
10898
|
conversationHistory: [{ role: "user", content: goal, timestamp: now }]
|
|
10704
10899
|
};
|
|
10705
|
-
const
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
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
|
+
}
|
|
10722
10922
|
}
|
|
10723
10923
|
/**
|
|
10724
10924
|
* Every subsequent user reply in the planning conversation — grill-me
|
|
@@ -10736,21 +10936,26 @@ var Session = class {
|
|
|
10736
10936
|
${userMessage}` : userMessage;
|
|
10737
10937
|
const aiService = this.aiService;
|
|
10738
10938
|
const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
this.
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
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();
|
|
10752
10958
|
}
|
|
10753
|
-
return this.settleTurn(turn, options);
|
|
10754
10959
|
}
|
|
10755
10960
|
/**
|
|
10756
10961
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
@@ -10789,7 +10994,10 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
10789
10994
|
maxRepairs: 2,
|
|
10790
10995
|
onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
|
|
10791
10996
|
});
|
|
10792
|
-
if ("plan" in settled)
|
|
10997
|
+
if ("plan" in settled) {
|
|
10998
|
+
await this.orchestrator.tick();
|
|
10999
|
+
return settled.plan;
|
|
11000
|
+
}
|
|
10793
11001
|
return this.applyConversationTurn(settled.turn);
|
|
10794
11002
|
}
|
|
10795
11003
|
/**
|
|
@@ -10804,9 +11012,9 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
10804
11012
|
const lines = this.store.planTasks.map(
|
|
10805
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(", ")}]`
|
|
10806
11014
|
);
|
|
10807
|
-
const
|
|
10808
|
-
const execNote = this.
|
|
10809
|
-
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.` : "";
|
|
10810
11018
|
const modelLines = [];
|
|
10811
11019
|
for (const runner of this.plan.runners) {
|
|
10812
11020
|
const models = this.modelsCache[runner] ?? [];
|
|
@@ -10848,8 +11056,12 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
10848
11056
|
this.plan.researchLog = [...this.plan.researchLog ?? [], ...turn.researchLog];
|
|
10849
11057
|
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
10850
11058
|
const coerced = coerceAssignments(result.tasks, allowlist, this.plan.runners, this.modelsCache);
|
|
10851
|
-
this.orchestrator.
|
|
10852
|
-
|
|
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
|
+
}
|
|
10853
11065
|
this.appendTranscript("assistant", content, now);
|
|
10854
11066
|
return true;
|
|
10855
11067
|
},
|
|
@@ -11085,8 +11297,33 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11085
11297
|
notify();
|
|
11086
11298
|
return this.plan;
|
|
11087
11299
|
}
|
|
11088
|
-
|
|
11089
|
-
|
|
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(
|
|
11090
11327
|
() => Boolean(this.store.update(taskId, changes)),
|
|
11091
11328
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
11092
11329
|
);
|
|
@@ -11096,8 +11333,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11096
11333
|
* because a runner change is never a single-field edit: the task's model,
|
|
11097
11334
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
11098
11335
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
11099
|
-
* That needs discovery, which is
|
|
11100
|
-
* than a branch inside the sync `updateTask`.
|
|
11336
|
+
* That needs discovery, which is why it is not a branch inside `updateTask`.
|
|
11101
11337
|
*
|
|
11102
11338
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
11103
11339
|
*/
|
|
@@ -11109,7 +11345,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11109
11345
|
const catalog = await this.catalogFor(runner);
|
|
11110
11346
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner));
|
|
11111
11347
|
if (Object.keys(changes).length === 0) return this.plan;
|
|
11112
|
-
return this.
|
|
11348
|
+
return this.editPlan(() => {
|
|
11113
11349
|
if (!this.store.update(taskId, changes)) return false;
|
|
11114
11350
|
this.admitRunner(runner, catalog.models);
|
|
11115
11351
|
return true;
|
|
@@ -11156,24 +11392,29 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11156
11392
|
if (models.length > 0) this.modelsCache = { ...this.modelsCache, [runner]: models };
|
|
11157
11393
|
}
|
|
11158
11394
|
/**
|
|
11159
|
-
* Replace one task's dependency list
|
|
11160
|
-
*
|
|
11161
|
-
*
|
|
11162
|
-
*
|
|
11163
|
-
* rather than each carrying a copy of the rule. Throws so a surface can say
|
|
11164
|
-
* 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.
|
|
11165
11399
|
*/
|
|
11166
|
-
setTaskDependencies(taskId, dependencies) {
|
|
11400
|
+
async setTaskDependencies(taskId, dependencies) {
|
|
11167
11401
|
if (!this.plan) return null;
|
|
11168
|
-
const check = canSetDependencies(this.store.planTasks, taskId, dependencies);
|
|
11169
|
-
if (!check.ok) throw new Error(check.error ?? "Those dependencies are not valid");
|
|
11170
11402
|
return this.updateTask(taskId, { dependencies });
|
|
11171
11403
|
}
|
|
11172
11404
|
async completeTask(taskId) {
|
|
11173
11405
|
await this.markTaskComplete(taskId);
|
|
11174
11406
|
}
|
|
11175
|
-
|
|
11176
|
-
|
|
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));
|
|
11177
11418
|
}
|
|
11178
11419
|
/**
|
|
11179
11420
|
* Add one task, filling in whatever the caller left unset. A task with no
|
|
@@ -11193,20 +11434,20 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
11193
11434
|
const runner = draft.assignedRunner ?? this.plan.runners[0];
|
|
11194
11435
|
const dependencies = (draft.dependencies ?? []).filter((id) => this.store.get(id));
|
|
11195
11436
|
const catalog = draft.type === "user" ? null : await this.catalogFor(runner);
|
|
11196
|
-
return this.
|
|
11437
|
+
return this.editPlan(() => {
|
|
11197
11438
|
this.store.add({ ...draft, ...catalog ? runnerAssignment(this.allowedCatalog(catalog, runner), draft) : {}, assignedRunner: runner, dependencies });
|
|
11198
11439
|
if (catalog) this.admitRunner(runner, catalog.models);
|
|
11199
11440
|
return true;
|
|
11200
11441
|
});
|
|
11201
11442
|
}
|
|
11202
|
-
mergeTasks(taskIdA, taskIdB) {
|
|
11203
|
-
return this.
|
|
11443
|
+
async mergeTasks(taskIdA, taskIdB) {
|
|
11444
|
+
return this.editPlan(() => (this.store.merge(taskIdA, taskIdB), true));
|
|
11204
11445
|
}
|
|
11205
|
-
mergeMultipleTasks(taskIds) {
|
|
11206
|
-
return this.
|
|
11446
|
+
async mergeMultipleTasks(taskIds) {
|
|
11447
|
+
return this.editPlan(() => (this.store.mergeMultiple(taskIds), true));
|
|
11207
11448
|
}
|
|
11208
|
-
splitTask(taskId, newTasks) {
|
|
11209
|
-
return this.
|
|
11449
|
+
async splitTask(taskId, newTasks) {
|
|
11450
|
+
return this.editPlan(() => (this.store.split(taskId, newTasks), true));
|
|
11210
11451
|
}
|
|
11211
11452
|
/**
|
|
11212
11453
|
* Planner-driven merge: validate compatibility up front, then ask the planner
|
|
@@ -11809,6 +12050,28 @@ function hasTmux(probe = defaultProbe) {
|
|
|
11809
12050
|
return false;
|
|
11810
12051
|
}
|
|
11811
12052
|
}
|
|
12053
|
+
var defaultHasBin = (bin) => {
|
|
12054
|
+
try {
|
|
12055
|
+
(0, import_child_process7.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
|
|
12056
|
+
return true;
|
|
12057
|
+
} catch {
|
|
12058
|
+
return false;
|
|
12059
|
+
}
|
|
12060
|
+
};
|
|
12061
|
+
function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platform, env = process.env) {
|
|
12062
|
+
const candidates = platform === "darwin" ? [["pbcopy", "pbcopy"]] : platform === "win32" ? [["clip.exe", "clip.exe"]] : [
|
|
12063
|
+
// Only prefer wl-copy when there is a Wayland display to talk to —
|
|
12064
|
+
// on X11 it is often installed but fails at copy time.
|
|
12065
|
+
...env.WAYLAND_DISPLAY ? [["wl-copy", "wl-copy"]] : [],
|
|
12066
|
+
["xclip", "xclip -selection clipboard"],
|
|
12067
|
+
["xsel", "xsel --clipboard --input"],
|
|
12068
|
+
["wl-copy", "wl-copy"]
|
|
12069
|
+
];
|
|
12070
|
+
for (const [bin, command] of candidates) {
|
|
12071
|
+
if (hasBin(bin)) return command;
|
|
12072
|
+
}
|
|
12073
|
+
return null;
|
|
12074
|
+
}
|
|
11812
12075
|
|
|
11813
12076
|
// src/services/TmuxRunner.ts
|
|
11814
12077
|
var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
|
|
@@ -11918,6 +12181,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
11918
12181
|
resolvePath;
|
|
11919
12182
|
pollIntervalMs;
|
|
11920
12183
|
logDir;
|
|
12184
|
+
clipboardCommand;
|
|
11921
12185
|
/** Retries get a freshly named window so a failed attempt's output stays inspectable. */
|
|
11922
12186
|
attempts = /* @__PURE__ */ new Map();
|
|
11923
12187
|
ready = null;
|
|
@@ -11929,6 +12193,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
11929
12193
|
this.resolvePath = deps.resolvePath ?? augmentedPath;
|
|
11930
12194
|
this.pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
11931
12195
|
this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
|
|
12196
|
+
this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
|
|
11932
12197
|
}
|
|
11933
12198
|
/**
|
|
11934
12199
|
* Reaps a session orphaned by a crashed prior daemon on the same port, then
|
|
@@ -11957,25 +12222,44 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
11957
12222
|
await this.configureScrolling();
|
|
11958
12223
|
}
|
|
11959
12224
|
/**
|
|
11960
|
-
*
|
|
11961
|
-
*
|
|
11962
|
-
*
|
|
11963
|
-
*
|
|
11964
|
-
*
|
|
11965
|
-
*
|
|
11966
|
-
*
|
|
11967
|
-
*
|
|
12225
|
+
* Gives an attached runner terminal wheel/PageUp scrolling over a deep
|
|
12226
|
+
* scrollback, and a mouse selection that reaches the system clipboard — none
|
|
12227
|
+
* of which a default tmux session does. Server-wide scoping is harmless: the
|
|
12228
|
+
* daemon owns a private socket (`tmuxSocketName`).
|
|
12229
|
+
*
|
|
12230
|
+
* `mouse on` is what costs the emulator's own drag-select, so tmux must hand
|
|
12231
|
+
* the selection back out itself: via a clipboard binary when there is one,
|
|
12232
|
+
* and via OSC 52 (`set-clipboard`) for what that cannot cover, such as
|
|
12233
|
+
* viewing over SSH. Naming the binary on the binding as well as in
|
|
12234
|
+
* `copy-command` keeps drag-to-copy working on tmux older than 3.2, which has
|
|
12235
|
+
* no `copy-command`.
|
|
11968
12236
|
*/
|
|
11969
12237
|
async configureScrolling() {
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
12238
|
+
const clip = this.clipboardCommand();
|
|
12239
|
+
const copySelection = clip ? ["copy-pipe-and-cancel", clip] : ["copy-pipe-and-cancel"];
|
|
12240
|
+
const commands = [
|
|
12241
|
+
// `history-limit` only governs windows created after it is set, so it
|
|
12242
|
+
// must run before any `new-window` — once, here at session creation.
|
|
12243
|
+
["set-option", "-g", "history-limit", "100000"],
|
|
12244
|
+
["set-option", "-g", "mouse", "on"],
|
|
12245
|
+
["set-option", "-g", "set-clipboard", "on"],
|
|
12246
|
+
...clip ? [["set-option", "-g", "copy-command", clip]] : [],
|
|
12247
|
+
["bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", ...copySelection],
|
|
12248
|
+
["bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", ...copySelection],
|
|
12249
|
+
// Root-table PageUp; otherwise the key goes to the inner app and never
|
|
12250
|
+
// reaches tmux's scrollback. Both mode tables, so `mode-keys` can't
|
|
12251
|
+
// decide whether paging works.
|
|
12252
|
+
["bind-key", "-n", "PageUp", "copy-mode", "-u"],
|
|
12253
|
+
["bind-key", "-T", "copy-mode", "PageUp", "send-keys", "-X", "page-up"],
|
|
12254
|
+
["bind-key", "-T", "copy-mode", "PageDown", "send-keys", "-X", "page-down"],
|
|
12255
|
+
["bind-key", "-T", "copy-mode-vi", "PageUp", "send-keys", "-X", "page-up"],
|
|
12256
|
+
["bind-key", "-T", "copy-mode-vi", "PageDown", "send-keys", "-X", "page-down"]
|
|
12257
|
+
];
|
|
12258
|
+
for (const args of commands) {
|
|
12259
|
+
try {
|
|
12260
|
+
await this.tmux(args);
|
|
12261
|
+
} catch {
|
|
12262
|
+
}
|
|
11979
12263
|
}
|
|
11980
12264
|
}
|
|
11981
12265
|
/**
|
|
@@ -12035,13 +12319,13 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
12035
12319
|
};
|
|
12036
12320
|
|
|
12037
12321
|
// src/plugins/RunnerRegistry.ts
|
|
12038
|
-
var
|
|
12322
|
+
var fs7 = __toESM(require("fs"));
|
|
12039
12323
|
var path12 = __toESM(require("path"));
|
|
12040
12324
|
var os5 = __toESM(require("os"));
|
|
12041
12325
|
var import_child_process9 = require("child_process");
|
|
12042
12326
|
|
|
12043
12327
|
// src/plugins/FsPluginStore.ts
|
|
12044
|
-
var
|
|
12328
|
+
var fs6 = __toESM(require("fs"));
|
|
12045
12329
|
var path11 = __toESM(require("path"));
|
|
12046
12330
|
var os4 = __toESM(require("os"));
|
|
12047
12331
|
function userPluginsDir() {
|
|
@@ -12054,9 +12338,9 @@ function isValidManifest(obj) {
|
|
|
12054
12338
|
}
|
|
12055
12339
|
function loadManifestFromDir(dir) {
|
|
12056
12340
|
const manifestPath = path11.join(dir, "manifest.json");
|
|
12057
|
-
if (!
|
|
12341
|
+
if (!fs6.existsSync(manifestPath)) return null;
|
|
12058
12342
|
try {
|
|
12059
|
-
const raw =
|
|
12343
|
+
const raw = fs6.readFileSync(manifestPath, "utf8");
|
|
12060
12344
|
const parsed = JSON.parse(raw);
|
|
12061
12345
|
if (!isValidManifest(parsed)) return null;
|
|
12062
12346
|
return parsed;
|
|
@@ -12066,29 +12350,29 @@ function loadManifestFromDir(dir) {
|
|
|
12066
12350
|
}
|
|
12067
12351
|
function copyDirSync(src, dest) {
|
|
12068
12352
|
ensureDir(dest);
|
|
12069
|
-
const entries =
|
|
12353
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
12070
12354
|
for (const entry of entries) {
|
|
12071
12355
|
const srcPath = path11.join(src, entry.name);
|
|
12072
12356
|
const destPath = path11.join(dest, entry.name);
|
|
12073
12357
|
if (entry.isDirectory()) {
|
|
12074
12358
|
copyDirSync(srcPath, destPath);
|
|
12075
12359
|
} else {
|
|
12076
|
-
|
|
12360
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
12077
12361
|
}
|
|
12078
12362
|
}
|
|
12079
12363
|
}
|
|
12080
12364
|
function rmDirSync(dir) {
|
|
12081
|
-
if (!
|
|
12082
|
-
const entries =
|
|
12365
|
+
if (!fs6.existsSync(dir)) return;
|
|
12366
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
12083
12367
|
for (const entry of entries) {
|
|
12084
12368
|
const p = path11.join(dir, entry.name);
|
|
12085
12369
|
if (entry.isDirectory()) {
|
|
12086
12370
|
rmDirSync(p);
|
|
12087
12371
|
} else {
|
|
12088
|
-
|
|
12372
|
+
fs6.unlinkSync(p);
|
|
12089
12373
|
}
|
|
12090
12374
|
}
|
|
12091
|
-
|
|
12375
|
+
fs6.rmdirSync(dir);
|
|
12092
12376
|
}
|
|
12093
12377
|
var FsPluginStore = class {
|
|
12094
12378
|
getUserPluginsDir() {
|
|
@@ -12096,10 +12380,10 @@ var FsPluginStore = class {
|
|
|
12096
12380
|
}
|
|
12097
12381
|
listUserPluginDirs() {
|
|
12098
12382
|
const dir = userPluginsDir();
|
|
12099
|
-
if (!
|
|
12383
|
+
if (!fs6.existsSync(dir)) return [];
|
|
12100
12384
|
let entries;
|
|
12101
12385
|
try {
|
|
12102
|
-
entries =
|
|
12386
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
12103
12387
|
} catch {
|
|
12104
12388
|
return [];
|
|
12105
12389
|
}
|
|
@@ -12118,20 +12402,20 @@ var FsPluginStore = class {
|
|
|
12118
12402
|
ensureDir(dir);
|
|
12119
12403
|
}
|
|
12120
12404
|
writeFile(filePath, content) {
|
|
12121
|
-
|
|
12405
|
+
fs6.writeFileSync(filePath, content);
|
|
12122
12406
|
}
|
|
12123
12407
|
readFile(filePath) {
|
|
12124
12408
|
try {
|
|
12125
|
-
return
|
|
12409
|
+
return fs6.readFileSync(filePath, "utf8");
|
|
12126
12410
|
} catch {
|
|
12127
12411
|
return null;
|
|
12128
12412
|
}
|
|
12129
12413
|
}
|
|
12130
12414
|
dirExists(path15) {
|
|
12131
|
-
return
|
|
12415
|
+
return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
|
|
12132
12416
|
}
|
|
12133
12417
|
exists(path15) {
|
|
12134
|
-
return
|
|
12418
|
+
return fs6.existsSync(path15);
|
|
12135
12419
|
}
|
|
12136
12420
|
};
|
|
12137
12421
|
|
|
@@ -12600,20 +12884,20 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
12600
12884
|
};
|
|
12601
12885
|
function fsReaddirSync(dir) {
|
|
12602
12886
|
try {
|
|
12603
|
-
return
|
|
12887
|
+
return fs7.readdirSync(dir);
|
|
12604
12888
|
} catch {
|
|
12605
12889
|
return [];
|
|
12606
12890
|
}
|
|
12607
12891
|
}
|
|
12608
12892
|
function fsChmodSync(fpath, mode) {
|
|
12609
12893
|
try {
|
|
12610
|
-
|
|
12894
|
+
fs7.chmodSync(fpath, mode);
|
|
12611
12895
|
} catch {
|
|
12612
12896
|
}
|
|
12613
12897
|
}
|
|
12614
12898
|
|
|
12615
12899
|
// src/utils/stateStore.ts
|
|
12616
|
-
var
|
|
12900
|
+
var fs8 = __toESM(require("fs"));
|
|
12617
12901
|
var path13 = __toESM(require("path"));
|
|
12618
12902
|
var STATE_FILE = "state.json";
|
|
12619
12903
|
function getStatePath(baseDir) {
|
|
@@ -12623,14 +12907,14 @@ function saveState(plan, baseDir) {
|
|
|
12623
12907
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
12624
12908
|
ensureDir(path13.dirname(statePath));
|
|
12625
12909
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
12626
|
-
|
|
12910
|
+
fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
12627
12911
|
}
|
|
12628
12912
|
function loadState(baseDir, logger = defaultLogger) {
|
|
12629
12913
|
const dir = baseDir ?? process.cwd();
|
|
12630
12914
|
const statePath = getStatePath(dir);
|
|
12631
|
-
if (!
|
|
12915
|
+
if (!fs8.existsSync(statePath)) return null;
|
|
12632
12916
|
try {
|
|
12633
|
-
const raw =
|
|
12917
|
+
const raw = fs8.readFileSync(statePath, "utf-8");
|
|
12634
12918
|
const parsed = JSON.parse(raw);
|
|
12635
12919
|
if (!Array.isArray(parsed.runners)) {
|
|
12636
12920
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -12645,15 +12929,15 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
12645
12929
|
function clearState(baseDir) {
|
|
12646
12930
|
const dir = baseDir ?? process.cwd();
|
|
12647
12931
|
const statePath = getStatePath(dir);
|
|
12648
|
-
if (
|
|
12932
|
+
if (fs8.existsSync(statePath)) fs8.unlinkSync(statePath);
|
|
12649
12933
|
}
|
|
12650
12934
|
function stateExists(baseDir) {
|
|
12651
12935
|
const dir = baseDir ?? process.cwd();
|
|
12652
|
-
return
|
|
12936
|
+
return fs8.existsSync(getStatePath(dir));
|
|
12653
12937
|
}
|
|
12654
12938
|
|
|
12655
12939
|
// src/services/SettingsService.ts
|
|
12656
|
-
var
|
|
12940
|
+
var fs9 = __toESM(require("fs"));
|
|
12657
12941
|
var path14 = __toESM(require("path"));
|
|
12658
12942
|
var os6 = __toESM(require("os"));
|
|
12659
12943
|
var DEFAULTS = {
|
|
@@ -12669,6 +12953,17 @@ function getSettingsPath() {
|
|
|
12669
12953
|
if (override && override.trim()) return override.trim();
|
|
12670
12954
|
return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
|
|
12671
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
|
+
}
|
|
12672
12967
|
var SettingsService = class {
|
|
12673
12968
|
filePath;
|
|
12674
12969
|
cache = null;
|
|
@@ -12689,7 +12984,7 @@ var SettingsService = class {
|
|
|
12689
12984
|
}
|
|
12690
12985
|
fileMtimeMs() {
|
|
12691
12986
|
try {
|
|
12692
|
-
return
|
|
12987
|
+
return fs9.statSync(this.filePath).mtimeMs;
|
|
12693
12988
|
} catch {
|
|
12694
12989
|
return null;
|
|
12695
12990
|
}
|
|
@@ -12762,6 +13057,26 @@ var SettingsService = class {
|
|
|
12762
13057
|
}
|
|
12763
13058
|
this.persist();
|
|
12764
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
|
+
}
|
|
12765
13080
|
getEnabledRunners() {
|
|
12766
13081
|
return this.getAll().enabledRunners;
|
|
12767
13082
|
}
|
|
@@ -12772,8 +13087,8 @@ var SettingsService = class {
|
|
|
12772
13087
|
}
|
|
12773
13088
|
load() {
|
|
12774
13089
|
try {
|
|
12775
|
-
if (
|
|
12776
|
-
const raw = JSON.parse(
|
|
13090
|
+
if (fs9.existsSync(this.filePath)) {
|
|
13091
|
+
const raw = JSON.parse(fs9.readFileSync(this.filePath, "utf-8"));
|
|
12777
13092
|
const settings = {
|
|
12778
13093
|
grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
|
|
12779
13094
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
@@ -12787,6 +13102,10 @@ var SettingsService = class {
|
|
|
12787
13102
|
if (raw.modelAllowlist !== void 0) {
|
|
12788
13103
|
settings.modelAllowlist = raw.modelAllowlist;
|
|
12789
13104
|
}
|
|
13105
|
+
const plannerModels = readPlannerModels(raw.plannerModels);
|
|
13106
|
+
if (plannerModels) {
|
|
13107
|
+
settings.plannerModels = plannerModels;
|
|
13108
|
+
}
|
|
12790
13109
|
if (Array.isArray(raw.enabledRunners)) {
|
|
12791
13110
|
settings.enabledRunners = raw.enabledRunners.map(String);
|
|
12792
13111
|
}
|
|
@@ -12798,13 +13117,48 @@ var SettingsService = class {
|
|
|
12798
13117
|
}
|
|
12799
13118
|
persist() {
|
|
12800
13119
|
const dir = path14.dirname(this.filePath);
|
|
12801
|
-
if (!
|
|
12802
|
-
|
|
13120
|
+
if (!fs9.existsSync(dir)) {
|
|
13121
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
12803
13122
|
}
|
|
12804
|
-
|
|
13123
|
+
fs9.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
12805
13124
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
12806
13125
|
}
|
|
12807
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
|
+
};
|
|
12808
13162
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12809
13163
|
0 && (module.exports = {
|
|
12810
13164
|
ALL_PROVIDERS,
|
|
@@ -12828,6 +13182,7 @@ var SettingsService = class {
|
|
|
12828
13182
|
DENY_ALL,
|
|
12829
13183
|
EmbeddedNewlineError,
|
|
12830
13184
|
EnvConfig,
|
|
13185
|
+
ExecutableNotFoundError,
|
|
12831
13186
|
FsPluginStore,
|
|
12832
13187
|
GIT_READONLY_SUBCOMMANDS,
|
|
12833
13188
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
@@ -12848,14 +13203,17 @@ var SettingsService = class {
|
|
|
12848
13203
|
PROVIDER_PRIORITY,
|
|
12849
13204
|
PROVIDER_SHORT_LABEL,
|
|
12850
13205
|
PendingApprovals,
|
|
13206
|
+
PlanEditError,
|
|
12851
13207
|
PlanParseError,
|
|
12852
13208
|
PlanStore,
|
|
12853
13209
|
Planner,
|
|
13210
|
+
PlannerModelMemory,
|
|
12854
13211
|
REFUSED_COMMANDS,
|
|
12855
13212
|
RunnerInstallation,
|
|
12856
13213
|
RunnerRegistry,
|
|
12857
13214
|
SEARCH_EXCLUSIONS,
|
|
12858
13215
|
STATE_DIR,
|
|
13216
|
+
STOPPED_TOOL_RESULT,
|
|
12859
13217
|
SYMBOL_LANGUAGES,
|
|
12860
13218
|
Session,
|
|
12861
13219
|
SettingsService,
|
|
@@ -12866,9 +13224,11 @@ var SettingsService = class {
|
|
|
12866
13224
|
TmuxRunner,
|
|
12867
13225
|
VerdictEngine,
|
|
12868
13226
|
WINDOWS_MAX_COMMAND_LINE,
|
|
13227
|
+
WorkspaceNotFoundError,
|
|
12869
13228
|
addTaskToPlan,
|
|
12870
13229
|
applyHeadLimit,
|
|
12871
13230
|
applyTaskOps,
|
|
13231
|
+
assertWorkspaceExists,
|
|
12872
13232
|
augmentPromptWithPriorOutputs,
|
|
12873
13233
|
augmentedPath,
|
|
12874
13234
|
buildConversationSystemPrompt,
|
|
@@ -12901,6 +13261,7 @@ var SettingsService = class {
|
|
|
12901
13261
|
clearAugmentedPathCache,
|
|
12902
13262
|
clearResearchShellCache,
|
|
12903
13263
|
clearState,
|
|
13264
|
+
clipboardCopyCommand,
|
|
12904
13265
|
coerceAssignments,
|
|
12905
13266
|
collectDirectDependencyOutputs,
|
|
12906
13267
|
collectProviderCredentials,
|
|
@@ -12941,6 +13302,7 @@ var SettingsService = class {
|
|
|
12941
13302
|
hasTmux,
|
|
12942
13303
|
includeGlobFor,
|
|
12943
13304
|
isCliProvider,
|
|
13305
|
+
isExecutableResolved,
|
|
12944
13306
|
isOpenAiProvider,
|
|
12945
13307
|
killTree,
|
|
12946
13308
|
knownModelId,
|