@ordewell/core 0.4.5 → 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 +232 -32
- package/dist/index.d.ts +232 -32
- package/dist/index.js +606 -289
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +601 -291
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -329,6 +329,16 @@ function dialectFor(dialect) {
|
|
|
329
329
|
const resolved = dialect ?? (process.platform === "win32" ? "cmd" : "posix");
|
|
330
330
|
return resolved === "cmd" ? CMD_DIALECT : POSIX_DIALECT;
|
|
331
331
|
}
|
|
332
|
+
function isDevNullTarget(target) {
|
|
333
|
+
if (!target.startsWith("/")) return false;
|
|
334
|
+
const resolved = [];
|
|
335
|
+
for (const seg of target.split("/")) {
|
|
336
|
+
if (seg === "" || seg === ".") continue;
|
|
337
|
+
if (seg === "..") resolved.pop();
|
|
338
|
+
else resolved.push(seg);
|
|
339
|
+
}
|
|
340
|
+
return `/${resolved.join("/")}` === "/dev/null";
|
|
341
|
+
}
|
|
332
342
|
function matchParen(s, open) {
|
|
333
343
|
let depth = 0;
|
|
334
344
|
for (let i = open; i < s.length; i++) {
|
|
@@ -339,7 +349,7 @@ function matchParen(s, open) {
|
|
|
339
349
|
}
|
|
340
350
|
function lex(command, nested, dialect) {
|
|
341
351
|
const segments = [];
|
|
342
|
-
let
|
|
352
|
+
let unsafeRedirect;
|
|
343
353
|
let processSubstitution = false;
|
|
344
354
|
let unbalanced = false;
|
|
345
355
|
let tokens = [];
|
|
@@ -348,7 +358,19 @@ function lex(command, nested, dialect) {
|
|
|
348
358
|
let started = false;
|
|
349
359
|
let piped = false;
|
|
350
360
|
let quote = "";
|
|
351
|
-
|
|
361
|
+
let redirectTargetMode = false;
|
|
362
|
+
let redirectOperator = "";
|
|
363
|
+
const endToken = (hardBoundary = true) => {
|
|
364
|
+
if (redirectTargetMode) {
|
|
365
|
+
if (!started && !hardBoundary) return;
|
|
366
|
+
if (!unsafeRedirect && !isDevNullTarget(current)) {
|
|
367
|
+
unsafeRedirect = { operator: redirectOperator, target: current };
|
|
368
|
+
}
|
|
369
|
+
redirectTargetMode = false;
|
|
370
|
+
current = "";
|
|
371
|
+
started = false;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
352
374
|
if (started) tokens.push(current);
|
|
353
375
|
current = "";
|
|
354
376
|
started = false;
|
|
@@ -436,9 +458,34 @@ function lex(command, nested, dialect) {
|
|
|
436
458
|
i += 2;
|
|
437
459
|
continue;
|
|
438
460
|
}
|
|
439
|
-
|
|
461
|
+
let fd = "";
|
|
462
|
+
if (started && /^[0-9]+$/.test(current)) {
|
|
463
|
+
fd = current;
|
|
464
|
+
current = "";
|
|
465
|
+
started = false;
|
|
466
|
+
} else {
|
|
467
|
+
endToken();
|
|
468
|
+
}
|
|
469
|
+
const doubled = command[i + 1] === c;
|
|
470
|
+
const opLen = doubled ? 2 : 1;
|
|
471
|
+
if (c === ">") {
|
|
472
|
+
const dup = /^&([0-9]+)(?![0-9])/.exec(command.slice(i + opLen));
|
|
473
|
+
if (dup) {
|
|
474
|
+
i += opLen + dup[0].length;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
redirectTargetMode = true;
|
|
478
|
+
redirectOperator = fd + c.repeat(opLen);
|
|
479
|
+
}
|
|
480
|
+
i += opLen;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (c === "&" && command[i + 1] === ">") {
|
|
440
484
|
endToken();
|
|
441
|
-
|
|
485
|
+
const doubled = command[i + 2] === ">";
|
|
486
|
+
redirectTargetMode = true;
|
|
487
|
+
redirectOperator = doubled ? "&>>" : "&>";
|
|
488
|
+
i += doubled ? 3 : 2;
|
|
442
489
|
continue;
|
|
443
490
|
}
|
|
444
491
|
if (c === "(" || c === ")") {
|
|
@@ -458,7 +505,7 @@ function lex(command, nested, dialect) {
|
|
|
458
505
|
continue;
|
|
459
506
|
}
|
|
460
507
|
if (/\s/.test(c)) {
|
|
461
|
-
endToken();
|
|
508
|
+
endToken(false);
|
|
462
509
|
i++;
|
|
463
510
|
continue;
|
|
464
511
|
}
|
|
@@ -468,7 +515,7 @@ function lex(command, nested, dialect) {
|
|
|
468
515
|
}
|
|
469
516
|
if (quote !== "") unbalanced = true;
|
|
470
517
|
endSegment(false);
|
|
471
|
-
return { segments,
|
|
518
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
472
519
|
}
|
|
473
520
|
function binaryName(token, dialect) {
|
|
474
521
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -488,7 +535,7 @@ function lexAll(command, dialect) {
|
|
|
488
535
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
489
536
|
const inner = lex(queue.shift(), queue, dialect);
|
|
490
537
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
491
|
-
all.
|
|
538
|
+
all.unsafeRedirect ??= inner.unsafeRedirect;
|
|
492
539
|
all.processSubstitution ||= inner.processSubstitution;
|
|
493
540
|
all.unbalanced ||= inner.unbalanced;
|
|
494
541
|
}
|
|
@@ -537,8 +584,9 @@ function refusalFor(seg) {
|
|
|
537
584
|
if (seg.binary === "git") {
|
|
538
585
|
const sub = seg.args.find((a) => !a.startsWith("-"));
|
|
539
586
|
if (sub === "branch" || sub === "tag") {
|
|
540
|
-
|
|
541
|
-
|
|
587
|
+
const flag = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a));
|
|
588
|
+
if (flag) {
|
|
589
|
+
return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
542
590
|
}
|
|
543
591
|
}
|
|
544
592
|
}
|
|
@@ -548,8 +596,9 @@ function refusalFor(seg) {
|
|
|
548
596
|
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.`;
|
|
549
597
|
}
|
|
550
598
|
}
|
|
551
|
-
|
|
552
|
-
|
|
599
|
+
const sedInPlace = seg.binary === "sed" ? seg.args.find((a) => a === "-i" || a === "--in-place" || /^-i./.test(a)) : void 0;
|
|
600
|
+
if (sedInPlace) {
|
|
601
|
+
return `"sed ${sedInPlace}" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
553
602
|
}
|
|
554
603
|
if (seg.binary === "awk" && seg.args.some((a) => a === "inplace")) {
|
|
555
604
|
return `"awk -i inplace" edits files in place. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
@@ -568,7 +617,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
568
617
|
const trimmed = command.trim();
|
|
569
618
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
570
619
|
const dialect = dialectFor(opts.dialect);
|
|
571
|
-
const { segments,
|
|
620
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
572
621
|
if (unbalanced) {
|
|
573
622
|
return {
|
|
574
623
|
tier: "refuse",
|
|
@@ -583,11 +632,12 @@ function classifyCommand(command, opts = {}) {
|
|
|
583
632
|
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."
|
|
584
633
|
};
|
|
585
634
|
}
|
|
586
|
-
if (
|
|
635
|
+
if (unsafeRedirect) {
|
|
636
|
+
const written = unsafeRedirect.target ? `"${unsafeRedirect.operator} ${unsafeRedirect.target}"` : `"${unsafeRedirect.operator}"`;
|
|
587
637
|
return {
|
|
588
638
|
tier: "refuse",
|
|
589
639
|
scope: "",
|
|
590
|
-
reason:
|
|
640
|
+
reason: `${written} writes to a file. You are a read-only planner \u2014 read the output instead, or describe the write as a task.`
|
|
591
641
|
};
|
|
592
642
|
}
|
|
593
643
|
if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
|
|
@@ -907,7 +957,7 @@ ${refs.output.trim()}`);
|
|
|
907
957
|
* anything else asks once and is remembered, and writes are refused outright
|
|
908
958
|
* because a planner that mutates the workspace has stopped being a planner.
|
|
909
959
|
*/
|
|
910
|
-
async bash(command) {
|
|
960
|
+
async bash(command, signal) {
|
|
911
961
|
const { tier, scope, reason } = classifyCommand(command, { dialect: this.researchShell.dialect });
|
|
912
962
|
if (tier === "refuse") {
|
|
913
963
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
@@ -930,7 +980,7 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
930
980
|
};
|
|
931
981
|
}
|
|
932
982
|
}
|
|
933
|
-
const outcome = await this.execBashImpl(command);
|
|
983
|
+
const outcome = await this.execBashImpl(command, signal);
|
|
934
984
|
if (outcome.success) return outcome;
|
|
935
985
|
const warning = researchShellWarning(this.researchShell);
|
|
936
986
|
return warning ? { ...outcome, output: `${outcome.output}
|
|
@@ -939,6 +989,82 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
939
989
|
}
|
|
940
990
|
};
|
|
941
991
|
|
|
992
|
+
// src/services/executeTool.ts
|
|
993
|
+
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
994
|
+
function num(value) {
|
|
995
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
996
|
+
}
|
|
997
|
+
function bool(value) {
|
|
998
|
+
return typeof value === "boolean" ? value : void 0;
|
|
999
|
+
}
|
|
1000
|
+
function str(value) {
|
|
1001
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function grepOptions(args) {
|
|
1004
|
+
const mode = str(args.output_mode ?? args.outputMode);
|
|
1005
|
+
return {
|
|
1006
|
+
include: str(args.include),
|
|
1007
|
+
path: str(args.path),
|
|
1008
|
+
outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
|
|
1009
|
+
contextBefore: num(args.context_before ?? args.contextBefore),
|
|
1010
|
+
contextAfter: num(args.context_after ?? args.contextAfter),
|
|
1011
|
+
literal: bool(args.literal),
|
|
1012
|
+
caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
|
|
1013
|
+
headLimit: num(args.head_limit ?? args.headLimit)
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
1017
|
+
async function executeTool(name, args, fs10, fetcher, signal) {
|
|
1018
|
+
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
1019
|
+
switch (name) {
|
|
1020
|
+
case "read_file": {
|
|
1021
|
+
const offset = num(args.offset);
|
|
1022
|
+
const limit = num(args.limit);
|
|
1023
|
+
const maxBytes = num(args.maxBytes);
|
|
1024
|
+
return fs10.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
1025
|
+
}
|
|
1026
|
+
case "read_files": {
|
|
1027
|
+
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
1028
|
+
return fs10.readFiles(paths);
|
|
1029
|
+
}
|
|
1030
|
+
case "glob":
|
|
1031
|
+
return fs10.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
1032
|
+
case "grep":
|
|
1033
|
+
return fs10.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
1034
|
+
case "find_symbol":
|
|
1035
|
+
return fs10.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
1036
|
+
case "list_dir":
|
|
1037
|
+
return fs10.listDir(String(args.path ?? ""), num(args.depth));
|
|
1038
|
+
case "bash":
|
|
1039
|
+
return fs10.bash(String(args.command ?? ""), signal);
|
|
1040
|
+
case "fetch": {
|
|
1041
|
+
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
1042
|
+
const url = String(args.url ?? "");
|
|
1043
|
+
const allowed = await fetcher.confirm(url);
|
|
1044
|
+
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
1045
|
+
return fetcher.fetch(url);
|
|
1046
|
+
}
|
|
1047
|
+
case "web_search": {
|
|
1048
|
+
if (!fetcher?.search) {
|
|
1049
|
+
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 };
|
|
1050
|
+
}
|
|
1051
|
+
const query = String(args.query ?? "").trim();
|
|
1052
|
+
if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
|
|
1053
|
+
return fetcher.search(query);
|
|
1054
|
+
}
|
|
1055
|
+
// Budget models hallucinate two kinds of tools: plan-committing ones
|
|
1056
|
+
// (create_task, submit_plan) and implementation ones (create_file,
|
|
1057
|
+
// write_file). Steer both back: you are a planner, tasks run later,
|
|
1058
|
+
// and the commit channel is raw JSON in the reply text.
|
|
1059
|
+
default:
|
|
1060
|
+
return {
|
|
1061
|
+
success: false,
|
|
1062
|
+
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).`,
|
|
1063
|
+
truncated: false
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
942
1068
|
// src/services/ApprovalPolicy.ts
|
|
943
1069
|
function scopeMatches(pattern, scope) {
|
|
944
1070
|
if (pattern === scope) return true;
|
|
@@ -2959,8 +3085,8 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
2959
3085
|
|
|
2960
3086
|
// src/services/ContextCollector.ts
|
|
2961
3087
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
2962
|
-
async function collectResearchContext(
|
|
2963
|
-
const collector = new ContextCollector(
|
|
3088
|
+
async function collectResearchContext(fs10, runners) {
|
|
3089
|
+
const collector = new ContextCollector(fs10);
|
|
2964
3090
|
let contextStr = "";
|
|
2965
3091
|
let firstContext = null;
|
|
2966
3092
|
for (const r of runners) {
|
|
@@ -2987,8 +3113,8 @@ ${firstContext.dirStructure}
|
|
|
2987
3113
|
return contextStr;
|
|
2988
3114
|
}
|
|
2989
3115
|
var ContextCollector = class {
|
|
2990
|
-
constructor(
|
|
2991
|
-
this.fs =
|
|
3116
|
+
constructor(fs10, registry) {
|
|
3117
|
+
this.fs = fs10;
|
|
2992
3118
|
this.registry = registry;
|
|
2993
3119
|
}
|
|
2994
3120
|
fs;
|
|
@@ -3024,80 +3150,6 @@ var ContextCollector = class {
|
|
|
3024
3150
|
}
|
|
3025
3151
|
};
|
|
3026
3152
|
|
|
3027
|
-
// src/services/executeTool.ts
|
|
3028
|
-
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
3029
|
-
function num(value) {
|
|
3030
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3031
|
-
}
|
|
3032
|
-
function bool(value) {
|
|
3033
|
-
return typeof value === "boolean" ? value : void 0;
|
|
3034
|
-
}
|
|
3035
|
-
function str(value) {
|
|
3036
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3037
|
-
}
|
|
3038
|
-
function grepOptions(args) {
|
|
3039
|
-
const mode = str(args.output_mode ?? args.outputMode);
|
|
3040
|
-
return {
|
|
3041
|
-
include: str(args.include),
|
|
3042
|
-
path: str(args.path),
|
|
3043
|
-
outputMode: GREP_OUTPUT_MODES.includes(mode) ? mode : void 0,
|
|
3044
|
-
contextBefore: num(args.context_before ?? args.contextBefore),
|
|
3045
|
-
contextAfter: num(args.context_after ?? args.contextAfter),
|
|
3046
|
-
literal: bool(args.literal),
|
|
3047
|
-
caseInsensitive: bool(args.case_insensitive ?? args.caseInsensitive),
|
|
3048
|
-
headLimit: num(args.head_limit ?? args.headLimit)
|
|
3049
|
-
};
|
|
3050
|
-
}
|
|
3051
|
-
async function executeTool(name, args, fs9, fetcher) {
|
|
3052
|
-
switch (name) {
|
|
3053
|
-
case "read_file": {
|
|
3054
|
-
const offset = num(args.offset);
|
|
3055
|
-
const limit = num(args.limit);
|
|
3056
|
-
const maxBytes = num(args.maxBytes);
|
|
3057
|
-
return fs9.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
3058
|
-
}
|
|
3059
|
-
case "read_files": {
|
|
3060
|
-
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
3061
|
-
return fs9.readFiles(paths);
|
|
3062
|
-
}
|
|
3063
|
-
case "glob":
|
|
3064
|
-
return fs9.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
3065
|
-
case "grep":
|
|
3066
|
-
return fs9.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
3067
|
-
case "find_symbol":
|
|
3068
|
-
return fs9.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
3069
|
-
case "list_dir":
|
|
3070
|
-
return fs9.listDir(String(args.path ?? ""), num(args.depth));
|
|
3071
|
-
case "bash":
|
|
3072
|
-
return fs9.bash(String(args.command ?? ""));
|
|
3073
|
-
case "fetch": {
|
|
3074
|
-
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
3075
|
-
const url = String(args.url ?? "");
|
|
3076
|
-
const allowed = await fetcher.confirm(url);
|
|
3077
|
-
if (!allowed) return { success: false, output: `URL fetch denied by user: ${url}`, truncated: false };
|
|
3078
|
-
return fetcher.fetch(url);
|
|
3079
|
-
}
|
|
3080
|
-
case "web_search": {
|
|
3081
|
-
if (!fetcher?.search) {
|
|
3082
|
-
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 };
|
|
3083
|
-
}
|
|
3084
|
-
const query = String(args.query ?? "").trim();
|
|
3085
|
-
if (!query) return { success: false, output: 'web_search requires a non-empty "query".', truncated: false };
|
|
3086
|
-
return fetcher.search(query);
|
|
3087
|
-
}
|
|
3088
|
-
// Budget models hallucinate two kinds of tools: plan-committing ones
|
|
3089
|
-
// (create_task, submit_plan) and implementation ones (create_file,
|
|
3090
|
-
// write_file). Steer both back: you are a planner, tasks run later,
|
|
3091
|
-
// and the commit channel is raw JSON in the reply text.
|
|
3092
|
-
default:
|
|
3093
|
-
return {
|
|
3094
|
-
success: false,
|
|
3095
|
-
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).`,
|
|
3096
|
-
truncated: false
|
|
3097
|
-
};
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
3153
|
// src/services/ResearchSubagents.ts
|
|
3102
3154
|
var SUBAGENT_LIMITS = {
|
|
3103
3155
|
/** Max spawn calls in flight at once; extra calls in the same turn wait for a slot. */
|
|
@@ -3110,23 +3162,23 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
3110
3162
|
function refused(output) {
|
|
3111
3163
|
return { success: false, output, truncated: false };
|
|
3112
3164
|
}
|
|
3113
|
-
function nonPromptingFs(
|
|
3114
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
3165
|
+
function nonPromptingFs(fs10) {
|
|
3166
|
+
const withinWorkspace = (p) => resolveWithin(fs10.getWorkspaceRoot(), p ?? ".").inside;
|
|
3115
3167
|
const outsideRefusal = (p) => refused(
|
|
3116
3168
|
`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.`
|
|
3117
3169
|
);
|
|
3118
3170
|
return {
|
|
3119
|
-
getWorkspaceRoot: () =>
|
|
3120
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
3171
|
+
getWorkspaceRoot: () => fs10.getWorkspaceRoot(),
|
|
3172
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs10.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
3121
3173
|
readFiles: (paths) => {
|
|
3122
3174
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
3123
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
3175
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs10.readFiles(paths);
|
|
3124
3176
|
},
|
|
3125
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
3126
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
3127
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
3128
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
3129
|
-
bash: async (command) => {
|
|
3177
|
+
glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
3178
|
+
grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs10.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
3179
|
+
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs10.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
3180
|
+
listDir: (p, depth) => withinWorkspace(p) ? fs10.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
3181
|
+
bash: async (command, signal) => {
|
|
3130
3182
|
const dialect = resolveResearchShell().dialect;
|
|
3131
3183
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
3132
3184
|
if (tier !== "auto") {
|
|
@@ -3136,13 +3188,13 @@ function nonPromptingFs(fs9) {
|
|
|
3136
3188
|
}
|
|
3137
3189
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
3138
3190
|
if (escaping) return outsideRefusal(escaping);
|
|
3139
|
-
return
|
|
3191
|
+
return fs10.bash(command, signal);
|
|
3140
3192
|
}
|
|
3141
3193
|
};
|
|
3142
3194
|
}
|
|
3143
3195
|
async function runLoop(prompt, deps) {
|
|
3144
3196
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
3145
|
-
const
|
|
3197
|
+
const fs10 = nonPromptingFs(deps.fs);
|
|
3146
3198
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
3147
3199
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
3148
3200
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -3159,7 +3211,7 @@ ${turn.text}`;
|
|
|
3159
3211
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
3160
3212
|
const toolArgs = JSON.stringify(tc.args);
|
|
3161
3213
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
3162
|
-
const res = await executeTool(tc.name, args,
|
|
3214
|
+
const res = await executeTool(tc.name, args, fs10, void 0, deps.signal);
|
|
3163
3215
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
3164
3216
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
3165
3217
|
const stepEntry = {
|
|
@@ -3274,7 +3326,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
3274
3326
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
3275
3327
|
* never fail a plan or a turn.
|
|
3276
3328
|
*/
|
|
3277
|
-
async executeSpawnAgent(args,
|
|
3329
|
+
async executeSpawnAgent(args, fs10, onProgress, signal, subagentId) {
|
|
3278
3330
|
if (!this.researchSubagentsEnabled) {
|
|
3279
3331
|
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 };
|
|
3280
3332
|
}
|
|
@@ -3288,20 +3340,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
3288
3340
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
3289
3341
|
return chat;
|
|
3290
3342
|
},
|
|
3291
|
-
fs:
|
|
3343
|
+
fs: fs10,
|
|
3292
3344
|
signal,
|
|
3293
3345
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
3294
3346
|
});
|
|
3295
3347
|
}
|
|
3296
3348
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
3297
|
-
static async collectResearchContext(
|
|
3298
|
-
return collectResearchContext(
|
|
3349
|
+
static async collectResearchContext(fs10, runners) {
|
|
3350
|
+
return collectResearchContext(fs10, runners);
|
|
3299
3351
|
}
|
|
3300
3352
|
/**
|
|
3301
3353
|
* Execute one round of tool calls and report each through onProgress.
|
|
3302
3354
|
* Returns the results to feed back plus the log entries produced.
|
|
3303
3355
|
*/
|
|
3304
|
-
async executeToolCalls(toolCalls,
|
|
3356
|
+
async executeToolCalls(toolCalls, fs10, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
3305
3357
|
const toolResults = [];
|
|
3306
3358
|
const logEntries = [];
|
|
3307
3359
|
let resultsText = "";
|
|
@@ -3320,18 +3372,18 @@ var BaseAiService = class _BaseAiService {
|
|
|
3320
3372
|
const subagentId = uuidv4();
|
|
3321
3373
|
spawnSubagentIds.set(i, subagentId);
|
|
3322
3374
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
3323
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
3375
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
|
|
3324
3376
|
}) : Promise.resolve(),
|
|
3325
3377
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
3326
3378
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
3327
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
3379
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs10, fetcher, signal));
|
|
3328
3380
|
}) : Promise.resolve()
|
|
3329
3381
|
]);
|
|
3330
3382
|
for (const [index, tc] of toolCalls.entries()) {
|
|
3331
3383
|
const toolArgs = normalizedArgs[index];
|
|
3332
3384
|
const preResult = precomputed.get(index);
|
|
3333
3385
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
3334
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
3386
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs10, fetcher, signal);
|
|
3335
3387
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
3336
3388
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
3337
3389
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -3517,7 +3569,7 @@ ${llmOutput}
|
|
|
3517
3569
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
3518
3570
|
* exhausted without a valid plan.
|
|
3519
3571
|
*/
|
|
3520
|
-
async runResearchLoop(chat, firstMessage,
|
|
3572
|
+
async runResearchLoop(chat, firstMessage, fs10, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
3521
3573
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
3522
3574
|
if (!initialLog && userGoal) {
|
|
3523
3575
|
researchLog.push({
|
|
@@ -3544,7 +3596,7 @@ ${llmOutput}
|
|
|
3544
3596
|
if (!turn.hasToolCalls) break;
|
|
3545
3597
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
3546
3598
|
turn.toolCalls,
|
|
3547
|
-
|
|
3599
|
+
fs10,
|
|
3548
3600
|
onProgress,
|
|
3549
3601
|
fetcher,
|
|
3550
3602
|
step,
|
|
@@ -3782,11 +3834,11 @@ ${repairHint}` }] : parts;
|
|
|
3782
3834
|
return this.consumePlanStream(result, onToken);
|
|
3783
3835
|
}
|
|
3784
3836
|
// --- IAiService implementation ---
|
|
3785
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
3837
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
3786
3838
|
this.ensureInit();
|
|
3787
3839
|
const { autonomousDefault } = modes;
|
|
3788
3840
|
const genModel = this.getPlanningModel();
|
|
3789
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
3841
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
3790
3842
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
3791
3843
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
3792
3844
|
|
|
@@ -3797,17 +3849,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
3797
3849
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
3798
3850
|
});
|
|
3799
3851
|
const researchChat = new GeminiResearchChat(chat);
|
|
3800
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
3852
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
3801
3853
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
3802
3854
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
3803
3855
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
3804
3856
|
}
|
|
3805
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
3857
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
3806
3858
|
this.ensureInit();
|
|
3807
3859
|
const { autonomousDefault } = modes;
|
|
3808
3860
|
let ctx = "";
|
|
3809
|
-
if (
|
|
3810
|
-
const collected = await new ContextCollector(
|
|
3861
|
+
if (fs10) {
|
|
3862
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
3811
3863
|
if (collected.aiflowContext) ctx = `
|
|
3812
3864
|
<aiflow_context>
|
|
3813
3865
|
${collected.aiflowContext}
|
|
@@ -3823,12 +3875,12 @@ ${collected.aiflowContext}
|
|
|
3823
3875
|
autonomousDefault
|
|
3824
3876
|
);
|
|
3825
3877
|
}
|
|
3826
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
3878
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
3827
3879
|
this.ensureInit();
|
|
3828
3880
|
const { autonomousDefault } = modes;
|
|
3829
3881
|
let aiflowContext;
|
|
3830
|
-
if (
|
|
3831
|
-
const collected = await new ContextCollector(
|
|
3882
|
+
if (fs10) {
|
|
3883
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
3832
3884
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
3833
3885
|
}
|
|
3834
3886
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -4068,12 +4120,12 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
4068
4120
|
}
|
|
4069
4121
|
}
|
|
4070
4122
|
// --- IAiService implementation ---
|
|
4071
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
4123
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
4072
4124
|
this.ensureInit();
|
|
4073
4125
|
const client = this.getClient();
|
|
4074
4126
|
const { autonomousDefault, researchSubagents: researchSubagentsEnabled } = modes;
|
|
4075
4127
|
this.researchSubagentsEnabled = researchSubagentsEnabled;
|
|
4076
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
4128
|
+
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
4077
4129
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
4078
4130
|
const userText = `User goal: ${userDescription}`;
|
|
4079
4131
|
const firstMessage = `${buildResearchToolsPrompt(researchSubagentsEnabled)}
|
|
@@ -4091,17 +4143,17 @@ ${userText}`;
|
|
|
4091
4143
|
toOpenAiTools(researchSubagentsEnabled),
|
|
4092
4144
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
4093
4145
|
);
|
|
4094
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
4146
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs10, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
4095
4147
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
4096
4148
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
4097
4149
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
4098
4150
|
}
|
|
4099
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
4151
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
4100
4152
|
this.ensureInit();
|
|
4101
4153
|
const { autonomousDefault } = modes;
|
|
4102
4154
|
let ctx = "";
|
|
4103
|
-
if (
|
|
4104
|
-
const collected = await new ContextCollector(
|
|
4155
|
+
if (fs10) {
|
|
4156
|
+
const collected = await new ContextCollector(fs10).collect(runners[0] ?? "claude-code");
|
|
4105
4157
|
if (collected.aiflowContext) ctx = `
|
|
4106
4158
|
<aiflow_context>
|
|
4107
4159
|
${collected.aiflowContext}
|
|
@@ -4117,12 +4169,12 @@ ${collected.aiflowContext}
|
|
|
4117
4169
|
autonomousDefault
|
|
4118
4170
|
);
|
|
4119
4171
|
}
|
|
4120
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
4172
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs10, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
4121
4173
|
this.ensureInit();
|
|
4122
4174
|
const { autonomousDefault } = modes;
|
|
4123
4175
|
let aiflowContext;
|
|
4124
|
-
if (
|
|
4125
|
-
const collected = await new ContextCollector(
|
|
4176
|
+
if (fs10) {
|
|
4177
|
+
const collected = await new ContextCollector(fs10).collect(existingPlan.runners[0] ?? "claude-code");
|
|
4126
4178
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
4127
4179
|
}
|
|
4128
4180
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -4740,6 +4792,7 @@ var PlanStore = class {
|
|
|
4740
4792
|
}
|
|
4741
4793
|
remove(taskId) {
|
|
4742
4794
|
if (!this._taskMap.has(taskId)) return;
|
|
4795
|
+
this.unblockDependents(taskId);
|
|
4743
4796
|
this._planTasks = removeTaskFromPlan(this._planTasks, taskId);
|
|
4744
4797
|
this.rebuild();
|
|
4745
4798
|
this.pruneTerminalSets();
|
|
@@ -4794,9 +4847,7 @@ ${taskB.description}`,
|
|
|
4794
4847
|
const userIds = /* @__PURE__ */ new Set([taskIdA, taskIdB]);
|
|
4795
4848
|
const sanitized = this._planTasks.filter((t) => !userIds.has(t.id)).map((t) => ({
|
|
4796
4849
|
...t,
|
|
4797
|
-
dependencies: t.dependencies.map(
|
|
4798
|
-
(depId) => depId === taskIdA || depId === taskIdB ? mergedId : depId
|
|
4799
|
-
)
|
|
4850
|
+
dependencies: [...new Set(t.dependencies.map((depId) => userIds.has(depId) ? mergedId : depId))]
|
|
4800
4851
|
}));
|
|
4801
4852
|
this._planTasks = renumberTasks([...sanitized, merged]);
|
|
4802
4853
|
this.rebuild();
|
|
@@ -4808,11 +4859,12 @@ ${taskB.description}`,
|
|
|
4808
4859
|
const original = this._taskMap.get(taskId);
|
|
4809
4860
|
if (!original) throw new Error(`Task ${taskId} not found`);
|
|
4810
4861
|
if (!newTaskSpecs.length) throw new Error("Must provide at least one new task spec");
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4862
|
+
const finalized = [];
|
|
4863
|
+
newTaskSpecs.forEach((spec, i) => {
|
|
4864
|
+
finalized.push(createTask({
|
|
4813
4865
|
...spec,
|
|
4814
4866
|
order: original.order + i,
|
|
4815
|
-
dependencies: i === 0 ? [...original.dependencies] : [
|
|
4867
|
+
dependencies: i === 0 ? [...original.dependencies] : [finalized[i - 1].id],
|
|
4816
4868
|
type: spec.type ?? original.type,
|
|
4817
4869
|
assignedRunner: spec.assignedRunner ?? original.assignedRunner,
|
|
4818
4870
|
assignedModel: spec.assignedModel ?? original.assignedModel,
|
|
@@ -4820,17 +4872,7 @@ ${taskB.description}`,
|
|
|
4820
4872
|
autonomy: spec.autonomy ?? original.autonomy,
|
|
4821
4873
|
sliceType: spec.sliceType ?? original.sliceType,
|
|
4822
4874
|
userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered
|
|
4823
|
-
})
|
|
4824
|
-
);
|
|
4825
|
-
const newTaskIds = /* @__PURE__ */ new Map();
|
|
4826
|
-
newTasks.forEach((t, i) => newTaskIds.set(i, t.id));
|
|
4827
|
-
const finalized = newTasks.map((t) => {
|
|
4828
|
-
const deps = t.dependencies.map((depId) => {
|
|
4829
|
-
const idx = newTaskSpecs.findIndex((s) => (s.id ?? "") === depId);
|
|
4830
|
-
if (idx >= 0) return newTaskIds.get(idx) ?? depId;
|
|
4831
|
-
return depId;
|
|
4832
|
-
});
|
|
4833
|
-
return { ...t, dependencies: deps };
|
|
4875
|
+
}));
|
|
4834
4876
|
});
|
|
4835
4877
|
const tailId = finalized[finalized.length - 1].id;
|
|
4836
4878
|
const updatedOthers = this._planTasks.filter((t) => t.id !== taskId).map((t) => ({
|
|
@@ -5075,6 +5117,15 @@ var TaskOrchestrator = class {
|
|
|
5075
5117
|
get isRunning() {
|
|
5076
5118
|
return this.running || this.startingTaskIds.size > 0 || this.activeTaskSessions.size > 0;
|
|
5077
5119
|
}
|
|
5120
|
+
/**
|
|
5121
|
+
* A runner is executing *right now*. Narrower than {@link isRunning}, which
|
|
5122
|
+
* also covers the armed-but-idle scheduler `tick()` deliberately leaves
|
|
5123
|
+
* behind when a plan is paused on a user task, a checkpoint or a hold —
|
|
5124
|
+
* nothing is executing then, so nothing is reading the plan mid-mutation.
|
|
5125
|
+
*/
|
|
5126
|
+
get hasLiveWork() {
|
|
5127
|
+
return this.activeTaskSessions.size > 0 || this.startingTaskIds.size > 0;
|
|
5128
|
+
}
|
|
5078
5129
|
get isReviewApproved() {
|
|
5079
5130
|
return this.reviewApproved;
|
|
5080
5131
|
}
|
|
@@ -5121,18 +5172,31 @@ var TaskOrchestrator = class {
|
|
|
5121
5172
|
this.retryCounts.clear();
|
|
5122
5173
|
this.onHold.clear();
|
|
5123
5174
|
}
|
|
5175
|
+
/**
|
|
5176
|
+
* Adopt an edited plan without letting go of the run in progress. Everything
|
|
5177
|
+
* the scheduler owns — live sessions, holds, retry counts, the verifier and
|
|
5178
|
+
* the review approval — survives, because `loadPlan` clears all of it and a
|
|
5179
|
+
* mid-run edit is not a new run. Only the tasks change.
|
|
5180
|
+
*
|
|
5181
|
+
* The ids with live sessions are defended: one dropped from the edited plan is
|
|
5182
|
+
* carried over (its verdict still has to land somewhere), and one whose status
|
|
5183
|
+
* the snapshot predates is put back to `in_progress` — adopting the snapshot's
|
|
5184
|
+
* "pending" would offer the scheduler work a runner is already doing.
|
|
5185
|
+
*/
|
|
5124
5186
|
reconcilePlan(newTasks, planRunners = ["claude-code"]) {
|
|
5187
|
+
const adopted = [...newTasks];
|
|
5125
5188
|
for (const taskId of this.activeTaskSessions.keys()) {
|
|
5126
5189
|
const task = this.store.get(taskId);
|
|
5127
5190
|
if (!task) continue;
|
|
5128
|
-
const
|
|
5129
|
-
if (
|
|
5130
|
-
|
|
5131
|
-
} else if (
|
|
5132
|
-
console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${
|
|
5191
|
+
const at = adopted.findIndex((t) => t.id === taskId);
|
|
5192
|
+
if (at < 0) {
|
|
5193
|
+
adopted.push({ ...task });
|
|
5194
|
+
} else if (adopted[at].status !== "in_progress") {
|
|
5195
|
+
console.warn(`[TaskOrchestrator] Running task "${task.title}" status changed to "${adopted[at].status}" in modified plan, using orchestrator truth`);
|
|
5196
|
+
adopted[at] = { ...adopted[at], status: "in_progress" };
|
|
5133
5197
|
}
|
|
5134
5198
|
}
|
|
5135
|
-
this.store.load(
|
|
5199
|
+
this.store.load(adopted, planRunners);
|
|
5136
5200
|
this.planStatus = "running";
|
|
5137
5201
|
}
|
|
5138
5202
|
async start() {
|
|
@@ -5253,6 +5317,21 @@ ${output.slice(-3e3)}`);
|
|
|
5253
5317
|
this.emit("onTaskChanged");
|
|
5254
5318
|
await this.tick();
|
|
5255
5319
|
}
|
|
5320
|
+
/**
|
|
5321
|
+
* Let go of a task that is leaving the plan. A live runner is cancelled
|
|
5322
|
+
* through {@link cancelTask} so the verifier generation is bumped before the
|
|
5323
|
+
* process dies; a spawn still in flight is dropped from `startingTaskIds`,
|
|
5324
|
+
* which is what makes {@link startTask} kill the session it is about to
|
|
5325
|
+
* receive. The id's remaining bookkeeping goes too — a hold or retry count
|
|
5326
|
+
* kept for a task that no longer exists would be inherited by nothing and
|
|
5327
|
+
* leaves `isRunning` reading true forever.
|
|
5328
|
+
*/
|
|
5329
|
+
async releaseTask(taskId) {
|
|
5330
|
+
this.startingTaskIds.delete(taskId);
|
|
5331
|
+
if (this.activeTaskSessions.has(taskId)) await this.cancelTask(taskId);
|
|
5332
|
+
this.onHold.delete(taskId);
|
|
5333
|
+
this.retryCounts.delete(taskId);
|
|
5334
|
+
}
|
|
5256
5335
|
async markTaskComplete(taskId) {
|
|
5257
5336
|
const task = this.store.get(taskId);
|
|
5258
5337
|
if (!task || task.status === "completed") return;
|
|
@@ -5712,28 +5791,28 @@ function posixBinDirs() {
|
|
|
5712
5791
|
];
|
|
5713
5792
|
}
|
|
5714
5793
|
function windowsBinDirs(env, home) {
|
|
5715
|
-
const
|
|
5716
|
-
const under = (base, ...parts) => base ? [
|
|
5794
|
+
const join14 = (...parts) => path5.win32.join(...parts);
|
|
5795
|
+
const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
|
|
5717
5796
|
const appData = env.APPDATA;
|
|
5718
5797
|
const localAppData = env.LOCALAPPDATA;
|
|
5719
5798
|
const programData = env.ProgramData;
|
|
5720
5799
|
return [
|
|
5721
5800
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5801
|
+
join14(home, ".local", "bin"),
|
|
5802
|
+
join14(home, ".opencode", "bin"),
|
|
5803
|
+
join14(home, ".claude", "bin"),
|
|
5804
|
+
join14(home, ".codex", "bin"),
|
|
5726
5805
|
...under(localAppData, "Programs", "claude"),
|
|
5727
5806
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
5728
5807
|
// literal spelling is kept as a fallback for a host that does not export
|
|
5729
5808
|
// APPDATA (a service account, a stripped CI container).
|
|
5730
5809
|
...under(appData, "npm"),
|
|
5731
|
-
|
|
5810
|
+
join14(home, "AppData", "Roaming", "npm"),
|
|
5732
5811
|
...under(localAppData, "pnpm"),
|
|
5733
5812
|
...under(localAppData, "Yarn", "bin"),
|
|
5734
|
-
|
|
5813
|
+
join14(home, ".bun", "bin"),
|
|
5735
5814
|
// Windows package managers.
|
|
5736
|
-
|
|
5815
|
+
join14(home, "scoop", "shims"),
|
|
5737
5816
|
...under(env.SCOOP, "shims"),
|
|
5738
5817
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
5739
5818
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -5743,7 +5822,7 @@ function windowsBinDirs(env, home) {
|
|
|
5743
5822
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
5744
5823
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
5745
5824
|
...under(localAppData, "Volta", "bin"),
|
|
5746
|
-
|
|
5825
|
+
join14(home, ".volta", "bin")
|
|
5747
5826
|
];
|
|
5748
5827
|
}
|
|
5749
5828
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -5866,6 +5945,26 @@ Install the native executable \u2014 Claude Code's Windows installer, Codex's re
|
|
|
5866
5945
|
function hasLineBreak(parts) {
|
|
5867
5946
|
return parts.some((part) => /[\r\n]/.test(part));
|
|
5868
5947
|
}
|
|
5948
|
+
var ExecutableNotFoundError = class extends Error {
|
|
5949
|
+
constructor(command, searchedPath) {
|
|
5950
|
+
super(`Could not find "${command}" on PATH.
|
|
5951
|
+
|
|
5952
|
+
Searched: ${searchedPath || "(empty PATH)"}`);
|
|
5953
|
+
this.command = command;
|
|
5954
|
+
this.searchedPath = searchedPath;
|
|
5955
|
+
this.name = "ExecutableNotFoundError";
|
|
5956
|
+
}
|
|
5957
|
+
command;
|
|
5958
|
+
searchedPath;
|
|
5959
|
+
};
|
|
5960
|
+
function isResolvableOnPosixPath(command, PATH, exists) {
|
|
5961
|
+
if (command.includes("/")) return exists(command);
|
|
5962
|
+
return PATH.split(path6.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path6.join(dir, command)));
|
|
5963
|
+
}
|
|
5964
|
+
function isExecutableResolved(command, plan, PATH, deps = {}) {
|
|
5965
|
+
if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
|
|
5966
|
+
return isResolvableOnPosixPath(command, PATH, deps.exists ?? defaultExists2);
|
|
5967
|
+
}
|
|
5869
5968
|
function defaultExists2(candidate) {
|
|
5870
5969
|
try {
|
|
5871
5970
|
return fsSync2.statSync(candidate).isFile();
|
|
@@ -6625,6 +6724,10 @@ var ModelDiscovery = class {
|
|
|
6625
6724
|
clear() {
|
|
6626
6725
|
this.cache.clear();
|
|
6627
6726
|
}
|
|
6727
|
+
/** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
|
|
6728
|
+
getCached(runner) {
|
|
6729
|
+
return this.cache.get(runner);
|
|
6730
|
+
}
|
|
6628
6731
|
};
|
|
6629
6732
|
|
|
6630
6733
|
// src/services/ModelCatalog.ts
|
|
@@ -6943,6 +7046,17 @@ var ModelResolver = class {
|
|
|
6943
7046
|
refreshRunnerModels() {
|
|
6944
7047
|
this.discovery.clear();
|
|
6945
7048
|
}
|
|
7049
|
+
/**
|
|
7050
|
+
* Synchronous reads of whatever the last `modelsForRunners`/`pickerOptions`
|
|
7051
|
+
* call already cached — never trigger discovery or a fetch. For callers
|
|
7052
|
+
* (e.g. a settings write) that need a catalog without going async.
|
|
7053
|
+
*/
|
|
7054
|
+
getCachedRunnerModels(runner) {
|
|
7055
|
+
return this.discovery.getCached(runner) ?? [];
|
|
7056
|
+
}
|
|
7057
|
+
getCachedPickerOptions() {
|
|
7058
|
+
return this.cachedPickerOptions ?? [];
|
|
7059
|
+
}
|
|
6946
7060
|
invalidate() {
|
|
6947
7061
|
this.discovery.clear();
|
|
6948
7062
|
this.cachedPickerOptions = null;
|
|
@@ -7070,6 +7184,28 @@ var RunnerInstallation = class {
|
|
|
7070
7184
|
// src/services/harness/CliAgentAiService.ts
|
|
7071
7185
|
import { spawn as nodeSpawn } from "child_process";
|
|
7072
7186
|
|
|
7187
|
+
// src/utils/workspace.ts
|
|
7188
|
+
import * as fs3 from "fs";
|
|
7189
|
+
var WorkspaceNotFoundError = class extends Error {
|
|
7190
|
+
constructor(workspace) {
|
|
7191
|
+
super(`Workspace "${workspace}" does not exist.`);
|
|
7192
|
+
this.workspace = workspace;
|
|
7193
|
+
this.name = "WorkspaceNotFoundError";
|
|
7194
|
+
}
|
|
7195
|
+
workspace;
|
|
7196
|
+
};
|
|
7197
|
+
function defaultIsDirectory(candidate) {
|
|
7198
|
+
try {
|
|
7199
|
+
return fs3.statSync(candidate).isDirectory();
|
|
7200
|
+
} catch {
|
|
7201
|
+
return false;
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
function assertWorkspaceExists(workspace, deps = {}) {
|
|
7205
|
+
const isDirectory = deps.isDirectory ?? defaultIsDirectory;
|
|
7206
|
+
if (!isDirectory(workspace)) throw new WorkspaceNotFoundError(workspace);
|
|
7207
|
+
}
|
|
7208
|
+
|
|
7073
7209
|
// src/services/harness/AgentAdapter.ts
|
|
7074
7210
|
var LineBuffer = class {
|
|
7075
7211
|
buffer = "";
|
|
@@ -7123,6 +7259,7 @@ var StdioAgentAdapter = class {
|
|
|
7123
7259
|
async handshake(_opts) {
|
|
7124
7260
|
}
|
|
7125
7261
|
async start(opts) {
|
|
7262
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
7126
7263
|
this.processEnded = new Promise((resolve3) => {
|
|
7127
7264
|
this.markEnded = resolve3;
|
|
7128
7265
|
});
|
|
@@ -7134,6 +7271,9 @@ var StdioAgentAdapter = class {
|
|
|
7134
7271
|
platform: this.deps.platform,
|
|
7135
7272
|
resolvePath
|
|
7136
7273
|
});
|
|
7274
|
+
if (!isExecutableResolved(spec.command, launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
7275
|
+
throw new ExecutableNotFoundError(spec.command, PATH);
|
|
7276
|
+
}
|
|
7137
7277
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
7138
7278
|
env: this.spawnEnv,
|
|
7139
7279
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7756,12 +7896,16 @@ var OpenCodeAdapter = class {
|
|
|
7756
7896
|
opts = null;
|
|
7757
7897
|
async start(opts) {
|
|
7758
7898
|
this.opts = opts;
|
|
7899
|
+
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
7759
7900
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
7760
7901
|
const PATH = await resolvePath();
|
|
7761
7902
|
const launch = await planDirectLaunch("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
|
|
7762
7903
|
platform: this.deps.platform,
|
|
7763
7904
|
resolvePath
|
|
7764
7905
|
});
|
|
7906
|
+
if (!isExecutableResolved("opencode", launch, PATH, { platform: this.deps.platform, exists: this.deps.exists })) {
|
|
7907
|
+
throw new ExecutableNotFoundError("opencode", PATH);
|
|
7908
|
+
}
|
|
7765
7909
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
7766
7910
|
env: withPath(process.env, PATH),
|
|
7767
7911
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -8075,7 +8219,9 @@ var CliAgentAiService = class {
|
|
|
8075
8219
|
spawn: deps.spawn ?? nodeSpawn,
|
|
8076
8220
|
fetch: deps.fetch ?? globalThis.fetch,
|
|
8077
8221
|
resolvePath: deps.resolvePath,
|
|
8078
|
-
platform: deps.platform
|
|
8222
|
+
platform: deps.platform,
|
|
8223
|
+
isDirectory: deps.isDirectory,
|
|
8224
|
+
exists: deps.exists
|
|
8079
8225
|
};
|
|
8080
8226
|
this.makeAdapter = deps.createAdapter ?? defaultAdapter;
|
|
8081
8227
|
this.workspaceRoot = deps.workspaceRoot ?? (() => process.cwd());
|
|
@@ -8420,8 +8566,8 @@ var CliAgentAiService = class {
|
|
|
8420
8566
|
this.lastNativeSessionId = previousSessionId;
|
|
8421
8567
|
}
|
|
8422
8568
|
}
|
|
8423
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
8424
|
-
const contextStr = await collectResearchContext(
|
|
8569
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs10, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
8570
|
+
const contextStr = await collectResearchContext(fs10, runners);
|
|
8425
8571
|
const researchLog = [
|
|
8426
8572
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
8427
8573
|
];
|
|
@@ -8791,16 +8937,16 @@ function executionSummary(tasks) {
|
|
|
8791
8937
|
}
|
|
8792
8938
|
|
|
8793
8939
|
// src/utils/sessionStore.ts
|
|
8794
|
-
import * as
|
|
8940
|
+
import * as fs5 from "fs";
|
|
8795
8941
|
import * as path10 from "path";
|
|
8796
8942
|
|
|
8797
8943
|
// src/utils/fsHelpers.ts
|
|
8798
|
-
import * as
|
|
8944
|
+
import * as fs4 from "fs";
|
|
8799
8945
|
import * as path9 from "path";
|
|
8800
8946
|
var STATE_DIR = ".ordewell";
|
|
8801
8947
|
function ensureDir(dir) {
|
|
8802
|
-
if (!
|
|
8803
|
-
|
|
8948
|
+
if (!fs4.existsSync(dir)) {
|
|
8949
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
8804
8950
|
}
|
|
8805
8951
|
}
|
|
8806
8952
|
function getStateDir(baseDir) {
|
|
@@ -8845,16 +8991,16 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
8845
8991
|
};
|
|
8846
8992
|
const filename = sessionFilename(meta);
|
|
8847
8993
|
const filePath = path10.join(sessionsDir, filename);
|
|
8848
|
-
|
|
8994
|
+
fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
8849
8995
|
return meta;
|
|
8850
8996
|
}
|
|
8851
8997
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
8852
8998
|
const sessionsDir = getSessionsDir(baseDir);
|
|
8853
|
-
if (!
|
|
8854
|
-
const files =
|
|
8999
|
+
if (!fs5.existsSync(sessionsDir)) return [];
|
|
9000
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
8855
9001
|
return files.map((f) => {
|
|
8856
9002
|
try {
|
|
8857
|
-
const raw =
|
|
9003
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
8858
9004
|
const parsed = JSON.parse(raw);
|
|
8859
9005
|
if (hasScalarRunner(parsed)) {
|
|
8860
9006
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -8869,10 +9015,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
8869
9015
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
8870
9016
|
}
|
|
8871
9017
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
8872
|
-
const files =
|
|
9018
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
8873
9019
|
for (const f of files) {
|
|
8874
9020
|
try {
|
|
8875
|
-
const raw =
|
|
9021
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
|
|
8876
9022
|
const session = JSON.parse(raw);
|
|
8877
9023
|
if (session.meta.id === sessionId) return f;
|
|
8878
9024
|
} catch (err) {
|
|
@@ -8883,11 +9029,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
8883
9029
|
}
|
|
8884
9030
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
8885
9031
|
const sessionsDir = getSessionsDir(baseDir);
|
|
8886
|
-
if (!
|
|
9032
|
+
if (!fs5.existsSync(sessionsDir)) return null;
|
|
8887
9033
|
try {
|
|
8888
9034
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
8889
9035
|
if (file) {
|
|
8890
|
-
const raw =
|
|
9036
|
+
const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
|
|
8891
9037
|
const parsed = JSON.parse(raw);
|
|
8892
9038
|
if (hasScalarRunner(parsed)) {
|
|
8893
9039
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -8917,11 +9063,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
8917
9063
|
}
|
|
8918
9064
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
8919
9065
|
const sessionsDir = getSessionsDir(baseDir);
|
|
8920
|
-
if (!
|
|
9066
|
+
if (!fs5.existsSync(sessionsDir)) return false;
|
|
8921
9067
|
try {
|
|
8922
9068
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
8923
9069
|
if (file) {
|
|
8924
|
-
|
|
9070
|
+
fs5.unlinkSync(path10.join(sessionsDir, file));
|
|
8925
9071
|
return true;
|
|
8926
9072
|
}
|
|
8927
9073
|
} catch (err) {
|
|
@@ -8931,6 +9077,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
8931
9077
|
}
|
|
8932
9078
|
|
|
8933
9079
|
// src/services/createSession.ts
|
|
9080
|
+
var PlanEditError = class extends Error {
|
|
9081
|
+
constructor(message) {
|
|
9082
|
+
super(message);
|
|
9083
|
+
this.name = "PlanEditError";
|
|
9084
|
+
}
|
|
9085
|
+
};
|
|
8934
9086
|
function sessionRuntimeSettings(settings) {
|
|
8935
9087
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
|
|
8936
9088
|
}
|
|
@@ -9203,12 +9355,41 @@ var Session = class {
|
|
|
9203
9355
|
get isExecuting() {
|
|
9204
9356
|
return this.orchestrator.isRunning;
|
|
9205
9357
|
}
|
|
9358
|
+
/** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
|
|
9359
|
+
get hasLiveWork() {
|
|
9360
|
+
return this.orchestrator.hasLiveWork;
|
|
9361
|
+
}
|
|
9206
9362
|
get status() {
|
|
9207
9363
|
return this.orchestrator.status;
|
|
9208
9364
|
}
|
|
9209
9365
|
get sessionConfig() {
|
|
9210
9366
|
return this.config;
|
|
9211
9367
|
}
|
|
9368
|
+
/**
|
|
9369
|
+
* Deny every parked approval as soon as a planning turn is aborted, for the
|
|
9370
|
+
* same reason `beginFreshPlan` does it: a prompt raised by a turn nobody is
|
|
9371
|
+
* waiting on has no one left to serve, and denying it unblocks the research
|
|
9372
|
+
* loop instead of stranding it. Without this the request sat out its
|
|
9373
|
+
* five-minute timeout and the loop then carried on as if nothing had
|
|
9374
|
+
* happened — the abort was real, but invisible until long after the user
|
|
9375
|
+
* pressed stop.
|
|
9376
|
+
*
|
|
9377
|
+
* The listener is returned as a disposer rather than left attached: callers
|
|
9378
|
+
* own the signal and may reuse it, and a leaked listener would deny the
|
|
9379
|
+
* *next* turn's prompts the moment that stale signal aborted.
|
|
9380
|
+
*/
|
|
9381
|
+
denyApprovalsOnAbort(signal) {
|
|
9382
|
+
if (!signal) return () => {
|
|
9383
|
+
};
|
|
9384
|
+
if (signal.aborted) {
|
|
9385
|
+
this.approvals.clear();
|
|
9386
|
+
return () => {
|
|
9387
|
+
};
|
|
9388
|
+
}
|
|
9389
|
+
const onAbort = () => this.approvals.clear();
|
|
9390
|
+
signal.addEventListener("abort", onAbort);
|
|
9391
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
9392
|
+
}
|
|
9212
9393
|
async startExecution() {
|
|
9213
9394
|
if (!this.plan || !this.store.planTasks.length) throw new Error("No plan to execute");
|
|
9214
9395
|
this.store.clearLog();
|
|
@@ -9231,19 +9412,25 @@ var Session = class {
|
|
|
9231
9412
|
const settings = this.settingsFn();
|
|
9232
9413
|
const { modelAllowlist } = settings;
|
|
9233
9414
|
const modes = plannerModesFrom(settings, this.config.autonomousMode);
|
|
9234
|
-
const
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9415
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
9416
|
+
let plan;
|
|
9417
|
+
try {
|
|
9418
|
+
plan = await this.planner.generate({
|
|
9419
|
+
goal,
|
|
9420
|
+
runners: chosenRunners,
|
|
9421
|
+
modelsByRunner,
|
|
9422
|
+
runnerModes,
|
|
9423
|
+
autonomousDefault: this.config.autonomousMode,
|
|
9424
|
+
fs: this.fsAdapter,
|
|
9425
|
+
fetcher: this.fetcher,
|
|
9426
|
+
onProgress: (p) => this.translateProgress(p),
|
|
9427
|
+
signal: options?.signal,
|
|
9428
|
+
perRunnerAllowlist: modelAllowlist,
|
|
9429
|
+
modes
|
|
9430
|
+
});
|
|
9431
|
+
} finally {
|
|
9432
|
+
releaseAbort();
|
|
9433
|
+
}
|
|
9247
9434
|
this.plan = plan;
|
|
9248
9435
|
this.orchestrator.loadPlan(plan.tasks, plan.runners);
|
|
9249
9436
|
this.store.resetForRun({ preserveCompleted: false });
|
|
@@ -9280,23 +9467,28 @@ var Session = class {
|
|
|
9280
9467
|
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
|
|
9281
9468
|
conversationHistory: [{ role: "user", content: goal, timestamp: now }]
|
|
9282
9469
|
};
|
|
9283
|
-
const
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9470
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
9471
|
+
try {
|
|
9472
|
+
const turn = await this.aiService.startConversation({
|
|
9473
|
+
goal,
|
|
9474
|
+
runners: chosenRunners,
|
|
9475
|
+
modelsByRunner: filteredModels,
|
|
9476
|
+
runnerModes,
|
|
9477
|
+
autonomousDefault: this.config.autonomousMode,
|
|
9478
|
+
grillMeEnabled: grillMeEnabled ?? false,
|
|
9479
|
+
prdEnabled: prdEnabled ?? false,
|
|
9480
|
+
reviewEnabled: reviewEnabled ?? false,
|
|
9481
|
+
verificationEnabled: verificationEnabled ?? false,
|
|
9482
|
+
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
9483
|
+
fs: this.fsAdapter,
|
|
9484
|
+
fetcher: this.fetcher,
|
|
9485
|
+
onProgress: (p) => this.translateProgress(p),
|
|
9486
|
+
signal: options?.signal
|
|
9487
|
+
});
|
|
9488
|
+
return await this.settleTurn(turn, options);
|
|
9489
|
+
} finally {
|
|
9490
|
+
releaseAbort();
|
|
9491
|
+
}
|
|
9300
9492
|
}
|
|
9301
9493
|
/**
|
|
9302
9494
|
* Every subsequent user reply in the planning conversation — grill-me
|
|
@@ -9314,21 +9506,26 @@ var Session = class {
|
|
|
9314
9506
|
${userMessage}` : userMessage;
|
|
9315
9507
|
const aiService = this.aiService;
|
|
9316
9508
|
const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
this.
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9509
|
+
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
9510
|
+
try {
|
|
9511
|
+
let turn = canContinueLive ? await aiService.continueConversation(
|
|
9512
|
+
outgoing,
|
|
9513
|
+
(p) => this.translateProgress(p),
|
|
9514
|
+
options?.signal
|
|
9515
|
+
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
9516
|
+
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
9517
|
+
this.queueMessage(userMessage);
|
|
9518
|
+
this.plan.queuedMessages = this.getQueuedMessages();
|
|
9519
|
+
turn = {
|
|
9520
|
+
kind: "message",
|
|
9521
|
+
text: `Execution is running, so I queued your change \u2014 it will be applied between task batches (${this.queuedCount} queued).`,
|
|
9522
|
+
researchLog: turn.researchLog
|
|
9523
|
+
};
|
|
9524
|
+
}
|
|
9525
|
+
return await this.settleTurn(turn, options);
|
|
9526
|
+
} finally {
|
|
9527
|
+
releaseAbort();
|
|
9330
9528
|
}
|
|
9331
|
-
return this.settleTurn(turn, options);
|
|
9332
9529
|
}
|
|
9333
9530
|
/**
|
|
9334
9531
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
@@ -9367,7 +9564,10 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
9367
9564
|
maxRepairs: 2,
|
|
9368
9565
|
onExhausted: ({ reply, errors }) => invalidOps(errors, reply.researchLog)
|
|
9369
9566
|
});
|
|
9370
|
-
if ("plan" in settled)
|
|
9567
|
+
if ("plan" in settled) {
|
|
9568
|
+
await this.orchestrator.tick();
|
|
9569
|
+
return settled.plan;
|
|
9570
|
+
}
|
|
9371
9571
|
return this.applyConversationTurn(settled.turn);
|
|
9372
9572
|
}
|
|
9373
9573
|
/**
|
|
@@ -9382,9 +9582,9 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
9382
9582
|
const lines = this.store.planTasks.map(
|
|
9383
9583
|
(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(", ")}]`
|
|
9384
9584
|
);
|
|
9385
|
-
const
|
|
9386
|
-
const execNote = this.
|
|
9387
|
-
Execution is RUNNING${
|
|
9585
|
+
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
9586
|
+
const execNote = this.hasLiveWork ? `
|
|
9587
|
+
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.` : "";
|
|
9388
9588
|
const modelLines = [];
|
|
9389
9589
|
for (const runner of this.plan.runners) {
|
|
9390
9590
|
const models = this.modelsCache[runner] ?? [];
|
|
@@ -9426,8 +9626,12 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9426
9626
|
this.plan.researchLog = [...this.plan.researchLog ?? [], ...turn.researchLog];
|
|
9427
9627
|
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
9428
9628
|
const coerced = coerceAssignments(result.tasks, allowlist, this.plan.runners, this.modelsCache);
|
|
9429
|
-
this.orchestrator.
|
|
9430
|
-
|
|
9629
|
+
if (this.orchestrator.isRunning) {
|
|
9630
|
+
this.orchestrator.reconcilePlan(coerced, this.plan.runners);
|
|
9631
|
+
} else {
|
|
9632
|
+
this.orchestrator.loadPlan(coerced, this.plan.runners);
|
|
9633
|
+
this.store.resetForRun();
|
|
9634
|
+
}
|
|
9431
9635
|
this.appendTranscript("assistant", content, now);
|
|
9432
9636
|
return true;
|
|
9433
9637
|
},
|
|
@@ -9663,8 +9867,33 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9663
9867
|
notify();
|
|
9664
9868
|
return this.plan;
|
|
9665
9869
|
}
|
|
9666
|
-
|
|
9667
|
-
|
|
9870
|
+
/**
|
|
9871
|
+
* The direct-edit seam: {@link mutatePlan} plus the reschedule every
|
|
9872
|
+
* structural edit owes an armed scheduler. Nothing else wakes one after a
|
|
9873
|
+
* hand edit — the queue-drain path never runs, because a direct edit never
|
|
9874
|
+
* queues — so a task the edit just unblocked would sit ready and never
|
|
9875
|
+
* start. `tick()` no-ops while the scheduler is idle, so this costs nothing
|
|
9876
|
+
* during plain planning. The planner-driven path re-ticks in
|
|
9877
|
+
* {@link settleTurn} instead; it must not tick twice.
|
|
9878
|
+
*/
|
|
9879
|
+
async editPlan(op, notify) {
|
|
9880
|
+
const plan = this.mutatePlan(op, notify);
|
|
9881
|
+
if (!plan) return null;
|
|
9882
|
+
await this.orchestrator.tick();
|
|
9883
|
+
return plan;
|
|
9884
|
+
}
|
|
9885
|
+
/**
|
|
9886
|
+
* Patch one task's fields. A hand-set dependency list is the one patch that
|
|
9887
|
+
* can leave the plan unschedulable, so it goes through the same
|
|
9888
|
+
* {@link canSetDependencies} guard the planner's pickers use rather than a
|
|
9889
|
+
* second copy of the rule — and throws, so the surface can say why.
|
|
9890
|
+
*/
|
|
9891
|
+
async updateTask(taskId, changes) {
|
|
9892
|
+
if (changes.dependencies) {
|
|
9893
|
+
const check = canSetDependencies(this.store.planTasks, taskId, changes.dependencies);
|
|
9894
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those dependencies are not valid");
|
|
9895
|
+
}
|
|
9896
|
+
return this.editPlan(
|
|
9668
9897
|
() => Boolean(this.store.update(taskId, changes)),
|
|
9669
9898
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
9670
9899
|
);
|
|
@@ -9674,8 +9903,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9674
9903
|
* because a runner change is never a single-field edit: the task's model,
|
|
9675
9904
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
9676
9905
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
9677
|
-
* That needs discovery, which is
|
|
9678
|
-
* than a branch inside the sync `updateTask`.
|
|
9906
|
+
* That needs discovery, which is why it is not a branch inside `updateTask`.
|
|
9679
9907
|
*
|
|
9680
9908
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
9681
9909
|
*/
|
|
@@ -9687,7 +9915,7 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9687
9915
|
const catalog = await this.catalogFor(runner);
|
|
9688
9916
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner));
|
|
9689
9917
|
if (Object.keys(changes).length === 0) return this.plan;
|
|
9690
|
-
return this.
|
|
9918
|
+
return this.editPlan(() => {
|
|
9691
9919
|
if (!this.store.update(taskId, changes)) return false;
|
|
9692
9920
|
this.admitRunner(runner, catalog.models);
|
|
9693
9921
|
return true;
|
|
@@ -9734,24 +9962,29 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9734
9962
|
if (models.length > 0) this.modelsCache = { ...this.modelsCache, [runner]: models };
|
|
9735
9963
|
}
|
|
9736
9964
|
/**
|
|
9737
|
-
* Replace one task's dependency list
|
|
9738
|
-
*
|
|
9739
|
-
*
|
|
9740
|
-
*
|
|
9741
|
-
* rather than each carrying a copy of the rule. Throws so a surface can say
|
|
9742
|
-
* why the edit was refused.
|
|
9965
|
+
* Replace one task's dependency list — the named entry point the surfaces'
|
|
9966
|
+
* dependency pickers call. The guard itself lives in {@link updateTask}, so
|
|
9967
|
+
* a dependency list arriving as a plain field patch is rejected by the same
|
|
9968
|
+
* rule instead of slipping past it.
|
|
9743
9969
|
*/
|
|
9744
|
-
setTaskDependencies(taskId, dependencies) {
|
|
9970
|
+
async setTaskDependencies(taskId, dependencies) {
|
|
9745
9971
|
if (!this.plan) return null;
|
|
9746
|
-
const check = canSetDependencies(this.store.planTasks, taskId, dependencies);
|
|
9747
|
-
if (!check.ok) throw new Error(check.error ?? "Those dependencies are not valid");
|
|
9748
9972
|
return this.updateTask(taskId, { dependencies });
|
|
9749
9973
|
}
|
|
9750
9974
|
async completeTask(taskId) {
|
|
9751
9975
|
await this.markTaskComplete(taskId);
|
|
9752
9976
|
}
|
|
9753
|
-
|
|
9754
|
-
|
|
9977
|
+
/**
|
|
9978
|
+
* Delete one task. A running task is cancelled first: the plan can drop it
|
|
9979
|
+
* either way, but nothing can reach its runner afterwards — the tmux session
|
|
9980
|
+
* outlives the plan and the orchestrator keeps counting it as active. The
|
|
9981
|
+
* planner-driven path refuses instead (see {@link applyTaskOps}); a user
|
|
9982
|
+
* deleting their own task means it.
|
|
9983
|
+
*/
|
|
9984
|
+
async removeTask(taskId) {
|
|
9985
|
+
if (!this.plan || !this.store.get(taskId)) return null;
|
|
9986
|
+
await this.orchestrator.releaseTask(taskId);
|
|
9987
|
+
return this.editPlan(() => (this.store.remove(taskId), true));
|
|
9755
9988
|
}
|
|
9756
9989
|
/**
|
|
9757
9990
|
* Add one task, filling in whatever the caller left unset. A task with no
|
|
@@ -9771,20 +10004,20 @@ Execution is RUNNING${running.length ? ` \u2014 these tasks are locked: ${runnin
|
|
|
9771
10004
|
const runner = draft.assignedRunner ?? this.plan.runners[0];
|
|
9772
10005
|
const dependencies = (draft.dependencies ?? []).filter((id) => this.store.get(id));
|
|
9773
10006
|
const catalog = draft.type === "user" ? null : await this.catalogFor(runner);
|
|
9774
|
-
return this.
|
|
10007
|
+
return this.editPlan(() => {
|
|
9775
10008
|
this.store.add({ ...draft, ...catalog ? runnerAssignment(this.allowedCatalog(catalog, runner), draft) : {}, assignedRunner: runner, dependencies });
|
|
9776
10009
|
if (catalog) this.admitRunner(runner, catalog.models);
|
|
9777
10010
|
return true;
|
|
9778
10011
|
});
|
|
9779
10012
|
}
|
|
9780
|
-
mergeTasks(taskIdA, taskIdB) {
|
|
9781
|
-
return this.
|
|
10013
|
+
async mergeTasks(taskIdA, taskIdB) {
|
|
10014
|
+
return this.editPlan(() => (this.store.merge(taskIdA, taskIdB), true));
|
|
9782
10015
|
}
|
|
9783
|
-
mergeMultipleTasks(taskIds) {
|
|
9784
|
-
return this.
|
|
10016
|
+
async mergeMultipleTasks(taskIds) {
|
|
10017
|
+
return this.editPlan(() => (this.store.mergeMultiple(taskIds), true));
|
|
9785
10018
|
}
|
|
9786
|
-
splitTask(taskId, newTasks) {
|
|
9787
|
-
return this.
|
|
10019
|
+
async splitTask(taskId, newTasks) {
|
|
10020
|
+
return this.editPlan(() => (this.store.split(taskId, newTasks), true));
|
|
9788
10021
|
}
|
|
9789
10022
|
/**
|
|
9790
10023
|
* Planner-driven merge: validate compatibility up front, then ask the planner
|
|
@@ -10298,7 +10531,7 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
10298
10531
|
import { execFile as execFile2 } from "child_process";
|
|
10299
10532
|
import { promisify as promisify4 } from "util";
|
|
10300
10533
|
import { tmpdir } from "os";
|
|
10301
|
-
import { join as
|
|
10534
|
+
import { join as join9 } from "path";
|
|
10302
10535
|
import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
10303
10536
|
|
|
10304
10537
|
// src/utils/tmux.ts
|
|
@@ -10583,7 +10816,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
10583
10816
|
windowName,
|
|
10584
10817
|
this.execFileImpl,
|
|
10585
10818
|
this.pollIntervalMs,
|
|
10586
|
-
|
|
10819
|
+
join9(this.logDir, `${id}.log`),
|
|
10587
10820
|
this.socket
|
|
10588
10821
|
);
|
|
10589
10822
|
invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
|
|
@@ -10594,13 +10827,13 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
10594
10827
|
};
|
|
10595
10828
|
|
|
10596
10829
|
// src/plugins/RunnerRegistry.ts
|
|
10597
|
-
import * as
|
|
10830
|
+
import * as fs7 from "fs";
|
|
10598
10831
|
import * as path12 from "path";
|
|
10599
10832
|
import * as os5 from "os";
|
|
10600
10833
|
import { execSync as execSync3 } from "child_process";
|
|
10601
10834
|
|
|
10602
10835
|
// src/plugins/FsPluginStore.ts
|
|
10603
|
-
import * as
|
|
10836
|
+
import * as fs6 from "fs";
|
|
10604
10837
|
import * as path11 from "path";
|
|
10605
10838
|
import * as os4 from "os";
|
|
10606
10839
|
function userPluginsDir() {
|
|
@@ -10613,9 +10846,9 @@ function isValidManifest(obj) {
|
|
|
10613
10846
|
}
|
|
10614
10847
|
function loadManifestFromDir(dir) {
|
|
10615
10848
|
const manifestPath = path11.join(dir, "manifest.json");
|
|
10616
|
-
if (!
|
|
10849
|
+
if (!fs6.existsSync(manifestPath)) return null;
|
|
10617
10850
|
try {
|
|
10618
|
-
const raw =
|
|
10851
|
+
const raw = fs6.readFileSync(manifestPath, "utf8");
|
|
10619
10852
|
const parsed = JSON.parse(raw);
|
|
10620
10853
|
if (!isValidManifest(parsed)) return null;
|
|
10621
10854
|
return parsed;
|
|
@@ -10625,29 +10858,29 @@ function loadManifestFromDir(dir) {
|
|
|
10625
10858
|
}
|
|
10626
10859
|
function copyDirSync(src, dest) {
|
|
10627
10860
|
ensureDir(dest);
|
|
10628
|
-
const entries =
|
|
10861
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
10629
10862
|
for (const entry of entries) {
|
|
10630
10863
|
const srcPath = path11.join(src, entry.name);
|
|
10631
10864
|
const destPath = path11.join(dest, entry.name);
|
|
10632
10865
|
if (entry.isDirectory()) {
|
|
10633
10866
|
copyDirSync(srcPath, destPath);
|
|
10634
10867
|
} else {
|
|
10635
|
-
|
|
10868
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
10636
10869
|
}
|
|
10637
10870
|
}
|
|
10638
10871
|
}
|
|
10639
10872
|
function rmDirSync(dir) {
|
|
10640
|
-
if (!
|
|
10641
|
-
const entries =
|
|
10873
|
+
if (!fs6.existsSync(dir)) return;
|
|
10874
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
10642
10875
|
for (const entry of entries) {
|
|
10643
10876
|
const p = path11.join(dir, entry.name);
|
|
10644
10877
|
if (entry.isDirectory()) {
|
|
10645
10878
|
rmDirSync(p);
|
|
10646
10879
|
} else {
|
|
10647
|
-
|
|
10880
|
+
fs6.unlinkSync(p);
|
|
10648
10881
|
}
|
|
10649
10882
|
}
|
|
10650
|
-
|
|
10883
|
+
fs6.rmdirSync(dir);
|
|
10651
10884
|
}
|
|
10652
10885
|
var FsPluginStore = class {
|
|
10653
10886
|
getUserPluginsDir() {
|
|
@@ -10655,10 +10888,10 @@ var FsPluginStore = class {
|
|
|
10655
10888
|
}
|
|
10656
10889
|
listUserPluginDirs() {
|
|
10657
10890
|
const dir = userPluginsDir();
|
|
10658
|
-
if (!
|
|
10891
|
+
if (!fs6.existsSync(dir)) return [];
|
|
10659
10892
|
let entries;
|
|
10660
10893
|
try {
|
|
10661
|
-
entries =
|
|
10894
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
10662
10895
|
} catch {
|
|
10663
10896
|
return [];
|
|
10664
10897
|
}
|
|
@@ -10677,20 +10910,20 @@ var FsPluginStore = class {
|
|
|
10677
10910
|
ensureDir(dir);
|
|
10678
10911
|
}
|
|
10679
10912
|
writeFile(filePath, content) {
|
|
10680
|
-
|
|
10913
|
+
fs6.writeFileSync(filePath, content);
|
|
10681
10914
|
}
|
|
10682
10915
|
readFile(filePath) {
|
|
10683
10916
|
try {
|
|
10684
|
-
return
|
|
10917
|
+
return fs6.readFileSync(filePath, "utf8");
|
|
10685
10918
|
} catch {
|
|
10686
10919
|
return null;
|
|
10687
10920
|
}
|
|
10688
10921
|
}
|
|
10689
10922
|
dirExists(path15) {
|
|
10690
|
-
return
|
|
10923
|
+
return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
|
|
10691
10924
|
}
|
|
10692
10925
|
exists(path15) {
|
|
10693
|
-
return
|
|
10926
|
+
return fs6.existsSync(path15);
|
|
10694
10927
|
}
|
|
10695
10928
|
};
|
|
10696
10929
|
|
|
@@ -11159,20 +11392,20 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
11159
11392
|
};
|
|
11160
11393
|
function fsReaddirSync(dir) {
|
|
11161
11394
|
try {
|
|
11162
|
-
return
|
|
11395
|
+
return fs7.readdirSync(dir);
|
|
11163
11396
|
} catch {
|
|
11164
11397
|
return [];
|
|
11165
11398
|
}
|
|
11166
11399
|
}
|
|
11167
11400
|
function fsChmodSync(fpath, mode) {
|
|
11168
11401
|
try {
|
|
11169
|
-
|
|
11402
|
+
fs7.chmodSync(fpath, mode);
|
|
11170
11403
|
} catch {
|
|
11171
11404
|
}
|
|
11172
11405
|
}
|
|
11173
11406
|
|
|
11174
11407
|
// src/utils/stateStore.ts
|
|
11175
|
-
import * as
|
|
11408
|
+
import * as fs8 from "fs";
|
|
11176
11409
|
import * as path13 from "path";
|
|
11177
11410
|
var STATE_FILE = "state.json";
|
|
11178
11411
|
function getStatePath(baseDir) {
|
|
@@ -11182,14 +11415,14 @@ function saveState(plan, baseDir) {
|
|
|
11182
11415
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
11183
11416
|
ensureDir(path13.dirname(statePath));
|
|
11184
11417
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
11185
|
-
|
|
11418
|
+
fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
11186
11419
|
}
|
|
11187
11420
|
function loadState(baseDir, logger = defaultLogger) {
|
|
11188
11421
|
const dir = baseDir ?? process.cwd();
|
|
11189
11422
|
const statePath = getStatePath(dir);
|
|
11190
|
-
if (!
|
|
11423
|
+
if (!fs8.existsSync(statePath)) return null;
|
|
11191
11424
|
try {
|
|
11192
|
-
const raw =
|
|
11425
|
+
const raw = fs8.readFileSync(statePath, "utf-8");
|
|
11193
11426
|
const parsed = JSON.parse(raw);
|
|
11194
11427
|
if (!Array.isArray(parsed.runners)) {
|
|
11195
11428
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -11204,15 +11437,15 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
11204
11437
|
function clearState(baseDir) {
|
|
11205
11438
|
const dir = baseDir ?? process.cwd();
|
|
11206
11439
|
const statePath = getStatePath(dir);
|
|
11207
|
-
if (
|
|
11440
|
+
if (fs8.existsSync(statePath)) fs8.unlinkSync(statePath);
|
|
11208
11441
|
}
|
|
11209
11442
|
function stateExists(baseDir) {
|
|
11210
11443
|
const dir = baseDir ?? process.cwd();
|
|
11211
|
-
return
|
|
11444
|
+
return fs8.existsSync(getStatePath(dir));
|
|
11212
11445
|
}
|
|
11213
11446
|
|
|
11214
11447
|
// src/services/SettingsService.ts
|
|
11215
|
-
import * as
|
|
11448
|
+
import * as fs9 from "fs";
|
|
11216
11449
|
import * as path14 from "path";
|
|
11217
11450
|
import * as os6 from "os";
|
|
11218
11451
|
var DEFAULTS = {
|
|
@@ -11228,6 +11461,17 @@ function getSettingsPath() {
|
|
|
11228
11461
|
if (override && override.trim()) return override.trim();
|
|
11229
11462
|
return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
|
|
11230
11463
|
}
|
|
11464
|
+
function readPlannerModels(raw) {
|
|
11465
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
11466
|
+
const out = {};
|
|
11467
|
+
for (const [provider, entry] of Object.entries(raw)) {
|
|
11468
|
+
if (!entry || typeof entry !== "object") continue;
|
|
11469
|
+
const { model, effort } = entry;
|
|
11470
|
+
if (typeof model !== "string" || !model.trim()) continue;
|
|
11471
|
+
out[provider] = typeof effort === "string" && effort.trim() ? { model, effort } : { model };
|
|
11472
|
+
}
|
|
11473
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
11474
|
+
}
|
|
11231
11475
|
var SettingsService = class {
|
|
11232
11476
|
filePath;
|
|
11233
11477
|
cache = null;
|
|
@@ -11248,7 +11492,7 @@ var SettingsService = class {
|
|
|
11248
11492
|
}
|
|
11249
11493
|
fileMtimeMs() {
|
|
11250
11494
|
try {
|
|
11251
|
-
return
|
|
11495
|
+
return fs9.statSync(this.filePath).mtimeMs;
|
|
11252
11496
|
} catch {
|
|
11253
11497
|
return null;
|
|
11254
11498
|
}
|
|
@@ -11321,6 +11565,26 @@ var SettingsService = class {
|
|
|
11321
11565
|
}
|
|
11322
11566
|
this.persist();
|
|
11323
11567
|
}
|
|
11568
|
+
getPlannerModel(provider) {
|
|
11569
|
+
return this.getAll().plannerModels?.[provider];
|
|
11570
|
+
}
|
|
11571
|
+
setPlannerModel(provider, entry) {
|
|
11572
|
+
this.getAll();
|
|
11573
|
+
if (entry === void 0) {
|
|
11574
|
+
if (this.cache.plannerModels) {
|
|
11575
|
+
delete this.cache.plannerModels[provider];
|
|
11576
|
+
if (Object.keys(this.cache.plannerModels).length === 0) {
|
|
11577
|
+
this.cache.plannerModels = void 0;
|
|
11578
|
+
}
|
|
11579
|
+
}
|
|
11580
|
+
} else {
|
|
11581
|
+
if (!this.cache.plannerModels) {
|
|
11582
|
+
this.cache.plannerModels = {};
|
|
11583
|
+
}
|
|
11584
|
+
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
11585
|
+
}
|
|
11586
|
+
this.persist();
|
|
11587
|
+
}
|
|
11324
11588
|
getEnabledRunners() {
|
|
11325
11589
|
return this.getAll().enabledRunners;
|
|
11326
11590
|
}
|
|
@@ -11331,8 +11595,8 @@ var SettingsService = class {
|
|
|
11331
11595
|
}
|
|
11332
11596
|
load() {
|
|
11333
11597
|
try {
|
|
11334
|
-
if (
|
|
11335
|
-
const raw = JSON.parse(
|
|
11598
|
+
if (fs9.existsSync(this.filePath)) {
|
|
11599
|
+
const raw = JSON.parse(fs9.readFileSync(this.filePath, "utf-8"));
|
|
11336
11600
|
const settings = {
|
|
11337
11601
|
grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
|
|
11338
11602
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
@@ -11346,6 +11610,10 @@ var SettingsService = class {
|
|
|
11346
11610
|
if (raw.modelAllowlist !== void 0) {
|
|
11347
11611
|
settings.modelAllowlist = raw.modelAllowlist;
|
|
11348
11612
|
}
|
|
11613
|
+
const plannerModels = readPlannerModels(raw.plannerModels);
|
|
11614
|
+
if (plannerModels) {
|
|
11615
|
+
settings.plannerModels = plannerModels;
|
|
11616
|
+
}
|
|
11349
11617
|
if (Array.isArray(raw.enabledRunners)) {
|
|
11350
11618
|
settings.enabledRunners = raw.enabledRunners.map(String);
|
|
11351
11619
|
}
|
|
@@ -11357,13 +11625,48 @@ var SettingsService = class {
|
|
|
11357
11625
|
}
|
|
11358
11626
|
persist() {
|
|
11359
11627
|
const dir = path14.dirname(this.filePath);
|
|
11360
|
-
if (!
|
|
11361
|
-
|
|
11628
|
+
if (!fs9.existsSync(dir)) {
|
|
11629
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
11362
11630
|
}
|
|
11363
|
-
|
|
11631
|
+
fs9.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
11364
11632
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
11365
11633
|
}
|
|
11366
11634
|
};
|
|
11635
|
+
|
|
11636
|
+
// src/services/PlannerModelMemory.ts
|
|
11637
|
+
var PlannerModelMemory = class {
|
|
11638
|
+
constructor(store) {
|
|
11639
|
+
this.store = store;
|
|
11640
|
+
}
|
|
11641
|
+
store;
|
|
11642
|
+
/** Record what the user picked for `provider`; a blank model forgets it instead. */
|
|
11643
|
+
remember(provider, model, effort) {
|
|
11644
|
+
const id = model.trim();
|
|
11645
|
+
if (!id) {
|
|
11646
|
+
this.store.setPlannerModel(provider, void 0);
|
|
11647
|
+
return;
|
|
11648
|
+
}
|
|
11649
|
+
const level = effort?.trim();
|
|
11650
|
+
this.store.setPlannerModel(provider, level ? { model: id, effort: level } : { model: id });
|
|
11651
|
+
}
|
|
11652
|
+
/**
|
|
11653
|
+
* What the model and effort should become when `provider` takes over the
|
|
11654
|
+
* planner, given the models it actually serves. The `source` is what the
|
|
11655
|
+
* three surfaces word their notice from — restored, defaulted, or nothing.
|
|
11656
|
+
*/
|
|
11657
|
+
recall(provider, catalog) {
|
|
11658
|
+
const remembered = this.store.getPlannerModel(provider);
|
|
11659
|
+
const match = remembered && catalog.find((c) => c.id === remembered.model);
|
|
11660
|
+
if (remembered && match) {
|
|
11661
|
+
const level = remembered.effort ?? "";
|
|
11662
|
+
const survives = (match.variants ?? []).some((v) => v.id === level);
|
|
11663
|
+
return { model: match.id, effort: survives ? level : "", source: "remembered" };
|
|
11664
|
+
}
|
|
11665
|
+
const first = catalog[0];
|
|
11666
|
+
if (first) return { model: first.id, effort: "", source: "catalog-default" };
|
|
11667
|
+
return { model: "", effort: "", source: "none" };
|
|
11668
|
+
}
|
|
11669
|
+
};
|
|
11367
11670
|
export {
|
|
11368
11671
|
ALL_PROVIDERS,
|
|
11369
11672
|
AUTO_COMMANDS,
|
|
@@ -11386,6 +11689,7 @@ export {
|
|
|
11386
11689
|
DENY_ALL,
|
|
11387
11690
|
EmbeddedNewlineError,
|
|
11388
11691
|
EnvConfig,
|
|
11692
|
+
ExecutableNotFoundError,
|
|
11389
11693
|
FsPluginStore,
|
|
11390
11694
|
GIT_READONLY_SUBCOMMANDS,
|
|
11391
11695
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
@@ -11406,14 +11710,17 @@ export {
|
|
|
11406
11710
|
PROVIDER_PRIORITY,
|
|
11407
11711
|
PROVIDER_SHORT_LABEL,
|
|
11408
11712
|
PendingApprovals,
|
|
11713
|
+
PlanEditError,
|
|
11409
11714
|
PlanParseError,
|
|
11410
11715
|
PlanStore,
|
|
11411
11716
|
Planner,
|
|
11717
|
+
PlannerModelMemory,
|
|
11412
11718
|
REFUSED_COMMANDS,
|
|
11413
11719
|
RunnerInstallation,
|
|
11414
11720
|
RunnerRegistry,
|
|
11415
11721
|
SEARCH_EXCLUSIONS,
|
|
11416
11722
|
STATE_DIR,
|
|
11723
|
+
STOPPED_TOOL_RESULT,
|
|
11417
11724
|
SYMBOL_LANGUAGES,
|
|
11418
11725
|
Session,
|
|
11419
11726
|
SettingsService,
|
|
@@ -11424,9 +11731,11 @@ export {
|
|
|
11424
11731
|
TmuxRunner,
|
|
11425
11732
|
VerdictEngine,
|
|
11426
11733
|
WINDOWS_MAX_COMMAND_LINE,
|
|
11734
|
+
WorkspaceNotFoundError,
|
|
11427
11735
|
addTaskToPlan,
|
|
11428
11736
|
applyHeadLimit,
|
|
11429
11737
|
applyTaskOps,
|
|
11738
|
+
assertWorkspaceExists,
|
|
11430
11739
|
augmentPromptWithPriorOutputs,
|
|
11431
11740
|
augmentedPath,
|
|
11432
11741
|
buildConversationSystemPrompt,
|
|
@@ -11500,6 +11809,7 @@ export {
|
|
|
11500
11809
|
hasTmux,
|
|
11501
11810
|
includeGlobFor,
|
|
11502
11811
|
isCliProvider,
|
|
11812
|
+
isExecutableResolved,
|
|
11503
11813
|
isOpenAiProvider,
|
|
11504
11814
|
killTree,
|
|
11505
11815
|
knownModelId,
|