@ordewell/core 0.6.1 → 0.6.3
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/{IFileSystem-DuEiG_kA.d.ts → IFileSystem-DQEKyC6H.d.ts} +1 -1
- package/dist/{IFileSystem-d_SV7jAH.d.mts → IFileSystem-Qj2MWHn_.d.mts} +1 -1
- package/dist/{ModeResolver-DXxV4n_V.d.ts → ModeResolver-ByNpC0EP.d.ts} +1 -1
- package/dist/{ModeResolver-DIReYAOq.d.mts → ModeResolver-rqaY_4Ex.d.mts} +1 -1
- package/dist/{Task-ib_U0YP7.d.mts → Task-xqdxugt2.d.mts} +31 -1
- package/dist/{Task-ib_U0YP7.d.ts → Task-xqdxugt2.d.ts} +31 -1
- package/dist/index.d.mts +139 -97
- package/dist/index.d.ts +139 -97
- package/dist/index.js +934 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +914 -61
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +1 -1
- package/dist/order-labels.d.ts +1 -1
- package/dist/{parsing-C8Z22twN.d.ts → parsing-COPG8-Ep.d.ts} +2 -2
- package/dist/{parsing-DjRJJX-y.d.mts → parsing-DDZ1rUYa.d.mts} +2 -2
- package/dist/parsing.d.mts +3 -3
- package/dist/parsing.d.ts +3 -3
- package/dist/{plan-utils-DvHPzAfV.d.ts → plan-utils-Br8UAoiu.d.ts} +8 -3
- package/dist/{plan-utils-D4ioaM8q.d.mts → plan-utils-DMgajMvd.d.mts} +8 -3
- package/dist/plan-utils.d.mts +3 -3
- package/dist/plan-utils.d.ts +3 -3
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ __export(index_exports, {
|
|
|
67
67
|
FsPluginStore: () => FsPluginStore,
|
|
68
68
|
GIT_READONLY_SUBCOMMANDS: () => GIT_READONLY_SUBCOMMANDS,
|
|
69
69
|
GREP_DEFAULT_HEAD_LIMIT: () => GREP_DEFAULT_HEAD_LIMIT,
|
|
70
|
+
GREP_PCRE_PROBE_ARGS: () => GREP_PCRE_PROBE_ARGS,
|
|
70
71
|
GeminiService: () => GeminiService,
|
|
71
72
|
HeadlessRunner: () => HeadlessRunner,
|
|
72
73
|
HeadlessSession: () => HeadlessSession,
|
|
@@ -206,6 +207,7 @@ __export(index_exports, {
|
|
|
206
207
|
dependencyCandidates: () => dependencyCandidates,
|
|
207
208
|
dependentsOf: () => dependentsOf,
|
|
208
209
|
describeMergeResult: () => describeMergeResult,
|
|
210
|
+
digestTaskLog: () => digestTaskLog,
|
|
209
211
|
discoverGeminiModels: () => discoverGeminiModels,
|
|
210
212
|
drainNext: () => drainNext,
|
|
211
213
|
effectiveAllowlist: () => effectiveAllowlist,
|
|
@@ -238,6 +240,7 @@ __export(index_exports, {
|
|
|
238
240
|
getStateDir: () => getStateDir,
|
|
239
241
|
globalDataDir: () => globalDataDir,
|
|
240
242
|
grantScopeFor: () => grantScopeFor,
|
|
243
|
+
grepRegexFromProbe: () => grepRegexFromProbe,
|
|
241
244
|
hasHiddenDetail: () => hasHiddenDetail,
|
|
242
245
|
hasTmux: () => hasTmux,
|
|
243
246
|
holdPrompt: () => holdPrompt,
|
|
@@ -766,6 +769,8 @@ function plannerContextFill(usage) {
|
|
|
766
769
|
var HEAD_LINES = 60;
|
|
767
770
|
var TAIL_LINES = 40;
|
|
768
771
|
var MAX_LINE_CHARS = 2e3;
|
|
772
|
+
var DIGEST_ARGS_CHARS = 120;
|
|
773
|
+
var DIGEST_MESSAGE_CHARS = 1e3;
|
|
769
774
|
function capLine(line) {
|
|
770
775
|
return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
771
776
|
}
|
|
@@ -848,6 +853,33 @@ function coalesceTaskLog(events) {
|
|
|
848
853
|
}
|
|
849
854
|
return out;
|
|
850
855
|
}
|
|
856
|
+
function digestTaskLog(events, maxCalls) {
|
|
857
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
858
|
+
let lastText;
|
|
859
|
+
let lastError;
|
|
860
|
+
for (const event of events) {
|
|
861
|
+
if (event.type === "tool_result") outcomes.set(event.id, event.success);
|
|
862
|
+
else if (event.type === "text") lastText = event.text;
|
|
863
|
+
else if (event.type === "error") lastError = event.message;
|
|
864
|
+
}
|
|
865
|
+
const lines = [];
|
|
866
|
+
for (const event of events) {
|
|
867
|
+
if (event.type !== "tool_call") continue;
|
|
868
|
+
const outcome = outcomes.get(event.id);
|
|
869
|
+
const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
|
|
870
|
+
lines.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
|
|
871
|
+
}
|
|
872
|
+
const omitted = lines.length - maxCalls;
|
|
873
|
+
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines.slice(omitted)] : lines;
|
|
874
|
+
const gap = () => out.length > 0 ? [""] : [];
|
|
875
|
+
const said2 = lastText?.trim();
|
|
876
|
+
if (said2) {
|
|
877
|
+
const end = said2.length > DIGEST_MESSAGE_CHARS ? `\u2026${said2.slice(-DIGEST_MESSAGE_CHARS)}` : said2;
|
|
878
|
+
out.push(...gap(), "Its last message:", ...end.split("\n").map((l) => ` ${l}`));
|
|
879
|
+
}
|
|
880
|
+
if (lastError) out.push(...gap(), `It stopped with an error: ${lastError}`);
|
|
881
|
+
return out.join("\n");
|
|
882
|
+
}
|
|
851
883
|
|
|
852
884
|
// src/interfaces/IFileSystem.ts
|
|
853
885
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -3085,6 +3117,10 @@ function lex(command, nested, dialect) {
|
|
|
3085
3117
|
let unsafeRedirect;
|
|
3086
3118
|
let processSubstitution = false;
|
|
3087
3119
|
let unbalanced = false;
|
|
3120
|
+
let grouped = false;
|
|
3121
|
+
let joinedBy;
|
|
3122
|
+
let followable = true;
|
|
3123
|
+
let bareSegment = false;
|
|
3088
3124
|
let tokens = [];
|
|
3089
3125
|
let expandable = false;
|
|
3090
3126
|
let current = "";
|
|
@@ -3114,13 +3150,25 @@ function lex(command, nested, dialect) {
|
|
|
3114
3150
|
current = "";
|
|
3115
3151
|
started = false;
|
|
3116
3152
|
};
|
|
3117
|
-
const endSegment = (nextPiped) => {
|
|
3153
|
+
const endSegment = (nextPiped, next = "other", last = false) => {
|
|
3118
3154
|
endToken();
|
|
3119
|
-
if (
|
|
3155
|
+
if (!last && next === "other") followable = false;
|
|
3156
|
+
if (tokens.length > 0) {
|
|
3157
|
+
const seg = toSegment(tokens, piped, dialect);
|
|
3158
|
+
if (!seg.binary) {
|
|
3159
|
+
followable = false;
|
|
3160
|
+
bareSegment = true;
|
|
3161
|
+
}
|
|
3162
|
+
segments.push({ ...seg, expandable, inputs, joinedBy });
|
|
3163
|
+
} else if (!last || segments.length > 0 || joinedBy !== void 0) {
|
|
3164
|
+
followable = false;
|
|
3165
|
+
bareSegment = true;
|
|
3166
|
+
}
|
|
3120
3167
|
tokens = [];
|
|
3121
3168
|
inputs = [];
|
|
3122
3169
|
expandable = false;
|
|
3123
3170
|
piped = nextPiped;
|
|
3171
|
+
joinedBy = next;
|
|
3124
3172
|
};
|
|
3125
3173
|
let i = 0;
|
|
3126
3174
|
while (i < command.length) {
|
|
@@ -3242,17 +3290,18 @@ function lex(command, nested, dialect) {
|
|
|
3242
3290
|
}
|
|
3243
3291
|
if (c === "(" || c === ")") {
|
|
3244
3292
|
endToken();
|
|
3293
|
+
grouped = true;
|
|
3245
3294
|
i++;
|
|
3246
3295
|
continue;
|
|
3247
3296
|
}
|
|
3248
3297
|
if (c === "|") {
|
|
3249
3298
|
const double = command[i + 1] === "|";
|
|
3250
|
-
endSegment(!double);
|
|
3299
|
+
endSegment(!double, double ? "other" : "pipe");
|
|
3251
3300
|
i += double || command[i + 1] === "&" ? 2 : 1;
|
|
3252
3301
|
continue;
|
|
3253
3302
|
}
|
|
3254
3303
|
if (c === "&" || c === ";" || c === "\n") {
|
|
3255
|
-
endSegment(false);
|
|
3304
|
+
endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
|
|
3256
3305
|
i += command[i + 1] === c ? 2 : 1;
|
|
3257
3306
|
continue;
|
|
3258
3307
|
}
|
|
@@ -3269,8 +3318,8 @@ function lex(command, nested, dialect) {
|
|
|
3269
3318
|
i++;
|
|
3270
3319
|
}
|
|
3271
3320
|
if (quote !== "") unbalanced = true;
|
|
3272
|
-
endSegment(false);
|
|
3273
|
-
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
3321
|
+
endSegment(false, "other", true);
|
|
3322
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped, followable, bareSegment };
|
|
3274
3323
|
}
|
|
3275
3324
|
function binaryName(token, dialect) {
|
|
3276
3325
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -3410,10 +3459,12 @@ function unwrap(seg, dialect) {
|
|
|
3410
3459
|
}
|
|
3411
3460
|
return { seg: { ...current, assignments }, wrappers, runner };
|
|
3412
3461
|
}
|
|
3413
|
-
function lexAll(command, dialect) {
|
|
3462
|
+
function lexAll(command, dialect, cdpathSet = false) {
|
|
3414
3463
|
const queue = [];
|
|
3415
3464
|
const top = lex(command, queue, dialect);
|
|
3416
3465
|
const all = { ...top, segments: top.segments.filter((s) => s.binary) };
|
|
3466
|
+
markShellState(all.segments, top.bareSegment);
|
|
3467
|
+
if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
|
|
3417
3468
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
3418
3469
|
const inner = lex(queue.shift(), queue, dialect);
|
|
3419
3470
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
@@ -3423,15 +3474,61 @@ function lexAll(command, dialect) {
|
|
|
3423
3474
|
}
|
|
3424
3475
|
return all;
|
|
3425
3476
|
}
|
|
3477
|
+
function literalCdTarget(seg) {
|
|
3478
|
+
if (seg.binary !== "cd" || seg.assignments.length > 0 || seg.expandable || seg.inputs.length > 0) return void 0;
|
|
3479
|
+
if (seg.args.length !== 1) return void 0;
|
|
3480
|
+
const [target] = seg.args;
|
|
3481
|
+
return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
|
|
3482
|
+
}
|
|
3483
|
+
function followCd(segments) {
|
|
3484
|
+
if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
|
|
3485
|
+
const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
|
|
3486
|
+
if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
|
|
3487
|
+
let chain = [];
|
|
3488
|
+
segments.forEach((seg, i) => {
|
|
3489
|
+
seg.cwd = chain;
|
|
3490
|
+
const target = inPipeline(i) ? void 0 : literalCdTarget(seg);
|
|
3491
|
+
if (target !== void 0) chain = [...chain, target];
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
function markShellState(segments, bareSegment) {
|
|
3495
|
+
if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
|
|
3496
|
+
for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
|
|
3497
|
+
}
|
|
3498
|
+
var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
|
|
3499
|
+
"export",
|
|
3500
|
+
"set",
|
|
3501
|
+
"shopt",
|
|
3502
|
+
"declare",
|
|
3503
|
+
"typeset",
|
|
3504
|
+
"readonly",
|
|
3505
|
+
"local",
|
|
3506
|
+
"unset",
|
|
3507
|
+
"let",
|
|
3508
|
+
"source",
|
|
3509
|
+
".",
|
|
3510
|
+
"eval",
|
|
3511
|
+
"exec",
|
|
3512
|
+
"pushd",
|
|
3513
|
+
"popd",
|
|
3514
|
+
"alias",
|
|
3515
|
+
"unalias",
|
|
3516
|
+
"builtin",
|
|
3517
|
+
"command",
|
|
3518
|
+
"enable",
|
|
3519
|
+
"hash",
|
|
3520
|
+
"trap"
|
|
3521
|
+
]);
|
|
3426
3522
|
function looksLikePath(arg) {
|
|
3427
3523
|
if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
|
|
3428
3524
|
return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
|
|
3429
3525
|
}
|
|
3430
|
-
function
|
|
3526
|
+
function pathRefs(command, opts = {}) {
|
|
3431
3527
|
const dialect = dialectFor(opts.dialect);
|
|
3432
|
-
return lexAll(command, dialect).segments.flatMap((seg) => {
|
|
3528
|
+
return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
|
|
3433
3529
|
const patterns = patternArgs(seg);
|
|
3434
|
-
|
|
3530
|
+
const cwd = seg.cwd ?? [];
|
|
3531
|
+
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path27) => ({ path: path27, cwd }));
|
|
3435
3532
|
});
|
|
3436
3533
|
}
|
|
3437
3534
|
function pathIn(a) {
|
|
@@ -3595,7 +3692,8 @@ function scopeFor(seg) {
|
|
|
3595
3692
|
}
|
|
3596
3693
|
return [seg.binary, ...lead].join(" ");
|
|
3597
3694
|
}
|
|
3598
|
-
function isAuto(seg) {
|
|
3695
|
+
function isAuto(seg, opts) {
|
|
3696
|
+
if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
|
|
3599
3697
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
3600
3698
|
if (seg.expandable) return false;
|
|
3601
3699
|
if (seg.binary !== "git") return true;
|
|
@@ -3690,7 +3788,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3690
3788
|
const trimmed = command.trim();
|
|
3691
3789
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
3692
3790
|
const dialect = dialectFor(opts.dialect);
|
|
3693
|
-
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
3791
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect, opts.cdpathSet);
|
|
3694
3792
|
if (unbalanced) {
|
|
3695
3793
|
return {
|
|
3696
3794
|
tier: "refuse",
|
|
@@ -3727,7 +3825,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3727
3825
|
};
|
|
3728
3826
|
}
|
|
3729
3827
|
}
|
|
3730
|
-
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg));
|
|
3828
|
+
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
|
|
3731
3829
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
3732
3830
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3733
3831
|
return { tier: "ask", scope };
|
|
@@ -3797,6 +3895,9 @@ function researchShellWarning(shell) {
|
|
|
3797
3895
|
if (shell.dialect !== "cmd") return null;
|
|
3798
3896
|
return "No POSIX shell was found on this machine, so research commands run through cmd.exe, where most read-only tools (ls, cat, wc, head, grep, find) do not exist. Installing Git for Windows provides one and restores the full research toolset.";
|
|
3799
3897
|
}
|
|
3898
|
+
function researchPolicyOptions(shell) {
|
|
3899
|
+
return { dialect: shell.dialect, cdpathSet: !!process.env.CDPATH };
|
|
3900
|
+
}
|
|
3800
3901
|
|
|
3801
3902
|
// src/services/pathScope.ts
|
|
3802
3903
|
var os = __toESM(require("os"));
|
|
@@ -3807,11 +3908,15 @@ function expandHome(target) {
|
|
|
3807
3908
|
if (target === "~" || target.startsWith("~/")) return path2.join(home, target.slice(1));
|
|
3808
3909
|
return path2.join(path2.dirname(home), target.slice(1));
|
|
3809
3910
|
}
|
|
3810
|
-
function resolveWithin(root, target) {
|
|
3811
|
-
const abs = path2.resolve(
|
|
3911
|
+
function resolveWithin(root, target, from = root) {
|
|
3912
|
+
const abs = path2.resolve(from, expandHome(target));
|
|
3812
3913
|
const rel = path2.relative(root, abs);
|
|
3813
3914
|
return { abs, inside: rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel) };
|
|
3814
3915
|
}
|
|
3916
|
+
function resolveRef(root, ref) {
|
|
3917
|
+
const from = ref.cwd.reduce((dir, target) => resolveWithin(root, target, dir).abs, root);
|
|
3918
|
+
return resolveWithin(root, ref.path, from);
|
|
3919
|
+
}
|
|
3815
3920
|
function grantScopeFor(abs, kind) {
|
|
3816
3921
|
if (kind === "directory") return path2.join(abs, "*");
|
|
3817
3922
|
const parent = path2.dirname(abs);
|
|
@@ -4017,14 +4122,17 @@ ${refs.output.trim()}`);
|
|
|
4017
4122
|
outsidePaths(command) {
|
|
4018
4123
|
const root = this.getWorkspaceRoot();
|
|
4019
4124
|
const found = /* @__PURE__ */ new Map();
|
|
4020
|
-
for (const
|
|
4021
|
-
const { abs, inside } =
|
|
4125
|
+
for (const ref of pathRefs(command, this.policyOptions())) {
|
|
4126
|
+
const { abs, inside } = resolveRef(root, ref);
|
|
4022
4127
|
if (inside || isInertDevice(abs)) continue;
|
|
4023
4128
|
const scope = grantScopeFor(abs, "file");
|
|
4024
4129
|
if (!found.has(scope)) found.set(scope, abs);
|
|
4025
4130
|
}
|
|
4026
4131
|
return [...found].map(([scope, abs]) => ({ abs, scope }));
|
|
4027
4132
|
}
|
|
4133
|
+
policyOptions() {
|
|
4134
|
+
return researchPolicyOptions(this.researchShell);
|
|
4135
|
+
}
|
|
4028
4136
|
/**
|
|
4029
4137
|
* Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
|
|
4030
4138
|
* anything else asks once and is remembered, and writes are refused outright
|
|
@@ -4036,7 +4144,7 @@ ${refs.output.trim()}`);
|
|
|
4036
4144
|
* directory the next command names.
|
|
4037
4145
|
*/
|
|
4038
4146
|
async bash(command, signal) {
|
|
4039
|
-
const { tier, scope, reason } = classifyCommand(command,
|
|
4147
|
+
const { tier, scope, reason } = classifyCommand(command, this.policyOptions());
|
|
4040
4148
|
if (tier === "refuse") {
|
|
4041
4149
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
4042
4150
|
}
|
|
@@ -5212,10 +5320,78 @@ function buildGrepArgs(pattern, opts, root) {
|
|
|
5212
5320
|
function buildGlobArgs(pattern, root) {
|
|
5213
5321
|
return ["--files", ...SORT_BY_RECENCY, "--glob", pattern, ...exclusionArgs(), "--", root];
|
|
5214
5322
|
}
|
|
5215
|
-
|
|
5323
|
+
var GREP_PCRE_PROBE_ARGS = ["-P", "-e", "x", "/dev/null"];
|
|
5324
|
+
function grepRegexFromProbe(exitCode) {
|
|
5325
|
+
return exitCode === 0 || exitCode === 1 ? "pcre" : "ere";
|
|
5326
|
+
}
|
|
5327
|
+
function toPosixClasses(pattern) {
|
|
5328
|
+
const CLASS_ESCAPES = { s: "[:space:]", w: "[:alnum:]_", d: "[:digit:]", t: " " };
|
|
5329
|
+
let out = "";
|
|
5330
|
+
let i = 0;
|
|
5331
|
+
while (i < pattern.length) {
|
|
5332
|
+
const c = pattern[i];
|
|
5333
|
+
if (c === "\\") {
|
|
5334
|
+
out += pattern.slice(i, i + 2);
|
|
5335
|
+
i += 2;
|
|
5336
|
+
continue;
|
|
5337
|
+
}
|
|
5338
|
+
if (c !== "[") {
|
|
5339
|
+
out += c;
|
|
5340
|
+
i++;
|
|
5341
|
+
continue;
|
|
5342
|
+
}
|
|
5343
|
+
const start = i;
|
|
5344
|
+
i++;
|
|
5345
|
+
let negate = false;
|
|
5346
|
+
if (pattern[i] === "^") {
|
|
5347
|
+
negate = true;
|
|
5348
|
+
i++;
|
|
5349
|
+
}
|
|
5350
|
+
let body = "";
|
|
5351
|
+
let closeBracket = false;
|
|
5352
|
+
let openBracket = false;
|
|
5353
|
+
let dash = false;
|
|
5354
|
+
let closed = false;
|
|
5355
|
+
while (i < pattern.length) {
|
|
5356
|
+
const ch = pattern[i];
|
|
5357
|
+
if (ch === "]") {
|
|
5358
|
+
closed = true;
|
|
5359
|
+
i++;
|
|
5360
|
+
break;
|
|
5361
|
+
}
|
|
5362
|
+
if (ch === "[" && pattern[i + 1] === ":") {
|
|
5363
|
+
const end = pattern.indexOf(":]", i + 2);
|
|
5364
|
+
if (end !== -1) {
|
|
5365
|
+
body += pattern.slice(i, end + 2);
|
|
5366
|
+
i = end + 2;
|
|
5367
|
+
continue;
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
5371
|
+
const e = pattern[i + 1];
|
|
5372
|
+
i += 2;
|
|
5373
|
+
if (e in CLASS_ESCAPES) body += CLASS_ESCAPES[e];
|
|
5374
|
+
else if (e === "]") closeBracket = true;
|
|
5375
|
+
else if (e === "[") openBracket = true;
|
|
5376
|
+
else if (e === "-") dash = true;
|
|
5377
|
+
else body += e;
|
|
5378
|
+
continue;
|
|
5379
|
+
}
|
|
5380
|
+
body += ch;
|
|
5381
|
+
i++;
|
|
5382
|
+
}
|
|
5383
|
+
if (!closed) {
|
|
5384
|
+
out += pattern.slice(start);
|
|
5385
|
+
break;
|
|
5386
|
+
}
|
|
5387
|
+
out += `[${negate ? "^" : ""}${closeBracket ? "]" : ""}${body}${openBracket ? "[" : ""}${dash ? "-" : ""}]`;
|
|
5388
|
+
}
|
|
5389
|
+
return out;
|
|
5390
|
+
}
|
|
5391
|
+
function buildFallbackGrepArgs(pattern, opts, root, regex = "pcre") {
|
|
5216
5392
|
const args = ["-r", "-n"];
|
|
5217
5393
|
if (opts.literal) args.push("-F");
|
|
5218
|
-
else args.push("-P");
|
|
5394
|
+
else args.push(regex === "pcre" ? "-P" : "-E");
|
|
5219
5395
|
if (opts.caseInsensitive) args.push("-i");
|
|
5220
5396
|
if (opts.outputMode === "files") args.push("-l");
|
|
5221
5397
|
if (opts.outputMode === "count") args.push("-c");
|
|
@@ -5225,7 +5401,7 @@ function buildFallbackGrepArgs(pattern, opts, root) {
|
|
|
5225
5401
|
}
|
|
5226
5402
|
if (opts.include) args.push(`--include=${opts.include}`);
|
|
5227
5403
|
for (const dir of SEARCH_EXCLUSIONS) args.push(`--exclude-dir=${dir}`);
|
|
5228
|
-
args.push("-e", pattern, root);
|
|
5404
|
+
args.push("-e", !opts.literal && regex === "ere" ? toPosixClasses(pattern) : pattern, root);
|
|
5229
5405
|
return args;
|
|
5230
5406
|
}
|
|
5231
5407
|
function globSegmentToRegex(segment) {
|
|
@@ -9264,7 +9440,7 @@ function parseTaskOpsObject(json, text2) {
|
|
|
9264
9440
|
}
|
|
9265
9441
|
return ops;
|
|
9266
9442
|
}
|
|
9267
|
-
function
|
|
9443
|
+
function resolveRef2(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
9268
9444
|
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
9269
9445
|
const s = String(ref).trim();
|
|
9270
9446
|
const ownerIdx = handleOwner.get(s);
|
|
@@ -9477,7 +9653,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9477
9653
|
const bad = [];
|
|
9478
9654
|
const liveFlat = flattenTasks(tasks);
|
|
9479
9655
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
9480
|
-
const r =
|
|
9656
|
+
const r = resolveRef2(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
9481
9657
|
if ("error" in r) {
|
|
9482
9658
|
bad.push(`${String(d)} (${r.error})`);
|
|
9483
9659
|
continue;
|
|
@@ -9494,7 +9670,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9494
9670
|
const label = `op ${i + 1} (${op.op})`;
|
|
9495
9671
|
switch (op.op) {
|
|
9496
9672
|
case "update": {
|
|
9497
|
-
const ref =
|
|
9673
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9498
9674
|
if ("error" in ref) {
|
|
9499
9675
|
errors.push(`${label}: ${ref.error}`);
|
|
9500
9676
|
break;
|
|
@@ -9565,7 +9741,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9565
9741
|
break;
|
|
9566
9742
|
}
|
|
9567
9743
|
case "remove": {
|
|
9568
|
-
const ref =
|
|
9744
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9569
9745
|
if ("error" in ref) {
|
|
9570
9746
|
errors.push(`${label}: ${ref.error}`);
|
|
9571
9747
|
break;
|
|
@@ -9588,7 +9764,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9588
9764
|
break;
|
|
9589
9765
|
}
|
|
9590
9766
|
case "reorder": {
|
|
9591
|
-
const refs = (op.taskIds ?? []).map((r) =>
|
|
9767
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
9592
9768
|
const badRef = refs.find((r) => "error" in r);
|
|
9593
9769
|
if (badRef) {
|
|
9594
9770
|
errors.push(`${label}: ${badRef.error}`);
|
|
@@ -9610,7 +9786,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9610
9786
|
break;
|
|
9611
9787
|
}
|
|
9612
9788
|
case "merge": {
|
|
9613
|
-
const refs = (op.taskIds ?? []).map((r) =>
|
|
9789
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
9614
9790
|
const badRef = refs.find((r) => "error" in r);
|
|
9615
9791
|
if (badRef) {
|
|
9616
9792
|
errors.push(`${label}: ${badRef.error}`);
|
|
@@ -9680,7 +9856,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9680
9856
|
break;
|
|
9681
9857
|
}
|
|
9682
9858
|
case "split": {
|
|
9683
|
-
const ref =
|
|
9859
|
+
const ref = resolveRef2(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
9684
9860
|
if ("error" in ref) {
|
|
9685
9861
|
errors.push(`${label}: ${ref.error}`);
|
|
9686
9862
|
break;
|
|
@@ -9759,7 +9935,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9759
9935
|
break;
|
|
9760
9936
|
}
|
|
9761
9937
|
case "rearm": {
|
|
9762
|
-
const ref =
|
|
9938
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9763
9939
|
if ("error" in ref) {
|
|
9764
9940
|
errors.push(`${label}: ${ref.error}`);
|
|
9765
9941
|
break;
|
|
@@ -10402,15 +10578,15 @@ function nonPromptingFs(fs16) {
|
|
|
10402
10578
|
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs16.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
10403
10579
|
listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
10404
10580
|
bash: async (command, signal) => {
|
|
10405
|
-
const
|
|
10406
|
-
const { tier, reason } = classifyCommand(command,
|
|
10581
|
+
const policy = researchPolicyOptions(resolveResearchShell());
|
|
10582
|
+
const { tier, reason } = classifyCommand(command, policy);
|
|
10407
10583
|
if (tier !== "auto") {
|
|
10408
10584
|
return refused2(
|
|
10409
10585
|
tier === "refuse" ? `Command refused: ${reason}` : `"${command}" needs user approval, and research agents run in the background where nothing can be approved. Use the read-only research tools, and note in your digest that this command would answer the question.`
|
|
10410
10586
|
);
|
|
10411
10587
|
}
|
|
10412
|
-
const escaping =
|
|
10413
|
-
if (escaping) return outsideRefusal(escaping);
|
|
10588
|
+
const escaping = pathRefs(command, policy).map((ref) => resolveRef(fs16.getWorkspaceRoot(), ref)).find((r) => !r.inside);
|
|
10589
|
+
if (escaping) return outsideRefusal(escaping.abs);
|
|
10414
10590
|
return fs16.bash(command, signal);
|
|
10415
10591
|
}
|
|
10416
10592
|
};
|
|
@@ -11724,6 +11900,12 @@ var VerdictEngine = class {
|
|
|
11724
11900
|
idleSince = /* @__PURE__ */ new Map();
|
|
11725
11901
|
/** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
|
|
11726
11902
|
idlePaused = /* @__PURE__ */ new Set();
|
|
11903
|
+
/**
|
|
11904
|
+
* Open tool approvals per structured task. They arrive mid-turn and leave the
|
|
11905
|
+
* status alone (ADR-0018, W1), but a task waiting on one is waiting on the
|
|
11906
|
+
* user, so it is not idle either.
|
|
11907
|
+
*/
|
|
11908
|
+
openApprovals = /* @__PURE__ */ new Map();
|
|
11727
11909
|
onVerdict(listener) {
|
|
11728
11910
|
this.listeners.push(listener);
|
|
11729
11911
|
}
|
|
@@ -11752,7 +11934,7 @@ var VerdictEngine = class {
|
|
|
11752
11934
|
}
|
|
11753
11935
|
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
11754
11936
|
touchIdle(taskId, gen) {
|
|
11755
|
-
if (this.idlePaused.has(taskId)) return;
|
|
11937
|
+
if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
|
|
11756
11938
|
const existing = this.idleTimers.get(taskId);
|
|
11757
11939
|
if (existing) clearTimeout(existing);
|
|
11758
11940
|
if (this.idleSince.get(taskId)) {
|
|
@@ -11834,7 +12016,10 @@ ${line}
|
|
|
11834
12016
|
});
|
|
11835
12017
|
if (isStructuredSession(session)) {
|
|
11836
12018
|
session.onEvent((event) => {
|
|
11837
|
-
if (
|
|
12019
|
+
if (this.generations.get(task.id) !== gen) return;
|
|
12020
|
+
if (event.type === "turn_start") this.resumeIdle(task.id);
|
|
12021
|
+
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
12022
|
+
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
11838
12023
|
});
|
|
11839
12024
|
}
|
|
11840
12025
|
session.onExit((exitCode) => {
|
|
@@ -11845,6 +12030,18 @@ ${line}
|
|
|
11845
12030
|
for (const l of this.listeners) l(task.id, verdict);
|
|
11846
12031
|
});
|
|
11847
12032
|
}
|
|
12033
|
+
approvalOpened(taskId, approvalId) {
|
|
12034
|
+
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
12035
|
+
open.add(approvalId);
|
|
12036
|
+
this.openApprovals.set(taskId, open);
|
|
12037
|
+
this.clearIdle(taskId);
|
|
12038
|
+
}
|
|
12039
|
+
approvalClosed(taskId, approvalId, gen) {
|
|
12040
|
+
const open = this.openApprovals.get(taskId);
|
|
12041
|
+
if (!open?.delete(approvalId) || open.size) return;
|
|
12042
|
+
this.openApprovals.delete(taskId);
|
|
12043
|
+
this.touchIdle(taskId, gen);
|
|
12044
|
+
}
|
|
11848
12045
|
/** Scan only the new text plus the unmatched carry, so a long run stays linear. */
|
|
11849
12046
|
scanCheckpoints(taskId, session, text2) {
|
|
11850
12047
|
const scan = (this.checkpointCarry.get(taskId) ?? "") + text2;
|
|
@@ -11866,6 +12063,7 @@ ${line}
|
|
|
11866
12063
|
this.checkpointCarry.delete(taskId);
|
|
11867
12064
|
this.pausedSessions.delete(taskId);
|
|
11868
12065
|
this.idlePaused.delete(taskId);
|
|
12066
|
+
this.openApprovals.delete(taskId);
|
|
11869
12067
|
this.clearIdle(taskId);
|
|
11870
12068
|
}
|
|
11871
12069
|
/** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
|
|
@@ -11903,6 +12101,7 @@ ${line}
|
|
|
11903
12101
|
this.idleTimers.clear();
|
|
11904
12102
|
this.idleSince.clear();
|
|
11905
12103
|
this.idlePaused.clear();
|
|
12104
|
+
this.openApprovals.clear();
|
|
11906
12105
|
this.generations.clear();
|
|
11907
12106
|
}
|
|
11908
12107
|
decide(task, exitCode) {
|
|
@@ -13424,11 +13623,17 @@ function buildShellInvocation(command, args) {
|
|
|
13424
13623
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
13425
13624
|
return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
|
|
13426
13625
|
}
|
|
13626
|
+
var BSD_SCRIPT_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "freebsd", "openbsd", "netbsd"]);
|
|
13627
|
+
var BSD_SCRIPT_FEED = 'exec 5<&0; p=$(mktemp) || exit 1; { cat <&5 & echo $! >"$p"; wait; } 2>/dev/null | { script -q -e -F /dev/null /bin/sh -c "$0"; s=$?; kill "$(cat "$p")" 2>/dev/null; rm -f "$p"; exit $s; }';
|
|
13427
13628
|
function wrapWithPty(command, args, opts = {}) {
|
|
13428
13629
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
13429
13630
|
const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
|
|
13430
13631
|
const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
|
|
13431
|
-
|
|
13632
|
+
const line = `${watcher}${setup}${inner}`;
|
|
13633
|
+
if (BSD_SCRIPT_PLATFORMS.has(opts.platform ?? process.platform)) {
|
|
13634
|
+
return { command: "/bin/sh", args: ["-c", BSD_SCRIPT_FEED, line] };
|
|
13635
|
+
}
|
|
13636
|
+
return { command: "script", args: ["-q", "-e", "-f", "-c", line, "/dev/null"] };
|
|
13432
13637
|
}
|
|
13433
13638
|
|
|
13434
13639
|
// src/services/blockingPrompts.ts
|
|
@@ -13623,24 +13828,37 @@ var Block = /* @__PURE__ */ ((Block2) => {
|
|
|
13623
13828
|
Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
|
|
13624
13829
|
Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
|
|
13625
13830
|
Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
|
|
13831
|
+
Block2[Block2["IfHeadlessModel"] = 11] = "IfHeadlessModel";
|
|
13626
13832
|
return Block2;
|
|
13627
13833
|
})(Block || {});
|
|
13628
13834
|
function isInteractive(ctx) {
|
|
13629
13835
|
return ctx.interactive ?? !ctx.headless;
|
|
13630
13836
|
}
|
|
13837
|
+
function majorVersion(version) {
|
|
13838
|
+
const match = version?.match(/(\d+)\.\d+/);
|
|
13839
|
+
return match ? Number(match[1]) : void 0;
|
|
13840
|
+
}
|
|
13841
|
+
function versionedInvocation(manifest, ctx) {
|
|
13842
|
+
const major = majorVersion(ctx.runnerVersion);
|
|
13843
|
+
if (major === void 0) return void 0;
|
|
13844
|
+
return (manifest.runner.versioned ?? []).filter((v) => major >= v.minMajor).sort((a, b) => b.minMajor - a.minMajor)[0];
|
|
13845
|
+
}
|
|
13631
13846
|
function resolveArgs(manifest, ctx) {
|
|
13632
13847
|
const args = [];
|
|
13633
13848
|
const env = {};
|
|
13634
|
-
|
|
13635
|
-
|
|
13849
|
+
const versioned = versionedInvocation(manifest, ctx);
|
|
13850
|
+
const argsTemplate = versioned?.argsTemplate ?? manifest.runner.argsTemplate;
|
|
13851
|
+
const envTemplate = versioned ? versioned.env : manifest.runner.env;
|
|
13852
|
+
if (envTemplate) {
|
|
13853
|
+
for (const [key, value] of Object.entries(envTemplate)) {
|
|
13636
13854
|
const resolved = substituteSimple(value, ctx);
|
|
13637
13855
|
if (resolved.length > 0) env[key] = resolved;
|
|
13638
13856
|
}
|
|
13639
13857
|
}
|
|
13640
13858
|
let block = 0 /* None */;
|
|
13641
13859
|
let include = true;
|
|
13642
|
-
for (let i = 0; i <
|
|
13643
|
-
const raw =
|
|
13860
|
+
for (let i = 0; i < argsTemplate.length; i++) {
|
|
13861
|
+
const raw = argsTemplate[i];
|
|
13644
13862
|
const blockOpen = parseBlockOpen(raw);
|
|
13645
13863
|
if (blockOpen !== null) {
|
|
13646
13864
|
if (block !== 0 /* None */) throw new ResolveError(`Nested conditional block at position ${i}: "${raw}"`);
|
|
@@ -13699,6 +13917,8 @@ function parseBlockOpen(raw) {
|
|
|
13699
13917
|
return 9 /* IfInteractiveVariant */;
|
|
13700
13918
|
case "{{if projectTrust}}":
|
|
13701
13919
|
return 10 /* IfProjectTrust */;
|
|
13920
|
+
case "{{if headlessModel}}":
|
|
13921
|
+
return 11 /* IfHeadlessModel */;
|
|
13702
13922
|
default:
|
|
13703
13923
|
return null;
|
|
13704
13924
|
}
|
|
@@ -13738,6 +13958,9 @@ function shouldIncludeBlock(block, ctx) {
|
|
|
13738
13958
|
// there is no cwd to name.
|
|
13739
13959
|
case 10 /* IfProjectTrust */:
|
|
13740
13960
|
return isInteractive(ctx) && !!ctx.cwd;
|
|
13961
|
+
// A model flag that only the non-interactive subcommand takes.
|
|
13962
|
+
case 11 /* IfHeadlessModel */:
|
|
13963
|
+
return !!ctx.model && !isInteractive(ctx);
|
|
13741
13964
|
default:
|
|
13742
13965
|
return true;
|
|
13743
13966
|
}
|
|
@@ -13783,6 +14006,10 @@ function resolveToken(token, manifest, ctx) {
|
|
|
13783
14006
|
if (token === "{{model}}") return ctx.model || "";
|
|
13784
14007
|
if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
|
|
13785
14008
|
if (token === "{{mode}}") return ctx.mode || "build";
|
|
14009
|
+
if (token === "{{modelWithVariant}}") {
|
|
14010
|
+
if (!ctx.model) return "";
|
|
14011
|
+
return ctx.thinkingEffort ? `${ctx.model}#${ctx.thinkingEffort}` : ctx.model;
|
|
14012
|
+
}
|
|
13786
14013
|
if (token === "{{projectTrust}}") {
|
|
13787
14014
|
if (!ctx.cwd) return "";
|
|
13788
14015
|
return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
|
|
@@ -13857,13 +14084,21 @@ function resolveOpencodeVariantConfig(ctx) {
|
|
|
13857
14084
|
}
|
|
13858
14085
|
return JSON.stringify(config);
|
|
13859
14086
|
}
|
|
14087
|
+
function resolveOpencodeSessionConfig(ctx) {
|
|
14088
|
+
const mode = ctx.mode || "build";
|
|
14089
|
+
const pinned = resolveOpencodeVariantConfig(ctx);
|
|
14090
|
+
const config = pinned ? JSON.parse(pinned) : {};
|
|
14091
|
+
config.default_agent = mode;
|
|
14092
|
+
if (ctx.model && !pinned) config.agent = { [mode]: { model: ctx.model } };
|
|
14093
|
+
return JSON.stringify(config);
|
|
14094
|
+
}
|
|
13860
14095
|
function substituteSimple(value, ctx) {
|
|
13861
14096
|
const conditioned = value.replace(/\{\{if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, name, body) => {
|
|
13862
14097
|
const block = parseBlockOpen(`{{if ${name}}}`);
|
|
13863
14098
|
if (block === null) return match;
|
|
13864
14099
|
return shouldIncludeBlock(block, ctx) ? body : "";
|
|
13865
14100
|
});
|
|
13866
|
-
return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
|
|
14101
|
+
return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{opencodeSessionConfig\}\}/g, () => resolveOpencodeSessionConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
|
|
13867
14102
|
}
|
|
13868
14103
|
|
|
13869
14104
|
// src/utils/launch.ts
|
|
@@ -15566,9 +15801,502 @@ ${item.text}` : item.text });
|
|
|
15566
15801
|
|
|
15567
15802
|
// src/services/harness/OpenCodeAdapter.ts
|
|
15568
15803
|
var import_crypto2 = require("crypto");
|
|
15804
|
+
|
|
15805
|
+
// src/services/harness/OpenCodeV2.ts
|
|
15806
|
+
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
15807
|
+
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
15808
|
+
var IDLE_POLLS_BEFORE_START = 3;
|
|
15809
|
+
var PLANNER_RULES = [
|
|
15810
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
15811
|
+
{ action: "edit", resource: "*", effect: "deny" }
|
|
15812
|
+
];
|
|
15813
|
+
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
15814
|
+
var AUTO_APPROVALS = "auto";
|
|
15815
|
+
function newTurnState() {
|
|
15816
|
+
return { seen: /* @__PURE__ */ new Set(), toolNames: /* @__PURE__ */ new Map(), stepModels: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), briefs: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map(), streamedMessages: /* @__PURE__ */ new Set() };
|
|
15817
|
+
}
|
|
15818
|
+
function splitModelId(id) {
|
|
15819
|
+
const slash = id.indexOf("/");
|
|
15820
|
+
if (slash <= 0 || slash === id.length - 1) return null;
|
|
15821
|
+
return { providerID: id.slice(0, slash), id: id.slice(slash + 1) };
|
|
15822
|
+
}
|
|
15823
|
+
function delay(ms, signal) {
|
|
15824
|
+
return new Promise((resolve7) => {
|
|
15825
|
+
const timer = setTimeout(resolve7, ms);
|
|
15826
|
+
timer.unref?.();
|
|
15827
|
+
signal?.addEventListener("abort", () => {
|
|
15828
|
+
clearTimeout(timer);
|
|
15829
|
+
resolve7();
|
|
15830
|
+
}, { once: true });
|
|
15831
|
+
});
|
|
15832
|
+
}
|
|
15833
|
+
function contentText(content) {
|
|
15834
|
+
return (content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
|
|
15835
|
+
}
|
|
15836
|
+
function subagentDigest(output) {
|
|
15837
|
+
const inner = output.match(/<subagent[^>]*>\n?([\s\S]*?)\n?<\/subagent>/);
|
|
15838
|
+
return inner ? inner[1] : output;
|
|
15839
|
+
}
|
|
15840
|
+
function permissionReply(decision) {
|
|
15841
|
+
if (decision.decision === "allow") return { decision: "once" };
|
|
15842
|
+
if (decision.decision === "allowForTask") return { decision: "always" };
|
|
15843
|
+
const note = decision.note?.trim();
|
|
15844
|
+
return note ? { decision: "reject", message: note } : { decision: "reject" };
|
|
15845
|
+
}
|
|
15846
|
+
var OpenCodeV2 = class {
|
|
15847
|
+
constructor(host, opts, role) {
|
|
15848
|
+
this.host = host;
|
|
15849
|
+
this.opts = opts;
|
|
15850
|
+
this.role = role;
|
|
15851
|
+
this.agent = opts.kind === "task" ? opts.flags.permissionMode : "plan";
|
|
15852
|
+
const split = opts.model ? splitModelId(opts.model) : null;
|
|
15853
|
+
const variant = opts.kind === "task" ? opts.flags.effort : opts.effort;
|
|
15854
|
+
if (split) this.model = { ...split, ...variant ? { variant } : {} };
|
|
15855
|
+
}
|
|
15856
|
+
host;
|
|
15857
|
+
opts;
|
|
15858
|
+
role;
|
|
15859
|
+
sessionId = null;
|
|
15860
|
+
model = null;
|
|
15861
|
+
agent;
|
|
15862
|
+
turn = null;
|
|
15863
|
+
turnHasText = false;
|
|
15864
|
+
interruptRequested = false;
|
|
15865
|
+
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
15866
|
+
openPermissions = /* @__PURE__ */ new Map();
|
|
15867
|
+
nativeSessionId() {
|
|
15868
|
+
return this.sessionId;
|
|
15869
|
+
}
|
|
15870
|
+
async start() {
|
|
15871
|
+
const resume = this.opts.resumeSessionId;
|
|
15872
|
+
if (resume) {
|
|
15873
|
+
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
15874
|
+
if (existing?.data?.id) {
|
|
15875
|
+
this.sessionId = existing.data.id;
|
|
15876
|
+
if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
|
|
15877
|
+
await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
|
|
15878
|
+
await this.applyInstructions();
|
|
15879
|
+
return;
|
|
15880
|
+
}
|
|
15881
|
+
if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
|
|
15882
|
+
}
|
|
15883
|
+
const created = await this.host.json("POST", "/api/session", {
|
|
15884
|
+
agent: this.agent,
|
|
15885
|
+
...this.model ? { model: this.model } : {},
|
|
15886
|
+
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
15887
|
+
});
|
|
15888
|
+
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
15889
|
+
this.sessionId = created.data.id;
|
|
15890
|
+
await this.applyInstructions();
|
|
15891
|
+
}
|
|
15892
|
+
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
15893
|
+
async applyInstructions() {
|
|
15894
|
+
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
15895
|
+
const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: this.opts.systemPrompt });
|
|
15896
|
+
if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
|
|
15897
|
+
}
|
|
15898
|
+
async send(message, onEvent, signal, onActivity) {
|
|
15899
|
+
if (!this.sessionId) throw new Error(`OpenCode ${this.role} session is not started`);
|
|
15900
|
+
if (this.host.isExited()) {
|
|
15901
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
15902
|
+
return;
|
|
15903
|
+
}
|
|
15904
|
+
const state = newTurnState();
|
|
15905
|
+
this.turnHasText = false;
|
|
15906
|
+
this.interruptRequested = false;
|
|
15907
|
+
let markDone = () => {
|
|
15908
|
+
};
|
|
15909
|
+
const ended = new Promise((resolve7) => {
|
|
15910
|
+
markDone = resolve7;
|
|
15911
|
+
});
|
|
15912
|
+
const turn = { live: false, done: false, outcome: null, failure: null, ended, finish: () => {
|
|
15913
|
+
turn.done = true;
|
|
15914
|
+
markDone();
|
|
15915
|
+
} };
|
|
15916
|
+
this.turn = turn;
|
|
15917
|
+
const closeStream = await this.openStream(state, turn, onEvent, onActivity);
|
|
15918
|
+
const poll = new AbortController();
|
|
15919
|
+
try {
|
|
15920
|
+
try {
|
|
15921
|
+
await this.host.json("POST", `/api/session/${this.sessionId}/prompt`, { text: message }, signal);
|
|
15922
|
+
} catch (err) {
|
|
15923
|
+
if (signal?.aborted) return;
|
|
15924
|
+
onEvent({ type: "error", message: `OpenCode did not take the message: ${err instanceof Error ? err.message : String(err)}` });
|
|
15925
|
+
return;
|
|
15926
|
+
}
|
|
15927
|
+
void this.pollActive(turn, poll.signal);
|
|
15928
|
+
const aborted = new Promise((resolve7) => {
|
|
15929
|
+
if (signal?.aborted) resolve7();
|
|
15930
|
+
signal?.addEventListener("abort", () => resolve7(), { once: true });
|
|
15931
|
+
});
|
|
15932
|
+
await Promise.race([turn.ended, aborted, this.host.processEnded]);
|
|
15933
|
+
if (signal?.aborted) return;
|
|
15934
|
+
if (!turn.done) {
|
|
15935
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
15936
|
+
return;
|
|
15937
|
+
}
|
|
15938
|
+
await this.readBack(state, onEvent);
|
|
15939
|
+
for (const id of [...this.openPermissions.keys()]) {
|
|
15940
|
+
this.openPermissions.delete(id);
|
|
15941
|
+
onEvent({ type: "permission_cancelled", id });
|
|
15942
|
+
}
|
|
15943
|
+
if (turn.outcome === "interrupted" || this.interruptRequested) onEvent({ type: "turn_end", interrupted: true });
|
|
15944
|
+
else if (turn.outcome === "failed") onEvent({ type: "error", message: turn.failure ?? "OpenCode reported that the turn failed." });
|
|
15945
|
+
else onEvent({ type: "turn_end" });
|
|
15946
|
+
} finally {
|
|
15947
|
+
this.turn = null;
|
|
15948
|
+
poll.abort();
|
|
15949
|
+
await closeStream();
|
|
15950
|
+
}
|
|
15951
|
+
}
|
|
15952
|
+
/**
|
|
15953
|
+
* The stream can drop the end frame while still delivering the rest, so the
|
|
15954
|
+
* turn also asks which sessions are running. A session that is not, after
|
|
15955
|
+
* this turn's work was seen, is over; one never seen working after a few
|
|
15956
|
+
* polls had already finished before the stream connected.
|
|
15957
|
+
*/
|
|
15958
|
+
async pollActive(turn, signal) {
|
|
15959
|
+
let idlePolls = 0;
|
|
15960
|
+
while (!turn.done && !signal.aborted) {
|
|
15961
|
+
await delay(ACTIVE_POLL_INTERVAL_MS, signal);
|
|
15962
|
+
if (turn.done || signal.aborted) return;
|
|
15963
|
+
const active = await this.host.json("GET", "/api/session/active").catch(() => null);
|
|
15964
|
+
if (!active?.data) continue;
|
|
15965
|
+
if (this.sessionId && this.sessionId in active.data) {
|
|
15966
|
+
turn.live = true;
|
|
15967
|
+
idlePolls = 0;
|
|
15968
|
+
continue;
|
|
15969
|
+
}
|
|
15970
|
+
idlePolls++;
|
|
15971
|
+
if (turn.live || idlePolls >= IDLE_POLLS_BEFORE_START) {
|
|
15972
|
+
const messages = await this.messages();
|
|
15973
|
+
const idle = [...messages ?? []].reverse().find((m) => m.type === "idle");
|
|
15974
|
+
if (idle?.outcome === "failed" || idle?.outcome === "interrupted" || idle?.outcome === "succeeded") turn.outcome ??= idle.outcome;
|
|
15975
|
+
turn.outcome ??= "succeeded";
|
|
15976
|
+
if (!turn.done) turn.finish();
|
|
15977
|
+
}
|
|
15978
|
+
}
|
|
15979
|
+
}
|
|
15980
|
+
async messages() {
|
|
15981
|
+
const response = await this.host.json("GET", `/api/session/${this.sessionId}/message`).catch(() => null);
|
|
15982
|
+
return Array.isArray(response?.data) ? response.data : null;
|
|
15983
|
+
}
|
|
15984
|
+
/**
|
|
15985
|
+
* Whatever of the turn's own replies the stream missed. A message whose text
|
|
15986
|
+
* the stream delivered adds nothing here; one that lost its text frames still
|
|
15987
|
+
* reaches the plain-text channel, where the done marker is looked for.
|
|
15988
|
+
*/
|
|
15989
|
+
async readBack(state, onEvent) {
|
|
15990
|
+
const messages = await this.messages();
|
|
15991
|
+
if (!messages) return;
|
|
15992
|
+
const latestUser = messages.findIndex((m) => m.type === "user");
|
|
15993
|
+
const own = latestUser < 0 ? messages : messages.slice(0, latestUser);
|
|
15994
|
+
for (const message of [...own].reverse()) {
|
|
15995
|
+
if (message.type !== "assistant" || !message.id || state.streamedMessages.has(message.id)) continue;
|
|
15996
|
+
(message.content ?? []).forEach((part, index) => {
|
|
15997
|
+
if (part.type === "text" && part.text) this.emitText(`${message.id}:readback:${index}`, part.text, state, onEvent);
|
|
15998
|
+
});
|
|
15999
|
+
}
|
|
16000
|
+
}
|
|
16001
|
+
/**
|
|
16002
|
+
* Abort the running turn, keeping the server and its session. OpenCode
|
|
16003
|
+
* acknowledges with `session.execution.interrupted`, which also ends the turn.
|
|
16004
|
+
*/
|
|
16005
|
+
async interrupt(timeoutMs) {
|
|
16006
|
+
if (!this.sessionId || this.host.isExited()) return false;
|
|
16007
|
+
const turn = this.turn;
|
|
16008
|
+
this.interruptRequested = true;
|
|
16009
|
+
const response = await this.host.json("POST", `/api/session/${this.sessionId}/interrupt`).catch(() => null);
|
|
16010
|
+
if (!response) return false;
|
|
16011
|
+
if (!turn || turn.done) return true;
|
|
16012
|
+
let timer;
|
|
16013
|
+
const timedOut = new Promise((resolve7) => {
|
|
16014
|
+
timer = setTimeout(() => resolve7(false), timeoutMs);
|
|
16015
|
+
timer.unref?.();
|
|
16016
|
+
});
|
|
16017
|
+
const acknowledged = await Promise.race([turn.ended.then(() => true), this.host.processEnded.then(() => false), timedOut]);
|
|
16018
|
+
clearTimeout(timer);
|
|
16019
|
+
return acknowledged;
|
|
16020
|
+
}
|
|
16021
|
+
answerPermission(id, decision) {
|
|
16022
|
+
const sessionId = this.openPermissions.get(id);
|
|
16023
|
+
if (sessionId === void 0) return false;
|
|
16024
|
+
this.openPermissions.delete(id);
|
|
16025
|
+
void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
|
|
16026
|
+
});
|
|
16027
|
+
return true;
|
|
16028
|
+
}
|
|
16029
|
+
async replyPermission(id, sessionId, reply) {
|
|
16030
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
|
|
16031
|
+
}
|
|
16032
|
+
async openStream(state, turn, onEvent, onActivity) {
|
|
16033
|
+
const streamAbort = new AbortController();
|
|
16034
|
+
let connected = () => {
|
|
16035
|
+
};
|
|
16036
|
+
const ready = new Promise((resolve7) => {
|
|
16037
|
+
connected = resolve7;
|
|
16038
|
+
});
|
|
16039
|
+
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, state, turn, onEvent), connected, onActivity);
|
|
16040
|
+
await Promise.race([ready, new Promise((r) => {
|
|
16041
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
|
|
16042
|
+
t.unref?.();
|
|
16043
|
+
})]);
|
|
16044
|
+
return async () => {
|
|
16045
|
+
streamAbort.abort();
|
|
16046
|
+
await live.catch(() => {
|
|
16047
|
+
});
|
|
16048
|
+
};
|
|
16049
|
+
}
|
|
16050
|
+
async streamEvents(signal, onFrame, onConnected, onActivity) {
|
|
16051
|
+
const response = await this.host.openEvents(signal).catch(() => null);
|
|
16052
|
+
const body = response?.body;
|
|
16053
|
+
if (!body) {
|
|
16054
|
+
onConnected();
|
|
16055
|
+
return;
|
|
16056
|
+
}
|
|
16057
|
+
onConnected();
|
|
16058
|
+
const reader = body.getReader();
|
|
16059
|
+
const decoder = new TextDecoder();
|
|
16060
|
+
let buffer = "";
|
|
16061
|
+
for (; ; ) {
|
|
16062
|
+
const { done, value } = await reader.read().catch(() => ({ done: true, value: void 0 }));
|
|
16063
|
+
if (done) return;
|
|
16064
|
+
onActivity?.();
|
|
16065
|
+
buffer += decoder.decode(value, { stream: true });
|
|
16066
|
+
let newline;
|
|
16067
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
16068
|
+
const line = buffer.slice(0, newline).trim();
|
|
16069
|
+
buffer = buffer.slice(newline + 1);
|
|
16070
|
+
if (!line.startsWith("data:")) continue;
|
|
16071
|
+
try {
|
|
16072
|
+
onFrame(JSON.parse(line.slice(5).trim()));
|
|
16073
|
+
} catch {
|
|
16074
|
+
}
|
|
16075
|
+
}
|
|
16076
|
+
}
|
|
16077
|
+
}
|
|
16078
|
+
/**
|
|
16079
|
+
* One frame. Only this session and its children are followed: the stream is
|
|
16080
|
+
* the whole server's, and another client's session is none of this turn's business.
|
|
16081
|
+
*/
|
|
16082
|
+
onFrame(frame, state, turn, onEvent) {
|
|
16083
|
+
const data = frame.data;
|
|
16084
|
+
if (!data || !frame.type) return;
|
|
16085
|
+
if (frame.type === "session.created") {
|
|
16086
|
+
if (data.parentID && data.parentID === this.sessionId && data.sessionID && !state.children.has(data.sessionID)) state.children.set(data.sessionID, null);
|
|
16087
|
+
return;
|
|
16088
|
+
}
|
|
16089
|
+
const session = data.sessionID;
|
|
16090
|
+
if (!session) return;
|
|
16091
|
+
const isChild = session !== this.sessionId;
|
|
16092
|
+
if (isChild && !state.children.has(session)) return;
|
|
16093
|
+
if (!isChild) {
|
|
16094
|
+
if (frame.type === "session.execution.started") {
|
|
16095
|
+
turn.live = true;
|
|
16096
|
+
return;
|
|
16097
|
+
}
|
|
16098
|
+
if (frame.type === "session.execution.succeeded" || frame.type === "session.execution.failed" || frame.type === "session.execution.interrupted") {
|
|
16099
|
+
if (!turn.live || turn.done) return;
|
|
16100
|
+
turn.outcome = frame.type === "session.execution.succeeded" ? "succeeded" : frame.type === "session.execution.failed" ? "failed" : "interrupted";
|
|
16101
|
+
if (turn.outcome === "failed") turn.failure = data.error?.message ?? data.error?.type ?? null;
|
|
16102
|
+
turn.finish();
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
16105
|
+
}
|
|
16106
|
+
if (frame.type === "permission.asked") {
|
|
16107
|
+
if (this.role === "task") this.askPermission(data, session, state, onEvent);
|
|
16108
|
+
else this.denyPermission(data, session, state, onEvent);
|
|
16109
|
+
return;
|
|
16110
|
+
}
|
|
16111
|
+
if (frame.type === "permission.replied") {
|
|
16112
|
+
const id = data.requestID;
|
|
16113
|
+
if (id && this.openPermissions.delete(id)) onEvent({ type: "permission_cancelled", id });
|
|
16114
|
+
return;
|
|
16115
|
+
}
|
|
16116
|
+
let subagentId;
|
|
16117
|
+
if (isChild) {
|
|
16118
|
+
const owner = state.children.get(session);
|
|
16119
|
+
if (!owner) {
|
|
16120
|
+
state.heldFrames.set(session, [...state.heldFrames.get(session) ?? [], frame]);
|
|
16121
|
+
return;
|
|
16122
|
+
}
|
|
16123
|
+
subagentId = owner;
|
|
16124
|
+
}
|
|
16125
|
+
this.onContentFrame(frame, data, state, turn, onEvent, subagentId);
|
|
16126
|
+
}
|
|
16127
|
+
onContentFrame(frame, data, state, turn, onEvent, subagentId) {
|
|
16128
|
+
const message = data.assistantMessageID ?? "";
|
|
16129
|
+
switch (frame.type) {
|
|
16130
|
+
case "session.step.started":
|
|
16131
|
+
if (message && data.model?.providerID && data.model.id) state.stepModels.set(message, `${data.model.providerID}/${data.model.id}`);
|
|
16132
|
+
return;
|
|
16133
|
+
case "session.reasoning.delta":
|
|
16134
|
+
if (data.delta) onEvent({ type: "thinking_delta", text: data.delta, subagentId });
|
|
16135
|
+
return;
|
|
16136
|
+
case "session.reasoning.ended": {
|
|
16137
|
+
const key = `reasoning:${message}:${data.ordinal ?? 0}`;
|
|
16138
|
+
if (!data.text || state.seen.has(key)) return;
|
|
16139
|
+
state.seen.add(key);
|
|
16140
|
+
onEvent({ type: "thinking", text: data.text, subagentId });
|
|
16141
|
+
return;
|
|
16142
|
+
}
|
|
16143
|
+
case "session.text.delta":
|
|
16144
|
+
if (!subagentId && data.delta) this.onTextDelta(`${message}:${data.ordinal ?? 0}`, data.delta, state, onEvent);
|
|
16145
|
+
return;
|
|
16146
|
+
case "session.text.ended":
|
|
16147
|
+
if (subagentId || !data.text) return;
|
|
16148
|
+
if (message) state.streamedMessages.add(message);
|
|
16149
|
+
this.emitText(`${message}:${data.ordinal ?? 0}`, data.text, state, onEvent);
|
|
16150
|
+
return;
|
|
16151
|
+
case "session.tool.input.started":
|
|
16152
|
+
if (data.id && data.name) state.toolNames.set(data.id, data.name);
|
|
16153
|
+
return;
|
|
16154
|
+
case "session.tool.called": {
|
|
16155
|
+
const id = data.id;
|
|
16156
|
+
if (!id || state.seen.has(`call:${id}`)) return;
|
|
16157
|
+
state.seen.add(`call:${id}`);
|
|
16158
|
+
const input = data.input ?? {};
|
|
16159
|
+
const name = state.toolNames.get(id) ?? "tool";
|
|
16160
|
+
const brief = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
16161
|
+
state.briefs.set(id, brief);
|
|
16162
|
+
onEvent({ type: "tool_call", id, name, args: input, subagentId });
|
|
16163
|
+
return;
|
|
16164
|
+
}
|
|
16165
|
+
case "session.tool.progress":
|
|
16166
|
+
if (!subagentId) this.adoptChild(data, state, onEvent, turn);
|
|
16167
|
+
return;
|
|
16168
|
+
case "session.tool.success":
|
|
16169
|
+
case "session.tool.failed": {
|
|
16170
|
+
const id = data.id;
|
|
16171
|
+
if (!id || state.seen.has(`result:${id}`)) return;
|
|
16172
|
+
state.seen.add(`result:${id}`);
|
|
16173
|
+
const success = frame.type === "session.tool.success";
|
|
16174
|
+
const output = success ? contentText(data.content) : data.error?.message ?? data.error?.type ?? "";
|
|
16175
|
+
onEvent({ type: "tool_result", id, name: state.toolNames.get(id) ?? "tool", output, success, subagentId });
|
|
16176
|
+
if (!subagentId) this.finishChild(id, success, output, state, onEvent);
|
|
16177
|
+
return;
|
|
16178
|
+
}
|
|
16179
|
+
case "session.step.ended":
|
|
16180
|
+
case "session.step.failed":
|
|
16181
|
+
this.countUsage(data, state, onEvent, subagentId);
|
|
16182
|
+
return;
|
|
16183
|
+
default:
|
|
16184
|
+
}
|
|
16185
|
+
}
|
|
16186
|
+
adoptChild(data, state, onEvent, turn) {
|
|
16187
|
+
const child = data.metadata?.sessionID;
|
|
16188
|
+
const callId = data.id;
|
|
16189
|
+
if (!child || !callId || state.children.get(child)) return;
|
|
16190
|
+
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
16191
|
+
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
16192
|
+
state.children.set(child, callId);
|
|
16193
|
+
const held = state.heldFrames.get(child) ?? [];
|
|
16194
|
+
state.heldFrames.delete(child);
|
|
16195
|
+
for (const frame of held) this.onFrame(frame, state, turn, onEvent);
|
|
16196
|
+
}
|
|
16197
|
+
finishChild(callId, success, output, state, onEvent) {
|
|
16198
|
+
if (![...state.children.values()].includes(callId)) return;
|
|
16199
|
+
onEvent({ type: "subagent_finished", subagentId: callId, outcome: success ? "done" : "failed", digest: subagentDigest(output) });
|
|
16200
|
+
}
|
|
16201
|
+
/** One model call's usage, once, when its step ends. Zeros mean the call failed before the provider answered. */
|
|
16202
|
+
countUsage(data, state, onEvent, subagentId) {
|
|
16203
|
+
const message = data.assistantMessageID;
|
|
16204
|
+
const tokens = data.tokens;
|
|
16205
|
+
if (!message || !tokens || state.seen.has(`usage:${message}`)) return;
|
|
16206
|
+
state.seen.add(`usage:${message}`);
|
|
16207
|
+
const prompt = partedPromptUsage({ uncached: tokens.input, cacheRead: tokens.cache?.read, cacheWrite: tokens.cache?.write });
|
|
16208
|
+
const outputTokens = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
|
|
16209
|
+
if ((prompt.inputTokens ?? 0) + outputTokens === 0) return;
|
|
16210
|
+
const record = { source: "opencode", ...prompt, outputTokens };
|
|
16211
|
+
const model = state.stepModels.get(message);
|
|
16212
|
+
if (model) record.model = model;
|
|
16213
|
+
if (typeof data.cost === "number" && data.cost > 0) record.reportedCost = { amount: data.cost, currency: "USD" };
|
|
16214
|
+
if (subagentId) record.subagentId = subagentId;
|
|
16215
|
+
onEvent({ type: "usage", record });
|
|
16216
|
+
}
|
|
16217
|
+
/** One complete reply text, once. A message can carry text on both sides of a tool call, so each run after the first opens a paragraph. */
|
|
16218
|
+
emitText(id, text2, state, onEvent) {
|
|
16219
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
16220
|
+
state.seen.add(`text:${id}`);
|
|
16221
|
+
if (!text2.trim()) return;
|
|
16222
|
+
const lead = state.textRuns.get(id)?.lead ?? (this.turnHasText ? "\n\n" : "");
|
|
16223
|
+
onEvent({ type: "assistant_text", text: `${lead}${text2}` });
|
|
16224
|
+
this.turnHasText = true;
|
|
16225
|
+
}
|
|
16226
|
+
/**
|
|
16227
|
+
* Stream one piece of a reply text. The paragraph break goes out with the
|
|
16228
|
+
* first visible delta, so the deltas add up to exactly the text the completed
|
|
16229
|
+
* run then re-sends; a run that is only whitespace so far is held back.
|
|
16230
|
+
*/
|
|
16231
|
+
onTextDelta(id, delta, state, onEvent) {
|
|
16232
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
16233
|
+
const run = state.textRuns.get(id) ?? { held: "", lead: null };
|
|
16234
|
+
state.textRuns.set(id, run);
|
|
16235
|
+
if (run.lead !== null) {
|
|
16236
|
+
onEvent({ type: "assistant_text_delta", text: delta });
|
|
16237
|
+
return;
|
|
16238
|
+
}
|
|
16239
|
+
run.held += delta;
|
|
16240
|
+
if (!run.held.trim()) return;
|
|
16241
|
+
run.lead = this.turnHasText ? "\n\n" : "";
|
|
16242
|
+
this.turnHasText = true;
|
|
16243
|
+
onEvent({ type: "assistant_text_delta", text: `${run.lead}${run.held}` });
|
|
16244
|
+
}
|
|
16245
|
+
permissionRequest(ask) {
|
|
16246
|
+
const id = ask.id;
|
|
16247
|
+
if (!id) return null;
|
|
16248
|
+
const scope = (ask.resources ?? []).join(", ");
|
|
16249
|
+
return {
|
|
16250
|
+
type: "permission_request",
|
|
16251
|
+
id,
|
|
16252
|
+
name: ask.action ?? "permission",
|
|
16253
|
+
detail: JSON.stringify(scope ? { scope } : {}),
|
|
16254
|
+
input: ask.resources ? { resources: ask.resources } : {},
|
|
16255
|
+
// The patterns `always` would grant: the runner's own offer, and the only grounds for "Allow for this task".
|
|
16256
|
+
...ask.save?.length ? { suggestions: ask.save } : {},
|
|
16257
|
+
...ask.source?.id ? { toolUseId: ask.source.id } : {}
|
|
16258
|
+
};
|
|
16259
|
+
}
|
|
16260
|
+
/**
|
|
16261
|
+
* Deny one planner request. OpenCode blocks the turn until it is answered, so
|
|
16262
|
+
* this must answer: `reject` rather than a silent drop is the "absent answer
|
|
16263
|
+
* is a denial" invariant ADR-0008 states. The refusal is announced so the
|
|
16264
|
+
* timeline shows the planner reaching for something it may not have.
|
|
16265
|
+
*/
|
|
16266
|
+
denyPermission(ask, session, state, onEvent) {
|
|
16267
|
+
const request = this.permissionRequest(ask);
|
|
16268
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16269
|
+
state.seen.add(`perm:${request.id}`);
|
|
16270
|
+
onEvent(request);
|
|
16271
|
+
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
16272
|
+
});
|
|
16273
|
+
}
|
|
16274
|
+
/**
|
|
16275
|
+
* A task's request. Under a mode whose manifest sets `approvals: auto` it is
|
|
16276
|
+
* answered at once with what `opencode run --auto` answers, so the same plan
|
|
16277
|
+
* behaves the same on both transports (ADR-0001), and announced already
|
|
16278
|
+
* decided so the log still shows it. Any other mode leaves it open for an
|
|
16279
|
+
* approval card.
|
|
16280
|
+
*/
|
|
16281
|
+
askPermission(ask, session, state, onEvent) {
|
|
16282
|
+
const request = this.permissionRequest(ask);
|
|
16283
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16284
|
+
state.seen.add(`perm:${request.id}`);
|
|
16285
|
+
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
16286
|
+
onEvent({ ...request, decided: { decision: "allow" } });
|
|
16287
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
16288
|
+
});
|
|
16289
|
+
return;
|
|
16290
|
+
}
|
|
16291
|
+
this.openPermissions.set(request.id, session);
|
|
16292
|
+
onEvent(request);
|
|
16293
|
+
}
|
|
16294
|
+
};
|
|
16295
|
+
|
|
16296
|
+
// src/services/harness/OpenCodeAdapter.ts
|
|
15569
16297
|
var SERVER_READY_TIMEOUT_MS = 3e4;
|
|
15570
16298
|
var STDERR_TAIL_CHARS2 = 4e3;
|
|
15571
|
-
var
|
|
16299
|
+
var STREAM_CONNECT_TIMEOUT_MS2 = 5e3;
|
|
15572
16300
|
var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
15573
16301
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
15574
16302
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
@@ -15581,8 +16309,8 @@ var DISABLED_TOOLS = {
|
|
|
15581
16309
|
todowrite: false
|
|
15582
16310
|
};
|
|
15583
16311
|
var TASK_DISABLED_TOOLS = { question: false };
|
|
15584
|
-
var
|
|
15585
|
-
function
|
|
16312
|
+
var AUTO_APPROVALS2 = "auto";
|
|
16313
|
+
function splitModelId2(id) {
|
|
15586
16314
|
const slash = id.indexOf("/");
|
|
15587
16315
|
if (slash <= 0 || slash === id.length - 1) return null;
|
|
15588
16316
|
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
|
|
@@ -15638,7 +16366,7 @@ function unwrapFileToolOutput(output) {
|
|
|
15638
16366
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
15639
16367
|
return inner ? inner[1] : output;
|
|
15640
16368
|
}
|
|
15641
|
-
function
|
|
16369
|
+
function delay2(ms, signal) {
|
|
15642
16370
|
return new Promise((resolve7) => {
|
|
15643
16371
|
const timer = setTimeout(resolve7, ms);
|
|
15644
16372
|
timer.unref?.();
|
|
@@ -15648,10 +16376,10 @@ function delay(ms, signal) {
|
|
|
15648
16376
|
}, { once: true });
|
|
15649
16377
|
});
|
|
15650
16378
|
}
|
|
15651
|
-
function
|
|
16379
|
+
function newTurnState2() {
|
|
15652
16380
|
return { seen: /* @__PURE__ */ new Set(), assistantMessages: /* @__PURE__ */ new Set(), partTypes: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map() };
|
|
15653
16381
|
}
|
|
15654
|
-
function
|
|
16382
|
+
function permissionReply2(decision) {
|
|
15655
16383
|
if (decision.decision === "allow") return { reply: "once" };
|
|
15656
16384
|
if (decision.decision === "allowForTask") return { reply: "always" };
|
|
15657
16385
|
const note = decision.note?.trim();
|
|
@@ -15688,6 +16416,8 @@ var OpenCodeAdapter = class {
|
|
|
15688
16416
|
interruptRequested = false;
|
|
15689
16417
|
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
15690
16418
|
openPermissions = /* @__PURE__ */ new Map();
|
|
16419
|
+
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
16420
|
+
v2 = null;
|
|
15691
16421
|
async start(opts) {
|
|
15692
16422
|
if (opts.kind === "task") this.task = opts;
|
|
15693
16423
|
else this.planner = opts;
|
|
@@ -15702,14 +16432,14 @@ var OpenCodeAdapter = class {
|
|
|
15702
16432
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
15703
16433
|
}
|
|
15704
16434
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
15705
|
-
const password =
|
|
16435
|
+
const password = (0, import_crypto2.randomBytes)(24).toString("base64url");
|
|
15706
16436
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
15707
|
-
env: runnerEnv(PATH,
|
|
16437
|
+
env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
|
|
15708
16438
|
stdio: ["pipe", "pipe", "pipe"],
|
|
15709
16439
|
cwd: opts.cwd,
|
|
15710
16440
|
windowsVerbatimArguments: launch.verbatim
|
|
15711
16441
|
});
|
|
15712
|
-
|
|
16442
|
+
this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
|
|
15713
16443
|
this.process.stdin?.on("error", () => {
|
|
15714
16444
|
});
|
|
15715
16445
|
const banner = new Promise((resolve7) => {
|
|
@@ -15746,6 +16476,24 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
|
15746
16476
|
|
|
15747
16477
|
${this.stderrTail.trim()}` : ""}`);
|
|
15748
16478
|
}
|
|
16479
|
+
if (await this.speaksV2()) {
|
|
16480
|
+
this.v2 = new OpenCodeV2({
|
|
16481
|
+
request: (method, path27, body, signal) => this.request(method, path27, body, signal),
|
|
16482
|
+
json: (method, path27, body, signal) => this.json(method, path27, body, signal),
|
|
16483
|
+
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
16484
|
+
processEnded: this.processEnded,
|
|
16485
|
+
isExited: () => this.exited,
|
|
16486
|
+
exitMessage: () => this.exitMessage()
|
|
16487
|
+
}, opts, this.role());
|
|
16488
|
+
try {
|
|
16489
|
+
await this.v2.start();
|
|
16490
|
+
} catch (err) {
|
|
16491
|
+
this.dispose();
|
|
16492
|
+
throw err;
|
|
16493
|
+
}
|
|
16494
|
+
this.sessionId = this.v2.nativeSessionId();
|
|
16495
|
+
return;
|
|
16496
|
+
}
|
|
15749
16497
|
if (opts.resumeSessionId) {
|
|
15750
16498
|
const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
|
|
15751
16499
|
if (existing?.id) {
|
|
@@ -15769,12 +16517,17 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15769
16517
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
15770
16518
|
return;
|
|
15771
16519
|
}
|
|
16520
|
+
if (this.v2) {
|
|
16521
|
+
await this.v2.send(message, onEvent, signal, onActivity);
|
|
16522
|
+
if (signal?.aborted) this.dispose();
|
|
16523
|
+
return;
|
|
16524
|
+
}
|
|
15772
16525
|
if (this.task) return this.sendTask(this.task, message, onEvent, signal, onActivity);
|
|
15773
|
-
const turn =
|
|
16526
|
+
const turn = newTurnState2();
|
|
15774
16527
|
this.turnHasText = false;
|
|
15775
16528
|
const closeStream = await this.openStream(turn, onEvent, onActivity);
|
|
15776
16529
|
try {
|
|
15777
|
-
const model = this.planner?.model ?
|
|
16530
|
+
const model = this.planner?.model ? splitModelId2(this.planner.model) : null;
|
|
15778
16531
|
const body = {
|
|
15779
16532
|
parts: [{ type: "text", text: message }],
|
|
15780
16533
|
// The read-only guarantee: OpenCode's own plan agent has no write tools.
|
|
@@ -15815,7 +16568,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15815
16568
|
* {@link recoverReply}, never applies.
|
|
15816
16569
|
*/
|
|
15817
16570
|
async sendTask(task, message, onEvent, signal, onActivity) {
|
|
15818
|
-
const state =
|
|
16571
|
+
const state = newTurnState2();
|
|
15819
16572
|
this.turnHasText = false;
|
|
15820
16573
|
this.interruptRequested = false;
|
|
15821
16574
|
let markDone = () => {
|
|
@@ -15831,7 +16584,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15831
16584
|
const closeStream = await this.openStream(state, onEvent, onActivity);
|
|
15832
16585
|
const poll = new AbortController();
|
|
15833
16586
|
try {
|
|
15834
|
-
const model = task.model ?
|
|
16587
|
+
const model = task.model ? splitModelId2(task.model) : null;
|
|
15835
16588
|
const body = {
|
|
15836
16589
|
parts: [{ type: "text", text: message }],
|
|
15837
16590
|
// The manifest's meaning of the task's mode (ADR-0001): for OpenCode a mode is an agent.
|
|
@@ -15911,7 +16664,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15911
16664
|
*/
|
|
15912
16665
|
async pollStatus(turn, signal) {
|
|
15913
16666
|
while (!turn.done && !signal.aborted) {
|
|
15914
|
-
await
|
|
16667
|
+
await delay2(STATUS_POLL_INTERVAL_MS, signal);
|
|
15915
16668
|
if (turn.done || signal.aborted) return;
|
|
15916
16669
|
const busy = await this.sessionBusy();
|
|
15917
16670
|
if (busy === true) turn.live = true;
|
|
@@ -15958,6 +16711,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15958
16711
|
*/
|
|
15959
16712
|
async interrupt(timeoutMs) {
|
|
15960
16713
|
if (!this.process || !this.sessionId || this.exited) return false;
|
|
16714
|
+
if (this.v2) return this.v2.interrupt(timeoutMs);
|
|
15961
16715
|
const turn = this.taskTurn;
|
|
15962
16716
|
this.interruptRequested = true;
|
|
15963
16717
|
const posted = await this.json("POST", `/session/${this.sessionId}/abort`).then(() => true, () => false);
|
|
@@ -15978,13 +16732,23 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15978
16732
|
void this.processEnded.then(() => listener(this.exitCode));
|
|
15979
16733
|
}
|
|
15980
16734
|
answerPermission(id, decision) {
|
|
16735
|
+
if (this.v2) return this.process ? this.v2.answerPermission(id, decision) : false;
|
|
15981
16736
|
const sessionId = this.openPermissions.get(id);
|
|
15982
16737
|
if (sessionId === void 0 || !this.process) return false;
|
|
15983
16738
|
this.openPermissions.delete(id);
|
|
15984
|
-
void this.replyPermission(id, sessionId,
|
|
16739
|
+
void this.replyPermission(id, sessionId, permissionReply2(decision)).catch(() => {
|
|
15985
16740
|
});
|
|
15986
16741
|
return true;
|
|
15987
16742
|
}
|
|
16743
|
+
/**
|
|
16744
|
+
* OpenCode 2.x replaced the 1.x HTTP surface rather than extending it, and
|
|
16745
|
+
* only 2.x answers `/api/info` with its version. A 1.x server — or one that
|
|
16746
|
+
* answers with anything else — keeps the 1.x protocol this class speaks.
|
|
16747
|
+
*/
|
|
16748
|
+
async speaksV2() {
|
|
16749
|
+
const info = await this.json("GET", "/api/info").catch(() => null);
|
|
16750
|
+
return typeof info === "object" && info !== null && typeof info.version === "string";
|
|
16751
|
+
}
|
|
15988
16752
|
role() {
|
|
15989
16753
|
return this.task ? "task" : "planner";
|
|
15990
16754
|
}
|
|
@@ -16035,7 +16799,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16035
16799
|
if (pending?.info?.time?.completed || pending?.info?.error) return pending;
|
|
16036
16800
|
if (pending) onActivity?.();
|
|
16037
16801
|
if (Date.now() >= deadline) return null;
|
|
16038
|
-
await
|
|
16802
|
+
await delay2(RECOVERY_POLL_INTERVAL_MS, signal);
|
|
16039
16803
|
}
|
|
16040
16804
|
}
|
|
16041
16805
|
newestAssistantId(messages) {
|
|
@@ -16248,7 +17012,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16248
17012
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
16249
17013
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
16250
17014
|
};
|
|
16251
|
-
if (this.task?.flags.modeSettings.approvals ===
|
|
17015
|
+
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
16252
17016
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
16253
17017
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
16254
17018
|
});
|
|
@@ -16282,7 +17046,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16282
17046
|
});
|
|
16283
17047
|
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, turn, onEvent), connected, onActivity);
|
|
16284
17048
|
await Promise.race([streamReady, new Promise((r) => {
|
|
16285
|
-
const t = setTimeout(r,
|
|
17049
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS2);
|
|
16286
17050
|
t.unref?.();
|
|
16287
17051
|
})]);
|
|
16288
17052
|
return async () => {
|
|
@@ -16487,6 +17251,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16487
17251
|
observers = [];
|
|
16488
17252
|
tddEnabled;
|
|
16489
17253
|
readRunnerTransport;
|
|
17254
|
+
previousAttemptFromLog;
|
|
16490
17255
|
/**
|
|
16491
17256
|
* The setting as the latest run copied it when it opened. Every spawn of
|
|
16492
17257
|
* that run uses this, so flipping the setting mid-run changes the next run
|
|
@@ -16506,6 +17271,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16506
17271
|
this.workspaceEnv = deps.workspaceEnv;
|
|
16507
17272
|
this.tddEnabled = deps.tddEnabled;
|
|
16508
17273
|
this.readRunnerTransport = deps.runnerTransport;
|
|
17274
|
+
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
16509
17275
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
16510
17276
|
this.verifier.onVerdict((taskId, verdict) => this.onVerdict(taskId, verdict));
|
|
16511
17277
|
this.verifier.onCheckpoint((taskId, summary) => {
|
|
@@ -16561,7 +17327,8 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16561
17327
|
workspaceRoot,
|
|
16562
17328
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
16563
17329
|
tddEnabled: options.tddEnabled ?? (() => false),
|
|
16564
|
-
runnerTransport: options.runnerTransport ?? (() => "terminal")
|
|
17330
|
+
runnerTransport: options.runnerTransport ?? (() => "terminal"),
|
|
17331
|
+
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null)
|
|
16565
17332
|
});
|
|
16566
17333
|
return orchestrator;
|
|
16567
17334
|
}
|
|
@@ -16569,6 +17336,18 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16569
17336
|
getIdleSince(taskId) {
|
|
16570
17337
|
return this.verifier.getIdleSince(taskId);
|
|
16571
17338
|
}
|
|
17339
|
+
/**
|
|
17340
|
+
* What an ops retry is told about the attempt before. The saved log is read
|
|
17341
|
+
* whether or not the session was reloaded, so both behave alike; the terminal
|
|
17342
|
+
* buffer is the fallback for a runner that has no log. A reload clears the
|
|
17343
|
+
* attempt count along with the buffer, so a retry made since is what says
|
|
17344
|
+
* there was an attempt to report on.
|
|
17345
|
+
*/
|
|
17346
|
+
opsPreviousAttempt(taskId) {
|
|
17347
|
+
const saved = this.previousAttemptFromLog(taskId) ?? this.output.liveTail(taskId, { maxLines: OPS_RETRY_TAIL_LINES })?.text;
|
|
17348
|
+
if (saved !== void 0) return saved;
|
|
17349
|
+
return (this.retryCounts.get(taskId) ?? 0) > 0 ? "Its output is not available: this session was reloaded since it ran." : void 0;
|
|
17350
|
+
}
|
|
16572
17351
|
/** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
|
|
16573
17352
|
getLiveOutput(taskId, opts) {
|
|
16574
17353
|
return this.output.liveTail(taskId, opts);
|
|
@@ -17426,7 +18205,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17426
18205
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
17427
18206
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
17428
18207
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
17429
|
-
previousAttempt: attempt.ops ? this.
|
|
18208
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
17430
18209
|
});
|
|
17431
18210
|
this.lingering.close(task.id);
|
|
17432
18211
|
const env = await this.envForTask(cwd);
|
|
@@ -21107,6 +21886,13 @@ var PlanEditError = class extends Error {
|
|
|
21107
21886
|
this.name = "PlanEditError";
|
|
21108
21887
|
}
|
|
21109
21888
|
};
|
|
21889
|
+
var OPS_RETRY_DIGEST_CALLS = 20;
|
|
21890
|
+
function lastAttemptDigest(location, taskId) {
|
|
21891
|
+
const attempt = listTaskLogAttempts(location, taskId).pop();
|
|
21892
|
+
if (attempt === void 0) return null;
|
|
21893
|
+
const events = readTaskLog(location, taskId, attempt);
|
|
21894
|
+
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
21895
|
+
}
|
|
21110
21896
|
function sessionRuntimeSettings(settings) {
|
|
21111
21897
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
|
|
21112
21898
|
}
|
|
@@ -21191,7 +21977,8 @@ function createSession(deps) {
|
|
|
21191
21977
|
registry: deps.registry,
|
|
21192
21978
|
workspaceRoot: deps.workspaceRoot,
|
|
21193
21979
|
tddEnabled: () => deps.settings().tddEnabled,
|
|
21194
|
-
runnerTransport: () => deps.settings().runnerTransport ?? "terminal"
|
|
21980
|
+
runnerTransport: () => deps.settings().runnerTransport ?? "terminal",
|
|
21981
|
+
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId)
|
|
21195
21982
|
});
|
|
21196
21983
|
const usage = new PlannerUsageLedger();
|
|
21197
21984
|
const events = new SessionEventRelay({
|
|
@@ -22739,12 +23526,32 @@ function buildRunnerInvocation(opts) {
|
|
|
22739
23526
|
mode: opts.mode ?? "build",
|
|
22740
23527
|
headless: opts.headless,
|
|
22741
23528
|
interactive: opts.interactive,
|
|
22742
|
-
cwd: opts.cwd
|
|
23529
|
+
cwd: opts.cwd,
|
|
23530
|
+
runnerVersion: opts.runnerVersion
|
|
22743
23531
|
});
|
|
22744
23532
|
}
|
|
22745
23533
|
|
|
22746
23534
|
// src/services/HeadlessRunner.ts
|
|
23535
|
+
var import_child_process9 = require("child_process");
|
|
23536
|
+
|
|
23537
|
+
// src/services/runnerVersion.ts
|
|
22747
23538
|
var import_child_process8 = require("child_process");
|
|
23539
|
+
var VERSION_TIMEOUT_MS = 5e3;
|
|
23540
|
+
var cache = /* @__PURE__ */ new Map();
|
|
23541
|
+
var probeRunnerVersion = (command, PATH) => {
|
|
23542
|
+
const key = `${command}\0${PATH}`;
|
|
23543
|
+
let pending = cache.get(key);
|
|
23544
|
+
if (!pending) {
|
|
23545
|
+
pending = new Promise((resolve7) => {
|
|
23546
|
+
(0, import_child_process8.execFile)(command, ["--version"], { env: { ...process.env, PATH }, timeout: VERSION_TIMEOUT_MS }, (err, stdout) => {
|
|
23547
|
+
const line = String(stdout ?? "").trim();
|
|
23548
|
+
resolve7(err || !line ? void 0 : line);
|
|
23549
|
+
});
|
|
23550
|
+
});
|
|
23551
|
+
cache.set(key, pending);
|
|
23552
|
+
}
|
|
23553
|
+
return pending;
|
|
23554
|
+
};
|
|
22748
23555
|
|
|
22749
23556
|
// src/services/AbstractRunner.ts
|
|
22750
23557
|
var import_events = require("events");
|
|
@@ -22799,7 +23606,7 @@ var AbstractRunner = class {
|
|
|
22799
23606
|
function defaultHasScriptCmd() {
|
|
22800
23607
|
if (process.platform === "win32") return false;
|
|
22801
23608
|
try {
|
|
22802
|
-
(0,
|
|
23609
|
+
(0, import_child_process9.execSync)("which script", { stdio: "ignore" });
|
|
22803
23610
|
return true;
|
|
22804
23611
|
} catch {
|
|
22805
23612
|
return false;
|
|
@@ -22866,6 +23673,10 @@ Process error: ${err.message}
|
|
|
22866
23673
|
this.process.stdin.write(text2);
|
|
22867
23674
|
}
|
|
22868
23675
|
}
|
|
23676
|
+
/** Closes the runner's stdin: what it has read so far is all it will get. */
|
|
23677
|
+
endInput() {
|
|
23678
|
+
this.process?.stdin?.end();
|
|
23679
|
+
}
|
|
22869
23680
|
/** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
|
|
22870
23681
|
writeControl(text2) {
|
|
22871
23682
|
if (this.controlStream && !this.controlStream.destroyed) {
|
|
@@ -22878,6 +23689,7 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
22878
23689
|
hasScriptCmd;
|
|
22879
23690
|
resolvePath;
|
|
22880
23691
|
launchDeps;
|
|
23692
|
+
runnerVersion;
|
|
22881
23693
|
spawnCount = 0;
|
|
22882
23694
|
/**
|
|
22883
23695
|
* Session shape: a piped subprocess is not a terminal, so runners get their
|
|
@@ -22889,10 +23701,11 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
22889
23701
|
defaultInteractive = false;
|
|
22890
23702
|
constructor(deps = {}) {
|
|
22891
23703
|
super();
|
|
22892
|
-
this.spawnImpl = deps.spawnImpl ??
|
|
23704
|
+
this.spawnImpl = deps.spawnImpl ?? import_child_process9.spawn;
|
|
22893
23705
|
this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
|
|
22894
23706
|
this.resolvePath = deps.resolvePath ?? augmentedPath;
|
|
22895
23707
|
this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
|
|
23708
|
+
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
22896
23709
|
}
|
|
22897
23710
|
/**
|
|
22898
23711
|
* Unique per spawn, not per task: a retry reuses its task id and ids often
|
|
@@ -22910,6 +23723,9 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
22910
23723
|
/** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
|
|
22911
23724
|
async prepareLaunch(opts, ptyOptions) {
|
|
22912
23725
|
const interactive = this.defaultInteractive;
|
|
23726
|
+
const resolvedPath = await this.resolvePath();
|
|
23727
|
+
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
23728
|
+
const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, resolvedPath) : void 0;
|
|
22913
23729
|
const invocation = buildRunnerInvocation({
|
|
22914
23730
|
runner: opts.runner,
|
|
22915
23731
|
prompt: opts.prompt,
|
|
@@ -22920,18 +23736,18 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
22920
23736
|
headless: opts.headless ?? true,
|
|
22921
23737
|
interactive,
|
|
22922
23738
|
cwd: opts.cwd,
|
|
23739
|
+
runnerVersion,
|
|
22923
23740
|
registry: opts.registry
|
|
22924
23741
|
});
|
|
22925
|
-
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
22926
23742
|
const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
|
|
22927
|
-
const
|
|
22928
|
-
const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, ptyOptions) : { command: invocation.command, args: invocation.args };
|
|
23743
|
+
const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, { platform: this.launchDeps.platform, ...ptyOptions }) : { command: invocation.command, args: invocation.args };
|
|
22929
23744
|
return {
|
|
22930
23745
|
launch: await planDirectLaunch(command, args, this.launchDeps),
|
|
22931
23746
|
resolvedPath,
|
|
22932
23747
|
env: { ...opts.env, ...invocation.env },
|
|
22933
23748
|
pty,
|
|
22934
|
-
submitPromptKey: invocation.submitPromptKey
|
|
23749
|
+
submitPromptKey: invocation.submitPromptKey,
|
|
23750
|
+
closeStdin: !pty && !interactive && invocation.promptInArgs
|
|
22935
23751
|
};
|
|
22936
23752
|
}
|
|
22937
23753
|
async spawn(opts) {
|
|
@@ -22942,20 +23758,21 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
22942
23758
|
console.error(`[headless] Starting ${opts.runner} [${modeStr}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}${prepared.pty ? " (PTY)" : ""}`);
|
|
22943
23759
|
session.start(prepared.launch, opts.cwd, prepared.resolvedPath, prepared.env);
|
|
22944
23760
|
if (prepared.submitPromptKey) session.write("\r");
|
|
23761
|
+
if (prepared.closeStdin) session.endInput();
|
|
22945
23762
|
this.registerSession(id, session);
|
|
22946
23763
|
return session;
|
|
22947
23764
|
}
|
|
22948
23765
|
};
|
|
22949
23766
|
|
|
22950
23767
|
// src/services/TmuxRunner.ts
|
|
22951
|
-
var
|
|
23768
|
+
var import_child_process11 = require("child_process");
|
|
22952
23769
|
var import_util5 = require("util");
|
|
22953
23770
|
var import_os = require("os");
|
|
22954
23771
|
var import_path2 = require("path");
|
|
22955
23772
|
var import_fs3 = require("fs");
|
|
22956
23773
|
|
|
22957
23774
|
// src/utils/tmux.ts
|
|
22958
|
-
var
|
|
23775
|
+
var import_child_process10 = require("child_process");
|
|
22959
23776
|
function tmuxSessionName(port) {
|
|
22960
23777
|
return `ordewell-${port}`;
|
|
22961
23778
|
}
|
|
@@ -22968,7 +23785,7 @@ function tmuxWindowName(taskId, planSessionId) {
|
|
|
22968
23785
|
return `t-${scope}${slug(taskId).slice(0, 16)}`;
|
|
22969
23786
|
}
|
|
22970
23787
|
var defaultProbe = () => {
|
|
22971
|
-
(0,
|
|
23788
|
+
(0, import_child_process10.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
|
|
22972
23789
|
};
|
|
22973
23790
|
function hasTmux(probe = defaultProbe) {
|
|
22974
23791
|
try {
|
|
@@ -22980,7 +23797,7 @@ function hasTmux(probe = defaultProbe) {
|
|
|
22980
23797
|
}
|
|
22981
23798
|
var defaultHasBin = (bin) => {
|
|
22982
23799
|
try {
|
|
22983
|
-
(0,
|
|
23800
|
+
(0, import_child_process10.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
|
|
22984
23801
|
return true;
|
|
22985
23802
|
} catch {
|
|
22986
23803
|
return false;
|
|
@@ -23005,7 +23822,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
|
|
|
23005
23822
|
var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
|
|
23006
23823
|
var LIVENESS_INTERVAL_MS = 5e3;
|
|
23007
23824
|
var LIVENESS_MISSES = 2;
|
|
23008
|
-
var execFileAsync2 = (0, import_util5.promisify)(
|
|
23825
|
+
var execFileAsync2 = (0, import_util5.promisify)(import_child_process11.execFile);
|
|
23009
23826
|
var defaultExecFile3 = async (command, args) => {
|
|
23010
23827
|
const { stdout, stderr } = await execFileAsync2(command, args);
|
|
23011
23828
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
@@ -23155,6 +23972,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23155
23972
|
pollIntervalMs;
|
|
23156
23973
|
logDir;
|
|
23157
23974
|
clipboardCommand;
|
|
23975
|
+
runnerVersion;
|
|
23158
23976
|
/** Retries get a freshly named window so a failed attempt's output stays inspectable. */
|
|
23159
23977
|
attempts = /* @__PURE__ */ new Map();
|
|
23160
23978
|
ready = null;
|
|
@@ -23167,6 +23985,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23167
23985
|
this.pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
23168
23986
|
this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
|
|
23169
23987
|
this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
|
|
23988
|
+
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
23170
23989
|
}
|
|
23171
23990
|
/**
|
|
23172
23991
|
* Reaps a session orphaned by a crashed prior daemon on the same port, then
|
|
@@ -23250,6 +24069,8 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23250
24069
|
}
|
|
23251
24070
|
}
|
|
23252
24071
|
async spawn(opts) {
|
|
24072
|
+
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
24073
|
+
const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, await this.resolvePath()) : void 0;
|
|
23253
24074
|
const invocation = buildRunnerInvocation({
|
|
23254
24075
|
runner: opts.runner,
|
|
23255
24076
|
prompt: opts.prompt,
|
|
@@ -23265,6 +24086,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23265
24086
|
headless: opts.headless ?? true,
|
|
23266
24087
|
interactive: true,
|
|
23267
24088
|
cwd: opts.cwd,
|
|
24089
|
+
runnerVersion,
|
|
23268
24090
|
registry: opts.registry
|
|
23269
24091
|
});
|
|
23270
24092
|
await this.ensureSession();
|
|
@@ -23294,7 +24116,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
23294
24116
|
|
|
23295
24117
|
// src/services/StructuredRunner.ts
|
|
23296
24118
|
var import_events2 = require("events");
|
|
23297
|
-
var
|
|
24119
|
+
var import_child_process12 = require("child_process");
|
|
23298
24120
|
var DEFAULT_INTERRUPT_GRACE_MS = 5e3;
|
|
23299
24121
|
var TOOL_ARG_MAX_CHARS = 120;
|
|
23300
24122
|
var TURN_OPENING_EVENTS = /* @__PURE__ */ new Set(["assistant_text", "assistant_text_delta", "thinking", "thinking_delta", "tool_call"]);
|
|
@@ -23668,7 +24490,7 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
23668
24490
|
const session = new StructuredSession(id, opts.taskId, {
|
|
23669
24491
|
runner: opts.runner,
|
|
23670
24492
|
deps: {
|
|
23671
|
-
spawn:
|
|
24493
|
+
spawn: import_child_process12.spawn,
|
|
23672
24494
|
fetch: globalThis.fetch,
|
|
23673
24495
|
...this.processDeps,
|
|
23674
24496
|
// Already resolved by the caller (ADR-0016); resolving it again here
|
|
@@ -23763,7 +24585,7 @@ function tokensMatch(presented, expected) {
|
|
|
23763
24585
|
var fs13 = __toESM(require("fs"));
|
|
23764
24586
|
var path24 = __toESM(require("path"));
|
|
23765
24587
|
var os6 = __toESM(require("os"));
|
|
23766
|
-
var
|
|
24588
|
+
var import_child_process13 = require("child_process");
|
|
23767
24589
|
var import_crypto6 = require("crypto");
|
|
23768
24590
|
|
|
23769
24591
|
// src/plugins/FsPluginStore.ts
|
|
@@ -24181,6 +25003,37 @@ var OPENCODE_MANIFEST = {
|
|
|
24181
25003
|
env: {
|
|
24182
25004
|
OPENCODE_CONFIG_CONTENT: "{{if interactiveVariant}}{{opencodeVariantConfig}}{{/if}}"
|
|
24183
25005
|
},
|
|
25006
|
+
// OpenCode 2.x changed both shapes. Its TUI takes no `--model`/`--agent`,
|
|
25007
|
+
// so they ride on OPENCODE_CONFIG_CONTENT instead, and its `run` has no
|
|
25008
|
+
// `--variant` — the variant is a `#suffix` on the model id. Both run
|
|
25009
|
+
// `--standalone`: without it the TUI and `run` attach to the user's
|
|
25010
|
+
// background service, which neither reads this process's config nor
|
|
25011
|
+
// starts in the task's directory and environment.
|
|
25012
|
+
versioned: [{
|
|
25013
|
+
minMajor: 2,
|
|
25014
|
+
argsTemplate: [
|
|
25015
|
+
"{{if headlessSession}}",
|
|
25016
|
+
"run",
|
|
25017
|
+
"--agent",
|
|
25018
|
+
"{{mode}}",
|
|
25019
|
+
"{{/if}}",
|
|
25020
|
+
"{{if headlessModel}}",
|
|
25021
|
+
"--model",
|
|
25022
|
+
"{{modelWithVariant}}",
|
|
25023
|
+
"{{/if}}",
|
|
25024
|
+
"--standalone",
|
|
25025
|
+
"{{if buildMode}}",
|
|
25026
|
+
"--auto",
|
|
25027
|
+
"{{/if}}",
|
|
25028
|
+
"{{if interactive}}",
|
|
25029
|
+
"--prompt",
|
|
25030
|
+
"{{/if}}",
|
|
25031
|
+
"{{prompt}}"
|
|
25032
|
+
],
|
|
25033
|
+
env: {
|
|
25034
|
+
OPENCODE_CONFIG_CONTENT: "{{if interactive}}{{opencodeSessionConfig}}{{/if}}"
|
|
25035
|
+
}
|
|
25036
|
+
}],
|
|
24184
25037
|
promptInArgs: true,
|
|
24185
25038
|
requiresTty: true,
|
|
24186
25039
|
// `--prompt` only pre-fills the TUI's composer; it does not submit it.
|
|
@@ -24299,7 +25152,7 @@ function isReservedRunnerName(name) {
|
|
|
24299
25152
|
return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
|
|
24300
25153
|
}
|
|
24301
25154
|
function gitClone(url, destDir) {
|
|
24302
|
-
(0,
|
|
25155
|
+
(0, import_child_process13.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
|
|
24303
25156
|
stdio: "pipe",
|
|
24304
25157
|
timeout: 3e4,
|
|
24305
25158
|
// A repository that demands credentials should fail, not hang on a prompt.
|
|
@@ -24780,6 +25633,7 @@ var PlannerModelMemory = class {
|
|
|
24780
25633
|
FsPluginStore,
|
|
24781
25634
|
GIT_READONLY_SUBCOMMANDS,
|
|
24782
25635
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
25636
|
+
GREP_PCRE_PROBE_ARGS,
|
|
24783
25637
|
GeminiService,
|
|
24784
25638
|
HeadlessRunner,
|
|
24785
25639
|
HeadlessSession,
|
|
@@ -24919,6 +25773,7 @@ var PlannerModelMemory = class {
|
|
|
24919
25773
|
dependencyCandidates,
|
|
24920
25774
|
dependentsOf,
|
|
24921
25775
|
describeMergeResult,
|
|
25776
|
+
digestTaskLog,
|
|
24922
25777
|
discoverGeminiModels,
|
|
24923
25778
|
drainNext,
|
|
24924
25779
|
effectiveAllowlist,
|
|
@@ -24951,6 +25806,7 @@ var PlannerModelMemory = class {
|
|
|
24951
25806
|
getStateDir,
|
|
24952
25807
|
globalDataDir,
|
|
24953
25808
|
grantScopeFor,
|
|
25809
|
+
grepRegexFromProbe,
|
|
24954
25810
|
hasHiddenDetail,
|
|
24955
25811
|
hasTmux,
|
|
24956
25812
|
holdPrompt,
|