@ordewell/core 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ITerminalRunner-nupxhtPb.d.ts → ITerminalRunner-Bd-vAJnw.d.ts} +24 -2
- package/dist/{ITerminalRunner-DXlJop_f.d.mts → ITerminalRunner-ByeoLF57.d.mts} +24 -2
- package/dist/{ModeResolver-D1-2xJsE.d.mts → ModeResolver-DVJ7HV3k.d.mts} +1 -1
- package/dist/{ModeResolver-BBVktyiP.d.ts → ModeResolver-Dkig8ghQ.d.ts} +1 -1
- package/dist/{Task-CfTjRQeS.d.mts → Task-BxQkPlXO.d.mts} +88 -10
- package/dist/{Task-CfTjRQeS.d.ts → Task-BxQkPlXO.d.ts} +88 -10
- package/dist/{chunk-HQICT7HQ.mjs → chunk-T2S5O36I.mjs} +17 -4
- package/dist/chunk-T2S5O36I.mjs.map +1 -0
- package/dist/index.d.mts +151 -57
- package/dist/index.d.ts +151 -57
- package/dist/index.js +1179 -500
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1162 -498
- 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-Bv3g2Tu_.d.ts → parsing-CF_grC29.d.ts} +2 -2
- package/dist/{parsing-BJECTMOD.d.mts → parsing-DRp4dPC0.d.mts} +2 -2
- package/dist/parsing.d.mts +3 -3
- package/dist/parsing.d.ts +3 -3
- package/dist/{plan-utils-Cy-RsBSV.d.ts → plan-utils-CkNbqAmS.d.ts} +36 -3
- package/dist/{plan-utils-B5oIZkuI.d.mts → plan-utils-CtB3_Ovf.d.mts} +36 -3
- package/dist/plan-utils.d.mts +3 -3
- package/dist/plan-utils.d.ts +3 -3
- package/dist/plan-utils.js +7 -0
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +4 -0
- package/dist/testing.d.mts +20 -8
- package/dist/testing.d.ts +20 -8
- package/dist/testing.js +52 -4
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +47 -4
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-HQICT7HQ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
} from "./chunk-HPNUFBIT.mjs";
|
|
71
71
|
import {
|
|
72
72
|
SELF_REPO,
|
|
73
|
+
capConflictFiles,
|
|
73
74
|
handoffOf,
|
|
74
75
|
integrationBranchFor,
|
|
75
76
|
integrationBranchNameOf,
|
|
@@ -78,7 +79,7 @@ import {
|
|
|
78
79
|
migratePlanStateIsolation,
|
|
79
80
|
repoRootOf,
|
|
80
81
|
taskIsolationOf
|
|
81
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-T2S5O36I.mjs";
|
|
82
83
|
|
|
83
84
|
// src/interfaces/IFileSystem.ts
|
|
84
85
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -2636,33 +2637,33 @@ function grepOptions(args) {
|
|
|
2636
2637
|
};
|
|
2637
2638
|
}
|
|
2638
2639
|
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
2639
|
-
async function executeTool(name, args,
|
|
2640
|
-
const outcome = await runTool(name, args,
|
|
2640
|
+
async function executeTool(name, args, fs15, fetcher, signal) {
|
|
2641
|
+
const outcome = await runTool(name, args, fs15, fetcher, signal);
|
|
2641
2642
|
return { ...outcome, output: redactSecrets(outcome.output) };
|
|
2642
2643
|
}
|
|
2643
|
-
async function runTool(name, args,
|
|
2644
|
+
async function runTool(name, args, fs15, fetcher, signal) {
|
|
2644
2645
|
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
2645
2646
|
switch (name) {
|
|
2646
2647
|
case "read_file": {
|
|
2647
2648
|
const offset = num(args.offset);
|
|
2648
2649
|
const limit = num(args.limit);
|
|
2649
2650
|
const maxBytes = num(args.maxBytes);
|
|
2650
|
-
return
|
|
2651
|
+
return fs15.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
2651
2652
|
}
|
|
2652
2653
|
case "read_files": {
|
|
2653
2654
|
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
2654
|
-
return
|
|
2655
|
+
return fs15.readFiles(paths);
|
|
2655
2656
|
}
|
|
2656
2657
|
case "glob":
|
|
2657
|
-
return
|
|
2658
|
+
return fs15.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
2658
2659
|
case "grep":
|
|
2659
|
-
return
|
|
2660
|
+
return fs15.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
2660
2661
|
case "find_symbol":
|
|
2661
|
-
return
|
|
2662
|
+
return fs15.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
2662
2663
|
case "list_dir":
|
|
2663
|
-
return
|
|
2664
|
+
return fs15.listDir(String(args.path ?? ""), num(args.depth));
|
|
2664
2665
|
case "bash":
|
|
2665
|
-
return
|
|
2666
|
+
return fs15.bash(String(args.command ?? ""), signal);
|
|
2666
2667
|
case "fetch": {
|
|
2667
2668
|
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
2668
2669
|
const url = String(args.url ?? "");
|
|
@@ -2770,14 +2771,14 @@ var PendingApprovals = class {
|
|
|
2770
2771
|
ask(request) {
|
|
2771
2772
|
const id = `ap-${Date.now()}-${counter++}`;
|
|
2772
2773
|
const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2773
|
-
return new Promise((
|
|
2774
|
+
return new Promise((resolve7) => {
|
|
2774
2775
|
const settle = (granted) => {
|
|
2775
2776
|
const entry = this.entries.get(id);
|
|
2776
2777
|
if (!entry) return;
|
|
2777
2778
|
clearTimeout(entry.timer);
|
|
2778
2779
|
this.entries.delete(id);
|
|
2779
2780
|
this.opts.onSettled?.(id, granted);
|
|
2780
|
-
|
|
2781
|
+
resolve7(granted);
|
|
2781
2782
|
};
|
|
2782
2783
|
const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2783
2784
|
timer.unref?.();
|
|
@@ -2875,9 +2876,9 @@ function applyHeadLimit(stdout, headLimit) {
|
|
|
2875
2876
|
}
|
|
2876
2877
|
function formatSearchOutput(capped, root, opts) {
|
|
2877
2878
|
if (capped.rows.length === 0) return opts.emptyMessage;
|
|
2878
|
-
const
|
|
2879
|
-
const prefix = root ===
|
|
2880
|
-
const body = prefix ===
|
|
2879
|
+
const sep2 = opts.sep ?? path3.sep;
|
|
2880
|
+
const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
|
|
2881
|
+
const body = prefix === sep2 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
|
|
2881
2882
|
if (!capped.truncated) return body;
|
|
2882
2883
|
const hint = opts.hint ? ` ${opts.hint}` : "";
|
|
2883
2884
|
return `${body}
|
|
@@ -3468,6 +3469,15 @@ function configuredProviders(config) {
|
|
|
3468
3469
|
return out;
|
|
3469
3470
|
}
|
|
3470
3471
|
|
|
3472
|
+
// src/utils/maxParallel.ts
|
|
3473
|
+
var DEFAULT_MAX_PARALLEL = 3;
|
|
3474
|
+
function parseMaxParallel(value) {
|
|
3475
|
+
const text = String(value ?? "").trim();
|
|
3476
|
+
const n = typeof value === "number" ? value : Number(text);
|
|
3477
|
+
if (text === "" || !Number.isInteger(n) || n < 1) return null;
|
|
3478
|
+
return n;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3471
3481
|
// src/interfaces/BaseConfig.ts
|
|
3472
3482
|
function normalizeGeminiModel(id) {
|
|
3473
3483
|
return id.replace(/^gemini:/, "").replace(/^google\//, "");
|
|
@@ -3475,6 +3485,10 @@ function normalizeGeminiModel(id) {
|
|
|
3475
3485
|
function commaList(raw) {
|
|
3476
3486
|
return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3477
3487
|
}
|
|
3488
|
+
function nonNegativeInt(raw, fallback) {
|
|
3489
|
+
const trimmed = (raw ?? "").trim();
|
|
3490
|
+
return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
|
|
3491
|
+
}
|
|
3478
3492
|
var BaseConfig = class {
|
|
3479
3493
|
// --- Legacy named getters (backward compat) ---
|
|
3480
3494
|
get openAiBaseUrl() {
|
|
@@ -3668,7 +3682,7 @@ var BaseConfig = class {
|
|
|
3668
3682
|
return "";
|
|
3669
3683
|
}
|
|
3670
3684
|
get maxParallelSessions() {
|
|
3671
|
-
return
|
|
3685
|
+
return parseMaxParallel(process.env.ORDEWELL_MAX_PARALLEL) ?? DEFAULT_MAX_PARALLEL;
|
|
3672
3686
|
}
|
|
3673
3687
|
get researchEnabled() {
|
|
3674
3688
|
return process.env.ORDEWELL_RESEARCH_ENABLED !== "false";
|
|
@@ -3697,6 +3711,9 @@ var BaseConfig = class {
|
|
|
3697
3711
|
get worktreeLinks() {
|
|
3698
3712
|
return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
|
|
3699
3713
|
}
|
|
3714
|
+
get conflictRepairAttempts() {
|
|
3715
|
+
return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
|
|
3716
|
+
}
|
|
3700
3717
|
get approvalMode() {
|
|
3701
3718
|
const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
|
|
3702
3719
|
return raw === "allow" || raw === "deny" ? raw : "ask";
|
|
@@ -3756,13 +3773,17 @@ function landedNotice(landed) {
|
|
|
3756
3773
|
if (landed.length === 0) return "Nothing was merged.";
|
|
3757
3774
|
return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
|
|
3758
3775
|
}
|
|
3759
|
-
function
|
|
3776
|
+
function repairedNotice(repaired) {
|
|
3777
|
+
const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
|
|
3778
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
3779
|
+
return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
3780
|
+
}
|
|
3781
|
+
function describeMergeResult(result, branch, group, repaired = []) {
|
|
3760
3782
|
switch (result.outcome) {
|
|
3761
|
-
case "merged":
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
};
|
|
3783
|
+
case "merged": {
|
|
3784
|
+
const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
|
|
3785
|
+
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
3786
|
+
}
|
|
3766
3787
|
case "blocked":
|
|
3767
3788
|
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
|
|
3768
3789
|
case "conflict":
|
|
@@ -3783,7 +3804,7 @@ import { execFile, exec as exec2 } from "child_process";
|
|
|
3783
3804
|
import { promisify as promisify2 } from "util";
|
|
3784
3805
|
import { randomBytes } from "crypto";
|
|
3785
3806
|
import * as fs4 from "fs";
|
|
3786
|
-
import * as
|
|
3807
|
+
import * as path8 from "path";
|
|
3787
3808
|
|
|
3788
3809
|
// src/utils/shellPath.ts
|
|
3789
3810
|
import { exec } from "child_process";
|
|
@@ -3812,28 +3833,28 @@ function posixBinDirs() {
|
|
|
3812
3833
|
];
|
|
3813
3834
|
}
|
|
3814
3835
|
function windowsBinDirs(env, home) {
|
|
3815
|
-
const
|
|
3816
|
-
const under = (base, ...parts) => base ? [
|
|
3836
|
+
const join23 = (...parts) => path4.win32.join(...parts);
|
|
3837
|
+
const under = (base, ...parts) => base ? [join23(base, ...parts)] : [];
|
|
3817
3838
|
const appData = env.APPDATA;
|
|
3818
3839
|
const localAppData = env.LOCALAPPDATA;
|
|
3819
3840
|
const programData = env.ProgramData;
|
|
3820
3841
|
return [
|
|
3821
3842
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3843
|
+
join23(home, ".local", "bin"),
|
|
3844
|
+
join23(home, ".opencode", "bin"),
|
|
3845
|
+
join23(home, ".claude", "bin"),
|
|
3846
|
+
join23(home, ".codex", "bin"),
|
|
3826
3847
|
...under(localAppData, "Programs", "claude"),
|
|
3827
3848
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
3828
3849
|
// literal spelling is kept as a fallback for a host that does not export
|
|
3829
3850
|
// APPDATA (a service account, a stripped CI container).
|
|
3830
3851
|
...under(appData, "npm"),
|
|
3831
|
-
|
|
3852
|
+
join23(home, "AppData", "Roaming", "npm"),
|
|
3832
3853
|
...under(localAppData, "pnpm"),
|
|
3833
3854
|
...under(localAppData, "Yarn", "bin"),
|
|
3834
|
-
|
|
3855
|
+
join23(home, ".bun", "bin"),
|
|
3835
3856
|
// Windows package managers.
|
|
3836
|
-
|
|
3857
|
+
join23(home, "scoop", "shims"),
|
|
3837
3858
|
...under(env.SCOOP, "shims"),
|
|
3838
3859
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
3839
3860
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -3843,7 +3864,7 @@ function windowsBinDirs(env, home) {
|
|
|
3843
3864
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
3844
3865
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
3845
3866
|
...under(localAppData, "Volta", "bin"),
|
|
3846
|
-
|
|
3867
|
+
join23(home, ".volta", "bin")
|
|
3847
3868
|
];
|
|
3848
3869
|
}
|
|
3849
3870
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -3947,6 +3968,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
3947
3968
|
|
|
3948
3969
|
// src/services/worktreeLink.ts
|
|
3949
3970
|
import * as fs3 from "fs";
|
|
3971
|
+
import * as path7 from "path";
|
|
3950
3972
|
function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
3951
3973
|
const isDir = fs3.statSync(source).isDirectory();
|
|
3952
3974
|
if (platform !== "win32") {
|
|
@@ -3965,6 +3987,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
|
3965
3987
|
return "copy";
|
|
3966
3988
|
}
|
|
3967
3989
|
}
|
|
3990
|
+
function mirrorDir(source, target, opts) {
|
|
3991
|
+
const copied = [];
|
|
3992
|
+
const links = [];
|
|
3993
|
+
const mirror = (from, to, prefix, top) => {
|
|
3994
|
+
fs3.mkdirSync(to, { recursive: true });
|
|
3995
|
+
for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
|
|
3996
|
+
const name = path7.join(prefix, entry.name);
|
|
3997
|
+
const at = { from: path7.join(from, entry.name), to: path7.join(to, entry.name), name };
|
|
3998
|
+
if (entry.isSymbolicLink()) links.push(at);
|
|
3999
|
+
else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
|
|
4000
|
+
else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
mirror(source, target, "", true);
|
|
4004
|
+
let pending = links;
|
|
4005
|
+
for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
|
|
4006
|
+
before = pending.length;
|
|
4007
|
+
pending = pending.filter((link) => {
|
|
4008
|
+
const kind = relink(link.from, link.to, opts);
|
|
4009
|
+
if (kind === "copy") copied.push(link.name);
|
|
4010
|
+
return kind === null;
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
return copied;
|
|
4014
|
+
}
|
|
4015
|
+
function relink(from, to, opts) {
|
|
4016
|
+
const text = fs3.readlinkSync(from);
|
|
4017
|
+
const pointee = path7.isAbsolute(text) ? rebased(text, opts) : text;
|
|
4018
|
+
if (opts.platform !== "win32") {
|
|
4019
|
+
if (!fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) && fs3.existsSync(path7.resolve(path7.dirname(from), pointee))) {
|
|
4020
|
+
fs3.symlinkSync(rebased(path7.resolve(path7.dirname(from), pointee), opts), to);
|
|
4021
|
+
return "symlink";
|
|
4022
|
+
}
|
|
4023
|
+
fs3.symlinkSync(pointee, to);
|
|
4024
|
+
return "symlink";
|
|
4025
|
+
}
|
|
4026
|
+
const resolved = fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) ? path7.resolve(path7.dirname(to), pointee) : rebased(path7.resolve(path7.dirname(from), pointee), opts);
|
|
4027
|
+
try {
|
|
4028
|
+
fs3.statSync(resolved);
|
|
4029
|
+
} catch {
|
|
4030
|
+
return null;
|
|
4031
|
+
}
|
|
4032
|
+
return linkPath(resolved, to, opts.platform, opts.hardLink);
|
|
4033
|
+
}
|
|
4034
|
+
function rebased(pointee, opts) {
|
|
4035
|
+
const rel = path7.relative(opts.from, pointee);
|
|
4036
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
4037
|
+
return inside && !rel.split(/[\\/]/).includes("node_modules") ? path7.join(opts.to, rel) : pointee;
|
|
4038
|
+
}
|
|
3968
4039
|
|
|
3969
4040
|
// src/services/GitWorktreeIsolation.ts
|
|
3970
4041
|
var execFileAsync = promisify2(execFile);
|
|
@@ -3979,15 +4050,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
|
|
|
3979
4050
|
var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
|
|
3980
4051
|
var isEnvFile = (name) => name.startsWith(".env");
|
|
3981
4052
|
var INTEGRATION_DIR = "integration";
|
|
4053
|
+
var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
|
|
4054
|
+
var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
|
|
3982
4055
|
var NESTED_REPO_DEPTH = 2;
|
|
3983
4056
|
var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
|
|
3984
4057
|
var GITLINK_MODE = "160000";
|
|
3985
4058
|
function isInside(parent, child) {
|
|
3986
|
-
const rel =
|
|
3987
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4059
|
+
const rel = path8.relative(parent, child);
|
|
4060
|
+
return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
3988
4061
|
}
|
|
3989
4062
|
function hasGitEntry(dir) {
|
|
3990
|
-
return fs4.existsSync(
|
|
4063
|
+
return fs4.existsSync(path8.join(dir, ".git"));
|
|
3991
4064
|
}
|
|
3992
4065
|
function firstLine(text) {
|
|
3993
4066
|
return text.split(/\r?\n/, 1)[0].trim();
|
|
@@ -4003,12 +4076,20 @@ function statusPaths(porcelain) {
|
|
|
4003
4076
|
}
|
|
4004
4077
|
return paths;
|
|
4005
4078
|
}
|
|
4079
|
+
function leftoverMarkerFiles(check) {
|
|
4080
|
+
const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
|
|
4081
|
+
return [...new Set(files)];
|
|
4082
|
+
}
|
|
4083
|
+
function settleStatus(record, status) {
|
|
4084
|
+
record.status = status;
|
|
4085
|
+
delete record.repairBase;
|
|
4086
|
+
}
|
|
4006
4087
|
function sameDir(a, b) {
|
|
4007
4088
|
const real = (p) => {
|
|
4008
4089
|
try {
|
|
4009
4090
|
return fs4.realpathSync.native(p);
|
|
4010
4091
|
} catch {
|
|
4011
|
-
return
|
|
4092
|
+
return path8.resolve(p);
|
|
4012
4093
|
}
|
|
4013
4094
|
};
|
|
4014
4095
|
return real(a) === real(b);
|
|
@@ -4042,8 +4123,8 @@ function inactive(reason, repos) {
|
|
|
4042
4123
|
}
|
|
4043
4124
|
function groupPathOf(listed) {
|
|
4044
4125
|
const slashed = listed.trim().replace(/\\/g, "/");
|
|
4045
|
-
if (
|
|
4046
|
-
const rel =
|
|
4126
|
+
if (path8.posix.isAbsolute(slashed) || path8.win32.isAbsolute(slashed)) return null;
|
|
4127
|
+
const rel = path8.posix.normalize(slashed).replace(/\/+$/, "");
|
|
4047
4128
|
return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
|
|
4048
4129
|
}
|
|
4049
4130
|
function matchLinks(root, patterns) {
|
|
@@ -4059,9 +4140,26 @@ function matchLinks(root, patterns) {
|
|
|
4059
4140
|
}
|
|
4060
4141
|
function segmentMatches(root, base, segment) {
|
|
4061
4142
|
const under = (name) => base ? `${base}/${name}` : name;
|
|
4062
|
-
if (!/[*?]/.test(segment)) return lexists(
|
|
4143
|
+
if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
|
|
4063
4144
|
const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
|
|
4064
|
-
return listDir(
|
|
4145
|
+
return listDir(path8.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
|
|
4146
|
+
}
|
|
4147
|
+
function installDirs(root) {
|
|
4148
|
+
const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
|
|
4149
|
+
return ["node_modules", ...packages].filter((dir) => lexists(path8.join(root, dir)));
|
|
4150
|
+
}
|
|
4151
|
+
function workspaceGlobs(root) {
|
|
4152
|
+
let manifest;
|
|
4153
|
+
try {
|
|
4154
|
+
manifest = JSON.parse(fs4.readFileSync(path8.join(root, "package.json"), "utf8"));
|
|
4155
|
+
} catch {
|
|
4156
|
+
return [];
|
|
4157
|
+
}
|
|
4158
|
+
if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
|
|
4159
|
+
const { workspaces } = manifest;
|
|
4160
|
+
const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
|
|
4161
|
+
if (!Array.isArray(listed)) return [];
|
|
4162
|
+
return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
|
|
4065
4163
|
}
|
|
4066
4164
|
var GitWorktreeIsolation = class {
|
|
4067
4165
|
constructor(deps) {
|
|
@@ -4115,7 +4213,7 @@ var GitWorktreeIsolation = class {
|
|
|
4115
4213
|
groupPaths(workspaceRoot) {
|
|
4116
4214
|
const listed = this.deps.config.workspaceRepos;
|
|
4117
4215
|
if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
|
|
4118
|
-
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(
|
|
4216
|
+
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
|
|
4119
4217
|
return [...new Set(paths)].sort();
|
|
4120
4218
|
}
|
|
4121
4219
|
async committedRepos(workspaceRoot, paths) {
|
|
@@ -4131,7 +4229,7 @@ var GitWorktreeIsolation = class {
|
|
|
4131
4229
|
return status.ok ? status.stdout.trim() !== "" : null;
|
|
4132
4230
|
}
|
|
4133
4231
|
reposDirectlyIn(dir) {
|
|
4134
|
-
return this.childDirs(dir).filter((name) => hasGitEntry(
|
|
4232
|
+
return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
|
|
4135
4233
|
}
|
|
4136
4234
|
childDirs(dir) {
|
|
4137
4235
|
try {
|
|
@@ -4148,9 +4246,9 @@ var GitWorktreeIsolation = class {
|
|
|
4148
4246
|
async nestedRepos(workspaceRoot, toplevel) {
|
|
4149
4247
|
const found = [];
|
|
4150
4248
|
const walk = (rel, depth) => {
|
|
4151
|
-
for (const name of this.childDirs(
|
|
4249
|
+
for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
|
|
4152
4250
|
const child = rel ? `${rel}/${name}` : name;
|
|
4153
|
-
if (hasGitEntry(
|
|
4251
|
+
if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
|
|
4154
4252
|
else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
|
|
4155
4253
|
}
|
|
4156
4254
|
};
|
|
@@ -4171,7 +4269,7 @@ var GitWorktreeIsolation = class {
|
|
|
4171
4269
|
const [meta, file] = entry.split(" ");
|
|
4172
4270
|
if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
|
|
4173
4271
|
}
|
|
4174
|
-
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f",
|
|
4272
|
+
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
|
|
4175
4273
|
for (const entry of declared.stdout.split("\0")) {
|
|
4176
4274
|
const value = entry.split("\n")[1];
|
|
4177
4275
|
if (value) paths.add(value.replace(/\/+$/, ""));
|
|
@@ -4227,7 +4325,7 @@ var GitWorktreeIsolation = class {
|
|
|
4227
4325
|
async acceptsWorktree(run, repo) {
|
|
4228
4326
|
const dir = this.integrationDir(run, repo);
|
|
4229
4327
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
4230
|
-
fs4.mkdirSync(
|
|
4328
|
+
fs4.mkdirSync(path8.dirname(dir), { recursive: true });
|
|
4231
4329
|
const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
|
|
4232
4330
|
await this.removeWorktreeDir(run, repo, dir, []);
|
|
4233
4331
|
return added.ok;
|
|
@@ -4242,12 +4340,12 @@ var GitWorktreeIsolation = class {
|
|
|
4242
4340
|
if (isolated.includes(SELF_REPO)) return [];
|
|
4243
4341
|
const shared = [];
|
|
4244
4342
|
const walk = (rel) => {
|
|
4245
|
-
for (const name of listDir(
|
|
4343
|
+
for (const name of listDir(path8.join(workspaceRoot, rel))) {
|
|
4246
4344
|
if (!rel && (name === STATE_DIR || name === ".git")) continue;
|
|
4247
4345
|
const child = rel ? `${rel}/${name}` : name;
|
|
4248
4346
|
if (isolated.includes(child)) continue;
|
|
4249
4347
|
if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
|
|
4250
|
-
else if (resolves(
|
|
4348
|
+
else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
|
|
4251
4349
|
}
|
|
4252
4350
|
};
|
|
4253
4351
|
walk("");
|
|
@@ -4264,17 +4362,17 @@ var GitWorktreeIsolation = class {
|
|
|
4264
4362
|
const copied = [];
|
|
4265
4363
|
try {
|
|
4266
4364
|
for (const repo of run.repos) {
|
|
4267
|
-
const worktree =
|
|
4365
|
+
const worktree = path8.join(dir, repo.path);
|
|
4268
4366
|
const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
|
|
4269
|
-
fs4.mkdirSync(
|
|
4367
|
+
fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
|
|
4270
4368
|
await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
|
|
4271
4369
|
record.repos[repo.path] = { worktree, linked: [] };
|
|
4272
4370
|
}
|
|
4273
4371
|
run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
|
|
4274
4372
|
for (const rel of run.shared) {
|
|
4275
|
-
const target =
|
|
4276
|
-
fs4.mkdirSync(
|
|
4277
|
-
if (linkPath(
|
|
4373
|
+
const target = path8.join(dir, rel);
|
|
4374
|
+
fs4.mkdirSync(path8.dirname(target), { recursive: true });
|
|
4375
|
+
if (linkPath(path8.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
|
|
4278
4376
|
}
|
|
4279
4377
|
for (const repo of run.repos) {
|
|
4280
4378
|
const entry = record.repos[repo.path];
|
|
@@ -4282,7 +4380,7 @@ var GitWorktreeIsolation = class {
|
|
|
4282
4380
|
if (repo.path === SELF_REPO) cwd = inRepo;
|
|
4283
4381
|
const boot = await this.bootstrap(repo, inRepo);
|
|
4284
4382
|
entry.linked = boot.linked;
|
|
4285
|
-
copied.push(...boot.copied.map((name) =>
|
|
4383
|
+
copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
|
|
4286
4384
|
}
|
|
4287
4385
|
} catch (err) {
|
|
4288
4386
|
record.status = "failed";
|
|
@@ -4293,6 +4391,53 @@ var GitWorktreeIsolation = class {
|
|
|
4293
4391
|
return { cwd, branch, copied };
|
|
4294
4392
|
});
|
|
4295
4393
|
}
|
|
4394
|
+
reopen(task, run) {
|
|
4395
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
4396
|
+
const record = run.tasks[task.id];
|
|
4397
|
+
if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
|
|
4398
|
+
let cwd = record.workspace;
|
|
4399
|
+
const repairBase = {};
|
|
4400
|
+
for (const repo of run.repos) {
|
|
4401
|
+
const entry = record.repos[repo.path];
|
|
4402
|
+
if (!entry) continue;
|
|
4403
|
+
if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
|
|
4404
|
+
if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
|
|
4405
|
+
if (entry.changed || repo.path === record.conflictRepo) {
|
|
4406
|
+
repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
|
|
4410
|
+
record.repairs = (record.repairs ?? 0) + 1;
|
|
4411
|
+
record.repairBase = repairBase;
|
|
4412
|
+
record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
|
|
4413
|
+
record.status = "repairing";
|
|
4414
|
+
return { cwd, branch: record.branch, copied: [] };
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
async verifyRepair(task, run) {
|
|
4418
|
+
const record = run.tasks[task.id];
|
|
4419
|
+
if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
|
|
4420
|
+
for (const repo of run.repos) {
|
|
4421
|
+
const entry = record.repos[repo.path];
|
|
4422
|
+
if (!entry) continue;
|
|
4423
|
+
try {
|
|
4424
|
+
await this.commitWorktree(repo, record, entry);
|
|
4425
|
+
} catch {
|
|
4426
|
+
return { ok: false, reason: "failed", repo: repo.path };
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
for (const repo of run.repos) {
|
|
4430
|
+
const base = record.repairBase[repo.path];
|
|
4431
|
+
if (base === void 0) continue;
|
|
4432
|
+
const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
|
|
4433
|
+
if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
|
|
4434
|
+
const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
|
|
4435
|
+
if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
|
|
4436
|
+
const files = leftoverMarkerFiles(check.stdout);
|
|
4437
|
+
if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
|
|
4438
|
+
}
|
|
4439
|
+
return { ok: true };
|
|
4440
|
+
}
|
|
4296
4441
|
/**
|
|
4297
4442
|
* The place in a repo's worktree that matches where the workspace sits in
|
|
4298
4443
|
* the real repo: the worktree itself, or a subdirectory of it when the
|
|
@@ -4300,7 +4445,7 @@ var GitWorktreeIsolation = class {
|
|
|
4300
4445
|
*/
|
|
4301
4446
|
async inWorkspacePlace(repo, worktree) {
|
|
4302
4447
|
const prefix = await this.prefixOf(repo);
|
|
4303
|
-
return prefix ?
|
|
4448
|
+
return prefix ? path8.join(worktree, prefix) : worktree;
|
|
4304
4449
|
}
|
|
4305
4450
|
async prefixOf(repo) {
|
|
4306
4451
|
return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
|
|
@@ -4320,6 +4465,7 @@ var GitWorktreeIsolation = class {
|
|
|
4320
4465
|
if (!record) return;
|
|
4321
4466
|
if (opts.keep) {
|
|
4322
4467
|
if (record.status === "active") record.status = "kept";
|
|
4468
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4323
4469
|
return;
|
|
4324
4470
|
}
|
|
4325
4471
|
await this.removeTask(run, record, { dropRecord: true });
|
|
@@ -4339,6 +4485,7 @@ var GitWorktreeIsolation = class {
|
|
|
4339
4485
|
for (const record of Object.values(run.tasks)) {
|
|
4340
4486
|
if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
|
|
4341
4487
|
else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
|
|
4488
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4342
4489
|
}
|
|
4343
4490
|
await this.removeUnowned(run);
|
|
4344
4491
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
@@ -4433,14 +4580,59 @@ var GitWorktreeIsolation = class {
|
|
|
4433
4580
|
await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
|
|
4434
4581
|
}
|
|
4435
4582
|
await this.removeUnowned(run);
|
|
4436
|
-
if (
|
|
4437
|
-
for (const repo of run.repos)
|
|
4583
|
+
if (opts.integration !== "keep") {
|
|
4584
|
+
for (const repo of run.repos) {
|
|
4585
|
+
if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
|
|
4586
|
+
else await this.deleteIfMerged(repo.root, repo.integrationBranch);
|
|
4587
|
+
}
|
|
4438
4588
|
run.tasks = {};
|
|
4439
4589
|
this.removeIfEmpty(this.runRoot(run));
|
|
4440
4590
|
}
|
|
4441
4591
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4442
4592
|
});
|
|
4443
4593
|
}
|
|
4594
|
+
sweep(run) {
|
|
4595
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
4596
|
+
const failed = [];
|
|
4597
|
+
for (const repo of run.repos) {
|
|
4598
|
+
if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
|
|
4599
|
+
}
|
|
4600
|
+
if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
/** False when git failed anywhere; the other branches are still tried. */
|
|
4604
|
+
async sweepRepo(run, repo) {
|
|
4605
|
+
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4606
|
+
const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
|
|
4607
|
+
const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
|
|
4608
|
+
if (!worktrees.ok || !refs.ok) return false;
|
|
4609
|
+
const checkedOut = /* @__PURE__ */ new Set();
|
|
4610
|
+
const liveRuns = /* @__PURE__ */ new Set();
|
|
4611
|
+
for (const line of worktrees.stdout.split(/\r?\n/)) {
|
|
4612
|
+
if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
|
|
4613
|
+
const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
|
|
4614
|
+
if (runId) liveRuns.add(runId);
|
|
4615
|
+
}
|
|
4616
|
+
let ok = true;
|
|
4617
|
+
for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
4618
|
+
const branch = ref.slice("refs/heads/".length);
|
|
4619
|
+
const runId = RUN_BRANCH.exec(branch)?.[1];
|
|
4620
|
+
if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
|
|
4621
|
+
if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
|
|
4622
|
+
}
|
|
4623
|
+
return ok;
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Delete `branch` only if the repo's checked-out HEAD already contains it —
|
|
4627
|
+
* HEAD, not an upstream: what the user has checked out is what they merged
|
|
4628
|
+
* into. `-d` rather than `-D`, so git also refuses a branch a worktree has
|
|
4629
|
+
* checked out.
|
|
4630
|
+
*/
|
|
4631
|
+
async deleteIfMerged(root, branch) {
|
|
4632
|
+
const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
|
|
4633
|
+
if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
|
|
4634
|
+
return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
|
|
4635
|
+
}
|
|
4444
4636
|
async drain() {
|
|
4445
4637
|
try {
|
|
4446
4638
|
while (this.waiting.length > 0) {
|
|
@@ -4493,21 +4685,24 @@ var GitWorktreeIsolation = class {
|
|
|
4493
4685
|
return this.stopLanding(record, "failed");
|
|
4494
4686
|
}
|
|
4495
4687
|
for (const repo of changed) {
|
|
4496
|
-
const outcome = await this.mergeTask(run, repo, record);
|
|
4688
|
+
const { outcome, files } = await this.mergeTask(run, repo, record);
|
|
4497
4689
|
if (outcome === "merged") continue;
|
|
4498
|
-
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
|
|
4690
|
+
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
|
|
4499
4691
|
}
|
|
4500
4692
|
}
|
|
4501
4693
|
delete run.landing;
|
|
4502
|
-
record
|
|
4694
|
+
settleStatus(record, "merged");
|
|
4503
4695
|
delete record.conflictRepo;
|
|
4696
|
+
delete record.conflictFiles;
|
|
4504
4697
|
await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
|
|
4505
4698
|
return "merged";
|
|
4506
4699
|
}
|
|
4507
|
-
stopLanding(record, outcome, repo) {
|
|
4508
|
-
record
|
|
4700
|
+
stopLanding(record, outcome, repo, files) {
|
|
4701
|
+
settleStatus(record, outcome);
|
|
4509
4702
|
if (repo) record.conflictRepo = repo.path;
|
|
4510
4703
|
else delete record.conflictRepo;
|
|
4704
|
+
if (files && files.length > 0) record.conflictFiles = files;
|
|
4705
|
+
else delete record.conflictFiles;
|
|
4511
4706
|
return outcome;
|
|
4512
4707
|
}
|
|
4513
4708
|
/** Merge the task branch into one repo's integration branch. A merge that does not complete is aborted: it is Ordewell's own. */
|
|
@@ -4516,13 +4711,13 @@ var GitWorktreeIsolation = class {
|
|
|
4516
4711
|
try {
|
|
4517
4712
|
dir = await this.ensureIntegrationWorktree(run, repo);
|
|
4518
4713
|
} catch {
|
|
4519
|
-
return "failed";
|
|
4714
|
+
return { outcome: "failed", files: [] };
|
|
4520
4715
|
}
|
|
4521
4716
|
const merge = await this.tryGit(dir, ["merge", "--no-ff", "--no-edit", "-m", `Merge task ${record.order}: ${firstLine(record.title)}`, record.branch]);
|
|
4522
|
-
if (merge.ok) return "merged";
|
|
4523
|
-
const
|
|
4717
|
+
if (merge.ok) return { outcome: "merged", files: [] };
|
|
4718
|
+
const files = await this.unmergedPaths(dir);
|
|
4524
4719
|
if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
|
|
4525
|
-
return
|
|
4720
|
+
return { outcome: files.length > 0 ? "conflict" : "failed", files };
|
|
4526
4721
|
}
|
|
4527
4722
|
/**
|
|
4528
4723
|
* Return every repo of the landing in flight to its recorded tip, then
|
|
@@ -4590,15 +4785,15 @@ var GitWorktreeIsolation = class {
|
|
|
4590
4785
|
await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
|
|
4591
4786
|
}
|
|
4592
4787
|
integrationDir(run, repo) {
|
|
4593
|
-
return
|
|
4788
|
+
return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
|
|
4594
4789
|
}
|
|
4595
4790
|
async ensureIntegrationWorktree(run, repo) {
|
|
4596
4791
|
return this.admin(run.workspaceRoot, async () => {
|
|
4597
4792
|
const dir = this.integrationDir(run, repo);
|
|
4598
|
-
if (fs4.existsSync(
|
|
4793
|
+
if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
|
|
4599
4794
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4600
4795
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
4601
|
-
fs4.mkdirSync(
|
|
4796
|
+
fs4.mkdirSync(path8.dirname(dir), { recursive: true });
|
|
4602
4797
|
await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
|
|
4603
4798
|
return dir;
|
|
4604
4799
|
});
|
|
@@ -4633,20 +4828,20 @@ var GitWorktreeIsolation = class {
|
|
|
4633
4828
|
return;
|
|
4634
4829
|
}
|
|
4635
4830
|
for (const entry of entries) {
|
|
4636
|
-
const target =
|
|
4831
|
+
const target = path8.join(dir, entry.name);
|
|
4637
4832
|
if (entry.isSymbolicLink()) this.unlinkIfLink(target);
|
|
4638
4833
|
else if (entry.isDirectory()) this.unlinkLinksUnder(target);
|
|
4639
4834
|
}
|
|
4640
4835
|
}
|
|
4641
4836
|
/** Task workspaces and branches under this run that no record accounts for. */
|
|
4642
4837
|
async removeUnowned(run) {
|
|
4643
|
-
const owned = new Set(Object.values(run.tasks).map((r) =>
|
|
4838
|
+
const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
|
|
4644
4839
|
const root = this.runRoot(run);
|
|
4645
4840
|
if (fs4.existsSync(root)) {
|
|
4646
4841
|
for (const entry of fs4.readdirSync(root)) {
|
|
4647
|
-
const dir =
|
|
4648
|
-
if (entry === INTEGRATION_DIR || owned.has(
|
|
4649
|
-
for (const repo of run.repos) await this.removeWorktreeDir(run, repo,
|
|
4842
|
+
const dir = path8.join(root, entry);
|
|
4843
|
+
if (entry === INTEGRATION_DIR || owned.has(path8.resolve(dir))) continue;
|
|
4844
|
+
for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path8.join(dir, repo.path), []);
|
|
4650
4845
|
this.removeTaskWorkspace(run, dir);
|
|
4651
4846
|
}
|
|
4652
4847
|
}
|
|
@@ -4660,14 +4855,18 @@ var GitWorktreeIsolation = class {
|
|
|
4660
4855
|
}
|
|
4661
4856
|
}
|
|
4662
4857
|
async removeWorktreeDir(run, repo, dir, linked) {
|
|
4663
|
-
for (const name of linked
|
|
4858
|
+
for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
|
|
4859
|
+
const target = path8.join(dir, name);
|
|
4860
|
+
this.unlinkIfLink(target);
|
|
4861
|
+
this.unlinkLinksUnder(target);
|
|
4862
|
+
}
|
|
4664
4863
|
for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
|
|
4665
|
-
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(
|
|
4864
|
+
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
|
|
4666
4865
|
}
|
|
4667
4866
|
await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
|
|
4668
4867
|
if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
|
|
4669
4868
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4670
|
-
for (let parent =
|
|
4869
|
+
for (let parent = path8.dirname(dir); isInside(this.runRoot(run), parent); parent = path8.dirname(parent)) this.removeIfEmpty(parent);
|
|
4671
4870
|
}
|
|
4672
4871
|
unlinkIfLink(target) {
|
|
4673
4872
|
try {
|
|
@@ -4682,14 +4881,14 @@ var GitWorktreeIsolation = class {
|
|
|
4682
4881
|
}
|
|
4683
4882
|
}
|
|
4684
4883
|
runRoot(run) {
|
|
4685
|
-
return
|
|
4884
|
+
return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
|
|
4686
4885
|
}
|
|
4687
4886
|
namesFor(run, task) {
|
|
4688
4887
|
const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
|
|
4689
4888
|
let name = `${task.order}-${slug2}`;
|
|
4690
4889
|
const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
|
|
4691
4890
|
if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
|
|
4692
|
-
return { branch: `ordewell/${run.id}/${name}`, dir:
|
|
4891
|
+
return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
|
|
4693
4892
|
}
|
|
4694
4893
|
/**
|
|
4695
4894
|
* Make one repo's worktree runnable: the default artifacts, unless a setup
|
|
@@ -4702,16 +4901,26 @@ var GitWorktreeIsolation = class {
|
|
|
4702
4901
|
const linked = [];
|
|
4703
4902
|
const copied = [];
|
|
4704
4903
|
const link = (name) => {
|
|
4705
|
-
const target =
|
|
4904
|
+
const target = path8.join(cwd, name);
|
|
4706
4905
|
if (lexists(target)) return;
|
|
4707
|
-
fs4.mkdirSync(
|
|
4708
|
-
if (linkPath(
|
|
4906
|
+
fs4.mkdirSync(path8.dirname(target), { recursive: true });
|
|
4907
|
+
if (linkPath(path8.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
|
|
4908
|
+
linked.push(name);
|
|
4909
|
+
};
|
|
4910
|
+
const mirror = (name) => {
|
|
4911
|
+
const target = path8.join(cwd, name);
|
|
4912
|
+
if (lexists(target) || !fs4.existsSync(path8.dirname(target))) return;
|
|
4913
|
+
const source = path8.join(repo.root, name);
|
|
4914
|
+
if (!fs4.lstatSync(source).isDirectory()) return link(name);
|
|
4915
|
+
const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
|
|
4916
|
+
copied.push(...mirrored.map((entry) => path8.join(name, entry)));
|
|
4709
4917
|
linked.push(name);
|
|
4710
4918
|
};
|
|
4711
4919
|
if (!setup) {
|
|
4712
4920
|
for (const name of fs4.readdirSync(repo.root)) {
|
|
4713
|
-
if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
4921
|
+
if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
4714
4922
|
}
|
|
4923
|
+
for (const dir of installDirs(repo.root)) mirror(dir);
|
|
4715
4924
|
}
|
|
4716
4925
|
for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
|
|
4717
4926
|
if (setup) await this.runSetup(setup, repo, cwd);
|
|
@@ -5159,6 +5368,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
5159
5368
|
}
|
|
5160
5369
|
var ISOLATED_PARALLELISM_RULE = "- Each AI task runs in its own git worktree, and its work is merged in afterwards, so tasks that edit the same files can still run in parallel. Never add a dependency just because two tasks touch the same file \u2014 keep dependencies for genuine logical ordering.";
|
|
5161
5370
|
var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
|
|
5371
|
+
var SHARED_APPEND_FILE_RULE = "- Tasks that run in parallel must not each append to the same shared file (changelogs, registries, index lists) \u2014 give that edit to the final task, or to one task the others depend on.";
|
|
5162
5372
|
function repoGroupSection(isolated) {
|
|
5163
5373
|
if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
|
|
5164
5374
|
return [
|
|
@@ -5250,6 +5460,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
5250
5460
|
"DEPENDENCY & PARALLELISM:",
|
|
5251
5461
|
"- Independent slices should have NO dependencies \u2014 they run in parallel.",
|
|
5252
5462
|
"- Only add dependencies when a slice truly depends on artifacts another slice creates.",
|
|
5463
|
+
SHARED_APPEND_FILE_RULE,
|
|
5253
5464
|
...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
|
|
5254
5465
|
...repoGroupSection(variant.isolatedExecution),
|
|
5255
5466
|
"",
|
|
@@ -5351,6 +5562,7 @@ function corePlannerPrompt(isolatedExecution) {
|
|
|
5351
5562
|
isolatedExecution ? "- Independent vertical slices should have NO dependencies between them \u2014 they can run in parallel." : "- Independent vertical slices (no shared files/modules) should have NO dependencies between them \u2014 they can run in parallel.",
|
|
5352
5563
|
"- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
|
|
5353
5564
|
"- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
|
|
5565
|
+
SHARED_APPEND_FILE_RULE,
|
|
5354
5566
|
isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
|
|
5355
5567
|
...repoGroupSection(isolatedExecution),
|
|
5356
5568
|
"",
|
|
@@ -5628,6 +5840,31 @@ ${task.prompt ?? task.description}`;
|
|
|
5628
5840
|
asked
|
|
5629
5841
|
].join("\n");
|
|
5630
5842
|
}
|
|
5843
|
+
function buildConflictRepairPrompt(task, conflict, integrationBranch) {
|
|
5844
|
+
const { branch } = conflict;
|
|
5845
|
+
const files = conflict.conflictFiles ?? [];
|
|
5846
|
+
const asked = `What the task was asked to do:
|
|
5847
|
+
${task.prompt ?? task.description}`;
|
|
5848
|
+
const resolve7 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
|
|
5849
|
+
const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
|
|
5850
|
+
if (repos.length === 0) {
|
|
5851
|
+
return [
|
|
5852
|
+
`Task #${task.order} "${task.title}" passed, but merging its branch \`${branch}\` into \`${integrationBranch}\` conflicted${files.length > 0 ? ` in ${files.join(", ")}` : ""}. None of its work has landed yet.`,
|
|
5853
|
+
`This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve7} Never drop either side wholesale, and never abort the merge or reset it away.`,
|
|
5854
|
+
"Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
|
|
5855
|
+
"",
|
|
5856
|
+
asked
|
|
5857
|
+
].join("\n");
|
|
5858
|
+
}
|
|
5859
|
+
const stoppedIn = conflict.conflictRepo ?? repos[0];
|
|
5860
|
+
return [
|
|
5861
|
+
`Task #${task.order} "${task.title}" passed, but landing its branch \`${branch}\` on \`${integrationBranch}\` conflicted in ${stoppedIn}${files.length > 0 ? ` (${files.join(", ")})` : ""}. A task lands in every repository it changed or in none, so none of its work has landed yet.`,
|
|
5862
|
+
`This is the task's own workspace, with each repository at its usual path on \`${branch}\` and the task's work committed. In each repository the task changed \u2014 ${repos.join(", ")} \u2014 run \`git merge --no-edit ${integrationBranch}\` inside that repository's directory, and ${resolve7} Never drop either side wholesale, and never abort a merge or reset it away.`,
|
|
5863
|
+
"Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
|
|
5864
|
+
"",
|
|
5865
|
+
asked
|
|
5866
|
+
].join("\n");
|
|
5867
|
+
}
|
|
5631
5868
|
|
|
5632
5869
|
// src/services/PlanRepair.ts
|
|
5633
5870
|
async function repairLoop(opts) {
|
|
@@ -5958,8 +6195,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
|
|
|
5958
6195
|
|
|
5959
6196
|
// src/services/ContextCollector.ts
|
|
5960
6197
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
5961
|
-
async function collectResearchContext(
|
|
5962
|
-
const collector = new ContextCollector(
|
|
6198
|
+
async function collectResearchContext(fs15, runners) {
|
|
6199
|
+
const collector = new ContextCollector(fs15);
|
|
5963
6200
|
let contextStr = "";
|
|
5964
6201
|
let firstContext = null;
|
|
5965
6202
|
for (const r of runners) {
|
|
@@ -5986,8 +6223,8 @@ ${firstContext.dirStructure}
|
|
|
5986
6223
|
return contextStr;
|
|
5987
6224
|
}
|
|
5988
6225
|
var ContextCollector = class {
|
|
5989
|
-
constructor(
|
|
5990
|
-
this.fs =
|
|
6226
|
+
constructor(fs15, registry) {
|
|
6227
|
+
this.fs = fs15;
|
|
5991
6228
|
this.registry = registry;
|
|
5992
6229
|
}
|
|
5993
6230
|
fs;
|
|
@@ -6035,22 +6272,22 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
6035
6272
|
function refused(output) {
|
|
6036
6273
|
return { success: false, output, truncated: false };
|
|
6037
6274
|
}
|
|
6038
|
-
function nonPromptingFs(
|
|
6039
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
6275
|
+
function nonPromptingFs(fs15) {
|
|
6276
|
+
const withinWorkspace = (p) => resolveWithin(fs15.getWorkspaceRoot(), p ?? ".").inside;
|
|
6040
6277
|
const outsideRefusal = (p) => refused(
|
|
6041
6278
|
`Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
|
|
6042
6279
|
);
|
|
6043
6280
|
return {
|
|
6044
|
-
getWorkspaceRoot: () =>
|
|
6045
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
6281
|
+
getWorkspaceRoot: () => fs15.getWorkspaceRoot(),
|
|
6282
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs15.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
6046
6283
|
readFiles: (paths) => {
|
|
6047
6284
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
6048
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
6285
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs15.readFiles(paths);
|
|
6049
6286
|
},
|
|
6050
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
6051
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
6052
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
6053
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
6287
|
+
glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6288
|
+
grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6289
|
+
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs15.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6290
|
+
listDir: (p, depth) => withinWorkspace(p) ? fs15.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
6054
6291
|
bash: async (command, signal) => {
|
|
6055
6292
|
const dialect = resolveResearchShell().dialect;
|
|
6056
6293
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
@@ -6061,13 +6298,13 @@ function nonPromptingFs(fs14) {
|
|
|
6061
6298
|
}
|
|
6062
6299
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
6063
6300
|
if (escaping) return outsideRefusal(escaping);
|
|
6064
|
-
return
|
|
6301
|
+
return fs15.bash(command, signal);
|
|
6065
6302
|
}
|
|
6066
6303
|
};
|
|
6067
6304
|
}
|
|
6068
6305
|
async function runLoop(prompt, deps) {
|
|
6069
6306
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
6070
|
-
const
|
|
6307
|
+
const fs15 = nonPromptingFs(deps.fs);
|
|
6071
6308
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
6072
6309
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
6073
6310
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -6084,7 +6321,7 @@ ${turn.text}`;
|
|
|
6084
6321
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
6085
6322
|
const toolArgs = JSON.stringify(tc.args);
|
|
6086
6323
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
6087
|
-
const res = await executeTool(tc.name, args,
|
|
6324
|
+
const res = await executeTool(tc.name, args, fs15, void 0, deps.signal);
|
|
6088
6325
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
6089
6326
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
6090
6327
|
const stepEntry = {
|
|
@@ -6200,7 +6437,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
6200
6437
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
6201
6438
|
* never fail a plan or a turn.
|
|
6202
6439
|
*/
|
|
6203
|
-
async executeSpawnAgent(args,
|
|
6440
|
+
async executeSpawnAgent(args, fs15, onProgress, signal, subagentId) {
|
|
6204
6441
|
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
6205
6442
|
if (!prompt) {
|
|
6206
6443
|
return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
|
|
@@ -6211,20 +6448,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
6211
6448
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
6212
6449
|
return chat;
|
|
6213
6450
|
},
|
|
6214
|
-
fs:
|
|
6451
|
+
fs: fs15,
|
|
6215
6452
|
signal,
|
|
6216
6453
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
6217
6454
|
});
|
|
6218
6455
|
}
|
|
6219
6456
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
6220
|
-
static async collectResearchContext(
|
|
6221
|
-
return collectResearchContext(
|
|
6457
|
+
static async collectResearchContext(fs15, runners) {
|
|
6458
|
+
return collectResearchContext(fs15, runners);
|
|
6222
6459
|
}
|
|
6223
6460
|
/**
|
|
6224
6461
|
* Execute one round of tool calls and report each through onProgress.
|
|
6225
6462
|
* Returns the results to feed back plus the log entries produced.
|
|
6226
6463
|
*/
|
|
6227
|
-
async executeToolCalls(toolCalls,
|
|
6464
|
+
async executeToolCalls(toolCalls, fs15, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
6228
6465
|
const toolResults = [];
|
|
6229
6466
|
const logEntries = [];
|
|
6230
6467
|
let resultsText = "";
|
|
@@ -6243,18 +6480,18 @@ var BaseAiService = class _BaseAiService {
|
|
|
6243
6480
|
const subagentId = uuidv4();
|
|
6244
6481
|
spawnSubagentIds.set(i, subagentId);
|
|
6245
6482
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
6246
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
6483
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs15, onProgress, signal, subagentId));
|
|
6247
6484
|
}) : Promise.resolve(),
|
|
6248
6485
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
6249
6486
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
6250
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
6487
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs15, fetcher, signal));
|
|
6251
6488
|
}) : Promise.resolve()
|
|
6252
6489
|
]);
|
|
6253
6490
|
for (const [index, tc] of toolCalls.entries()) {
|
|
6254
6491
|
const toolArgs = normalizedArgs[index];
|
|
6255
6492
|
const preResult = precomputed.get(index);
|
|
6256
6493
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
6257
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
6494
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs15, fetcher, signal);
|
|
6258
6495
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
6259
6496
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
6260
6497
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -6440,7 +6677,7 @@ ${llmOutput}
|
|
|
6440
6677
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
6441
6678
|
* exhausted without a valid plan.
|
|
6442
6679
|
*/
|
|
6443
|
-
async runResearchLoop(chat, firstMessage,
|
|
6680
|
+
async runResearchLoop(chat, firstMessage, fs15, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
6444
6681
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
6445
6682
|
if (!initialLog && userGoal) {
|
|
6446
6683
|
researchLog.push({
|
|
@@ -6467,7 +6704,7 @@ ${llmOutput}
|
|
|
6467
6704
|
if (!turn.hasToolCalls) break;
|
|
6468
6705
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
6469
6706
|
turn.toolCalls,
|
|
6470
|
-
|
|
6707
|
+
fs15,
|
|
6471
6708
|
onProgress,
|
|
6472
6709
|
fetcher,
|
|
6473
6710
|
step,
|
|
@@ -6702,11 +6939,11 @@ ${repairHint}` }] : parts;
|
|
|
6702
6939
|
return this.consumePlanStream(result, onToken);
|
|
6703
6940
|
}
|
|
6704
6941
|
// --- IAiService implementation ---
|
|
6705
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
6942
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
6706
6943
|
this.ensureInit();
|
|
6707
6944
|
const { autonomousDefault } = modes;
|
|
6708
6945
|
const genModel = this.getPlanningModel();
|
|
6709
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
6946
|
+
const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
|
|
6710
6947
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
6711
6948
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
6712
6949
|
|
|
@@ -6717,17 +6954,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
6717
6954
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
6718
6955
|
});
|
|
6719
6956
|
const researchChat = new GeminiResearchChat(chat);
|
|
6720
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
6957
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
6721
6958
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
6722
6959
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
6723
6960
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
6724
6961
|
}
|
|
6725
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
6962
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
6726
6963
|
this.ensureInit();
|
|
6727
6964
|
const { autonomousDefault } = modes;
|
|
6728
6965
|
let ctx = "";
|
|
6729
|
-
if (
|
|
6730
|
-
const collected = await new ContextCollector(
|
|
6966
|
+
if (fs15) {
|
|
6967
|
+
const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
|
|
6731
6968
|
if (collected.aiflowContext) ctx = `
|
|
6732
6969
|
<aiflow_context>
|
|
6733
6970
|
${collected.aiflowContext}
|
|
@@ -6743,12 +6980,12 @@ ${collected.aiflowContext}
|
|
|
6743
6980
|
autonomousDefault
|
|
6744
6981
|
);
|
|
6745
6982
|
}
|
|
6746
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
6983
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
6747
6984
|
this.ensureInit();
|
|
6748
6985
|
const { autonomousDefault } = modes;
|
|
6749
6986
|
let aiflowContext;
|
|
6750
|
-
if (
|
|
6751
|
-
const collected = await new ContextCollector(
|
|
6987
|
+
if (fs15) {
|
|
6988
|
+
const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
|
|
6752
6989
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
6753
6990
|
}
|
|
6754
6991
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -6984,11 +7221,11 @@ ${buildResearchToolsPrompt()}` }
|
|
|
6984
7221
|
}
|
|
6985
7222
|
}
|
|
6986
7223
|
// --- IAiService implementation ---
|
|
6987
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
7224
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
6988
7225
|
this.ensureInit();
|
|
6989
7226
|
const client = this.getClient();
|
|
6990
7227
|
const { autonomousDefault } = modes;
|
|
6991
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
7228
|
+
const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
|
|
6992
7229
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
6993
7230
|
const userText = `User goal: ${userDescription}`;
|
|
6994
7231
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
@@ -7006,17 +7243,17 @@ ${userText}`;
|
|
|
7006
7243
|
toOpenAiTools(),
|
|
7007
7244
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
7008
7245
|
);
|
|
7009
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
7246
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
7010
7247
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
7011
7248
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
7012
7249
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
7013
7250
|
}
|
|
7014
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
7251
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
7015
7252
|
this.ensureInit();
|
|
7016
7253
|
const { autonomousDefault } = modes;
|
|
7017
7254
|
let ctx = "";
|
|
7018
|
-
if (
|
|
7019
|
-
const collected = await new ContextCollector(
|
|
7255
|
+
if (fs15) {
|
|
7256
|
+
const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
|
|
7020
7257
|
if (collected.aiflowContext) ctx = `
|
|
7021
7258
|
<aiflow_context>
|
|
7022
7259
|
${collected.aiflowContext}
|
|
@@ -7032,12 +7269,12 @@ ${collected.aiflowContext}
|
|
|
7032
7269
|
autonomousDefault
|
|
7033
7270
|
);
|
|
7034
7271
|
}
|
|
7035
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
7272
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
7036
7273
|
this.ensureInit();
|
|
7037
7274
|
const { autonomousDefault } = modes;
|
|
7038
7275
|
let aiflowContext;
|
|
7039
|
-
if (
|
|
7040
|
-
const collected = await new ContextCollector(
|
|
7276
|
+
if (fs15) {
|
|
7277
|
+
const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
|
|
7041
7278
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
7042
7279
|
}
|
|
7043
7280
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -7056,6 +7293,9 @@ ${collected.aiflowContext}
|
|
|
7056
7293
|
}
|
|
7057
7294
|
};
|
|
7058
7295
|
|
|
7296
|
+
// src/services/TaskOrchestrator.ts
|
|
7297
|
+
import * as path11 from "path";
|
|
7298
|
+
|
|
7059
7299
|
// src/services/promptAugment.ts
|
|
7060
7300
|
var MAX_TAIL_CHARS = 500;
|
|
7061
7301
|
var DEFAULT_PLAN_MAP_MAX_ENTRIES = 30;
|
|
@@ -7641,18 +7881,230 @@ ${line}
|
|
|
7641
7881
|
};
|
|
7642
7882
|
|
|
7643
7883
|
// src/services/transcriptCapture.ts
|
|
7644
|
-
import { readFileSync, readdirSync as
|
|
7884
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync4 } from "fs";
|
|
7885
|
+
import * as os4 from "os";
|
|
7886
|
+
import * as path10 from "path";
|
|
7887
|
+
|
|
7888
|
+
// src/services/workspaceEnv.ts
|
|
7889
|
+
import { execFile as execFile2 } from "child_process";
|
|
7890
|
+
import * as fs5 from "fs";
|
|
7645
7891
|
import * as os3 from "os";
|
|
7646
|
-
import * as
|
|
7892
|
+
import * as path9 from "path";
|
|
7893
|
+
|
|
7894
|
+
// src/services/settingsEnvAllowlist.ts
|
|
7895
|
+
var ORDEWELL_SETTABLE_ENV = [
|
|
7896
|
+
"AI_PROVIDER",
|
|
7897
|
+
"ORCHESTRATOR_MODEL",
|
|
7898
|
+
"ORDEWELL_SUBAGENT_MODEL",
|
|
7899
|
+
"ORDEWELL_PLANNER_EFFORT",
|
|
7900
|
+
"ORDEWELL_RESEARCH_ENABLED",
|
|
7901
|
+
"ORDEWELL_MAX_PARALLEL",
|
|
7902
|
+
"ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
|
|
7903
|
+
];
|
|
7904
|
+
var SETTINGS_ENV_REFUSED = [
|
|
7905
|
+
// Approval posture and pre-approved scope.
|
|
7906
|
+
"ORDEWELL_AUTONOMOUS_MODE",
|
|
7907
|
+
"ORDEWELL_APPROVAL_MODE",
|
|
7908
|
+
"ORDEWELL_APPROVAL_ALLOW",
|
|
7909
|
+
// Where settings are persisted.
|
|
7910
|
+
"ORDEWELL_SETTINGS_PATH",
|
|
7911
|
+
// Runtime options and dynamic loaders.
|
|
7912
|
+
"NODE_OPTIONS",
|
|
7913
|
+
"NODE_REPL_EXTERNAL_MODULE",
|
|
7914
|
+
"NODE_PATH",
|
|
7915
|
+
"ELECTRON_RUN_AS_NODE",
|
|
7916
|
+
"LD_PRELOAD",
|
|
7917
|
+
"LD_LIBRARY_PATH",
|
|
7918
|
+
"LD_AUDIT",
|
|
7919
|
+
"DYLD_INSERT_LIBRARIES",
|
|
7920
|
+
"DYLD_LIBRARY_PATH",
|
|
7921
|
+
"DYLD_FRAMEWORK_PATH",
|
|
7922
|
+
// Executable resolution.
|
|
7923
|
+
"PATH",
|
|
7924
|
+
"PATHEXT",
|
|
7925
|
+
"NODE"
|
|
7926
|
+
];
|
|
7927
|
+
var REFUSED = new Set(SETTINGS_ENV_REFUSED);
|
|
7928
|
+
function providerEnvKeys() {
|
|
7929
|
+
const keys = [];
|
|
7930
|
+
for (const meta of Object.values(ALL_PROVIDERS)) {
|
|
7931
|
+
if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
|
|
7932
|
+
if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
|
|
7933
|
+
for (const detect of meta.detectEnvVars) keys.push(detect);
|
|
7934
|
+
}
|
|
7935
|
+
return keys;
|
|
7936
|
+
}
|
|
7937
|
+
var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
|
|
7938
|
+
var SETTINGS_ENV_ALLOWLIST = new Set(
|
|
7939
|
+
[...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
|
|
7940
|
+
);
|
|
7941
|
+
function admitSettingsEnv(env) {
|
|
7942
|
+
const accepted = {};
|
|
7943
|
+
const rejected = [];
|
|
7944
|
+
for (const [key, value] of Object.entries(env)) {
|
|
7945
|
+
if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
|
|
7946
|
+
rejected.push(key);
|
|
7947
|
+
continue;
|
|
7948
|
+
}
|
|
7949
|
+
accepted[key] = value;
|
|
7950
|
+
}
|
|
7951
|
+
return { accepted, rejected };
|
|
7952
|
+
}
|
|
7953
|
+
|
|
7954
|
+
// src/services/workspaceEnv.ts
|
|
7955
|
+
var WORKSPACE_ENV_FILE = "env";
|
|
7956
|
+
var DIRENV_TIMEOUT_MS = 5e3;
|
|
7957
|
+
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7958
|
+
function defaultDeps() {
|
|
7959
|
+
return {
|
|
7960
|
+
run: (command, args, opts) => new Promise((resolve7) => {
|
|
7961
|
+
execFile2(command, args, { ...opts, timeout: DIRENV_TIMEOUT_MS, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
7962
|
+
const code = err ? typeof err.code === "number" ? Number(err.code) : -1 : 0;
|
|
7963
|
+
resolve7({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
7964
|
+
});
|
|
7965
|
+
}),
|
|
7966
|
+
readFile: (file) => {
|
|
7967
|
+
try {
|
|
7968
|
+
return fs5.readFileSync(file, "utf8");
|
|
7969
|
+
} catch {
|
|
7970
|
+
return null;
|
|
7971
|
+
}
|
|
7972
|
+
},
|
|
7973
|
+
isDirectory: (dir) => {
|
|
7974
|
+
try {
|
|
7975
|
+
return fs5.statSync(dir).isDirectory();
|
|
7976
|
+
} catch {
|
|
7977
|
+
return false;
|
|
7978
|
+
}
|
|
7979
|
+
},
|
|
7980
|
+
baseEnv: process.env,
|
|
7981
|
+
home: os3.homedir()
|
|
7982
|
+
};
|
|
7983
|
+
}
|
|
7984
|
+
var REFUSED2 = new Set(SETTINGS_ENV_REFUSED);
|
|
7985
|
+
async function resolveWorkspaceEnv(cwd, overrides = {}) {
|
|
7986
|
+
const deps = { ...defaultDeps(), ...overrides };
|
|
7987
|
+
const result = { env: {}, blockedEnvrc: null, refused: [], trackedEnvFile: null };
|
|
7988
|
+
if (!deps.isDirectory(cwd)) return result;
|
|
7989
|
+
const collected = {};
|
|
7990
|
+
const direnv = await fromDirenv(checkoutDirOf(cwd, deps), deps);
|
|
7991
|
+
result.blockedEnvrc = direnv.blocked;
|
|
7992
|
+
Object.assign(collected, direnv.env);
|
|
7993
|
+
const file = findUp(cwd, path9.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
|
|
7994
|
+
if (file) {
|
|
7995
|
+
const content = deps.readFile(file);
|
|
7996
|
+
if (content !== null) {
|
|
7997
|
+
if (await isTracked(file, deps)) result.trackedEnvFile = file;
|
|
7998
|
+
else Object.assign(collected, parseEnvFile(content, { ...deps.baseEnv, ...collected }, deps.home));
|
|
7999
|
+
}
|
|
8000
|
+
}
|
|
8001
|
+
for (const [key, value] of Object.entries(collected)) {
|
|
8002
|
+
if (!KEY.test(key)) continue;
|
|
8003
|
+
if (REFUSED2.has(key) || key.toUpperCase() === "PATH") result.refused.push(key);
|
|
8004
|
+
else result.env[key] = value;
|
|
8005
|
+
}
|
|
8006
|
+
return result;
|
|
8007
|
+
}
|
|
8008
|
+
async function workspaceEnvOf(cwd) {
|
|
8009
|
+
return (await resolveWorkspaceEnv(cwd)).env;
|
|
8010
|
+
}
|
|
8011
|
+
function checkoutDirOf(cwd, deps) {
|
|
8012
|
+
const parts = path9.resolve(cwd).split(path9.sep);
|
|
8013
|
+
const at = parts.findIndex((part, i) => part === STATE_DIR && parts[i + 1] === "worktrees");
|
|
8014
|
+
if (at < 0 || parts.length < at + 4) return cwd;
|
|
8015
|
+
const root = parts.slice(0, at).join(path9.sep) || path9.sep;
|
|
8016
|
+
const mapped = path9.join(root, ...parts.slice(at + 4));
|
|
8017
|
+
return deps.isDirectory(mapped) ? mapped : root;
|
|
8018
|
+
}
|
|
8019
|
+
async function fromDirenv(cwd, deps) {
|
|
8020
|
+
const setting = deps.baseEnv.ORDEWELL_DIRENV;
|
|
8021
|
+
if (setting === "false" || setting === "0") return { env: {}, blocked: null };
|
|
8022
|
+
const baseEnv = Object.fromEntries(Object.entries(deps.baseEnv).filter(([key]) => !key.startsWith("DIRENV_")));
|
|
8023
|
+
let out;
|
|
8024
|
+
try {
|
|
8025
|
+
out = await deps.run("direnv", ["export", "json"], { cwd, env: baseEnv });
|
|
8026
|
+
} catch {
|
|
8027
|
+
return { env: {}, blocked: null };
|
|
8028
|
+
}
|
|
8029
|
+
const blocked = out.stderr.match(/error (\S*\.envrc) is blocked/);
|
|
8030
|
+
if (blocked) return { env: {}, blocked: blocked[1] };
|
|
8031
|
+
if (out.code !== 0 || !out.stdout.trim()) return { env: {}, blocked: null };
|
|
8032
|
+
let parsed;
|
|
8033
|
+
try {
|
|
8034
|
+
parsed = JSON.parse(out.stdout);
|
|
8035
|
+
} catch {
|
|
8036
|
+
return { env: {}, blocked: null };
|
|
8037
|
+
}
|
|
8038
|
+
if (!parsed || typeof parsed !== "object") return { env: {}, blocked: null };
|
|
8039
|
+
const env = {};
|
|
8040
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
8041
|
+
if (typeof value === "string" && !key.startsWith("DIRENV_")) env[key] = value;
|
|
8042
|
+
}
|
|
8043
|
+
return { env, blocked: null };
|
|
8044
|
+
}
|
|
8045
|
+
function findUp(start, relative5, deps) {
|
|
8046
|
+
let dir = path9.resolve(start);
|
|
8047
|
+
for (; ; ) {
|
|
8048
|
+
const candidate = path9.join(dir, relative5);
|
|
8049
|
+
if (deps.readFile(candidate) !== null) return candidate;
|
|
8050
|
+
const parent = path9.dirname(dir);
|
|
8051
|
+
if (parent === dir) return null;
|
|
8052
|
+
dir = parent;
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
async function isTracked(file, deps) {
|
|
8056
|
+
try {
|
|
8057
|
+
const out = await deps.run("git", ["ls-files", "--error-unmatch", path9.basename(file)], { cwd: path9.dirname(file), env: deps.baseEnv });
|
|
8058
|
+
return out.code === 0;
|
|
8059
|
+
} catch {
|
|
8060
|
+
return false;
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
8063
|
+
function parseEnvFile(content, context, home) {
|
|
8064
|
+
const env = {};
|
|
8065
|
+
const lookup2 = (name) => env[name] ?? context[name] ?? "";
|
|
8066
|
+
const expand = (value) => {
|
|
8067
|
+
const tilde = value.startsWith("~/") ? path9.join(home, value.slice(2)) : value;
|
|
8068
|
+
return tilde.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => lookup2(braced ?? bare));
|
|
8069
|
+
};
|
|
8070
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
8071
|
+
const line = raw.trim().replace(/^export\s+/, "");
|
|
8072
|
+
if (!line || line.startsWith("#")) continue;
|
|
8073
|
+
const eq = line.indexOf("=");
|
|
8074
|
+
if (eq <= 0) continue;
|
|
8075
|
+
const key = line.slice(0, eq).trim();
|
|
8076
|
+
if (!KEY.test(key)) continue;
|
|
8077
|
+
const rest = line.slice(eq + 1).trim();
|
|
8078
|
+
if (rest.startsWith("'")) {
|
|
8079
|
+
const end = rest.indexOf("'", 1);
|
|
8080
|
+
env[key] = end > 0 ? rest.slice(1, end) : rest.slice(1);
|
|
8081
|
+
} else if (rest.startsWith('"')) {
|
|
8082
|
+
const end = rest.lastIndexOf('"');
|
|
8083
|
+
env[key] = expand((end > 0 ? rest.slice(1, end) : rest.slice(1)).replace(/\\n/g, "\n").replace(/\\"/g, '"'));
|
|
8084
|
+
} else {
|
|
8085
|
+
env[key] = expand(rest.replace(/\s+#.*$/, ""));
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
return env;
|
|
8089
|
+
}
|
|
8090
|
+
|
|
8091
|
+
// src/services/transcriptCapture.ts
|
|
7647
8092
|
var HomeTranscriptReader = class {
|
|
7648
8093
|
home;
|
|
8094
|
+
claudeConfigDirs;
|
|
7649
8095
|
constructor(opts = {}) {
|
|
7650
8096
|
const { homeDir } = opts;
|
|
7651
|
-
this.home = homeDir ? () => homeDir : () => process.env.HOME ||
|
|
8097
|
+
this.home = homeDir ? () => homeDir : () => process.env.HOME || os4.homedir();
|
|
8098
|
+
const workspaceEnv = opts.workspaceEnv ?? workspaceEnvOf;
|
|
8099
|
+
this.claudeConfigDirs = async (cwd) => [...new Set([
|
|
8100
|
+
(await workspaceEnv(cwd)).CLAUDE_CONFIG_DIR,
|
|
8101
|
+
homeDir ? void 0 : process.env.CLAUDE_CONFIG_DIR,
|
|
8102
|
+
path10.join(this.home(), ".claude")
|
|
8103
|
+
].filter((dir) => Boolean(dir)))];
|
|
7652
8104
|
}
|
|
7653
8105
|
async finalAssistantText(query, maxChars = 4e3) {
|
|
7654
8106
|
try {
|
|
7655
|
-
if (query.runner === "claude-code") return claudeFinal(this.
|
|
8107
|
+
if (query.runner === "claude-code") return claudeFinal(await this.claudeConfigDirs(query.cwd), query, maxChars);
|
|
7656
8108
|
if (query.runner === "opencode") return await opencodeFinal(this.home(), query, maxChars);
|
|
7657
8109
|
if (query.runner === "codex") return codexFinal(this.home(), query, maxChars);
|
|
7658
8110
|
} catch {
|
|
@@ -7661,22 +8113,22 @@ var HomeTranscriptReader = class {
|
|
|
7661
8113
|
}
|
|
7662
8114
|
};
|
|
7663
8115
|
var CLAUDE_PROJECT_NAME_MAX = 200;
|
|
7664
|
-
function claudeProjectDirs(
|
|
7665
|
-
const projects =
|
|
8116
|
+
function claudeProjectDirs(configDir2, cwd) {
|
|
8117
|
+
const projects = path10.join(configDir2, "projects");
|
|
7666
8118
|
const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
7667
8119
|
if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
|
|
7668
|
-
const dir =
|
|
7669
|
-
return
|
|
8120
|
+
const dir = path10.join(projects, munged);
|
|
8121
|
+
return existsSync4(dir) ? [dir] : [];
|
|
7670
8122
|
}
|
|
7671
|
-
if (!
|
|
8123
|
+
if (!existsSync4(projects)) return [];
|
|
7672
8124
|
const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
|
|
7673
|
-
return
|
|
8125
|
+
return readdirSync3(projects).filter((name) => name.startsWith(prefix)).map((name) => path10.join(projects, name));
|
|
7674
8126
|
}
|
|
7675
|
-
function claudeFinal(
|
|
8127
|
+
function claudeFinal(configDirs, query, maxChars) {
|
|
7676
8128
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
7677
|
-
const candidates = claudeProjectDirs(
|
|
8129
|
+
const candidates = configDirs.flatMap((dir) => claudeProjectDirs(dir, query.cwd)).flatMap((dir) => readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path10.join(dir, f))).filter((f) => cutoff ? statSync5(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs);
|
|
7678
8130
|
for (const file of candidates) {
|
|
7679
|
-
const raw =
|
|
8131
|
+
const raw = readFileSync3(file, "utf8");
|
|
7680
8132
|
if (!raw.includes(query.marker)) continue;
|
|
7681
8133
|
const text = claudeLastAssistant(raw);
|
|
7682
8134
|
if (text) return clamp(text, maxChars);
|
|
@@ -7710,8 +8162,8 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
7710
8162
|
} catch {
|
|
7711
8163
|
return null;
|
|
7712
8164
|
}
|
|
7713
|
-
const dbPath =
|
|
7714
|
-
if (!
|
|
8165
|
+
const dbPath = path10.join(home, ".local", "share", "opencode", "opencode.db");
|
|
8166
|
+
if (!existsSync4(dbPath)) return null;
|
|
7715
8167
|
let db;
|
|
7716
8168
|
try {
|
|
7717
8169
|
db = new DatabaseSync(dbPath, { open: true });
|
|
@@ -7763,8 +8215,8 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
7763
8215
|
}
|
|
7764
8216
|
}
|
|
7765
8217
|
function codexFinal(home, query, maxChars) {
|
|
7766
|
-
const root =
|
|
7767
|
-
if (!
|
|
8218
|
+
const root = path10.join(home, ".codex", "sessions");
|
|
8219
|
+
if (!existsSync4(root)) return null;
|
|
7768
8220
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
7769
8221
|
const files = [];
|
|
7770
8222
|
const stack = [root];
|
|
@@ -7772,15 +8224,15 @@ function codexFinal(home, query, maxChars) {
|
|
|
7772
8224
|
const dir = stack.pop();
|
|
7773
8225
|
let entries;
|
|
7774
8226
|
try {
|
|
7775
|
-
entries =
|
|
8227
|
+
entries = readdirSync3(dir);
|
|
7776
8228
|
} catch {
|
|
7777
8229
|
continue;
|
|
7778
8230
|
}
|
|
7779
8231
|
for (const e of entries) {
|
|
7780
|
-
const full =
|
|
8232
|
+
const full = path10.join(dir, e);
|
|
7781
8233
|
let st;
|
|
7782
8234
|
try {
|
|
7783
|
-
st =
|
|
8235
|
+
st = statSync5(full);
|
|
7784
8236
|
} catch {
|
|
7785
8237
|
continue;
|
|
7786
8238
|
}
|
|
@@ -7788,7 +8240,7 @@ function codexFinal(home, query, maxChars) {
|
|
|
7788
8240
|
else if (e.startsWith("rollout-") && e.endsWith(".jsonl") && (!cutoff || st.mtimeMs >= cutoff - 5e3)) files.push(full);
|
|
7789
8241
|
}
|
|
7790
8242
|
}
|
|
7791
|
-
files.sort((a, b) =>
|
|
8243
|
+
files.sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs);
|
|
7792
8244
|
for (const file of cutoff ? files : files.slice(0, 10)) {
|
|
7793
8245
|
const text = codexLastAssistant(file, query);
|
|
7794
8246
|
if (text) return clamp(text, maxChars);
|
|
@@ -7796,7 +8248,7 @@ function codexFinal(home, query, maxChars) {
|
|
|
7796
8248
|
return null;
|
|
7797
8249
|
}
|
|
7798
8250
|
function codexLastAssistant(file, query) {
|
|
7799
|
-
const raw =
|
|
8251
|
+
const raw = readFileSync3(file, "utf8");
|
|
7800
8252
|
if (!raw.includes(query.marker)) return null;
|
|
7801
8253
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7802
8254
|
let sawCwd = false;
|
|
@@ -8228,6 +8680,43 @@ ${taskB.description}`,
|
|
|
8228
8680
|
}
|
|
8229
8681
|
};
|
|
8230
8682
|
|
|
8683
|
+
// src/utils/shell.ts
|
|
8684
|
+
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
|
|
8685
|
+
function stripAnsi(text) {
|
|
8686
|
+
return text.replace(ANSI_RE, "");
|
|
8687
|
+
}
|
|
8688
|
+
function posixShellQuote(s) {
|
|
8689
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
8690
|
+
}
|
|
8691
|
+
function buildShellInvocation(command, args) {
|
|
8692
|
+
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
8693
|
+
return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
|
|
8694
|
+
}
|
|
8695
|
+
function wrapWithPty(command, args, opts = {}) {
|
|
8696
|
+
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
8697
|
+
const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
|
|
8698
|
+
const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
|
|
8699
|
+
return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
|
|
8700
|
+
}
|
|
8701
|
+
|
|
8702
|
+
// src/services/blockingPrompts.ts
|
|
8703
|
+
var TAIL = 4096;
|
|
8704
|
+
var squash = (text) => stripAnsi(text).replace(/\s+/g, "").toLowerCase();
|
|
8705
|
+
function watchBlockingPrompts(session, prompts, onPrompt) {
|
|
8706
|
+
if (prompts.length === 0) return;
|
|
8707
|
+
const pending = prompts.map((prompt) => ({ prompt, phrase: squash(prompt.phrase) }));
|
|
8708
|
+
let tail = "";
|
|
8709
|
+
session.onOutput((text) => {
|
|
8710
|
+
if (pending.length === 0) return;
|
|
8711
|
+
tail = (tail + squash(text)).slice(-TAIL);
|
|
8712
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
8713
|
+
if (!tail.includes(pending[i].phrase)) continue;
|
|
8714
|
+
onPrompt(pending[i].prompt);
|
|
8715
|
+
pending.splice(i, 1);
|
|
8716
|
+
}
|
|
8717
|
+
});
|
|
8718
|
+
}
|
|
8719
|
+
|
|
8231
8720
|
// src/services/TaskOrchestrator.ts
|
|
8232
8721
|
var SHARED_ROOT_TAIL = "tasks run in the workspace root without worktree isolation.";
|
|
8233
8722
|
function sharedRootNotice(reason, repos) {
|
|
@@ -8291,6 +8780,25 @@ var TaskOrchestrator = class {
|
|
|
8291
8780
|
*/
|
|
8292
8781
|
retryCounts = /* @__PURE__ */ new Map();
|
|
8293
8782
|
spawnCounts = /* @__PURE__ */ new Map();
|
|
8783
|
+
/**
|
|
8784
|
+
* The terminal a verdict left open, by task. It stays so the user can read
|
|
8785
|
+
* the agent's output or keep talking to it — but only while its worktree
|
|
8786
|
+
* does: once that is removed the agent sits in a deleted directory, and a
|
|
8787
|
+
* newer attempt in the same worktree would share it with a second agent.
|
|
8788
|
+
* Without this, every task of every run left one agent process running
|
|
8789
|
+
* until the daemon stopped.
|
|
8790
|
+
*/
|
|
8791
|
+
lingering = /* @__PURE__ */ new Map();
|
|
8792
|
+
/**
|
|
8793
|
+
* A full-plan run a failure paused. Retrying the failed task is the explicit
|
|
8794
|
+
* resume the pause waits for — without this a retry only reset the task to
|
|
8795
|
+
* pending and nothing ran until the user also re-ran the whole plan.
|
|
8796
|
+
*/
|
|
8797
|
+
haltedByFailure = false;
|
|
8798
|
+
/** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
|
|
8799
|
+
workspaceEnv = (cwd) => resolveWorkspaceEnv(cwd);
|
|
8800
|
+
/** What the workspace env has already warned about, so a run says it once, not per task. */
|
|
8801
|
+
envWarnings = /* @__PURE__ */ new Set();
|
|
8294
8802
|
/**
|
|
8295
8803
|
* Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
|
|
8296
8804
|
* They stay 'pending' — "not executed" — but the scheduler skips them until
|
|
@@ -8334,6 +8842,32 @@ var TaskOrchestrator = class {
|
|
|
8334
8842
|
setWorkspaceRoot(fn) {
|
|
8335
8843
|
this.workspaceRootFn = fn;
|
|
8336
8844
|
}
|
|
8845
|
+
setWorkspaceEnvResolver(resolve7) {
|
|
8846
|
+
this.workspaceEnv = resolve7;
|
|
8847
|
+
}
|
|
8848
|
+
/**
|
|
8849
|
+
* The variables a task's agent gets from its workspace. What cannot be
|
|
8850
|
+
* applied is said once — silently starting without them is how agents ran
|
|
8851
|
+
* under the wrong account when an edited `.envrc` was left unallowed.
|
|
8852
|
+
*/
|
|
8853
|
+
async envForTask(cwd) {
|
|
8854
|
+
const resolved = await this.workspaceEnv(cwd);
|
|
8855
|
+
const warn = (key, message) => {
|
|
8856
|
+
if (this.envWarnings.has(key)) return;
|
|
8857
|
+
this.envWarnings.add(key);
|
|
8858
|
+
this.notifications.warn(message);
|
|
8859
|
+
};
|
|
8860
|
+
if (resolved.blockedEnvrc) {
|
|
8861
|
+
warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path11.dirname(resolved.blockedEnvrc)} to use them.`);
|
|
8862
|
+
}
|
|
8863
|
+
if (resolved.trackedEnvFile) {
|
|
8864
|
+
warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
|
|
8865
|
+
}
|
|
8866
|
+
if (resolved.refused.length > 0) {
|
|
8867
|
+
warn(`refused:${resolved.refused.join(",")}`, `Ignored ${resolved.refused.join(", ")} from the workspace environment: Ordewell never passes these to agents.`);
|
|
8868
|
+
}
|
|
8869
|
+
return resolved.env;
|
|
8870
|
+
}
|
|
8337
8871
|
setRegistry(registry) {
|
|
8338
8872
|
this.registry = registry;
|
|
8339
8873
|
}
|
|
@@ -8415,7 +8949,7 @@ var TaskOrchestrator = class {
|
|
|
8415
8949
|
if (!this.isolationRun) return null;
|
|
8416
8950
|
const record = this.isolationRun.tasks[taskId];
|
|
8417
8951
|
if (!record) return { state: "none" };
|
|
8418
|
-
return taskIsolationOf(record);
|
|
8952
|
+
return taskIsolationOf(record, this.config.conflictRepairAttempts);
|
|
8419
8953
|
}
|
|
8420
8954
|
/**
|
|
8421
8955
|
* The plan's isolation as a surface shows it, for one with no stream to have
|
|
@@ -8424,7 +8958,7 @@ var TaskOrchestrator = class {
|
|
|
8424
8958
|
isolationView() {
|
|
8425
8959
|
const run = this.isolationRun;
|
|
8426
8960
|
if (!run) return null;
|
|
8427
|
-
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
|
|
8961
|
+
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
|
|
8428
8962
|
return { tasks, handoff: handoffOf(run) };
|
|
8429
8963
|
}
|
|
8430
8964
|
getAttemptSession(taskId) {
|
|
@@ -8460,24 +8994,62 @@ var TaskOrchestrator = class {
|
|
|
8460
8994
|
async reviewRunDiff() {
|
|
8461
8995
|
return this.isolation.reviewDiff(this.requireRun());
|
|
8462
8996
|
}
|
|
8463
|
-
/**
|
|
8997
|
+
/**
|
|
8998
|
+
* "Merge all": the run's integration branches into whatever the user has
|
|
8999
|
+
* checked out, in every repo or none. Once everything merged, the run has
|
|
9000
|
+
* nothing left to hand over, so it is cleared up and forgotten; a branch
|
|
9001
|
+
* the user's HEAD somehow does not contain stays for the next run's sweep.
|
|
9002
|
+
*/
|
|
8464
9003
|
async mergeRun() {
|
|
8465
9004
|
const run = this.requireRun();
|
|
8466
9005
|
const result = await this.isolation.mergeIntoCheckedOut(run);
|
|
8467
9006
|
const branch = integrationBranchNameOf(run);
|
|
8468
9007
|
const group = run.repos.some((r) => r.path !== SELF_REPO);
|
|
8469
|
-
const
|
|
9008
|
+
const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
|
|
9009
|
+
const { level, message } = describeMergeResult(result, branch, group, repaired);
|
|
8470
9010
|
this.notifications[level](message);
|
|
9011
|
+
if (result.outcome === "merged") await this.clearMergedRun(run);
|
|
8471
9012
|
return result;
|
|
8472
9013
|
}
|
|
9014
|
+
async clearMergedRun(run) {
|
|
9015
|
+
this.closeLingeringOf(run);
|
|
9016
|
+
try {
|
|
9017
|
+
await this.isolation.discard(run, { integration: "delete-merged" });
|
|
9018
|
+
} catch (err) {
|
|
9019
|
+
this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
9020
|
+
return;
|
|
9021
|
+
}
|
|
9022
|
+
this.forgetRun();
|
|
9023
|
+
}
|
|
8473
9024
|
/** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
|
|
8474
9025
|
async cleanupRun() {
|
|
8475
|
-
|
|
9026
|
+
this.closeLingeringOf(this.requireRun());
|
|
9027
|
+
await this.isolation.discard(this.requireRun(), { integration: "keep" });
|
|
8476
9028
|
this.emit("onIsolationChanged");
|
|
8477
9029
|
}
|
|
8478
9030
|
/** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
|
|
8479
9031
|
async discardRun() {
|
|
8480
|
-
|
|
9032
|
+
this.closeLingeringOf(this.requireRun());
|
|
9033
|
+
await this.isolation.discard(this.requireRun(), { integration: "delete" });
|
|
9034
|
+
this.forgetRun();
|
|
9035
|
+
}
|
|
9036
|
+
watchBlockingPrompts(task, attempt, session) {
|
|
9037
|
+
const manifest = this.registry?.get(attempt.runner)?.manifest;
|
|
9038
|
+
watchBlockingPrompts(session, manifest?.runner.blockingPrompts ?? [], (prompt) => {
|
|
9039
|
+
if (this.attempts.get(task.id) !== attempt) return;
|
|
9040
|
+
this.notifications.warn(`Task "${task.title}" is waiting for you: ${manifest?.displayName ?? attempt.runner} is asking ${prompt.asks}. Answer it in the task's terminal.`);
|
|
9041
|
+
});
|
|
9042
|
+
}
|
|
9043
|
+
closeLingering(taskId) {
|
|
9044
|
+
const sessionId = this.lingering.get(taskId);
|
|
9045
|
+
if (sessionId === void 0) return;
|
|
9046
|
+
this.lingering.delete(taskId);
|
|
9047
|
+
this.terminalRunner.stop(sessionId);
|
|
9048
|
+
}
|
|
9049
|
+
closeLingeringOf(run) {
|
|
9050
|
+
for (const taskId of Object.keys(run.tasks)) this.closeLingering(taskId);
|
|
9051
|
+
}
|
|
9052
|
+
forgetRun() {
|
|
8481
9053
|
this.isolationRun = null;
|
|
8482
9054
|
this.resolvers = {};
|
|
8483
9055
|
this.emit("onIsolationChanged");
|
|
@@ -8513,7 +9085,8 @@ var TaskOrchestrator = class {
|
|
|
8513
9085
|
}
|
|
8514
9086
|
loadPlan(tasks, planRunners = ["claude-code"]) {
|
|
8515
9087
|
this.store.load(tasks, planRunners);
|
|
8516
|
-
this.endAllAttempts("load");
|
|
9088
|
+
const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
|
|
9089
|
+
for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
8517
9090
|
if (!this.running) this.runMode = null;
|
|
8518
9091
|
this.blockedStart = null;
|
|
8519
9092
|
this.planStatus = "approved";
|
|
@@ -8563,17 +9136,25 @@ var TaskOrchestrator = class {
|
|
|
8563
9136
|
if (!await this.openRun(() => this.start()) || this.running) return;
|
|
8564
9137
|
console.log(`[TaskOrchestrator] Starting with ${this.store.allTasks.length} tasks (${this.store.allTasks.filter((t) => t.type === "ai" && t.prompt).length} AI ready)`);
|
|
8565
9138
|
this.running = true;
|
|
9139
|
+
this.haltedByFailure = false;
|
|
8566
9140
|
this.planStatus = "running";
|
|
8567
9141
|
this.emit("onTaskChanged");
|
|
8568
9142
|
await this.tick();
|
|
8569
9143
|
}
|
|
9144
|
+
haltOnFailure() {
|
|
9145
|
+
if (this.running) this.haltedByFailure = true;
|
|
9146
|
+
this.running = false;
|
|
9147
|
+
this.planStatus = "approved";
|
|
9148
|
+
}
|
|
8570
9149
|
stop() {
|
|
9150
|
+
this.haltedByFailure = false;
|
|
8571
9151
|
this.running = false;
|
|
8572
9152
|
this.planStatus = "approved";
|
|
8573
9153
|
this.terminalRunner.stopAll();
|
|
8574
|
-
const
|
|
8575
|
-
|
|
8576
|
-
|
|
9154
|
+
for (const a of this.endAllAttempts("stop")) {
|
|
9155
|
+
if (a.repair) this.store.markAwaitingUser(a.taskId);
|
|
9156
|
+
if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
9157
|
+
}
|
|
8577
9158
|
this.runMode = null;
|
|
8578
9159
|
this.blockedStart = null;
|
|
8579
9160
|
this.onHold.clear();
|
|
@@ -8589,6 +9170,7 @@ var TaskOrchestrator = class {
|
|
|
8589
9170
|
console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
|
|
8590
9171
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
8591
9172
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
9173
|
+
if (attempt.repair) return this.settleRepair(task, attempt, verdict);
|
|
8592
9174
|
const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
|
|
8593
9175
|
const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
|
|
8594
9176
|
if (this.attempts.get(taskId) !== attempt) return;
|
|
@@ -8602,13 +9184,15 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8602
9184
|
await this.landPassed(task, landing);
|
|
8603
9185
|
} else {
|
|
8604
9186
|
this.store.markFailed(taskId);
|
|
8605
|
-
this.
|
|
8606
|
-
this.planStatus = "approved";
|
|
9187
|
+
this.haltOnFailure();
|
|
8607
9188
|
this.notifications.error(`Task "${task.title}" failed verification: ${verdict.reason}`);
|
|
8608
9189
|
if (attempt.worktree) await this.releaseWorktree(taskId, { keep: true });
|
|
8609
9190
|
}
|
|
8610
9191
|
this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
|
|
8611
9192
|
this.logAndArchive(task, verdict);
|
|
9193
|
+
await this.afterVerdict();
|
|
9194
|
+
}
|
|
9195
|
+
async afterVerdict() {
|
|
8612
9196
|
this.emit("onTaskChanged");
|
|
8613
9197
|
if (!this.running) {
|
|
8614
9198
|
if (this.attempts.size === 0) {
|
|
@@ -8621,6 +9205,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8621
9205
|
}
|
|
8622
9206
|
await this.tick();
|
|
8623
9207
|
}
|
|
9208
|
+
/**
|
|
9209
|
+
* A repair's verdict decides only whether its work may try to land. It never
|
|
9210
|
+
* replaces the verdict the task's own work earned, and a repair that does not
|
|
9211
|
+
* land leaves the task waiting on the user — never a failed task, so never a
|
|
9212
|
+
* halted run.
|
|
9213
|
+
*/
|
|
9214
|
+
async settleRepair(task, attempt, verdict) {
|
|
9215
|
+
const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
|
|
9216
|
+
if (this.attempts.get(task.id) !== attempt) return;
|
|
9217
|
+
this.endAttempt(task.id, "verdict");
|
|
9218
|
+
if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
|
|
9219
|
+
else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
|
|
9220
|
+
else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
|
|
9221
|
+
else this.landUnmerged(task, landed.outcome);
|
|
9222
|
+
await this.afterVerdict();
|
|
9223
|
+
}
|
|
9224
|
+
/**
|
|
9225
|
+
* Evidence before the queue: the repair's work is committed and checked, and
|
|
9226
|
+
* only then merged, through the same serialized landing as any other task —
|
|
9227
|
+
* so a repair the tip has moved past again is a fresh conflict, not a pass.
|
|
9228
|
+
*/
|
|
9229
|
+
async landRepair(task, attempt) {
|
|
9230
|
+
const run = this.isolationRun;
|
|
9231
|
+
let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
|
|
9232
|
+
if (!run) return { evidence, outcome: "failed" };
|
|
9233
|
+
attempt.phase = "integrating";
|
|
9234
|
+
attempt.integration = (async () => {
|
|
9235
|
+
evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
|
|
9236
|
+
if (evidence.ok) return this.integrateWork(task);
|
|
9237
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
9238
|
+
return "conflict";
|
|
9239
|
+
})();
|
|
9240
|
+
const outcome = await attempt.integration;
|
|
9241
|
+
return { evidence, outcome };
|
|
9242
|
+
}
|
|
9243
|
+
async landedRepair(task, verdict) {
|
|
9244
|
+
const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
|
|
9245
|
+
this.store.markCompleted(task.id);
|
|
9246
|
+
this.store.unblockDependents(task.id);
|
|
9247
|
+
this.logAndArchive(task, task.verdict ?? verdict);
|
|
9248
|
+
this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
|
|
9249
|
+
await this.landResolved(task.id);
|
|
9250
|
+
}
|
|
9251
|
+
/** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
|
|
9252
|
+
async unrepaired(task, why) {
|
|
9253
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
9254
|
+
this.store.markAwaitingUser(task.id);
|
|
9255
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
9256
|
+
this.tell("warn", `The conflict repair of task "${task.title}" ${why}, so it did not land. Its ${group ? "worktrees are" : "worktree is"} kept \u2014 resolve it by hand and mark it complete, retry it, or resolve it as a task.`);
|
|
9257
|
+
}
|
|
9258
|
+
describeEvidence(evidence) {
|
|
9259
|
+
const run = this.isolationRun;
|
|
9260
|
+
const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
|
|
9261
|
+
const branch = run ? integrationBranchNameOf(run) : "the integration branch";
|
|
9262
|
+
switch (evidence.reason) {
|
|
9263
|
+
case "not-merged":
|
|
9264
|
+
return `finished, but its branch${inRepo} does not contain ${branch}`;
|
|
9265
|
+
case "conflict-markers": {
|
|
9266
|
+
const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
|
|
9267
|
+
return `finished, but left conflict markers in ${capConflictFiles(files)}`;
|
|
9268
|
+
}
|
|
9269
|
+
case "failed":
|
|
9270
|
+
return `finished, but git could not check its work${inRepo}`;
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
8624
9273
|
/**
|
|
8625
9274
|
* Merge a passed attempt's worktree. The attempt stays live while it waits on
|
|
8626
9275
|
* the module's merge queue, so a cancel, retry or stop in that window still
|
|
@@ -8654,18 +9303,44 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8654
9303
|
}
|
|
8655
9304
|
landUnmerged(task, landing) {
|
|
8656
9305
|
const branch = this.isolationRun ? integrationBranchNameOf(this.isolationRun) : "the integration branch";
|
|
8657
|
-
const
|
|
8658
|
-
const inRepo =
|
|
9306
|
+
const record = this.isolationRun?.tasks[task.id];
|
|
9307
|
+
const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
|
|
8659
9308
|
if (landing === "conflict") {
|
|
9309
|
+
const repair = this.nextRepair(task.id);
|
|
8660
9310
|
this.store.markAwaitingUser(task.id);
|
|
8661
|
-
|
|
9311
|
+
const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
|
|
9312
|
+
const conflicted = inRepo ? `Task "${task.title}" passed, but landing it on ${branch} conflicted in ${inRepo}${files}, so none of it landed.` : `Task "${task.title}" passed, but merging it into ${branch} conflicted${files}.`;
|
|
9313
|
+
if (repair) {
|
|
9314
|
+
this.notifications.warn(conflicted);
|
|
9315
|
+
if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
|
|
9316
|
+
else {
|
|
9317
|
+
this.store.markPending(task.id);
|
|
9318
|
+
this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
|
|
9319
|
+
}
|
|
9320
|
+
return;
|
|
9321
|
+
}
|
|
9322
|
+
this.notifications.warn(`${conflicted} ${inRepo ? "Its worktrees are" : "Its worktree is"} kept \u2014 resolve it by hand and mark it complete, retry it, or resolve it as a task.`);
|
|
9323
|
+
const whyNot = this.noRepairReason(task);
|
|
9324
|
+
if (whyNot) this.tell("info", whyNot);
|
|
8662
9325
|
} else {
|
|
8663
9326
|
this.store.markFailed(task.id);
|
|
8664
|
-
this.
|
|
8665
|
-
this.planStatus = "approved";
|
|
9327
|
+
this.haltOnFailure();
|
|
8666
9328
|
this.notifications.error(inRepo ? `Task "${task.title}" passed, but git could not integrate its work in ${inRepo}, so none of it landed. Its worktrees are kept for inspection.` : `Task "${task.title}" passed, but git could not integrate its work. Its worktree is kept for inspection.`);
|
|
8667
9329
|
}
|
|
8668
9330
|
}
|
|
9331
|
+
/** The repair a conflicted task is owed next; null when repair is off, used up, or there is no isolated run to repair it in. */
|
|
9332
|
+
nextRepair(taskId) {
|
|
9333
|
+
const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
|
|
9334
|
+
const limit = this.config.conflictRepairAttempts;
|
|
9335
|
+
const spent = record?.repairs ?? 0;
|
|
9336
|
+
return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
|
|
9337
|
+
}
|
|
9338
|
+
noRepairReason(task) {
|
|
9339
|
+
const limit = this.config.conflictRepairAttempts;
|
|
9340
|
+
if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
|
|
9341
|
+
const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
|
|
9342
|
+
return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
|
|
9343
|
+
}
|
|
8669
9344
|
/**
|
|
8670
9345
|
* Mark which added task resolves which conflict. The resolver merges the
|
|
8671
9346
|
* conflicted task's branch by hand in its own worktree; once that lands, the
|
|
@@ -8700,6 +9375,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8700
9375
|
await integration;
|
|
8701
9376
|
const run = this.isolationRun;
|
|
8702
9377
|
if (!run?.tasks[taskId]) return;
|
|
9378
|
+
if (!opts.keep) this.closeLingering(taskId);
|
|
8703
9379
|
try {
|
|
8704
9380
|
await this.isolation.release(run, taskId, opts);
|
|
8705
9381
|
} catch (err) {
|
|
@@ -8848,7 +9524,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8848
9524
|
this.onHold.delete(taskId);
|
|
8849
9525
|
this.emit("onTaskChanged");
|
|
8850
9526
|
await this.releaseWorktree(taskId, { keep: false }, ended?.integration);
|
|
8851
|
-
await this.
|
|
9527
|
+
if (this.haltedByFailure && !this.running) await this.start();
|
|
9528
|
+
else await this.tick();
|
|
8852
9529
|
}
|
|
8853
9530
|
/**
|
|
8854
9531
|
* Manually start a single AI task right now, bypassing dependency/readiness
|
|
@@ -8955,6 +9632,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8955
9632
|
cwd: null,
|
|
8956
9633
|
worktree: false,
|
|
8957
9634
|
integration: null,
|
|
9635
|
+
repair: this.nextRepair(task.id),
|
|
8958
9636
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8959
9637
|
};
|
|
8960
9638
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -8965,13 +9643,16 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8965
9643
|
const cwd = await this.resolveAttemptCwd(task, attempt);
|
|
8966
9644
|
attempt.cwd = cwd;
|
|
8967
9645
|
if (this.attempts.get(task.id) !== attempt) {
|
|
8968
|
-
if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep:
|
|
8969
|
-
return this.abandonSpawn(task);
|
|
9646
|
+
if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
|
|
9647
|
+
return this.abandonSpawn(task, attempt);
|
|
8970
9648
|
}
|
|
8971
|
-
const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
|
|
9649
|
+
const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
|
|
8972
9650
|
planMapEnabled: this.config.planMapEnabled,
|
|
8973
|
-
|
|
9651
|
+
// A merge to resolve is not new behaviour to drive test-first.
|
|
9652
|
+
tddEnabled: !attempt.repair && this.tddEnabled()
|
|
8974
9653
|
});
|
|
9654
|
+
this.closeLingering(task.id);
|
|
9655
|
+
const env = await this.envForTask(cwd);
|
|
8975
9656
|
const session = await this.terminalRunner.spawn({
|
|
8976
9657
|
taskId: task.id,
|
|
8977
9658
|
runner: attempt.runner,
|
|
@@ -8983,18 +9664,30 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8983
9664
|
cwd,
|
|
8984
9665
|
registry: this.registry ?? void 0,
|
|
8985
9666
|
order: task.order,
|
|
8986
|
-
title: task.title
|
|
9667
|
+
title: task.title,
|
|
9668
|
+
env
|
|
8987
9669
|
});
|
|
8988
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
|
|
9670
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
|
|
8989
9671
|
attempt.phase = "running";
|
|
8990
9672
|
attempt.session = session;
|
|
8991
9673
|
this.output.attach(task.id, session);
|
|
8992
9674
|
this.verifier.watch(task, session);
|
|
9675
|
+
this.watchBlockingPrompts(task, attempt, session);
|
|
8993
9676
|
this.emit("onTaskChanged");
|
|
8994
|
-
|
|
9677
|
+
if (attempt.repair) {
|
|
9678
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
9679
|
+
this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
|
|
9680
|
+
} else {
|
|
9681
|
+
this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
|
|
9682
|
+
}
|
|
8995
9683
|
} catch (err) {
|
|
8996
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
|
|
9684
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
|
|
8997
9685
|
this.endAttempt(task.id, "spawn-failed");
|
|
9686
|
+
if (attempt.repair) {
|
|
9687
|
+
await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
9688
|
+
this.emit("onTaskChanged");
|
|
9689
|
+
return this.tick();
|
|
9690
|
+
}
|
|
8998
9691
|
await this.releaseWorktree(task.id, { keep: false });
|
|
8999
9692
|
this.store.markPending(task.id);
|
|
9000
9693
|
this.onHold.add(task.id);
|
|
@@ -9008,17 +9701,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9008
9701
|
* and take back the claim it made. Only the claim — whatever ended the
|
|
9009
9702
|
* attempt may have decided the task since (mark complete, cancel, retry).
|
|
9010
9703
|
*/
|
|
9011
|
-
abandonSpawn(task, session) {
|
|
9704
|
+
abandonSpawn(task, attempt, session) {
|
|
9012
9705
|
session?.kill();
|
|
9013
9706
|
if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
|
|
9014
|
-
this.store.
|
|
9707
|
+
if (attempt.repair) this.store.markAwaitingUser(task.id);
|
|
9708
|
+
else this.store.markPending(task.id);
|
|
9015
9709
|
this.emit("onTaskChanged");
|
|
9016
9710
|
}
|
|
9711
|
+
repairPrompt(task) {
|
|
9712
|
+
const run = this.requireRun();
|
|
9713
|
+
const record = run.tasks[task.id];
|
|
9714
|
+
const conflict = {
|
|
9715
|
+
branch: record?.branch ?? "",
|
|
9716
|
+
repos: Object.keys(record?.repairBase ?? {}),
|
|
9717
|
+
...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
|
|
9718
|
+
...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
|
|
9719
|
+
};
|
|
9720
|
+
return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
|
|
9721
|
+
}
|
|
9017
9722
|
/**
|
|
9018
9723
|
* The one place an attempt's working directory is decided. Async so a
|
|
9019
9724
|
* per-attempt workspace (worktree isolation, #12) can be prepared here.
|
|
9020
9725
|
*/
|
|
9021
9726
|
async resolveAttemptCwd(task, attempt) {
|
|
9727
|
+
if (attempt.repair && this.isolationRun) {
|
|
9728
|
+
const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
|
|
9729
|
+
attempt.worktree = true;
|
|
9730
|
+
this.emit("onIsolationChanged");
|
|
9731
|
+
return cwd2;
|
|
9732
|
+
}
|
|
9022
9733
|
if (this.runMode !== "isolated" || !this.isolationRun) {
|
|
9023
9734
|
await this.releaseWorktree(task.id, { keep: false });
|
|
9024
9735
|
return this.workspaceRootFn();
|
|
@@ -9106,8 +9817,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9106
9817
|
}
|
|
9107
9818
|
}
|
|
9108
9819
|
this.runMode = "isolated";
|
|
9820
|
+
await this.sweep();
|
|
9109
9821
|
return true;
|
|
9110
9822
|
}
|
|
9823
|
+
/** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
|
|
9824
|
+
async sweep() {
|
|
9825
|
+
if (!this.isolationRun) return;
|
|
9826
|
+
try {
|
|
9827
|
+
await this.isolation.sweep(this.isolationRun);
|
|
9828
|
+
} catch (err) {
|
|
9829
|
+
this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
|
|
9830
|
+
}
|
|
9831
|
+
}
|
|
9111
9832
|
/**
|
|
9112
9833
|
* The plan's run carries on while anything has landed on its integration
|
|
9113
9834
|
* branch: a resumed plan's dependents must start from a tip that holds their
|
|
@@ -9120,14 +9841,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9120
9841
|
/**
|
|
9121
9842
|
* A run with nothing landed holds only superseded attempts, so it goes whole.
|
|
9122
9843
|
* One that cannot be continued for another reason — it ran from a different
|
|
9123
|
-
* workspace path — keeps its integration branch
|
|
9124
|
-
* work up.
|
|
9844
|
+
* workspace path — keeps its integration branch in each repo that has not
|
|
9845
|
+
* merged it: only the user gives landed work up.
|
|
9125
9846
|
*/
|
|
9126
9847
|
async mintRun(root) {
|
|
9127
9848
|
const previous = this.isolationRun;
|
|
9128
9849
|
if (previous) {
|
|
9129
9850
|
const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
|
|
9130
|
-
await this.isolation.discard(previous, {
|
|
9851
|
+
await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
|
|
9131
9852
|
this.isolationRun = null;
|
|
9132
9853
|
}
|
|
9133
9854
|
this.isolationRun = await this.isolation.startRun(root);
|
|
@@ -9184,6 +9905,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9184
9905
|
const attempt = this.attempts.get(taskId);
|
|
9185
9906
|
this.attempts.delete(taskId);
|
|
9186
9907
|
if (attempt) this.output.detach(taskId);
|
|
9908
|
+
if (reason === "verdict" && attempt?.session) this.lingering.set(taskId, attempt.session.id);
|
|
9187
9909
|
if (reason === "cancel" || reason === "release" || reason === "complete" || reason === "retry") {
|
|
9188
9910
|
const task = this.store.get(taskId);
|
|
9189
9911
|
if (task) this.verifier.clear(task);
|
|
@@ -9192,9 +9914,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9192
9914
|
return attempt;
|
|
9193
9915
|
}
|
|
9194
9916
|
endAllAttempts(reason) {
|
|
9195
|
-
|
|
9917
|
+
const ended = [...this.attempts.values()];
|
|
9918
|
+
for (const { taskId } of ended) this.endAttempt(taskId, reason);
|
|
9196
9919
|
this.verifier.reset();
|
|
9197
9920
|
if (reason === "load") this.output.reset();
|
|
9921
|
+
return ended;
|
|
9198
9922
|
}
|
|
9199
9923
|
};
|
|
9200
9924
|
|
|
@@ -9324,29 +10048,29 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
|
|
|
9324
10048
|
};
|
|
9325
10049
|
|
|
9326
10050
|
// src/services/ModelDiscovery.ts
|
|
9327
|
-
import * as
|
|
10051
|
+
import * as path14 from "path";
|
|
9328
10052
|
import { exec as exec3, spawn } from "child_process";
|
|
9329
10053
|
import { promisify as promisify3 } from "util";
|
|
9330
10054
|
import * as fsSync3 from "fs";
|
|
9331
10055
|
import * as osMod from "os";
|
|
9332
10056
|
|
|
9333
10057
|
// src/utils/globalDataDir.ts
|
|
9334
|
-
import * as
|
|
9335
|
-
import * as
|
|
9336
|
-
import * as
|
|
10058
|
+
import * as fs6 from "fs";
|
|
10059
|
+
import * as path12 from "path";
|
|
10060
|
+
import * as os5 from "os";
|
|
9337
10061
|
function globalDataDir() {
|
|
9338
|
-
return
|
|
10062
|
+
return path12.join(os5.homedir(), ".ordewell");
|
|
9339
10063
|
}
|
|
9340
10064
|
function copyDirSync(src, dest) {
|
|
9341
|
-
|
|
9342
|
-
const entries =
|
|
10065
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
10066
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
9343
10067
|
for (const entry of entries) {
|
|
9344
|
-
const srcPath =
|
|
9345
|
-
const destPath =
|
|
10068
|
+
const srcPath = path12.join(src, entry.name);
|
|
10069
|
+
const destPath = path12.join(dest, entry.name);
|
|
9346
10070
|
if (entry.isDirectory()) {
|
|
9347
10071
|
copyDirSync(srcPath, destPath);
|
|
9348
10072
|
} else {
|
|
9349
|
-
|
|
10073
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
9350
10074
|
}
|
|
9351
10075
|
}
|
|
9352
10076
|
}
|
|
@@ -9354,50 +10078,29 @@ var migrated = false;
|
|
|
9354
10078
|
function migrateOldConfigDir() {
|
|
9355
10079
|
if (migrated) return;
|
|
9356
10080
|
migrated = true;
|
|
9357
|
-
const oldDir =
|
|
9358
|
-
if (!
|
|
10081
|
+
const oldDir = path12.join(os5.homedir(), ".config", "ordewell");
|
|
10082
|
+
if (!fs6.existsSync(oldDir)) return;
|
|
9359
10083
|
const newDir = globalDataDir();
|
|
9360
10084
|
const liftFile = (name) => {
|
|
9361
|
-
const oldFile =
|
|
9362
|
-
const newFile =
|
|
9363
|
-
if (
|
|
9364
|
-
|
|
9365
|
-
|
|
10085
|
+
const oldFile = path12.join(oldDir, name);
|
|
10086
|
+
const newFile = path12.join(newDir, name);
|
|
10087
|
+
if (fs6.existsSync(oldFile) && !fs6.existsSync(newFile)) {
|
|
10088
|
+
fs6.mkdirSync(newDir, { recursive: true });
|
|
10089
|
+
fs6.copyFileSync(oldFile, newFile);
|
|
9366
10090
|
}
|
|
9367
10091
|
};
|
|
9368
10092
|
liftFile("settings.json");
|
|
9369
10093
|
liftFile(".env");
|
|
9370
|
-
const oldPlugins =
|
|
9371
|
-
const newPlugins =
|
|
9372
|
-
if (
|
|
10094
|
+
const oldPlugins = path12.join(oldDir, "plugins");
|
|
10095
|
+
const newPlugins = path12.join(newDir, "plugins");
|
|
10096
|
+
if (fs6.existsSync(oldPlugins) && !fs6.existsSync(newPlugins)) {
|
|
9373
10097
|
copyDirSync(oldPlugins, newPlugins);
|
|
9374
10098
|
}
|
|
9375
10099
|
}
|
|
9376
10100
|
|
|
9377
10101
|
// src/utils/launch.ts
|
|
9378
10102
|
import * as fsSync2 from "fs";
|
|
9379
|
-
import * as
|
|
9380
|
-
|
|
9381
|
-
// src/utils/shell.ts
|
|
9382
|
-
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
|
|
9383
|
-
function stripAnsi(text) {
|
|
9384
|
-
return text.replace(ANSI_RE, "");
|
|
9385
|
-
}
|
|
9386
|
-
function posixShellQuote(s) {
|
|
9387
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
9388
|
-
}
|
|
9389
|
-
function buildShellInvocation(command, args) {
|
|
9390
|
-
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
9391
|
-
return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
|
|
9392
|
-
}
|
|
9393
|
-
function wrapWithPty(command, args, opts = {}) {
|
|
9394
|
-
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
9395
|
-
const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
|
|
9396
|
-
const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
|
|
9397
|
-
return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
|
|
9398
|
-
}
|
|
9399
|
-
|
|
9400
|
-
// src/utils/launch.ts
|
|
10103
|
+
import * as path13 from "path";
|
|
9401
10104
|
var DIRECT_EXTENSIONS = [".exe", ".com"];
|
|
9402
10105
|
var BATCH_EXTENSIONS = [".cmd", ".bat"];
|
|
9403
10106
|
var POWERSHELL_EXTENSIONS = [".ps1"];
|
|
@@ -9405,7 +10108,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
|
|
|
9405
10108
|
var WINDOWS_MAX_COMMAND_LINE = 32767;
|
|
9406
10109
|
function defaultPowerShell() {
|
|
9407
10110
|
const root = process.env.SystemRoot || process.env.windir;
|
|
9408
|
-
return root ?
|
|
10111
|
+
return root ? path13.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
9409
10112
|
}
|
|
9410
10113
|
var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
|
|
9411
10114
|
var CommandLineTooLongError = class extends Error {
|
|
@@ -9457,7 +10160,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
|
|
|
9457
10160
|
};
|
|
9458
10161
|
function isResolvableOnPosixPath(command, PATH, exists) {
|
|
9459
10162
|
if (command.includes("/")) return exists(command);
|
|
9460
|
-
return PATH.split(
|
|
10163
|
+
return PATH.split(path13.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path13.join(dir, command)));
|
|
9461
10164
|
}
|
|
9462
10165
|
function isExecutableResolved(command, plan, PATH, deps = {}) {
|
|
9463
10166
|
if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
|
|
@@ -9497,9 +10200,9 @@ function cmdArgs(line) {
|
|
|
9497
10200
|
return ["/d", "/s", "/c", `"${line}"`];
|
|
9498
10201
|
}
|
|
9499
10202
|
function candidateExtensions(command, pathExt) {
|
|
9500
|
-
const declared = pathExt.split(
|
|
10203
|
+
const declared = pathExt.split(path13.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
9501
10204
|
const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
|
|
9502
|
-
const ext =
|
|
10205
|
+
const ext = path13.win32.extname(command).toLowerCase();
|
|
9503
10206
|
if (ext && known.has(ext)) {
|
|
9504
10207
|
const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
|
|
9505
10208
|
return { direct: [], cmd: [], powershell: [], [route]: [""] };
|
|
@@ -9520,13 +10223,13 @@ async function findWindowsExecutables(command, deps) {
|
|
|
9520
10223
|
const exists = deps.exists ?? defaultExists2;
|
|
9521
10224
|
const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
|
|
9522
10225
|
const byRoute = candidateExtensions(command, pathExt);
|
|
9523
|
-
const dirs =
|
|
9524
|
-
const base =
|
|
10226
|
+
const dirs = path13.win32.basename(command) !== command ? [path13.win32.dirname(path13.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path13.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
10227
|
+
const base = path13.win32.basename(command);
|
|
9525
10228
|
const found = [];
|
|
9526
10229
|
for (const route of ROUTES) {
|
|
9527
10230
|
outer: for (const ext of byRoute[route]) {
|
|
9528
10231
|
for (const dir of dirs) {
|
|
9529
|
-
const candidate =
|
|
10232
|
+
const candidate = path13.win32.join(dir, `${base}${ext}`);
|
|
9530
10233
|
if (exists(candidate)) {
|
|
9531
10234
|
found.push({ file: candidate, route });
|
|
9532
10235
|
break outer;
|
|
@@ -9572,10 +10275,10 @@ async function planShellLaunch(command, args, deps = {}) {
|
|
|
9572
10275
|
}
|
|
9573
10276
|
|
|
9574
10277
|
// src/utils/processTree.ts
|
|
9575
|
-
import { execFile as
|
|
10278
|
+
import { execFile as execFile3 } from "child_process";
|
|
9576
10279
|
var HARD_KILL_DELAY_MS = 5e3;
|
|
9577
10280
|
function defaultExecFile2(file, args, cb) {
|
|
9578
|
-
|
|
10281
|
+
execFile3(file, args, (err) => cb(err ?? null));
|
|
9579
10282
|
}
|
|
9580
10283
|
function killTree(proc, deps = {}) {
|
|
9581
10284
|
if (!proc || proc.killed || typeof proc.exitCode === "number") return;
|
|
@@ -9621,7 +10324,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9621
10324
|
const PATH = await augmentedPath();
|
|
9622
10325
|
const launch = await planDirectLaunch(command, args).catch(() => null);
|
|
9623
10326
|
if (!launch) return null;
|
|
9624
|
-
return new Promise((
|
|
10327
|
+
return new Promise((resolve7) => {
|
|
9625
10328
|
let child;
|
|
9626
10329
|
try {
|
|
9627
10330
|
child = spawn(launch.file, launch.args, {
|
|
@@ -9630,7 +10333,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9630
10333
|
windowsVerbatimArguments: launch.verbatim
|
|
9631
10334
|
});
|
|
9632
10335
|
} catch {
|
|
9633
|
-
|
|
10336
|
+
resolve7(null);
|
|
9634
10337
|
return;
|
|
9635
10338
|
}
|
|
9636
10339
|
let settled = false;
|
|
@@ -9639,7 +10342,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9639
10342
|
settled = true;
|
|
9640
10343
|
clearTimeout(timer);
|
|
9641
10344
|
killTree(child);
|
|
9642
|
-
|
|
10345
|
+
resolve7(value);
|
|
9643
10346
|
};
|
|
9644
10347
|
const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
|
|
9645
10348
|
child.on("error", () => finish(null));
|
|
@@ -9706,7 +10409,7 @@ function geminiDiscoveryCacheKey(apiKey) {
|
|
|
9706
10409
|
return {
|
|
9707
10410
|
key: `gemini-models:${hash}`,
|
|
9708
10411
|
dir,
|
|
9709
|
-
file:
|
|
10412
|
+
file: path14.join(dir, `gemini_models_${hash}.json`)
|
|
9710
10413
|
};
|
|
9711
10414
|
}
|
|
9712
10415
|
function readGeminiCache(apiKey) {
|
|
@@ -10229,8 +10932,8 @@ var ModelDiscovery = class {
|
|
|
10229
10932
|
};
|
|
10230
10933
|
|
|
10231
10934
|
// src/services/ModelCatalog.ts
|
|
10232
|
-
import * as
|
|
10233
|
-
import * as
|
|
10935
|
+
import * as fs7 from "fs";
|
|
10936
|
+
import * as path15 from "path";
|
|
10234
10937
|
var CACHE_KEY = "model-catalog";
|
|
10235
10938
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
10236
10939
|
function storageKey(baseUrl) {
|
|
@@ -10282,17 +10985,17 @@ var ModelCatalog = class {
|
|
|
10282
10985
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
10283
10986
|
const key = storageKey(url);
|
|
10284
10987
|
const cacheDir = globalCacheDir();
|
|
10285
|
-
const cacheFile =
|
|
10286
|
-
if (
|
|
10988
|
+
const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
10989
|
+
if (fs7.existsSync(cacheFile)) fs7.rmSync(cacheFile);
|
|
10287
10990
|
} catch {
|
|
10288
10991
|
}
|
|
10289
10992
|
}
|
|
10290
10993
|
static readCache(key) {
|
|
10291
10994
|
try {
|
|
10292
10995
|
const cacheDir = globalCacheDir();
|
|
10293
|
-
const cacheFile =
|
|
10294
|
-
if (!
|
|
10295
|
-
const raw =
|
|
10996
|
+
const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
10997
|
+
if (!fs7.existsSync(cacheFile)) return null;
|
|
10998
|
+
const raw = fs7.readFileSync(cacheFile, "utf8");
|
|
10296
10999
|
const data = JSON.parse(raw);
|
|
10297
11000
|
if (Date.now() - data.cachedAt > CACHE_TTL_MS) return null;
|
|
10298
11001
|
return JSON.stringify(data.models);
|
|
@@ -10303,9 +11006,9 @@ var ModelCatalog = class {
|
|
|
10303
11006
|
static writeCache(key, models) {
|
|
10304
11007
|
try {
|
|
10305
11008
|
const cacheDir = globalCacheDir();
|
|
10306
|
-
|
|
10307
|
-
const cacheFile =
|
|
10308
|
-
|
|
11009
|
+
fs7.mkdirSync(cacheDir, { recursive: true });
|
|
11010
|
+
const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
11011
|
+
fs7.writeFileSync(
|
|
10309
11012
|
cacheFile,
|
|
10310
11013
|
JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
|
|
10311
11014
|
);
|
|
@@ -10681,71 +11384,12 @@ var RunnerInstallation = class {
|
|
|
10681
11384
|
}
|
|
10682
11385
|
};
|
|
10683
11386
|
|
|
10684
|
-
// src/services/settingsEnvAllowlist.ts
|
|
10685
|
-
var ORDEWELL_SETTABLE_ENV = [
|
|
10686
|
-
"AI_PROVIDER",
|
|
10687
|
-
"ORCHESTRATOR_MODEL",
|
|
10688
|
-
"ORDEWELL_SUBAGENT_MODEL",
|
|
10689
|
-
"ORDEWELL_PLANNER_EFFORT",
|
|
10690
|
-
"ORDEWELL_RESEARCH_ENABLED",
|
|
10691
|
-
"ORDEWELL_MAX_PARALLEL"
|
|
10692
|
-
];
|
|
10693
|
-
var SETTINGS_ENV_REFUSED = [
|
|
10694
|
-
// Approval posture and pre-approved scope.
|
|
10695
|
-
"ORDEWELL_AUTONOMOUS_MODE",
|
|
10696
|
-
"ORDEWELL_APPROVAL_MODE",
|
|
10697
|
-
"ORDEWELL_APPROVAL_ALLOW",
|
|
10698
|
-
// Where settings are persisted.
|
|
10699
|
-
"ORDEWELL_SETTINGS_PATH",
|
|
10700
|
-
// Runtime options and dynamic loaders.
|
|
10701
|
-
"NODE_OPTIONS",
|
|
10702
|
-
"NODE_REPL_EXTERNAL_MODULE",
|
|
10703
|
-
"NODE_PATH",
|
|
10704
|
-
"ELECTRON_RUN_AS_NODE",
|
|
10705
|
-
"LD_PRELOAD",
|
|
10706
|
-
"LD_LIBRARY_PATH",
|
|
10707
|
-
"LD_AUDIT",
|
|
10708
|
-
"DYLD_INSERT_LIBRARIES",
|
|
10709
|
-
"DYLD_LIBRARY_PATH",
|
|
10710
|
-
"DYLD_FRAMEWORK_PATH",
|
|
10711
|
-
// Executable resolution.
|
|
10712
|
-
"PATH",
|
|
10713
|
-
"PATHEXT",
|
|
10714
|
-
"NODE"
|
|
10715
|
-
];
|
|
10716
|
-
var REFUSED = new Set(SETTINGS_ENV_REFUSED);
|
|
10717
|
-
function providerEnvKeys() {
|
|
10718
|
-
const keys = [];
|
|
10719
|
-
for (const meta of Object.values(ALL_PROVIDERS)) {
|
|
10720
|
-
if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
|
|
10721
|
-
if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
|
|
10722
|
-
for (const detect of meta.detectEnvVars) keys.push(detect);
|
|
10723
|
-
}
|
|
10724
|
-
return keys;
|
|
10725
|
-
}
|
|
10726
|
-
var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
|
|
10727
|
-
var SETTINGS_ENV_ALLOWLIST = new Set(
|
|
10728
|
-
[...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
|
|
10729
|
-
);
|
|
10730
|
-
function admitSettingsEnv(env) {
|
|
10731
|
-
const accepted = {};
|
|
10732
|
-
const rejected = [];
|
|
10733
|
-
for (const [key, value] of Object.entries(env)) {
|
|
10734
|
-
if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
|
|
10735
|
-
rejected.push(key);
|
|
10736
|
-
continue;
|
|
10737
|
-
}
|
|
10738
|
-
accepted[key] = value;
|
|
10739
|
-
}
|
|
10740
|
-
return { accepted, rejected };
|
|
10741
|
-
}
|
|
10742
|
-
|
|
10743
11387
|
// src/services/harness/CliAgentAiService.ts
|
|
10744
11388
|
import { spawn as nodeSpawn } from "child_process";
|
|
10745
11389
|
|
|
10746
11390
|
// src/utils/workspace.ts
|
|
10747
|
-
import * as
|
|
10748
|
-
import * as
|
|
11391
|
+
import * as fs8 from "fs";
|
|
11392
|
+
import * as path16 from "path";
|
|
10749
11393
|
var WorkspaceNotFoundError = class extends Error {
|
|
10750
11394
|
constructor(workspace) {
|
|
10751
11395
|
super(`Workspace "${workspace}" does not exist.`);
|
|
@@ -10756,7 +11400,7 @@ var WorkspaceNotFoundError = class extends Error {
|
|
|
10756
11400
|
};
|
|
10757
11401
|
function defaultIsDirectory(candidate) {
|
|
10758
11402
|
try {
|
|
10759
|
-
return
|
|
11403
|
+
return fs8.statSync(candidate).isDirectory();
|
|
10760
11404
|
} catch {
|
|
10761
11405
|
return false;
|
|
10762
11406
|
}
|
|
@@ -10792,8 +11436,8 @@ var PROJECT_MARKER_FILES = [
|
|
|
10792
11436
|
"CMakeLists.txt"
|
|
10793
11437
|
];
|
|
10794
11438
|
function assertWorkspaceIsProject(workspace, deps = {}) {
|
|
10795
|
-
const realpath = deps.realpath ?? ((candidate) =>
|
|
10796
|
-
const exists = deps.exists ?? ((candidate) =>
|
|
11439
|
+
const realpath = deps.realpath ?? ((candidate) => fs8.realpathSync(candidate));
|
|
11440
|
+
const exists = deps.exists ?? ((candidate) => fs8.existsSync(candidate));
|
|
10797
11441
|
let resolved;
|
|
10798
11442
|
try {
|
|
10799
11443
|
resolved = realpath(workspace);
|
|
@@ -10801,7 +11445,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
|
|
|
10801
11445
|
throw new WorkspaceNotAProjectError(workspace);
|
|
10802
11446
|
}
|
|
10803
11447
|
const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
|
|
10804
|
-
(marker2) => exists(
|
|
11448
|
+
(marker2) => exists(path16.join(resolved, marker2))
|
|
10805
11449
|
);
|
|
10806
11450
|
if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
|
|
10807
11451
|
}
|
|
@@ -10869,13 +11513,14 @@ var StdioAgentAdapter = class {
|
|
|
10869
11513
|
}
|
|
10870
11514
|
async start(opts) {
|
|
10871
11515
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
10872
|
-
this.processEnded = new Promise((
|
|
10873
|
-
this.markEnded =
|
|
11516
|
+
this.processEnded = new Promise((resolve7) => {
|
|
11517
|
+
this.markEnded = resolve7;
|
|
10874
11518
|
});
|
|
10875
11519
|
const spec = this.spawnSpec(opts);
|
|
10876
11520
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
10877
11521
|
const PATH = await resolvePath();
|
|
10878
|
-
|
|
11522
|
+
const workspace = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
11523
|
+
this.spawnEnv = withPath(process.env, PATH, { ...workspace, ...spec.env });
|
|
10879
11524
|
const launch = await planDirectLaunch(spec.command, spec.args, {
|
|
10880
11525
|
platform: this.deps.platform,
|
|
10881
11526
|
resolvePath
|
|
@@ -10921,7 +11566,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10921
11566
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
10922
11567
|
return;
|
|
10923
11568
|
}
|
|
10924
|
-
await new Promise((
|
|
11569
|
+
await new Promise((resolve7) => {
|
|
10925
11570
|
let settled = false;
|
|
10926
11571
|
const finish = () => {
|
|
10927
11572
|
if (settled) return;
|
|
@@ -10932,7 +11577,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10932
11577
|
signal?.removeEventListener("abort", onAbort);
|
|
10933
11578
|
this.process?.removeListener("exit", onExit);
|
|
10934
11579
|
this.process?.removeListener("error", onExit);
|
|
10935
|
-
|
|
11580
|
+
resolve7();
|
|
10936
11581
|
};
|
|
10937
11582
|
const emit = (event) => {
|
|
10938
11583
|
onEvent(event);
|
|
@@ -11147,14 +11792,14 @@ function codexSandboxUnavailableMessage() {
|
|
|
11147
11792
|
].join("\n");
|
|
11148
11793
|
}
|
|
11149
11794
|
function runProbe(deps, cwd, env, flags) {
|
|
11150
|
-
return new Promise((
|
|
11795
|
+
return new Promise((resolve7) => {
|
|
11151
11796
|
let output = "";
|
|
11152
11797
|
let settled = false;
|
|
11153
11798
|
const finish = (code) => {
|
|
11154
11799
|
if (settled) return;
|
|
11155
11800
|
settled = true;
|
|
11156
11801
|
clearTimeout(timer);
|
|
11157
|
-
|
|
11802
|
+
resolve7({ code, output });
|
|
11158
11803
|
};
|
|
11159
11804
|
const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
11160
11805
|
timer.unref?.();
|
|
@@ -11237,8 +11882,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11237
11882
|
async handshake(opts) {
|
|
11238
11883
|
this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
|
|
11239
11884
|
if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
|
|
11240
|
-
const ready = new Promise((
|
|
11241
|
-
this.settleHandshake =
|
|
11885
|
+
const ready = new Promise((resolve7) => {
|
|
11886
|
+
this.settleHandshake = resolve7;
|
|
11242
11887
|
});
|
|
11243
11888
|
this.writeLine({
|
|
11244
11889
|
jsonrpc: "2.0",
|
|
@@ -11251,8 +11896,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11251
11896
|
// A binary that rejects `app-server` outright dies immediately; waiting
|
|
11252
11897
|
// out the timeout would turn a one-line diagnostic into a 30s stall.
|
|
11253
11898
|
this.processEnded.then(() => false),
|
|
11254
|
-
new Promise((
|
|
11255
|
-
const t = setTimeout(() =>
|
|
11899
|
+
new Promise((resolve7) => {
|
|
11900
|
+
const t = setTimeout(() => resolve7(false), HANDSHAKE_TIMEOUT_MS);
|
|
11256
11901
|
t.unref?.();
|
|
11257
11902
|
})
|
|
11258
11903
|
]);
|
|
@@ -11529,12 +12174,12 @@ function describeError(err) {
|
|
|
11529
12174
|
return lines.join(": ");
|
|
11530
12175
|
}
|
|
11531
12176
|
function delay(ms, signal) {
|
|
11532
|
-
return new Promise((
|
|
11533
|
-
const timer = setTimeout(
|
|
12177
|
+
return new Promise((resolve7) => {
|
|
12178
|
+
const timer = setTimeout(resolve7, ms);
|
|
11534
12179
|
timer.unref?.();
|
|
11535
12180
|
signal?.addEventListener("abort", () => {
|
|
11536
12181
|
clearTimeout(timer);
|
|
11537
|
-
|
|
12182
|
+
resolve7();
|
|
11538
12183
|
}, { once: true });
|
|
11539
12184
|
});
|
|
11540
12185
|
}
|
|
@@ -11568,17 +12213,17 @@ var OpenCodeAdapter = class {
|
|
|
11568
12213
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
11569
12214
|
}
|
|
11570
12215
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
11571
|
-
env: withPath(process.env, PATH),
|
|
12216
|
+
env: withPath(process.env, PATH, await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd)),
|
|
11572
12217
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11573
12218
|
cwd: opts.cwd,
|
|
11574
12219
|
windowsVerbatimArguments: launch.verbatim
|
|
11575
12220
|
});
|
|
11576
|
-
const banner = new Promise((
|
|
12221
|
+
const banner = new Promise((resolve7) => {
|
|
11577
12222
|
let seen = "";
|
|
11578
12223
|
const scan = (chunk) => {
|
|
11579
12224
|
seen += chunk.toString();
|
|
11580
12225
|
const match = seen.match(/https?:\/\/[^\s]+/);
|
|
11581
|
-
if (match)
|
|
12226
|
+
if (match) resolve7(match[0].replace(/[.,)]$/, ""));
|
|
11582
12227
|
};
|
|
11583
12228
|
this.process.stdout?.on("data", scan);
|
|
11584
12229
|
this.process.stderr?.on("data", (chunk) => {
|
|
@@ -11587,15 +12232,15 @@ var OpenCodeAdapter = class {
|
|
|
11587
12232
|
});
|
|
11588
12233
|
this.process.on("exit", () => {
|
|
11589
12234
|
this.exited = true;
|
|
11590
|
-
|
|
12235
|
+
resolve7(null);
|
|
11591
12236
|
});
|
|
11592
12237
|
this.process.on("error", (err) => {
|
|
11593
12238
|
this.exited = true;
|
|
11594
12239
|
this.stderrTail = (this.stderrTail + `
|
|
11595
12240
|
${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
11596
|
-
|
|
12241
|
+
resolve7(null);
|
|
11597
12242
|
});
|
|
11598
|
-
const timer = setTimeout(() =>
|
|
12243
|
+
const timer = setTimeout(() => resolve7(null), SERVER_READY_TIMEOUT_MS);
|
|
11599
12244
|
timer.unref?.();
|
|
11600
12245
|
});
|
|
11601
12246
|
this.baseUrl = await banner;
|
|
@@ -11628,8 +12273,8 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11628
12273
|
const streamAbort = new AbortController();
|
|
11629
12274
|
let connected = () => {
|
|
11630
12275
|
};
|
|
11631
|
-
const streamReady = new Promise((
|
|
11632
|
-
connected =
|
|
12276
|
+
const streamReady = new Promise((resolve7) => {
|
|
12277
|
+
connected = resolve7;
|
|
11633
12278
|
});
|
|
11634
12279
|
const live = this.streamEvents(
|
|
11635
12280
|
streamAbort.signal,
|
|
@@ -11833,14 +12478,14 @@ ${part.text}` : part.text });
|
|
|
11833
12478
|
}
|
|
11834
12479
|
}
|
|
11835
12480
|
}
|
|
11836
|
-
async json(method,
|
|
11837
|
-
const response = await this.deps.fetch(`${this.baseUrl}${
|
|
12481
|
+
async json(method, path25, body, signal) {
|
|
12482
|
+
const response = await this.deps.fetch(`${this.baseUrl}${path25}`, {
|
|
11838
12483
|
method,
|
|
11839
12484
|
headers: { "content-type": "application/json" },
|
|
11840
12485
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
11841
12486
|
...signal ? { signal } : {}
|
|
11842
12487
|
});
|
|
11843
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
12488
|
+
if (!response.ok) throw new Error(`${method} ${path25} failed: ${response.status} ${response.statusText}`);
|
|
11844
12489
|
return await response.json().catch(() => null);
|
|
11845
12490
|
}
|
|
11846
12491
|
nativeSessionId() {
|
|
@@ -12294,8 +12939,8 @@ var CliAgentAiService = class {
|
|
|
12294
12939
|
this.lastNativeSessionId = previousSessionId;
|
|
12295
12940
|
}
|
|
12296
12941
|
}
|
|
12297
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
12298
|
-
const contextStr = await collectResearchContext(
|
|
12942
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
12943
|
+
const contextStr = await collectResearchContext(fs15, runners);
|
|
12299
12944
|
const researchLog = [
|
|
12300
12945
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
12301
12946
|
];
|
|
@@ -12509,33 +13154,9 @@ var PlannerConversation = class {
|
|
|
12509
13154
|
if (m.role !== "user" || index === 0) return [];
|
|
12510
13155
|
const line = m.content.split("\n")[0];
|
|
12511
13156
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
12512
|
-
return [{ index, preview, timestamp: m.timestamp }];
|
|
13157
|
+
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
12513
13158
|
});
|
|
12514
13159
|
}
|
|
12515
|
-
/**
|
|
12516
|
-
* Cut the dialogue back to just before the user message at `index` (a
|
|
12517
|
-
* position in the transcript), discarding it and everything after. The task
|
|
12518
|
-
* list is the host's and rides along untouched — rewinding moves where the
|
|
12519
|
-
* conversation resumes, not what the plan is. The planner's research trace
|
|
12520
|
-
* goes back to the same point, by time: its entries carry no link to the
|
|
12521
|
-
* message that caused them.
|
|
12522
|
-
*/
|
|
12523
|
-
rewind(index) {
|
|
12524
|
-
this.assertIdle("rewind the conversation");
|
|
12525
|
-
const plan = this.requirePlan();
|
|
12526
|
-
if (!this.rewindTargets().some((t) => t.index === index)) {
|
|
12527
|
-
if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
|
|
12528
|
-
throw new ConversationEditError(this.transcript[0]?.kind === "compaction" ? "The conversation was condensed there \u2014 a rewind cannot reach back past the summary." : "The first message is the goal \u2014 start a new session to change it.");
|
|
12529
|
-
}
|
|
12530
|
-
const cutoff = this.transcript[index].timestamp;
|
|
12531
|
-
const rewound = this.host.mutate(() => {
|
|
12532
|
-
plan.conversationHistory = this.transcript.slice(0, index);
|
|
12533
|
-
plan.researchLog = (plan.researchLog ?? []).filter((e) => e.timestamp < cutoff);
|
|
12534
|
-
return true;
|
|
12535
|
-
}, () => this.host.broadcastPlan());
|
|
12536
|
-
this.reset();
|
|
12537
|
-
return rewound;
|
|
12538
|
-
}
|
|
12539
13160
|
/**
|
|
12540
13161
|
* Replace the transcript with a summary of it, keeping the last two
|
|
12541
13162
|
* exchanges as they were, and drop the live context so the next message
|
|
@@ -12611,6 +13232,28 @@ var PlannerConversation = class {
|
|
|
12611
13232
|
researchLog: plan?.researchLog ?? []
|
|
12612
13233
|
});
|
|
12613
13234
|
}
|
|
13235
|
+
/**
|
|
13236
|
+
* A copy of the dialogue as it stood just before the user message at
|
|
13237
|
+
* `index` (a position from {@link rewindTargets}) — what a rewind forks
|
|
13238
|
+
* from — plus that message's full text, for a surface to offer back. This
|
|
13239
|
+
* conversation is not touched. The research trace is cut at the same point
|
|
13240
|
+
* by time: its entries carry no link to the message that caused them.
|
|
13241
|
+
*/
|
|
13242
|
+
cloneBefore(index) {
|
|
13243
|
+
this.assertIdle("rewind the conversation");
|
|
13244
|
+
if (!this.rewindTargets().some((t) => t.index === index)) {
|
|
13245
|
+
if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
|
|
13246
|
+
throw new ConversationEditError(this.transcript[0]?.kind === "compaction" ? "The conversation was condensed there \u2014 a rewind cannot reach back past the summary." : "The first message is the goal \u2014 start a new session to change it.");
|
|
13247
|
+
}
|
|
13248
|
+
const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
|
|
13249
|
+
return {
|
|
13250
|
+
dialogue: structuredClone({
|
|
13251
|
+
conversationHistory: this.transcript.slice(0, index),
|
|
13252
|
+
researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
|
|
13253
|
+
}),
|
|
13254
|
+
rewoundMessage
|
|
13255
|
+
};
|
|
13256
|
+
}
|
|
12614
13257
|
/** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
|
|
12615
13258
|
recordModification(request, requestedAt, taskCount) {
|
|
12616
13259
|
this.append("user", request, { timestamp: requestedAt });
|
|
@@ -13196,25 +13839,25 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
13196
13839
|
}
|
|
13197
13840
|
|
|
13198
13841
|
// src/services/SkillsService.ts
|
|
13199
|
-
import * as
|
|
13200
|
-
import * as
|
|
13842
|
+
import * as fs9 from "fs";
|
|
13843
|
+
import * as path18 from "path";
|
|
13201
13844
|
|
|
13202
13845
|
// src/services/builtinSkills.ts
|
|
13203
|
-
import * as
|
|
13846
|
+
import * as path17 from "path";
|
|
13204
13847
|
import { fileURLToPath } from "url";
|
|
13205
13848
|
function builtinSkillsDir() {
|
|
13206
|
-
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname :
|
|
13849
|
+
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path17.dirname(fileURLToPath(
|
|
13207
13850
|
// @ts-expect-error - import.meta is only valid under an ESM module setting
|
|
13208
13851
|
import.meta.url
|
|
13209
13852
|
));
|
|
13210
|
-
return
|
|
13853
|
+
return path17.join(dir, "..", "skills");
|
|
13211
13854
|
}
|
|
13212
13855
|
|
|
13213
13856
|
// src/services/SkillsService.ts
|
|
13214
13857
|
var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
|
|
13215
13858
|
var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
|
|
13216
13859
|
function parseSkillFile(filePath, name, source) {
|
|
13217
|
-
const raw =
|
|
13860
|
+
const raw = fs9.readFileSync(filePath, "utf8");
|
|
13218
13861
|
const frontmatter = {};
|
|
13219
13862
|
let content = raw;
|
|
13220
13863
|
if (raw.startsWith("---\n")) {
|
|
@@ -13243,23 +13886,23 @@ function parseSkillFile(filePath, name, source) {
|
|
|
13243
13886
|
return { name, description: metadata.description, metadata, content, path: filePath, source };
|
|
13244
13887
|
}
|
|
13245
13888
|
function copyDirSync2(src, dest) {
|
|
13246
|
-
|
|
13247
|
-
const entries =
|
|
13889
|
+
fs9.mkdirSync(dest, { recursive: true });
|
|
13890
|
+
const entries = fs9.readdirSync(src, { withFileTypes: true });
|
|
13248
13891
|
for (const entry of entries) {
|
|
13249
|
-
const srcPath =
|
|
13250
|
-
const destPath =
|
|
13892
|
+
const srcPath = path18.join(src, entry.name);
|
|
13893
|
+
const destPath = path18.join(dest, entry.name);
|
|
13251
13894
|
if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
|
|
13252
|
-
else
|
|
13895
|
+
else fs9.copyFileSync(srcPath, destPath);
|
|
13253
13896
|
}
|
|
13254
13897
|
}
|
|
13255
13898
|
function readDir(dir, source) {
|
|
13256
|
-
if (!
|
|
13257
|
-
const entries =
|
|
13899
|
+
if (!fs9.existsSync(dir)) return [];
|
|
13900
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
13258
13901
|
const skills = [];
|
|
13259
13902
|
for (const entry of entries) {
|
|
13260
13903
|
if (!entry.isDirectory()) continue;
|
|
13261
|
-
const skillFile =
|
|
13262
|
-
if (!
|
|
13904
|
+
const skillFile = path18.join(dir, entry.name, "SKILL.md");
|
|
13905
|
+
if (!fs9.existsSync(skillFile)) continue;
|
|
13263
13906
|
const parsed = parseSkillFile(skillFile, entry.name, source);
|
|
13264
13907
|
if (parsed) skills.push(parsed);
|
|
13265
13908
|
}
|
|
@@ -13271,22 +13914,22 @@ var SkillsService = class {
|
|
|
13271
13914
|
}
|
|
13272
13915
|
workspaceRoot;
|
|
13273
13916
|
localDir() {
|
|
13274
|
-
return this.workspaceRoot ?
|
|
13917
|
+
return this.workspaceRoot ? path18.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
|
|
13275
13918
|
}
|
|
13276
13919
|
globalDir() {
|
|
13277
|
-
return
|
|
13920
|
+
return path18.join(globalDataDir(), "skills");
|
|
13278
13921
|
}
|
|
13279
13922
|
seed(name) {
|
|
13280
|
-
const dest =
|
|
13281
|
-
if (
|
|
13923
|
+
const dest = path18.join(this.globalDir(), name);
|
|
13924
|
+
if (fs9.existsSync(dest)) return false;
|
|
13282
13925
|
let srcDir;
|
|
13283
13926
|
try {
|
|
13284
|
-
srcDir =
|
|
13927
|
+
srcDir = path18.join(builtinSkillsDir(), name);
|
|
13285
13928
|
} catch (err) {
|
|
13286
13929
|
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
13287
13930
|
return false;
|
|
13288
13931
|
}
|
|
13289
|
-
if (!
|
|
13932
|
+
if (!fs9.existsSync(srcDir)) {
|
|
13290
13933
|
console.warn(`Built-in skill not found in package: ${name}`);
|
|
13291
13934
|
return false;
|
|
13292
13935
|
}
|
|
@@ -13300,14 +13943,14 @@ var SkillsService = class {
|
|
|
13300
13943
|
isUnmodifiedRetiredSeed(dir, expectedName) {
|
|
13301
13944
|
let entries;
|
|
13302
13945
|
try {
|
|
13303
|
-
entries =
|
|
13946
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
13304
13947
|
} catch {
|
|
13305
13948
|
return false;
|
|
13306
13949
|
}
|
|
13307
13950
|
if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
|
|
13308
13951
|
let raw;
|
|
13309
13952
|
try {
|
|
13310
|
-
raw =
|
|
13953
|
+
raw = fs9.readFileSync(path18.join(dir, "SKILL.md"), "utf8");
|
|
13311
13954
|
} catch {
|
|
13312
13955
|
return false;
|
|
13313
13956
|
}
|
|
@@ -13324,11 +13967,11 @@ var SkillsService = class {
|
|
|
13324
13967
|
}
|
|
13325
13968
|
pruneRetired() {
|
|
13326
13969
|
for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
|
|
13327
|
-
const dest =
|
|
13328
|
-
if (!
|
|
13970
|
+
const dest = path18.join(this.globalDir(), name);
|
|
13971
|
+
if (!fs9.existsSync(dest)) continue;
|
|
13329
13972
|
if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
|
|
13330
13973
|
try {
|
|
13331
|
-
|
|
13974
|
+
fs9.rmSync(dest, { recursive: true, force: true });
|
|
13332
13975
|
} catch (err) {
|
|
13333
13976
|
console.warn(`Could not prune retired skill ${name}: ${String(err)}`);
|
|
13334
13977
|
}
|
|
@@ -13339,11 +13982,11 @@ var SkillsService = class {
|
|
|
13339
13982
|
if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
|
|
13340
13983
|
const localDir = this.localDir();
|
|
13341
13984
|
if (localDir) {
|
|
13342
|
-
const localFile =
|
|
13343
|
-
if (
|
|
13985
|
+
const localFile = path18.join(localDir, name, "SKILL.md");
|
|
13986
|
+
if (fs9.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
|
|
13344
13987
|
}
|
|
13345
|
-
const globalFile =
|
|
13346
|
-
if (
|
|
13988
|
+
const globalFile = path18.join(this.globalDir(), name, "SKILL.md");
|
|
13989
|
+
if (fs9.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
|
|
13347
13990
|
return void 0;
|
|
13348
13991
|
}
|
|
13349
13992
|
listSkills() {
|
|
@@ -13370,8 +14013,8 @@ function createSkillsService(workspaceRoot) {
|
|
|
13370
14013
|
}
|
|
13371
14014
|
|
|
13372
14015
|
// src/utils/sessionStore.ts
|
|
13373
|
-
import * as
|
|
13374
|
-
import * as
|
|
14016
|
+
import * as fs10 from "fs";
|
|
14017
|
+
import * as path19 from "path";
|
|
13375
14018
|
|
|
13376
14019
|
// src/utils/sessionId.ts
|
|
13377
14020
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -13382,7 +14025,7 @@ function mintSessionId() {
|
|
|
13382
14025
|
// src/utils/sessionStore.ts
|
|
13383
14026
|
var SESSIONS_SUBDIR = "sessions";
|
|
13384
14027
|
function getSessionsDir(baseDir) {
|
|
13385
|
-
return
|
|
14028
|
+
return path19.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
13386
14029
|
}
|
|
13387
14030
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
13388
14031
|
function hasScalarRunner(parsed) {
|
|
@@ -13404,11 +14047,11 @@ function legacySessionFilename(session) {
|
|
|
13404
14047
|
return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
|
|
13405
14048
|
}
|
|
13406
14049
|
function dropLegacyFile(sessionsDir, meta) {
|
|
13407
|
-
const legacy =
|
|
13408
|
-
if (legacy ===
|
|
14050
|
+
const legacy = path19.join(sessionsDir, legacySessionFilename(meta));
|
|
14051
|
+
if (legacy === path19.join(sessionsDir, sessionFilename(meta)) || !fs10.existsSync(legacy)) return;
|
|
13409
14052
|
try {
|
|
13410
|
-
const held = JSON.parse(
|
|
13411
|
-
if (held === meta.id)
|
|
14053
|
+
const held = JSON.parse(fs10.readFileSync(legacy, "utf-8")).meta?.id;
|
|
14054
|
+
if (held === meta.id) fs10.unlinkSync(legacy);
|
|
13412
14055
|
} catch {
|
|
13413
14056
|
}
|
|
13414
14057
|
}
|
|
@@ -13431,18 +14074,18 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
13431
14074
|
plan: JSON.parse(JSON.stringify(plan))
|
|
13432
14075
|
};
|
|
13433
14076
|
const filename = sessionFilename(meta);
|
|
13434
|
-
const filePath =
|
|
13435
|
-
|
|
14077
|
+
const filePath = path19.join(sessionsDir, filename);
|
|
14078
|
+
fs10.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
13436
14079
|
dropLegacyFile(sessionsDir, meta);
|
|
13437
14080
|
return meta;
|
|
13438
14081
|
}
|
|
13439
14082
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
13440
14083
|
const sessionsDir = getSessionsDir(baseDir);
|
|
13441
|
-
if (!
|
|
13442
|
-
const files =
|
|
14084
|
+
if (!fs10.existsSync(sessionsDir)) return [];
|
|
14085
|
+
const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
13443
14086
|
return files.map((f) => {
|
|
13444
14087
|
try {
|
|
13445
|
-
const raw =
|
|
14088
|
+
const raw = fs10.readFileSync(path19.join(sessionsDir, f), "utf-8");
|
|
13446
14089
|
const parsed = JSON.parse(raw);
|
|
13447
14090
|
if (hasScalarRunner(parsed)) {
|
|
13448
14091
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -13457,10 +14100,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
13457
14100
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
13458
14101
|
}
|
|
13459
14102
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
13460
|
-
const files =
|
|
14103
|
+
const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
13461
14104
|
for (const f of files) {
|
|
13462
14105
|
try {
|
|
13463
|
-
const raw =
|
|
14106
|
+
const raw = fs10.readFileSync(path19.join(sessionsDir, f), "utf-8");
|
|
13464
14107
|
const session = JSON.parse(raw);
|
|
13465
14108
|
if (session.meta.id === sessionId) return f;
|
|
13466
14109
|
} catch (err) {
|
|
@@ -13471,11 +14114,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
13471
14114
|
}
|
|
13472
14115
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
13473
14116
|
const sessionsDir = getSessionsDir(baseDir);
|
|
13474
|
-
if (!
|
|
14117
|
+
if (!fs10.existsSync(sessionsDir)) return null;
|
|
13475
14118
|
try {
|
|
13476
14119
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
13477
14120
|
if (file) {
|
|
13478
|
-
const raw =
|
|
14121
|
+
const raw = fs10.readFileSync(path19.join(sessionsDir, file), "utf-8");
|
|
13479
14122
|
const parsed = JSON.parse(raw);
|
|
13480
14123
|
if (hasScalarRunner(parsed)) {
|
|
13481
14124
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -13506,11 +14149,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
13506
14149
|
}
|
|
13507
14150
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
13508
14151
|
const sessionsDir = getSessionsDir(baseDir);
|
|
13509
|
-
if (!
|
|
14152
|
+
if (!fs10.existsSync(sessionsDir)) return false;
|
|
13510
14153
|
try {
|
|
13511
14154
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
13512
14155
|
if (file) {
|
|
13513
|
-
|
|
14156
|
+
fs10.unlinkSync(path19.join(sessionsDir, file));
|
|
13514
14157
|
return true;
|
|
13515
14158
|
}
|
|
13516
14159
|
} catch (err) {
|
|
@@ -14042,26 +14685,30 @@ var Session = class {
|
|
|
14042
14685
|
*/
|
|
14043
14686
|
forkConversation() {
|
|
14044
14687
|
if (!this.plan) throw new ConversationEditError("No planning conversation to fork");
|
|
14045
|
-
|
|
14046
|
-
const sessionId = mintSessionId();
|
|
14047
|
-
const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
|
|
14048
|
-
saveSession(plan, this.goal, this.workspace, sessionId);
|
|
14049
|
-
return { sessionId, goal: this.goal, workspace: this.workspace };
|
|
14688
|
+
return this.saveFork(this.conversation.clone());
|
|
14050
14689
|
}
|
|
14051
14690
|
/**
|
|
14052
|
-
*
|
|
14053
|
-
* position from {@link rewindTargets})
|
|
14054
|
-
*
|
|
14055
|
-
*
|
|
14691
|
+
* Fork the conversation from just before a user message (a transcript
|
|
14692
|
+
* position from {@link rewindTargets}): the fork holds everything said
|
|
14693
|
+
* before it and the current task list, like {@link forkConversation}, and
|
|
14694
|
+
* this session keeps its whole history. The message's full text comes back
|
|
14695
|
+
* so a surface can offer it for resending.
|
|
14056
14696
|
*/
|
|
14057
14697
|
rewindConversation(userMessageIndex) {
|
|
14058
14698
|
if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
|
|
14059
|
-
|
|
14699
|
+
const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
|
|
14700
|
+
return { ...this.saveFork(dialogue), rewoundMessage };
|
|
14701
|
+
}
|
|
14702
|
+
saveFork(dialogue) {
|
|
14703
|
+
const sessionId = mintSessionId();
|
|
14704
|
+
const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
|
|
14705
|
+
saveSession(plan, this.goal, this.workspace, sessionId);
|
|
14706
|
+
return { sessionId, goal: this.goal, workspace: this.workspace };
|
|
14060
14707
|
}
|
|
14061
14708
|
/**
|
|
14062
14709
|
* Condense the conversation on the user's say-so: a hidden planner turn
|
|
14063
14710
|
* summarises it, and the summary replaces everything but the last two
|
|
14064
|
-
* exchanges. Conversation only
|
|
14711
|
+
* exchanges. Conversation only — the tasks, and any run
|
|
14065
14712
|
* executing them, are untouched. Atomic: a failed or stopped turn changes
|
|
14066
14713
|
* nothing.
|
|
14067
14714
|
*/
|
|
@@ -14187,6 +14834,10 @@ var Session = class {
|
|
|
14187
14834
|
}
|
|
14188
14835
|
await this.orchestrator.runTask(taskId);
|
|
14189
14836
|
}
|
|
14837
|
+
/** Start whatever the scheduler can now fit — after the parallel limit was raised mid-run, say. */
|
|
14838
|
+
async reschedule() {
|
|
14839
|
+
await this.orchestrator.tick();
|
|
14840
|
+
}
|
|
14190
14841
|
async retryTask(taskId) {
|
|
14191
14842
|
await this.orchestrator.retryTask(taskId);
|
|
14192
14843
|
this.persist();
|
|
@@ -14334,8 +14985,9 @@ var Session = class {
|
|
|
14334
14985
|
* The opt-in way through a merge conflict: add an AI task, on the conflicted
|
|
14335
14986
|
* task's own runner and model, that merges its branch by hand in a worktree
|
|
14336
14987
|
* of its own. When that task lands, the conflicted one lands through it (see
|
|
14337
|
-
* {@link TaskOrchestrator.linkConflictResolver}).
|
|
14338
|
-
*
|
|
14988
|
+
* {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
|
|
14989
|
+
* conflict is a repair, a new attempt of the same task (ADR-0015); a task is
|
|
14990
|
+
* only ever added to the plan by this call.
|
|
14339
14991
|
*/
|
|
14340
14992
|
async resolveConflictAsTask(taskId) {
|
|
14341
14993
|
if (!this.plan) return null;
|
|
@@ -15066,7 +15718,7 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
15066
15718
|
return {
|
|
15067
15719
|
launch: await planDirectLaunch(command, args, this.launchDeps),
|
|
15068
15720
|
resolvedPath,
|
|
15069
|
-
env: invocation.env,
|
|
15721
|
+
env: { ...opts.env, ...invocation.env },
|
|
15070
15722
|
pty,
|
|
15071
15723
|
submitPromptKey: invocation.submitPromptKey
|
|
15072
15724
|
};
|
|
@@ -15085,11 +15737,11 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
15085
15737
|
};
|
|
15086
15738
|
|
|
15087
15739
|
// src/services/TmuxRunner.ts
|
|
15088
|
-
import { execFile as
|
|
15740
|
+
import { execFile as execFile4 } from "child_process";
|
|
15089
15741
|
import { promisify as promisify5 } from "util";
|
|
15090
15742
|
import { tmpdir } from "os";
|
|
15091
|
-
import { join as
|
|
15092
|
-
import { existsSync as
|
|
15743
|
+
import { join as join17 } from "path";
|
|
15744
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
15093
15745
|
|
|
15094
15746
|
// src/utils/tmux.ts
|
|
15095
15747
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -15140,7 +15792,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
|
|
|
15140
15792
|
|
|
15141
15793
|
// src/services/TmuxRunner.ts
|
|
15142
15794
|
var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
|
|
15143
|
-
var execFileAsync2 = promisify5(
|
|
15795
|
+
var execFileAsync2 = promisify5(execFile4);
|
|
15144
15796
|
var defaultExecFile3 = async (command, args) => {
|
|
15145
15797
|
const { stdout, stderr } = await execFileAsync2(command, args);
|
|
15146
15798
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
@@ -15196,7 +15848,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
15196
15848
|
if (this.exited) return;
|
|
15197
15849
|
let content;
|
|
15198
15850
|
try {
|
|
15199
|
-
content =
|
|
15851
|
+
content = existsSync11(this.logPath) ? readFileSync8(this.logPath, "utf8") : "";
|
|
15200
15852
|
} catch {
|
|
15201
15853
|
return;
|
|
15202
15854
|
}
|
|
@@ -15375,10 +16027,10 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
15375
16027
|
windowName,
|
|
15376
16028
|
this.execFileImpl,
|
|
15377
16029
|
this.pollIntervalMs,
|
|
15378
|
-
|
|
16030
|
+
join17(this.logDir, `${id}.log`),
|
|
15379
16031
|
this.socket
|
|
15380
16032
|
);
|
|
15381
|
-
invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
|
|
16033
|
+
invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
|
|
15382
16034
|
await session.start(invocation.command, invocation.args, opts.cwd, invocation.env);
|
|
15383
16035
|
if (invocation.submitPromptKey) session.write("\r");
|
|
15384
16036
|
this.registerSession(id, session);
|
|
@@ -15388,8 +16040,8 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
15388
16040
|
|
|
15389
16041
|
// src/utils/daemonToken.ts
|
|
15390
16042
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
15391
|
-
import { closeSync, mkdirSync as
|
|
15392
|
-
import { join as
|
|
16043
|
+
import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeSync } from "fs";
|
|
16044
|
+
import { join as join18 } from "path";
|
|
15393
16045
|
var BEARER_PREFIX = "bearer ";
|
|
15394
16046
|
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
15395
16047
|
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
@@ -15397,12 +16049,12 @@ function configDir() {
|
|
|
15397
16049
|
return globalDataDir();
|
|
15398
16050
|
}
|
|
15399
16051
|
function daemonTokenPath(port) {
|
|
15400
|
-
return
|
|
16052
|
+
return join18(configDir(), `server-${port}.token`);
|
|
15401
16053
|
}
|
|
15402
16054
|
function mintDaemonToken(port) {
|
|
15403
16055
|
const token = randomBytes3(32).toString("base64url");
|
|
15404
16056
|
const file = daemonTokenPath(port);
|
|
15405
|
-
|
|
16057
|
+
mkdirSync9(configDir(), { recursive: true });
|
|
15406
16058
|
try {
|
|
15407
16059
|
unlinkSync3(file);
|
|
15408
16060
|
} catch {
|
|
@@ -15417,7 +16069,7 @@ function mintDaemonToken(port) {
|
|
|
15417
16069
|
}
|
|
15418
16070
|
function readDaemonToken(port) {
|
|
15419
16071
|
try {
|
|
15420
|
-
const token =
|
|
16072
|
+
const token = readFileSync9(daemonTokenPath(port), "utf8").trim();
|
|
15421
16073
|
return token === "" ? void 0 : token;
|
|
15422
16074
|
} catch {
|
|
15423
16075
|
return void 0;
|
|
@@ -15458,18 +16110,18 @@ function tokensMatch(presented, expected) {
|
|
|
15458
16110
|
}
|
|
15459
16111
|
|
|
15460
16112
|
// src/plugins/RunnerRegistry.ts
|
|
15461
|
-
import * as
|
|
15462
|
-
import * as
|
|
15463
|
-
import * as
|
|
16113
|
+
import * as fs12 from "fs";
|
|
16114
|
+
import * as path22 from "path";
|
|
16115
|
+
import * as os6 from "os";
|
|
15464
16116
|
import { execFileSync } from "child_process";
|
|
15465
16117
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
15466
16118
|
|
|
15467
16119
|
// src/plugins/FsPluginStore.ts
|
|
15468
|
-
import * as
|
|
15469
|
-
import * as
|
|
16120
|
+
import * as fs11 from "fs";
|
|
16121
|
+
import * as path21 from "path";
|
|
15470
16122
|
|
|
15471
16123
|
// src/plugins/pluginNames.ts
|
|
15472
|
-
import * as
|
|
16124
|
+
import * as path20 from "path";
|
|
15473
16125
|
var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
15474
16126
|
var MAX_PLUGIN_NAME_LENGTH = 64;
|
|
15475
16127
|
function isPlainPluginName(name) {
|
|
@@ -15484,10 +16136,10 @@ function assertPlainPluginName(name) {
|
|
|
15484
16136
|
}
|
|
15485
16137
|
function resolvePluginInstallDir(pluginsDir, name) {
|
|
15486
16138
|
assertPlainPluginName(name);
|
|
15487
|
-
const root =
|
|
15488
|
-
const dest =
|
|
15489
|
-
const
|
|
15490
|
-
if (
|
|
16139
|
+
const root = path20.resolve(pluginsDir);
|
|
16140
|
+
const dest = path20.resolve(root, name);
|
|
16141
|
+
const relative5 = path20.relative(root, dest);
|
|
16142
|
+
if (relative5 !== name || relative5 === "" || path20.isAbsolute(relative5)) {
|
|
15491
16143
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
15492
16144
|
}
|
|
15493
16145
|
return dest;
|
|
@@ -15502,13 +16154,13 @@ function isValidManifest(obj) {
|
|
|
15502
16154
|
|
|
15503
16155
|
// src/plugins/FsPluginStore.ts
|
|
15504
16156
|
function userPluginsDir() {
|
|
15505
|
-
return
|
|
16157
|
+
return path21.join(globalDataDir(), "plugins");
|
|
15506
16158
|
}
|
|
15507
16159
|
function loadManifestFromDir(dir) {
|
|
15508
|
-
const manifestPath =
|
|
15509
|
-
if (!
|
|
16160
|
+
const manifestPath = path21.join(dir, "manifest.json");
|
|
16161
|
+
if (!fs11.existsSync(manifestPath)) return null;
|
|
15510
16162
|
try {
|
|
15511
|
-
const raw =
|
|
16163
|
+
const raw = fs11.readFileSync(manifestPath, "utf8");
|
|
15512
16164
|
const parsed = JSON.parse(raw);
|
|
15513
16165
|
if (!isValidManifest(parsed)) return null;
|
|
15514
16166
|
return parsed;
|
|
@@ -15518,29 +16170,29 @@ function loadManifestFromDir(dir) {
|
|
|
15518
16170
|
}
|
|
15519
16171
|
function copyDirSync3(src, dest) {
|
|
15520
16172
|
ensureDir(dest);
|
|
15521
|
-
const entries =
|
|
16173
|
+
const entries = fs11.readdirSync(src, { withFileTypes: true });
|
|
15522
16174
|
for (const entry of entries) {
|
|
15523
|
-
const srcPath =
|
|
15524
|
-
const destPath =
|
|
16175
|
+
const srcPath = path21.join(src, entry.name);
|
|
16176
|
+
const destPath = path21.join(dest, entry.name);
|
|
15525
16177
|
if (entry.isDirectory()) {
|
|
15526
16178
|
copyDirSync3(srcPath, destPath);
|
|
15527
16179
|
} else {
|
|
15528
|
-
|
|
16180
|
+
fs11.copyFileSync(srcPath, destPath);
|
|
15529
16181
|
}
|
|
15530
16182
|
}
|
|
15531
16183
|
}
|
|
15532
16184
|
function rmDirSync(dir) {
|
|
15533
|
-
if (!
|
|
15534
|
-
const entries =
|
|
16185
|
+
if (!fs11.existsSync(dir)) return;
|
|
16186
|
+
const entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
15535
16187
|
for (const entry of entries) {
|
|
15536
|
-
const p =
|
|
16188
|
+
const p = path21.join(dir, entry.name);
|
|
15537
16189
|
if (entry.isDirectory()) {
|
|
15538
16190
|
rmDirSync(p);
|
|
15539
16191
|
} else {
|
|
15540
|
-
|
|
16192
|
+
fs11.unlinkSync(p);
|
|
15541
16193
|
}
|
|
15542
16194
|
}
|
|
15543
|
-
|
|
16195
|
+
fs11.rmdirSync(dir);
|
|
15544
16196
|
}
|
|
15545
16197
|
var FsPluginStore = class {
|
|
15546
16198
|
getUserPluginsDir() {
|
|
@@ -15548,10 +16200,10 @@ var FsPluginStore = class {
|
|
|
15548
16200
|
}
|
|
15549
16201
|
listUserPluginDirs() {
|
|
15550
16202
|
const dir = userPluginsDir();
|
|
15551
|
-
if (!
|
|
16203
|
+
if (!fs11.existsSync(dir)) return [];
|
|
15552
16204
|
let entries;
|
|
15553
16205
|
try {
|
|
15554
|
-
entries =
|
|
16206
|
+
entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
15555
16207
|
} catch {
|
|
15556
16208
|
return [];
|
|
15557
16209
|
}
|
|
@@ -15559,7 +16211,7 @@ var FsPluginStore = class {
|
|
|
15559
16211
|
}
|
|
15560
16212
|
listDir(dir) {
|
|
15561
16213
|
try {
|
|
15562
|
-
return
|
|
16214
|
+
return fs11.readdirSync(dir);
|
|
15563
16215
|
} catch {
|
|
15564
16216
|
return [];
|
|
15565
16217
|
}
|
|
@@ -15577,20 +16229,20 @@ var FsPluginStore = class {
|
|
|
15577
16229
|
ensureDir(dir);
|
|
15578
16230
|
}
|
|
15579
16231
|
writeFile(filePath, content) {
|
|
15580
|
-
|
|
16232
|
+
fs11.writeFileSync(filePath, content);
|
|
15581
16233
|
}
|
|
15582
16234
|
readFile(filePath) {
|
|
15583
16235
|
try {
|
|
15584
|
-
return
|
|
16236
|
+
return fs11.readFileSync(filePath, "utf8");
|
|
15585
16237
|
} catch {
|
|
15586
16238
|
return null;
|
|
15587
16239
|
}
|
|
15588
16240
|
}
|
|
15589
|
-
dirExists(
|
|
15590
|
-
return
|
|
16241
|
+
dirExists(path25) {
|
|
16242
|
+
return fs11.existsSync(path25) && fs11.statSync(path25).isDirectory();
|
|
15591
16243
|
}
|
|
15592
|
-
exists(
|
|
15593
|
-
return
|
|
16244
|
+
exists(path25) {
|
|
16245
|
+
return fs11.existsSync(path25);
|
|
15594
16246
|
}
|
|
15595
16247
|
};
|
|
15596
16248
|
|
|
@@ -15617,7 +16269,15 @@ var CLAUDE_CODE_MANIFEST = {
|
|
|
15617
16269
|
"{{/if}}",
|
|
15618
16270
|
"{{prompt}}"
|
|
15619
16271
|
],
|
|
15620
|
-
promptInArgs: true
|
|
16272
|
+
promptInArgs: true,
|
|
16273
|
+
// Interactive sessions show these even with --dangerously-skip-permissions:
|
|
16274
|
+
// a folder Claude Code has never been trusted in (a fresh clone, or a repo
|
|
16275
|
+
// only ever planned in headlessly), and the first Bypass Permissions run.
|
|
16276
|
+
blockingPrompts: [
|
|
16277
|
+
{ phrase: "Is this a project you created or one you trust", asks: "whether to trust the task's folder" },
|
|
16278
|
+
{ phrase: "Do you trust the files in this folder", asks: "whether to trust the task's folder" },
|
|
16279
|
+
{ phrase: "running in Bypass Permissions mode", asks: "you to accept Bypass Permissions mode" }
|
|
16280
|
+
]
|
|
15621
16281
|
},
|
|
15622
16282
|
features: {
|
|
15623
16283
|
modelSelection: true,
|
|
@@ -16022,7 +16682,7 @@ var RunnerRegistry = class {
|
|
|
16022
16682
|
return entry?.source === "builtin";
|
|
16023
16683
|
}
|
|
16024
16684
|
installFromPath(sourcePath) {
|
|
16025
|
-
const absPath =
|
|
16685
|
+
const absPath = path22.resolve(sourcePath);
|
|
16026
16686
|
if (!this.store.dirExists(absPath)) {
|
|
16027
16687
|
throw new Error(`Plugin source must be a directory: ${absPath}`);
|
|
16028
16688
|
}
|
|
@@ -16034,7 +16694,7 @@ var RunnerRegistry = class {
|
|
|
16034
16694
|
}
|
|
16035
16695
|
installFromGit(url) {
|
|
16036
16696
|
const validated = assertInstallablePluginUrl(url);
|
|
16037
|
-
const tmpDir =
|
|
16697
|
+
const tmpDir = path22.join(os6.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
|
|
16038
16698
|
this.store.ensureDir(tmpDir);
|
|
16039
16699
|
try {
|
|
16040
16700
|
try {
|
|
@@ -16047,7 +16707,7 @@ var RunnerRegistry = class {
|
|
|
16047
16707
|
const hasManifest = contents.some((f) => f === "manifest.json");
|
|
16048
16708
|
let sourceDir = tmpDir;
|
|
16049
16709
|
if (!hasManifest) {
|
|
16050
|
-
const onlyEntry = contents.length === 1 ?
|
|
16710
|
+
const onlyEntry = contents.length === 1 ? path22.join(tmpDir, contents[0]) : void 0;
|
|
16051
16711
|
if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
|
|
16052
16712
|
throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
|
|
16053
16713
|
}
|
|
@@ -16095,7 +16755,7 @@ var RunnerRegistry = class {
|
|
|
16095
16755
|
}
|
|
16096
16756
|
createSkeleton(name, outputDir) {
|
|
16097
16757
|
assertPlainPluginName(name);
|
|
16098
|
-
const dir =
|
|
16758
|
+
const dir = path22.resolve(outputDir, name);
|
|
16099
16759
|
this.store.ensureDir(dir);
|
|
16100
16760
|
const manifest = {
|
|
16101
16761
|
name,
|
|
@@ -16127,8 +16787,8 @@ var RunnerRegistry = class {
|
|
|
16127
16787
|
]
|
|
16128
16788
|
}
|
|
16129
16789
|
};
|
|
16130
|
-
this.store.writeFile(
|
|
16131
|
-
const runSh =
|
|
16790
|
+
this.store.writeFile(path22.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
16791
|
+
const runSh = path22.join(dir, "run.sh");
|
|
16132
16792
|
const scriptContent = `#!/usr/bin/env bash
|
|
16133
16793
|
# Custom runner script for ${name}
|
|
16134
16794
|
# Receives:
|
|
@@ -16154,30 +16814,30 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
16154
16814
|
};
|
|
16155
16815
|
function fsChmodSync(fpath, mode) {
|
|
16156
16816
|
try {
|
|
16157
|
-
|
|
16817
|
+
fs12.chmodSync(fpath, mode);
|
|
16158
16818
|
} catch {
|
|
16159
16819
|
}
|
|
16160
16820
|
}
|
|
16161
16821
|
|
|
16162
16822
|
// src/utils/stateStore.ts
|
|
16163
|
-
import * as
|
|
16164
|
-
import * as
|
|
16823
|
+
import * as fs13 from "fs";
|
|
16824
|
+
import * as path23 from "path";
|
|
16165
16825
|
var STATE_FILE = "state.json";
|
|
16166
16826
|
function getStatePath(baseDir) {
|
|
16167
|
-
return
|
|
16827
|
+
return path23.join(getStateDir(baseDir), STATE_FILE);
|
|
16168
16828
|
}
|
|
16169
16829
|
function saveState(plan, baseDir) {
|
|
16170
16830
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
16171
|
-
ensureDir(
|
|
16831
|
+
ensureDir(path23.dirname(statePath));
|
|
16172
16832
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
16173
|
-
|
|
16833
|
+
fs13.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
16174
16834
|
}
|
|
16175
16835
|
function loadState(baseDir, logger = defaultLogger) {
|
|
16176
16836
|
const dir = baseDir ?? process.cwd();
|
|
16177
16837
|
const statePath = getStatePath(dir);
|
|
16178
|
-
if (!
|
|
16838
|
+
if (!fs13.existsSync(statePath)) return null;
|
|
16179
16839
|
try {
|
|
16180
|
-
const raw =
|
|
16840
|
+
const raw = fs13.readFileSync(statePath, "utf-8");
|
|
16181
16841
|
const parsed = JSON.parse(raw);
|
|
16182
16842
|
if (!Array.isArray(parsed.runners)) {
|
|
16183
16843
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -16194,16 +16854,16 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
16194
16854
|
function clearState(baseDir) {
|
|
16195
16855
|
const dir = baseDir ?? process.cwd();
|
|
16196
16856
|
const statePath = getStatePath(dir);
|
|
16197
|
-
if (
|
|
16857
|
+
if (fs13.existsSync(statePath)) fs13.unlinkSync(statePath);
|
|
16198
16858
|
}
|
|
16199
16859
|
function stateExists(baseDir) {
|
|
16200
16860
|
const dir = baseDir ?? process.cwd();
|
|
16201
|
-
return
|
|
16861
|
+
return fs13.existsSync(getStatePath(dir));
|
|
16202
16862
|
}
|
|
16203
16863
|
|
|
16204
16864
|
// src/services/SettingsService.ts
|
|
16205
|
-
import * as
|
|
16206
|
-
import * as
|
|
16865
|
+
import * as fs14 from "fs";
|
|
16866
|
+
import * as path24 from "path";
|
|
16207
16867
|
var DEFAULTS = {
|
|
16208
16868
|
tdd: { enabled: true },
|
|
16209
16869
|
verification: { enabled: false }
|
|
@@ -16211,7 +16871,7 @@ var DEFAULTS = {
|
|
|
16211
16871
|
function getSettingsPath() {
|
|
16212
16872
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
16213
16873
|
if (override && override.trim()) return override.trim();
|
|
16214
|
-
return
|
|
16874
|
+
return path24.join(globalDataDir(), "settings.json");
|
|
16215
16875
|
}
|
|
16216
16876
|
function readPlannerModels(raw) {
|
|
16217
16877
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -16244,7 +16904,7 @@ var SettingsService = class {
|
|
|
16244
16904
|
}
|
|
16245
16905
|
fileMtimeMs() {
|
|
16246
16906
|
try {
|
|
16247
|
-
return
|
|
16907
|
+
return fs14.statSync(this.filePath).mtimeMs;
|
|
16248
16908
|
} catch {
|
|
16249
16909
|
return null;
|
|
16250
16910
|
}
|
|
@@ -16316,8 +16976,8 @@ var SettingsService = class {
|
|
|
16316
16976
|
load() {
|
|
16317
16977
|
migrateOldConfigDir();
|
|
16318
16978
|
try {
|
|
16319
|
-
if (
|
|
16320
|
-
const raw = JSON.parse(
|
|
16979
|
+
if (fs14.existsSync(this.filePath)) {
|
|
16980
|
+
const raw = JSON.parse(fs14.readFileSync(this.filePath, "utf-8"));
|
|
16321
16981
|
const settings = {
|
|
16322
16982
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
16323
16983
|
verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled }
|
|
@@ -16339,11 +16999,11 @@ var SettingsService = class {
|
|
|
16339
16999
|
return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
|
|
16340
17000
|
}
|
|
16341
17001
|
persist() {
|
|
16342
|
-
const dir =
|
|
16343
|
-
if (!
|
|
16344
|
-
|
|
17002
|
+
const dir = path24.dirname(this.filePath);
|
|
17003
|
+
if (!fs14.existsSync(dir)) {
|
|
17004
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
16345
17005
|
}
|
|
16346
|
-
|
|
17006
|
+
fs14.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
16347
17007
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
16348
17008
|
}
|
|
16349
17009
|
};
|
|
@@ -16409,6 +17069,7 @@ export {
|
|
|
16409
17069
|
ConversationEditError,
|
|
16410
17070
|
DAEMON_SUBPROTOCOL,
|
|
16411
17071
|
DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
|
|
17072
|
+
DEFAULT_MAX_PARALLEL,
|
|
16412
17073
|
DENY_ALL,
|
|
16413
17074
|
EmbeddedNewlineError,
|
|
16414
17075
|
EnvConfig,
|
|
@@ -16482,6 +17143,7 @@ export {
|
|
|
16482
17143
|
augmentPromptWithPriorOutputs,
|
|
16483
17144
|
augmentedPath,
|
|
16484
17145
|
bearerHeaderValue,
|
|
17146
|
+
buildConflictRepairPrompt,
|
|
16485
17147
|
buildConflictResolutionPrompt,
|
|
16486
17148
|
buildConversationSystemPrompt,
|
|
16487
17149
|
buildFallbackGrepArgs,
|
|
@@ -16501,6 +17163,7 @@ export {
|
|
|
16501
17163
|
canMergeTasks,
|
|
16502
17164
|
canSetDependencies,
|
|
16503
17165
|
canSplitTask,
|
|
17166
|
+
capConflictFiles,
|
|
16504
17167
|
checkDepsResolve,
|
|
16505
17168
|
checkImmutableLog,
|
|
16506
17169
|
checkInProgress,
|
|
@@ -16589,6 +17252,7 @@ export {
|
|
|
16589
17252
|
modifyValidationFeedback,
|
|
16590
17253
|
normalizeAgentArgs,
|
|
16591
17254
|
normalizeGeminiModel,
|
|
17255
|
+
parseMaxParallel,
|
|
16592
17256
|
parsePartialPlan,
|
|
16593
17257
|
parsePlanJson,
|
|
16594
17258
|
parseTaskOpsJson,
|