@ordewell/core 0.5.0 → 0.5.1
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-DXlJop_f.d.mts → ITerminalRunner-DCjsa2-I.d.mts} +6 -1
- package/dist/{ITerminalRunner-nupxhtPb.d.ts → ITerminalRunner-H1yT8LTy.d.ts} +6 -1
- 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 +82 -57
- package/dist/index.d.ts +82 -57
- package/dist/index.js +658 -252
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +657 -264
- 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;
|
|
@@ -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((resolve6) => {
|
|
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
|
+
resolve6(granted);
|
|
2781
2782
|
};
|
|
2782
2783
|
const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2783
2784
|
timer.unref?.();
|
|
@@ -3475,6 +3476,10 @@ function normalizeGeminiModel(id) {
|
|
|
3475
3476
|
function commaList(raw) {
|
|
3476
3477
|
return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3477
3478
|
}
|
|
3479
|
+
function nonNegativeInt(raw, fallback) {
|
|
3480
|
+
const trimmed = (raw ?? "").trim();
|
|
3481
|
+
return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
|
|
3482
|
+
}
|
|
3478
3483
|
var BaseConfig = class {
|
|
3479
3484
|
// --- Legacy named getters (backward compat) ---
|
|
3480
3485
|
get openAiBaseUrl() {
|
|
@@ -3697,6 +3702,9 @@ var BaseConfig = class {
|
|
|
3697
3702
|
get worktreeLinks() {
|
|
3698
3703
|
return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
|
|
3699
3704
|
}
|
|
3705
|
+
get conflictRepairAttempts() {
|
|
3706
|
+
return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
|
|
3707
|
+
}
|
|
3700
3708
|
get approvalMode() {
|
|
3701
3709
|
const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
|
|
3702
3710
|
return raw === "allow" || raw === "deny" ? raw : "ask";
|
|
@@ -3756,13 +3764,17 @@ function landedNotice(landed) {
|
|
|
3756
3764
|
if (landed.length === 0) return "Nothing was merged.";
|
|
3757
3765
|
return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
|
|
3758
3766
|
}
|
|
3759
|
-
function
|
|
3767
|
+
function repairedNotice(repaired) {
|
|
3768
|
+
const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
|
|
3769
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
3770
|
+
return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
3771
|
+
}
|
|
3772
|
+
function describeMergeResult(result, branch, group, repaired = []) {
|
|
3760
3773
|
switch (result.outcome) {
|
|
3761
|
-
case "merged":
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
};
|
|
3774
|
+
case "merged": {
|
|
3775
|
+
const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
|
|
3776
|
+
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
3777
|
+
}
|
|
3766
3778
|
case "blocked":
|
|
3767
3779
|
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
|
|
3768
3780
|
case "conflict":
|
|
@@ -3783,7 +3795,7 @@ import { execFile, exec as exec2 } from "child_process";
|
|
|
3783
3795
|
import { promisify as promisify2 } from "util";
|
|
3784
3796
|
import { randomBytes } from "crypto";
|
|
3785
3797
|
import * as fs4 from "fs";
|
|
3786
|
-
import * as
|
|
3798
|
+
import * as path8 from "path";
|
|
3787
3799
|
|
|
3788
3800
|
// src/utils/shellPath.ts
|
|
3789
3801
|
import { exec } from "child_process";
|
|
@@ -3812,28 +3824,28 @@ function posixBinDirs() {
|
|
|
3812
3824
|
];
|
|
3813
3825
|
}
|
|
3814
3826
|
function windowsBinDirs(env, home) {
|
|
3815
|
-
const
|
|
3816
|
-
const under = (base, ...parts) => base ? [
|
|
3827
|
+
const join22 = (...parts) => path4.win32.join(...parts);
|
|
3828
|
+
const under = (base, ...parts) => base ? [join22(base, ...parts)] : [];
|
|
3817
3829
|
const appData = env.APPDATA;
|
|
3818
3830
|
const localAppData = env.LOCALAPPDATA;
|
|
3819
3831
|
const programData = env.ProgramData;
|
|
3820
3832
|
return [
|
|
3821
3833
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3834
|
+
join22(home, ".local", "bin"),
|
|
3835
|
+
join22(home, ".opencode", "bin"),
|
|
3836
|
+
join22(home, ".claude", "bin"),
|
|
3837
|
+
join22(home, ".codex", "bin"),
|
|
3826
3838
|
...under(localAppData, "Programs", "claude"),
|
|
3827
3839
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
3828
3840
|
// literal spelling is kept as a fallback for a host that does not export
|
|
3829
3841
|
// APPDATA (a service account, a stripped CI container).
|
|
3830
3842
|
...under(appData, "npm"),
|
|
3831
|
-
|
|
3843
|
+
join22(home, "AppData", "Roaming", "npm"),
|
|
3832
3844
|
...under(localAppData, "pnpm"),
|
|
3833
3845
|
...under(localAppData, "Yarn", "bin"),
|
|
3834
|
-
|
|
3846
|
+
join22(home, ".bun", "bin"),
|
|
3835
3847
|
// Windows package managers.
|
|
3836
|
-
|
|
3848
|
+
join22(home, "scoop", "shims"),
|
|
3837
3849
|
...under(env.SCOOP, "shims"),
|
|
3838
3850
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
3839
3851
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -3843,7 +3855,7 @@ function windowsBinDirs(env, home) {
|
|
|
3843
3855
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
3844
3856
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
3845
3857
|
...under(localAppData, "Volta", "bin"),
|
|
3846
|
-
|
|
3858
|
+
join22(home, ".volta", "bin")
|
|
3847
3859
|
];
|
|
3848
3860
|
}
|
|
3849
3861
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -3947,6 +3959,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
3947
3959
|
|
|
3948
3960
|
// src/services/worktreeLink.ts
|
|
3949
3961
|
import * as fs3 from "fs";
|
|
3962
|
+
import * as path7 from "path";
|
|
3950
3963
|
function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
3951
3964
|
const isDir = fs3.statSync(source).isDirectory();
|
|
3952
3965
|
if (platform !== "win32") {
|
|
@@ -3965,6 +3978,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
|
3965
3978
|
return "copy";
|
|
3966
3979
|
}
|
|
3967
3980
|
}
|
|
3981
|
+
function mirrorDir(source, target, opts) {
|
|
3982
|
+
const copied = [];
|
|
3983
|
+
const links = [];
|
|
3984
|
+
const mirror = (from, to, prefix, top) => {
|
|
3985
|
+
fs3.mkdirSync(to, { recursive: true });
|
|
3986
|
+
for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
|
|
3987
|
+
const name = path7.join(prefix, entry.name);
|
|
3988
|
+
const at = { from: path7.join(from, entry.name), to: path7.join(to, entry.name), name };
|
|
3989
|
+
if (entry.isSymbolicLink()) links.push(at);
|
|
3990
|
+
else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
|
|
3991
|
+
else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
mirror(source, target, "", true);
|
|
3995
|
+
let pending = links;
|
|
3996
|
+
for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
|
|
3997
|
+
before = pending.length;
|
|
3998
|
+
pending = pending.filter((link) => {
|
|
3999
|
+
const kind = relink(link.from, link.to, opts);
|
|
4000
|
+
if (kind === "copy") copied.push(link.name);
|
|
4001
|
+
return kind === null;
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
return copied;
|
|
4005
|
+
}
|
|
4006
|
+
function relink(from, to, opts) {
|
|
4007
|
+
const text = fs3.readlinkSync(from);
|
|
4008
|
+
const pointee = path7.isAbsolute(text) ? rebased(text, opts) : text;
|
|
4009
|
+
if (opts.platform !== "win32") {
|
|
4010
|
+
if (!fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) && fs3.existsSync(path7.resolve(path7.dirname(from), pointee))) {
|
|
4011
|
+
fs3.symlinkSync(rebased(path7.resolve(path7.dirname(from), pointee), opts), to);
|
|
4012
|
+
return "symlink";
|
|
4013
|
+
}
|
|
4014
|
+
fs3.symlinkSync(pointee, to);
|
|
4015
|
+
return "symlink";
|
|
4016
|
+
}
|
|
4017
|
+
const resolved = fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) ? path7.resolve(path7.dirname(to), pointee) : rebased(path7.resolve(path7.dirname(from), pointee), opts);
|
|
4018
|
+
try {
|
|
4019
|
+
fs3.statSync(resolved);
|
|
4020
|
+
} catch {
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
return linkPath(resolved, to, opts.platform, opts.hardLink);
|
|
4024
|
+
}
|
|
4025
|
+
function rebased(pointee, opts) {
|
|
4026
|
+
const rel = path7.relative(opts.from, pointee);
|
|
4027
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
4028
|
+
return inside && !rel.split(/[\\/]/).includes("node_modules") ? path7.join(opts.to, rel) : pointee;
|
|
4029
|
+
}
|
|
3968
4030
|
|
|
3969
4031
|
// src/services/GitWorktreeIsolation.ts
|
|
3970
4032
|
var execFileAsync = promisify2(execFile);
|
|
@@ -3979,15 +4041,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
|
|
|
3979
4041
|
var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
|
|
3980
4042
|
var isEnvFile = (name) => name.startsWith(".env");
|
|
3981
4043
|
var INTEGRATION_DIR = "integration";
|
|
4044
|
+
var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
|
|
4045
|
+
var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
|
|
3982
4046
|
var NESTED_REPO_DEPTH = 2;
|
|
3983
4047
|
var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
|
|
3984
4048
|
var GITLINK_MODE = "160000";
|
|
3985
4049
|
function isInside(parent, child) {
|
|
3986
|
-
const rel =
|
|
3987
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4050
|
+
const rel = path8.relative(parent, child);
|
|
4051
|
+
return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
3988
4052
|
}
|
|
3989
4053
|
function hasGitEntry(dir) {
|
|
3990
|
-
return fs4.existsSync(
|
|
4054
|
+
return fs4.existsSync(path8.join(dir, ".git"));
|
|
3991
4055
|
}
|
|
3992
4056
|
function firstLine(text) {
|
|
3993
4057
|
return text.split(/\r?\n/, 1)[0].trim();
|
|
@@ -4003,12 +4067,20 @@ function statusPaths(porcelain) {
|
|
|
4003
4067
|
}
|
|
4004
4068
|
return paths;
|
|
4005
4069
|
}
|
|
4070
|
+
function leftoverMarkerFiles(check) {
|
|
4071
|
+
const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
|
|
4072
|
+
return [...new Set(files)];
|
|
4073
|
+
}
|
|
4074
|
+
function settleStatus(record, status) {
|
|
4075
|
+
record.status = status;
|
|
4076
|
+
delete record.repairBase;
|
|
4077
|
+
}
|
|
4006
4078
|
function sameDir(a, b) {
|
|
4007
4079
|
const real = (p) => {
|
|
4008
4080
|
try {
|
|
4009
4081
|
return fs4.realpathSync.native(p);
|
|
4010
4082
|
} catch {
|
|
4011
|
-
return
|
|
4083
|
+
return path8.resolve(p);
|
|
4012
4084
|
}
|
|
4013
4085
|
};
|
|
4014
4086
|
return real(a) === real(b);
|
|
@@ -4042,8 +4114,8 @@ function inactive(reason, repos) {
|
|
|
4042
4114
|
}
|
|
4043
4115
|
function groupPathOf(listed) {
|
|
4044
4116
|
const slashed = listed.trim().replace(/\\/g, "/");
|
|
4045
|
-
if (
|
|
4046
|
-
const rel =
|
|
4117
|
+
if (path8.posix.isAbsolute(slashed) || path8.win32.isAbsolute(slashed)) return null;
|
|
4118
|
+
const rel = path8.posix.normalize(slashed).replace(/\/+$/, "");
|
|
4047
4119
|
return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
|
|
4048
4120
|
}
|
|
4049
4121
|
function matchLinks(root, patterns) {
|
|
@@ -4059,9 +4131,26 @@ function matchLinks(root, patterns) {
|
|
|
4059
4131
|
}
|
|
4060
4132
|
function segmentMatches(root, base, segment) {
|
|
4061
4133
|
const under = (name) => base ? `${base}/${name}` : name;
|
|
4062
|
-
if (!/[*?]/.test(segment)) return lexists(
|
|
4134
|
+
if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
|
|
4063
4135
|
const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
|
|
4064
|
-
return listDir(
|
|
4136
|
+
return listDir(path8.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
|
|
4137
|
+
}
|
|
4138
|
+
function installDirs(root) {
|
|
4139
|
+
const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
|
|
4140
|
+
return ["node_modules", ...packages].filter((dir) => lexists(path8.join(root, dir)));
|
|
4141
|
+
}
|
|
4142
|
+
function workspaceGlobs(root) {
|
|
4143
|
+
let manifest;
|
|
4144
|
+
try {
|
|
4145
|
+
manifest = JSON.parse(fs4.readFileSync(path8.join(root, "package.json"), "utf8"));
|
|
4146
|
+
} catch {
|
|
4147
|
+
return [];
|
|
4148
|
+
}
|
|
4149
|
+
if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
|
|
4150
|
+
const { workspaces } = manifest;
|
|
4151
|
+
const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
|
|
4152
|
+
if (!Array.isArray(listed)) return [];
|
|
4153
|
+
return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
|
|
4065
4154
|
}
|
|
4066
4155
|
var GitWorktreeIsolation = class {
|
|
4067
4156
|
constructor(deps) {
|
|
@@ -4115,7 +4204,7 @@ var GitWorktreeIsolation = class {
|
|
|
4115
4204
|
groupPaths(workspaceRoot) {
|
|
4116
4205
|
const listed = this.deps.config.workspaceRepos;
|
|
4117
4206
|
if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
|
|
4118
|
-
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(
|
|
4207
|
+
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
|
|
4119
4208
|
return [...new Set(paths)].sort();
|
|
4120
4209
|
}
|
|
4121
4210
|
async committedRepos(workspaceRoot, paths) {
|
|
@@ -4131,7 +4220,7 @@ var GitWorktreeIsolation = class {
|
|
|
4131
4220
|
return status.ok ? status.stdout.trim() !== "" : null;
|
|
4132
4221
|
}
|
|
4133
4222
|
reposDirectlyIn(dir) {
|
|
4134
|
-
return this.childDirs(dir).filter((name) => hasGitEntry(
|
|
4223
|
+
return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
|
|
4135
4224
|
}
|
|
4136
4225
|
childDirs(dir) {
|
|
4137
4226
|
try {
|
|
@@ -4148,9 +4237,9 @@ var GitWorktreeIsolation = class {
|
|
|
4148
4237
|
async nestedRepos(workspaceRoot, toplevel) {
|
|
4149
4238
|
const found = [];
|
|
4150
4239
|
const walk = (rel, depth) => {
|
|
4151
|
-
for (const name of this.childDirs(
|
|
4240
|
+
for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
|
|
4152
4241
|
const child = rel ? `${rel}/${name}` : name;
|
|
4153
|
-
if (hasGitEntry(
|
|
4242
|
+
if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
|
|
4154
4243
|
else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
|
|
4155
4244
|
}
|
|
4156
4245
|
};
|
|
@@ -4171,7 +4260,7 @@ var GitWorktreeIsolation = class {
|
|
|
4171
4260
|
const [meta, file] = entry.split(" ");
|
|
4172
4261
|
if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
|
|
4173
4262
|
}
|
|
4174
|
-
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f",
|
|
4263
|
+
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
|
|
4175
4264
|
for (const entry of declared.stdout.split("\0")) {
|
|
4176
4265
|
const value = entry.split("\n")[1];
|
|
4177
4266
|
if (value) paths.add(value.replace(/\/+$/, ""));
|
|
@@ -4227,7 +4316,7 @@ var GitWorktreeIsolation = class {
|
|
|
4227
4316
|
async acceptsWorktree(run, repo) {
|
|
4228
4317
|
const dir = this.integrationDir(run, repo);
|
|
4229
4318
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
4230
|
-
fs4.mkdirSync(
|
|
4319
|
+
fs4.mkdirSync(path8.dirname(dir), { recursive: true });
|
|
4231
4320
|
const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
|
|
4232
4321
|
await this.removeWorktreeDir(run, repo, dir, []);
|
|
4233
4322
|
return added.ok;
|
|
@@ -4242,12 +4331,12 @@ var GitWorktreeIsolation = class {
|
|
|
4242
4331
|
if (isolated.includes(SELF_REPO)) return [];
|
|
4243
4332
|
const shared = [];
|
|
4244
4333
|
const walk = (rel) => {
|
|
4245
|
-
for (const name of listDir(
|
|
4334
|
+
for (const name of listDir(path8.join(workspaceRoot, rel))) {
|
|
4246
4335
|
if (!rel && (name === STATE_DIR || name === ".git")) continue;
|
|
4247
4336
|
const child = rel ? `${rel}/${name}` : name;
|
|
4248
4337
|
if (isolated.includes(child)) continue;
|
|
4249
4338
|
if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
|
|
4250
|
-
else if (resolves(
|
|
4339
|
+
else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
|
|
4251
4340
|
}
|
|
4252
4341
|
};
|
|
4253
4342
|
walk("");
|
|
@@ -4264,17 +4353,17 @@ var GitWorktreeIsolation = class {
|
|
|
4264
4353
|
const copied = [];
|
|
4265
4354
|
try {
|
|
4266
4355
|
for (const repo of run.repos) {
|
|
4267
|
-
const worktree =
|
|
4356
|
+
const worktree = path8.join(dir, repo.path);
|
|
4268
4357
|
const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
|
|
4269
|
-
fs4.mkdirSync(
|
|
4358
|
+
fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
|
|
4270
4359
|
await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
|
|
4271
4360
|
record.repos[repo.path] = { worktree, linked: [] };
|
|
4272
4361
|
}
|
|
4273
4362
|
run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
|
|
4274
4363
|
for (const rel of run.shared) {
|
|
4275
|
-
const target =
|
|
4276
|
-
fs4.mkdirSync(
|
|
4277
|
-
if (linkPath(
|
|
4364
|
+
const target = path8.join(dir, rel);
|
|
4365
|
+
fs4.mkdirSync(path8.dirname(target), { recursive: true });
|
|
4366
|
+
if (linkPath(path8.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
|
|
4278
4367
|
}
|
|
4279
4368
|
for (const repo of run.repos) {
|
|
4280
4369
|
const entry = record.repos[repo.path];
|
|
@@ -4282,7 +4371,7 @@ var GitWorktreeIsolation = class {
|
|
|
4282
4371
|
if (repo.path === SELF_REPO) cwd = inRepo;
|
|
4283
4372
|
const boot = await this.bootstrap(repo, inRepo);
|
|
4284
4373
|
entry.linked = boot.linked;
|
|
4285
|
-
copied.push(...boot.copied.map((name) =>
|
|
4374
|
+
copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
|
|
4286
4375
|
}
|
|
4287
4376
|
} catch (err) {
|
|
4288
4377
|
record.status = "failed";
|
|
@@ -4293,6 +4382,53 @@ var GitWorktreeIsolation = class {
|
|
|
4293
4382
|
return { cwd, branch, copied };
|
|
4294
4383
|
});
|
|
4295
4384
|
}
|
|
4385
|
+
reopen(task, run) {
|
|
4386
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
4387
|
+
const record = run.tasks[task.id];
|
|
4388
|
+
if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
|
|
4389
|
+
let cwd = record.workspace;
|
|
4390
|
+
const repairBase = {};
|
|
4391
|
+
for (const repo of run.repos) {
|
|
4392
|
+
const entry = record.repos[repo.path];
|
|
4393
|
+
if (!entry) continue;
|
|
4394
|
+
if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
|
|
4395
|
+
if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
|
|
4396
|
+
if (entry.changed || repo.path === record.conflictRepo) {
|
|
4397
|
+
repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
|
|
4401
|
+
record.repairs = (record.repairs ?? 0) + 1;
|
|
4402
|
+
record.repairBase = repairBase;
|
|
4403
|
+
record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
|
|
4404
|
+
record.status = "repairing";
|
|
4405
|
+
return { cwd, branch: record.branch, copied: [] };
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
async verifyRepair(task, run) {
|
|
4409
|
+
const record = run.tasks[task.id];
|
|
4410
|
+
if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
|
|
4411
|
+
for (const repo of run.repos) {
|
|
4412
|
+
const entry = record.repos[repo.path];
|
|
4413
|
+
if (!entry) continue;
|
|
4414
|
+
try {
|
|
4415
|
+
await this.commitWorktree(repo, record, entry);
|
|
4416
|
+
} catch {
|
|
4417
|
+
return { ok: false, reason: "failed", repo: repo.path };
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
for (const repo of run.repos) {
|
|
4421
|
+
const base = record.repairBase[repo.path];
|
|
4422
|
+
if (base === void 0) continue;
|
|
4423
|
+
const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
|
|
4424
|
+
if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
|
|
4425
|
+
const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
|
|
4426
|
+
if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
|
|
4427
|
+
const files = leftoverMarkerFiles(check.stdout);
|
|
4428
|
+
if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
|
|
4429
|
+
}
|
|
4430
|
+
return { ok: true };
|
|
4431
|
+
}
|
|
4296
4432
|
/**
|
|
4297
4433
|
* The place in a repo's worktree that matches where the workspace sits in
|
|
4298
4434
|
* the real repo: the worktree itself, or a subdirectory of it when the
|
|
@@ -4300,7 +4436,7 @@ var GitWorktreeIsolation = class {
|
|
|
4300
4436
|
*/
|
|
4301
4437
|
async inWorkspacePlace(repo, worktree) {
|
|
4302
4438
|
const prefix = await this.prefixOf(repo);
|
|
4303
|
-
return prefix ?
|
|
4439
|
+
return prefix ? path8.join(worktree, prefix) : worktree;
|
|
4304
4440
|
}
|
|
4305
4441
|
async prefixOf(repo) {
|
|
4306
4442
|
return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
|
|
@@ -4320,6 +4456,7 @@ var GitWorktreeIsolation = class {
|
|
|
4320
4456
|
if (!record) return;
|
|
4321
4457
|
if (opts.keep) {
|
|
4322
4458
|
if (record.status === "active") record.status = "kept";
|
|
4459
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4323
4460
|
return;
|
|
4324
4461
|
}
|
|
4325
4462
|
await this.removeTask(run, record, { dropRecord: true });
|
|
@@ -4339,6 +4476,7 @@ var GitWorktreeIsolation = class {
|
|
|
4339
4476
|
for (const record of Object.values(run.tasks)) {
|
|
4340
4477
|
if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
|
|
4341
4478
|
else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
|
|
4479
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4342
4480
|
}
|
|
4343
4481
|
await this.removeUnowned(run);
|
|
4344
4482
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
@@ -4433,14 +4571,59 @@ var GitWorktreeIsolation = class {
|
|
|
4433
4571
|
await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
|
|
4434
4572
|
}
|
|
4435
4573
|
await this.removeUnowned(run);
|
|
4436
|
-
if (
|
|
4437
|
-
for (const repo of run.repos)
|
|
4574
|
+
if (opts.integration !== "keep") {
|
|
4575
|
+
for (const repo of run.repos) {
|
|
4576
|
+
if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
|
|
4577
|
+
else await this.deleteIfMerged(repo.root, repo.integrationBranch);
|
|
4578
|
+
}
|
|
4438
4579
|
run.tasks = {};
|
|
4439
4580
|
this.removeIfEmpty(this.runRoot(run));
|
|
4440
4581
|
}
|
|
4441
4582
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4442
4583
|
});
|
|
4443
4584
|
}
|
|
4585
|
+
sweep(run) {
|
|
4586
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
4587
|
+
const failed = [];
|
|
4588
|
+
for (const repo of run.repos) {
|
|
4589
|
+
if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
|
|
4590
|
+
}
|
|
4591
|
+
if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
|
|
4592
|
+
});
|
|
4593
|
+
}
|
|
4594
|
+
/** False when git failed anywhere; the other branches are still tried. */
|
|
4595
|
+
async sweepRepo(run, repo) {
|
|
4596
|
+
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4597
|
+
const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
|
|
4598
|
+
const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
|
|
4599
|
+
if (!worktrees.ok || !refs.ok) return false;
|
|
4600
|
+
const checkedOut = /* @__PURE__ */ new Set();
|
|
4601
|
+
const liveRuns = /* @__PURE__ */ new Set();
|
|
4602
|
+
for (const line of worktrees.stdout.split(/\r?\n/)) {
|
|
4603
|
+
if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
|
|
4604
|
+
const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
|
|
4605
|
+
if (runId) liveRuns.add(runId);
|
|
4606
|
+
}
|
|
4607
|
+
let ok = true;
|
|
4608
|
+
for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
4609
|
+
const branch = ref.slice("refs/heads/".length);
|
|
4610
|
+
const runId = RUN_BRANCH.exec(branch)?.[1];
|
|
4611
|
+
if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
|
|
4612
|
+
if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
|
|
4613
|
+
}
|
|
4614
|
+
return ok;
|
|
4615
|
+
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Delete `branch` only if the repo's checked-out HEAD already contains it —
|
|
4618
|
+
* HEAD, not an upstream: what the user has checked out is what they merged
|
|
4619
|
+
* into. `-d` rather than `-D`, so git also refuses a branch a worktree has
|
|
4620
|
+
* checked out.
|
|
4621
|
+
*/
|
|
4622
|
+
async deleteIfMerged(root, branch) {
|
|
4623
|
+
const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
|
|
4624
|
+
if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
|
|
4625
|
+
return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
|
|
4626
|
+
}
|
|
4444
4627
|
async drain() {
|
|
4445
4628
|
try {
|
|
4446
4629
|
while (this.waiting.length > 0) {
|
|
@@ -4493,21 +4676,24 @@ var GitWorktreeIsolation = class {
|
|
|
4493
4676
|
return this.stopLanding(record, "failed");
|
|
4494
4677
|
}
|
|
4495
4678
|
for (const repo of changed) {
|
|
4496
|
-
const outcome = await this.mergeTask(run, repo, record);
|
|
4679
|
+
const { outcome, files } = await this.mergeTask(run, repo, record);
|
|
4497
4680
|
if (outcome === "merged") continue;
|
|
4498
|
-
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
|
|
4681
|
+
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
|
|
4499
4682
|
}
|
|
4500
4683
|
}
|
|
4501
4684
|
delete run.landing;
|
|
4502
|
-
record
|
|
4685
|
+
settleStatus(record, "merged");
|
|
4503
4686
|
delete record.conflictRepo;
|
|
4687
|
+
delete record.conflictFiles;
|
|
4504
4688
|
await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
|
|
4505
4689
|
return "merged";
|
|
4506
4690
|
}
|
|
4507
|
-
stopLanding(record, outcome, repo) {
|
|
4508
|
-
record
|
|
4691
|
+
stopLanding(record, outcome, repo, files) {
|
|
4692
|
+
settleStatus(record, outcome);
|
|
4509
4693
|
if (repo) record.conflictRepo = repo.path;
|
|
4510
4694
|
else delete record.conflictRepo;
|
|
4695
|
+
if (files && files.length > 0) record.conflictFiles = files;
|
|
4696
|
+
else delete record.conflictFiles;
|
|
4511
4697
|
return outcome;
|
|
4512
4698
|
}
|
|
4513
4699
|
/** 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 +4702,13 @@ var GitWorktreeIsolation = class {
|
|
|
4516
4702
|
try {
|
|
4517
4703
|
dir = await this.ensureIntegrationWorktree(run, repo);
|
|
4518
4704
|
} catch {
|
|
4519
|
-
return "failed";
|
|
4705
|
+
return { outcome: "failed", files: [] };
|
|
4520
4706
|
}
|
|
4521
4707
|
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
|
|
4708
|
+
if (merge.ok) return { outcome: "merged", files: [] };
|
|
4709
|
+
const files = await this.unmergedPaths(dir);
|
|
4524
4710
|
if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
|
|
4525
|
-
return
|
|
4711
|
+
return { outcome: files.length > 0 ? "conflict" : "failed", files };
|
|
4526
4712
|
}
|
|
4527
4713
|
/**
|
|
4528
4714
|
* Return every repo of the landing in flight to its recorded tip, then
|
|
@@ -4590,15 +4776,15 @@ var GitWorktreeIsolation = class {
|
|
|
4590
4776
|
await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
|
|
4591
4777
|
}
|
|
4592
4778
|
integrationDir(run, repo) {
|
|
4593
|
-
return
|
|
4779
|
+
return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
|
|
4594
4780
|
}
|
|
4595
4781
|
async ensureIntegrationWorktree(run, repo) {
|
|
4596
4782
|
return this.admin(run.workspaceRoot, async () => {
|
|
4597
4783
|
const dir = this.integrationDir(run, repo);
|
|
4598
|
-
if (fs4.existsSync(
|
|
4784
|
+
if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
|
|
4599
4785
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4600
4786
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
4601
|
-
fs4.mkdirSync(
|
|
4787
|
+
fs4.mkdirSync(path8.dirname(dir), { recursive: true });
|
|
4602
4788
|
await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
|
|
4603
4789
|
return dir;
|
|
4604
4790
|
});
|
|
@@ -4633,20 +4819,20 @@ var GitWorktreeIsolation = class {
|
|
|
4633
4819
|
return;
|
|
4634
4820
|
}
|
|
4635
4821
|
for (const entry of entries) {
|
|
4636
|
-
const target =
|
|
4822
|
+
const target = path8.join(dir, entry.name);
|
|
4637
4823
|
if (entry.isSymbolicLink()) this.unlinkIfLink(target);
|
|
4638
4824
|
else if (entry.isDirectory()) this.unlinkLinksUnder(target);
|
|
4639
4825
|
}
|
|
4640
4826
|
}
|
|
4641
4827
|
/** Task workspaces and branches under this run that no record accounts for. */
|
|
4642
4828
|
async removeUnowned(run) {
|
|
4643
|
-
const owned = new Set(Object.values(run.tasks).map((r) =>
|
|
4829
|
+
const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
|
|
4644
4830
|
const root = this.runRoot(run);
|
|
4645
4831
|
if (fs4.existsSync(root)) {
|
|
4646
4832
|
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,
|
|
4833
|
+
const dir = path8.join(root, entry);
|
|
4834
|
+
if (entry === INTEGRATION_DIR || owned.has(path8.resolve(dir))) continue;
|
|
4835
|
+
for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path8.join(dir, repo.path), []);
|
|
4650
4836
|
this.removeTaskWorkspace(run, dir);
|
|
4651
4837
|
}
|
|
4652
4838
|
}
|
|
@@ -4660,14 +4846,18 @@ var GitWorktreeIsolation = class {
|
|
|
4660
4846
|
}
|
|
4661
4847
|
}
|
|
4662
4848
|
async removeWorktreeDir(run, repo, dir, linked) {
|
|
4663
|
-
for (const name of linked
|
|
4849
|
+
for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
|
|
4850
|
+
const target = path8.join(dir, name);
|
|
4851
|
+
this.unlinkIfLink(target);
|
|
4852
|
+
this.unlinkLinksUnder(target);
|
|
4853
|
+
}
|
|
4664
4854
|
for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
|
|
4665
|
-
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(
|
|
4855
|
+
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
|
|
4666
4856
|
}
|
|
4667
4857
|
await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
|
|
4668
4858
|
if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
|
|
4669
4859
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
4670
|
-
for (let parent =
|
|
4860
|
+
for (let parent = path8.dirname(dir); isInside(this.runRoot(run), parent); parent = path8.dirname(parent)) this.removeIfEmpty(parent);
|
|
4671
4861
|
}
|
|
4672
4862
|
unlinkIfLink(target) {
|
|
4673
4863
|
try {
|
|
@@ -4682,14 +4872,14 @@ var GitWorktreeIsolation = class {
|
|
|
4682
4872
|
}
|
|
4683
4873
|
}
|
|
4684
4874
|
runRoot(run) {
|
|
4685
|
-
return
|
|
4875
|
+
return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
|
|
4686
4876
|
}
|
|
4687
4877
|
namesFor(run, task) {
|
|
4688
4878
|
const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
|
|
4689
4879
|
let name = `${task.order}-${slug2}`;
|
|
4690
4880
|
const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
|
|
4691
4881
|
if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
|
|
4692
|
-
return { branch: `ordewell/${run.id}/${name}`, dir:
|
|
4882
|
+
return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
|
|
4693
4883
|
}
|
|
4694
4884
|
/**
|
|
4695
4885
|
* Make one repo's worktree runnable: the default artifacts, unless a setup
|
|
@@ -4702,16 +4892,26 @@ var GitWorktreeIsolation = class {
|
|
|
4702
4892
|
const linked = [];
|
|
4703
4893
|
const copied = [];
|
|
4704
4894
|
const link = (name) => {
|
|
4705
|
-
const target =
|
|
4895
|
+
const target = path8.join(cwd, name);
|
|
4706
4896
|
if (lexists(target)) return;
|
|
4707
|
-
fs4.mkdirSync(
|
|
4708
|
-
if (linkPath(
|
|
4897
|
+
fs4.mkdirSync(path8.dirname(target), { recursive: true });
|
|
4898
|
+
if (linkPath(path8.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
|
|
4899
|
+
linked.push(name);
|
|
4900
|
+
};
|
|
4901
|
+
const mirror = (name) => {
|
|
4902
|
+
const target = path8.join(cwd, name);
|
|
4903
|
+
if (lexists(target) || !fs4.existsSync(path8.dirname(target))) return;
|
|
4904
|
+
const source = path8.join(repo.root, name);
|
|
4905
|
+
if (!fs4.lstatSync(source).isDirectory()) return link(name);
|
|
4906
|
+
const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
|
|
4907
|
+
copied.push(...mirrored.map((entry) => path8.join(name, entry)));
|
|
4709
4908
|
linked.push(name);
|
|
4710
4909
|
};
|
|
4711
4910
|
if (!setup) {
|
|
4712
4911
|
for (const name of fs4.readdirSync(repo.root)) {
|
|
4713
|
-
if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
4912
|
+
if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
4714
4913
|
}
|
|
4914
|
+
for (const dir of installDirs(repo.root)) mirror(dir);
|
|
4715
4915
|
}
|
|
4716
4916
|
for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
|
|
4717
4917
|
if (setup) await this.runSetup(setup, repo, cwd);
|
|
@@ -5159,6 +5359,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
5159
5359
|
}
|
|
5160
5360
|
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
5361
|
var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
|
|
5362
|
+
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
5363
|
function repoGroupSection(isolated) {
|
|
5163
5364
|
if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
|
|
5164
5365
|
return [
|
|
@@ -5250,6 +5451,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
5250
5451
|
"DEPENDENCY & PARALLELISM:",
|
|
5251
5452
|
"- Independent slices should have NO dependencies \u2014 they run in parallel.",
|
|
5252
5453
|
"- Only add dependencies when a slice truly depends on artifacts another slice creates.",
|
|
5454
|
+
SHARED_APPEND_FILE_RULE,
|
|
5253
5455
|
...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
|
|
5254
5456
|
...repoGroupSection(variant.isolatedExecution),
|
|
5255
5457
|
"",
|
|
@@ -5351,6 +5553,7 @@ function corePlannerPrompt(isolatedExecution) {
|
|
|
5351
5553
|
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
5554
|
"- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
|
|
5353
5555
|
"- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
|
|
5556
|
+
SHARED_APPEND_FILE_RULE,
|
|
5354
5557
|
isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
|
|
5355
5558
|
...repoGroupSection(isolatedExecution),
|
|
5356
5559
|
"",
|
|
@@ -5628,6 +5831,31 @@ ${task.prompt ?? task.description}`;
|
|
|
5628
5831
|
asked
|
|
5629
5832
|
].join("\n");
|
|
5630
5833
|
}
|
|
5834
|
+
function buildConflictRepairPrompt(task, conflict, integrationBranch) {
|
|
5835
|
+
const { branch } = conflict;
|
|
5836
|
+
const files = conflict.conflictFiles ?? [];
|
|
5837
|
+
const asked = `What the task was asked to do:
|
|
5838
|
+
${task.prompt ?? task.description}`;
|
|
5839
|
+
const resolve6 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
|
|
5840
|
+
const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
|
|
5841
|
+
if (repos.length === 0) {
|
|
5842
|
+
return [
|
|
5843
|
+
`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.`,
|
|
5844
|
+
`This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve6} Never drop either side wholesale, and never abort the merge or reset it away.`,
|
|
5845
|
+
"Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
|
|
5846
|
+
"",
|
|
5847
|
+
asked
|
|
5848
|
+
].join("\n");
|
|
5849
|
+
}
|
|
5850
|
+
const stoppedIn = conflict.conflictRepo ?? repos[0];
|
|
5851
|
+
return [
|
|
5852
|
+
`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.`,
|
|
5853
|
+
`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 ${resolve6} Never drop either side wholesale, and never abort a merge or reset it away.`,
|
|
5854
|
+
"Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
|
|
5855
|
+
"",
|
|
5856
|
+
asked
|
|
5857
|
+
].join("\n");
|
|
5858
|
+
}
|
|
5631
5859
|
|
|
5632
5860
|
// src/services/PlanRepair.ts
|
|
5633
5861
|
async function repairLoop(opts) {
|
|
@@ -7641,9 +7869,9 @@ ${line}
|
|
|
7641
7869
|
};
|
|
7642
7870
|
|
|
7643
7871
|
// src/services/transcriptCapture.ts
|
|
7644
|
-
import { readFileSync, readdirSync as
|
|
7872
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync3, statSync as statSync4, existsSync as existsSync4 } from "fs";
|
|
7645
7873
|
import * as os3 from "os";
|
|
7646
|
-
import * as
|
|
7874
|
+
import * as path9 from "path";
|
|
7647
7875
|
var HomeTranscriptReader = class {
|
|
7648
7876
|
home;
|
|
7649
7877
|
constructor(opts = {}) {
|
|
@@ -7662,21 +7890,21 @@ var HomeTranscriptReader = class {
|
|
|
7662
7890
|
};
|
|
7663
7891
|
var CLAUDE_PROJECT_NAME_MAX = 200;
|
|
7664
7892
|
function claudeProjectDirs(home, cwd) {
|
|
7665
|
-
const projects =
|
|
7893
|
+
const projects = path9.join(home, ".claude", "projects");
|
|
7666
7894
|
const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
7667
7895
|
if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
|
|
7668
|
-
const dir =
|
|
7669
|
-
return
|
|
7896
|
+
const dir = path9.join(projects, munged);
|
|
7897
|
+
return existsSync4(dir) ? [dir] : [];
|
|
7670
7898
|
}
|
|
7671
|
-
if (!
|
|
7899
|
+
if (!existsSync4(projects)) return [];
|
|
7672
7900
|
const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
|
|
7673
|
-
return
|
|
7901
|
+
return readdirSync3(projects).filter((name) => name.startsWith(prefix)).map((name) => path9.join(projects, name));
|
|
7674
7902
|
}
|
|
7675
7903
|
function claudeFinal(home, query, maxChars) {
|
|
7676
7904
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
7677
|
-
const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) =>
|
|
7905
|
+
const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path9.join(dir, f))).filter((f) => cutoff ? statSync4(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs);
|
|
7678
7906
|
for (const file of candidates) {
|
|
7679
|
-
const raw =
|
|
7907
|
+
const raw = readFileSync2(file, "utf8");
|
|
7680
7908
|
if (!raw.includes(query.marker)) continue;
|
|
7681
7909
|
const text = claudeLastAssistant(raw);
|
|
7682
7910
|
if (text) return clamp(text, maxChars);
|
|
@@ -7710,8 +7938,8 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
7710
7938
|
} catch {
|
|
7711
7939
|
return null;
|
|
7712
7940
|
}
|
|
7713
|
-
const dbPath =
|
|
7714
|
-
if (!
|
|
7941
|
+
const dbPath = path9.join(home, ".local", "share", "opencode", "opencode.db");
|
|
7942
|
+
if (!existsSync4(dbPath)) return null;
|
|
7715
7943
|
let db;
|
|
7716
7944
|
try {
|
|
7717
7945
|
db = new DatabaseSync(dbPath, { open: true });
|
|
@@ -7763,8 +7991,8 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
7763
7991
|
}
|
|
7764
7992
|
}
|
|
7765
7993
|
function codexFinal(home, query, maxChars) {
|
|
7766
|
-
const root =
|
|
7767
|
-
if (!
|
|
7994
|
+
const root = path9.join(home, ".codex", "sessions");
|
|
7995
|
+
if (!existsSync4(root)) return null;
|
|
7768
7996
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
7769
7997
|
const files = [];
|
|
7770
7998
|
const stack = [root];
|
|
@@ -7772,12 +8000,12 @@ function codexFinal(home, query, maxChars) {
|
|
|
7772
8000
|
const dir = stack.pop();
|
|
7773
8001
|
let entries;
|
|
7774
8002
|
try {
|
|
7775
|
-
entries =
|
|
8003
|
+
entries = readdirSync3(dir);
|
|
7776
8004
|
} catch {
|
|
7777
8005
|
continue;
|
|
7778
8006
|
}
|
|
7779
8007
|
for (const e of entries) {
|
|
7780
|
-
const full =
|
|
8008
|
+
const full = path9.join(dir, e);
|
|
7781
8009
|
let st;
|
|
7782
8010
|
try {
|
|
7783
8011
|
st = statSync4(full);
|
|
@@ -7796,7 +8024,7 @@ function codexFinal(home, query, maxChars) {
|
|
|
7796
8024
|
return null;
|
|
7797
8025
|
}
|
|
7798
8026
|
function codexLastAssistant(file, query) {
|
|
7799
|
-
const raw =
|
|
8027
|
+
const raw = readFileSync2(file, "utf8");
|
|
7800
8028
|
if (!raw.includes(query.marker)) return null;
|
|
7801
8029
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7802
8030
|
let sawCwd = false;
|
|
@@ -8415,7 +8643,7 @@ var TaskOrchestrator = class {
|
|
|
8415
8643
|
if (!this.isolationRun) return null;
|
|
8416
8644
|
const record = this.isolationRun.tasks[taskId];
|
|
8417
8645
|
if (!record) return { state: "none" };
|
|
8418
|
-
return taskIsolationOf(record);
|
|
8646
|
+
return taskIsolationOf(record, this.config.conflictRepairAttempts);
|
|
8419
8647
|
}
|
|
8420
8648
|
/**
|
|
8421
8649
|
* The plan's isolation as a surface shows it, for one with no stream to have
|
|
@@ -8424,7 +8652,7 @@ var TaskOrchestrator = class {
|
|
|
8424
8652
|
isolationView() {
|
|
8425
8653
|
const run = this.isolationRun;
|
|
8426
8654
|
if (!run) return null;
|
|
8427
|
-
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
|
|
8655
|
+
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
|
|
8428
8656
|
return { tasks, handoff: handoffOf(run) };
|
|
8429
8657
|
}
|
|
8430
8658
|
getAttemptSession(taskId) {
|
|
@@ -8460,24 +8688,43 @@ var TaskOrchestrator = class {
|
|
|
8460
8688
|
async reviewRunDiff() {
|
|
8461
8689
|
return this.isolation.reviewDiff(this.requireRun());
|
|
8462
8690
|
}
|
|
8463
|
-
/**
|
|
8691
|
+
/**
|
|
8692
|
+
* "Merge all": the run's integration branches into whatever the user has
|
|
8693
|
+
* checked out, in every repo or none. Once everything merged, the run has
|
|
8694
|
+
* nothing left to hand over, so it is cleared up and forgotten; a branch
|
|
8695
|
+
* the user's HEAD somehow does not contain stays for the next run's sweep.
|
|
8696
|
+
*/
|
|
8464
8697
|
async mergeRun() {
|
|
8465
8698
|
const run = this.requireRun();
|
|
8466
8699
|
const result = await this.isolation.mergeIntoCheckedOut(run);
|
|
8467
8700
|
const branch = integrationBranchNameOf(run);
|
|
8468
8701
|
const group = run.repos.some((r) => r.path !== SELF_REPO);
|
|
8469
|
-
const
|
|
8702
|
+
const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
|
|
8703
|
+
const { level, message } = describeMergeResult(result, branch, group, repaired);
|
|
8470
8704
|
this.notifications[level](message);
|
|
8705
|
+
if (result.outcome === "merged") await this.clearMergedRun(run);
|
|
8471
8706
|
return result;
|
|
8472
8707
|
}
|
|
8708
|
+
async clearMergedRun(run) {
|
|
8709
|
+
try {
|
|
8710
|
+
await this.isolation.discard(run, { integration: "delete-merged" });
|
|
8711
|
+
} catch (err) {
|
|
8712
|
+
this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
8713
|
+
return;
|
|
8714
|
+
}
|
|
8715
|
+
this.forgetRun();
|
|
8716
|
+
}
|
|
8473
8717
|
/** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
|
|
8474
8718
|
async cleanupRun() {
|
|
8475
|
-
await this.isolation.discard(this.requireRun(), {
|
|
8719
|
+
await this.isolation.discard(this.requireRun(), { integration: "keep" });
|
|
8476
8720
|
this.emit("onIsolationChanged");
|
|
8477
8721
|
}
|
|
8478
8722
|
/** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
|
|
8479
8723
|
async discardRun() {
|
|
8480
|
-
await this.isolation.discard(this.requireRun(), {
|
|
8724
|
+
await this.isolation.discard(this.requireRun(), { integration: "delete" });
|
|
8725
|
+
this.forgetRun();
|
|
8726
|
+
}
|
|
8727
|
+
forgetRun() {
|
|
8481
8728
|
this.isolationRun = null;
|
|
8482
8729
|
this.resolvers = {};
|
|
8483
8730
|
this.emit("onIsolationChanged");
|
|
@@ -8513,7 +8760,8 @@ var TaskOrchestrator = class {
|
|
|
8513
8760
|
}
|
|
8514
8761
|
loadPlan(tasks, planRunners = ["claude-code"]) {
|
|
8515
8762
|
this.store.load(tasks, planRunners);
|
|
8516
|
-
this.endAllAttempts("load");
|
|
8763
|
+
const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
|
|
8764
|
+
for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
8517
8765
|
if (!this.running) this.runMode = null;
|
|
8518
8766
|
this.blockedStart = null;
|
|
8519
8767
|
this.planStatus = "approved";
|
|
@@ -8571,9 +8819,10 @@ var TaskOrchestrator = class {
|
|
|
8571
8819
|
this.running = false;
|
|
8572
8820
|
this.planStatus = "approved";
|
|
8573
8821
|
this.terminalRunner.stopAll();
|
|
8574
|
-
const
|
|
8575
|
-
|
|
8576
|
-
|
|
8822
|
+
for (const a of this.endAllAttempts("stop")) {
|
|
8823
|
+
if (a.repair) this.store.markAwaitingUser(a.taskId);
|
|
8824
|
+
if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
8825
|
+
}
|
|
8577
8826
|
this.runMode = null;
|
|
8578
8827
|
this.blockedStart = null;
|
|
8579
8828
|
this.onHold.clear();
|
|
@@ -8589,6 +8838,7 @@ var TaskOrchestrator = class {
|
|
|
8589
8838
|
console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
|
|
8590
8839
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
8591
8840
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
8841
|
+
if (attempt.repair) return this.settleRepair(task, attempt, verdict);
|
|
8592
8842
|
const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
|
|
8593
8843
|
const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
|
|
8594
8844
|
if (this.attempts.get(taskId) !== attempt) return;
|
|
@@ -8609,6 +8859,9 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8609
8859
|
}
|
|
8610
8860
|
this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
|
|
8611
8861
|
this.logAndArchive(task, verdict);
|
|
8862
|
+
await this.afterVerdict();
|
|
8863
|
+
}
|
|
8864
|
+
async afterVerdict() {
|
|
8612
8865
|
this.emit("onTaskChanged");
|
|
8613
8866
|
if (!this.running) {
|
|
8614
8867
|
if (this.attempts.size === 0) {
|
|
@@ -8621,6 +8874,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8621
8874
|
}
|
|
8622
8875
|
await this.tick();
|
|
8623
8876
|
}
|
|
8877
|
+
/**
|
|
8878
|
+
* A repair's verdict decides only whether its work may try to land. It never
|
|
8879
|
+
* replaces the verdict the task's own work earned, and a repair that does not
|
|
8880
|
+
* land leaves the task waiting on the user — never a failed task, so never a
|
|
8881
|
+
* halted run.
|
|
8882
|
+
*/
|
|
8883
|
+
async settleRepair(task, attempt, verdict) {
|
|
8884
|
+
const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
|
|
8885
|
+
if (this.attempts.get(task.id) !== attempt) return;
|
|
8886
|
+
this.endAttempt(task.id, "verdict");
|
|
8887
|
+
if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
|
|
8888
|
+
else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
|
|
8889
|
+
else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
|
|
8890
|
+
else this.landUnmerged(task, landed.outcome);
|
|
8891
|
+
await this.afterVerdict();
|
|
8892
|
+
}
|
|
8893
|
+
/**
|
|
8894
|
+
* Evidence before the queue: the repair's work is committed and checked, and
|
|
8895
|
+
* only then merged, through the same serialized landing as any other task —
|
|
8896
|
+
* so a repair the tip has moved past again is a fresh conflict, not a pass.
|
|
8897
|
+
*/
|
|
8898
|
+
async landRepair(task, attempt) {
|
|
8899
|
+
const run = this.isolationRun;
|
|
8900
|
+
let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
|
|
8901
|
+
if (!run) return { evidence, outcome: "failed" };
|
|
8902
|
+
attempt.phase = "integrating";
|
|
8903
|
+
attempt.integration = (async () => {
|
|
8904
|
+
evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
|
|
8905
|
+
if (evidence.ok) return this.integrateWork(task);
|
|
8906
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
8907
|
+
return "conflict";
|
|
8908
|
+
})();
|
|
8909
|
+
const outcome = await attempt.integration;
|
|
8910
|
+
return { evidence, outcome };
|
|
8911
|
+
}
|
|
8912
|
+
async landedRepair(task, verdict) {
|
|
8913
|
+
const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
|
|
8914
|
+
this.store.markCompleted(task.id);
|
|
8915
|
+
this.store.unblockDependents(task.id);
|
|
8916
|
+
this.logAndArchive(task, task.verdict ?? verdict);
|
|
8917
|
+
this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
|
|
8918
|
+
await this.landResolved(task.id);
|
|
8919
|
+
}
|
|
8920
|
+
/** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
|
|
8921
|
+
async unrepaired(task, why) {
|
|
8922
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
8923
|
+
this.store.markAwaitingUser(task.id);
|
|
8924
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
8925
|
+
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.`);
|
|
8926
|
+
}
|
|
8927
|
+
describeEvidence(evidence) {
|
|
8928
|
+
const run = this.isolationRun;
|
|
8929
|
+
const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
|
|
8930
|
+
const branch = run ? integrationBranchNameOf(run) : "the integration branch";
|
|
8931
|
+
switch (evidence.reason) {
|
|
8932
|
+
case "not-merged":
|
|
8933
|
+
return `finished, but its branch${inRepo} does not contain ${branch}`;
|
|
8934
|
+
case "conflict-markers": {
|
|
8935
|
+
const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
|
|
8936
|
+
return `finished, but left conflict markers in ${capConflictFiles(files)}`;
|
|
8937
|
+
}
|
|
8938
|
+
case "failed":
|
|
8939
|
+
return `finished, but git could not check its work${inRepo}`;
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8624
8942
|
/**
|
|
8625
8943
|
* Merge a passed attempt's worktree. The attempt stays live while it waits on
|
|
8626
8944
|
* the module's merge queue, so a cancel, retry or stop in that window still
|
|
@@ -8654,11 +8972,25 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8654
8972
|
}
|
|
8655
8973
|
landUnmerged(task, landing) {
|
|
8656
8974
|
const branch = this.isolationRun ? integrationBranchNameOf(this.isolationRun) : "the integration branch";
|
|
8657
|
-
const
|
|
8658
|
-
const inRepo =
|
|
8975
|
+
const record = this.isolationRun?.tasks[task.id];
|
|
8976
|
+
const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
|
|
8659
8977
|
if (landing === "conflict") {
|
|
8978
|
+
const repair = this.nextRepair(task.id);
|
|
8660
8979
|
this.store.markAwaitingUser(task.id);
|
|
8661
|
-
|
|
8980
|
+
const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
|
|
8981
|
+
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}.`;
|
|
8982
|
+
if (repair) {
|
|
8983
|
+
this.notifications.warn(conflicted);
|
|
8984
|
+
if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
|
|
8985
|
+
else {
|
|
8986
|
+
this.store.markPending(task.id);
|
|
8987
|
+
this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
|
|
8988
|
+
}
|
|
8989
|
+
return;
|
|
8990
|
+
}
|
|
8991
|
+
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.`);
|
|
8992
|
+
const whyNot = this.noRepairReason(task);
|
|
8993
|
+
if (whyNot) this.tell("info", whyNot);
|
|
8662
8994
|
} else {
|
|
8663
8995
|
this.store.markFailed(task.id);
|
|
8664
8996
|
this.running = false;
|
|
@@ -8666,6 +8998,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8666
8998
|
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
8999
|
}
|
|
8668
9000
|
}
|
|
9001
|
+
/** 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. */
|
|
9002
|
+
nextRepair(taskId) {
|
|
9003
|
+
const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
|
|
9004
|
+
const limit = this.config.conflictRepairAttempts;
|
|
9005
|
+
const spent = record?.repairs ?? 0;
|
|
9006
|
+
return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
|
|
9007
|
+
}
|
|
9008
|
+
noRepairReason(task) {
|
|
9009
|
+
const limit = this.config.conflictRepairAttempts;
|
|
9010
|
+
if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
|
|
9011
|
+
const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
|
|
9012
|
+
return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
|
|
9013
|
+
}
|
|
8669
9014
|
/**
|
|
8670
9015
|
* Mark which added task resolves which conflict. The resolver merges the
|
|
8671
9016
|
* conflicted task's branch by hand in its own worktree; once that lands, the
|
|
@@ -8955,6 +9300,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8955
9300
|
cwd: null,
|
|
8956
9301
|
worktree: false,
|
|
8957
9302
|
integration: null,
|
|
9303
|
+
repair: this.nextRepair(task.id),
|
|
8958
9304
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8959
9305
|
};
|
|
8960
9306
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -8965,12 +9311,13 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8965
9311
|
const cwd = await this.resolveAttemptCwd(task, attempt);
|
|
8966
9312
|
attempt.cwd = cwd;
|
|
8967
9313
|
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);
|
|
9314
|
+
if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
|
|
9315
|
+
return this.abandonSpawn(task, attempt);
|
|
8970
9316
|
}
|
|
8971
|
-
const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
|
|
9317
|
+
const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
|
|
8972
9318
|
planMapEnabled: this.config.planMapEnabled,
|
|
8973
|
-
|
|
9319
|
+
// A merge to resolve is not new behaviour to drive test-first.
|
|
9320
|
+
tddEnabled: !attempt.repair && this.tddEnabled()
|
|
8974
9321
|
});
|
|
8975
9322
|
const session = await this.terminalRunner.spawn({
|
|
8976
9323
|
taskId: task.id,
|
|
@@ -8985,16 +9332,26 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
8985
9332
|
order: task.order,
|
|
8986
9333
|
title: task.title
|
|
8987
9334
|
});
|
|
8988
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
|
|
9335
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
|
|
8989
9336
|
attempt.phase = "running";
|
|
8990
9337
|
attempt.session = session;
|
|
8991
9338
|
this.output.attach(task.id, session);
|
|
8992
9339
|
this.verifier.watch(task, session);
|
|
8993
9340
|
this.emit("onTaskChanged");
|
|
8994
|
-
|
|
9341
|
+
if (attempt.repair) {
|
|
9342
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
9343
|
+
this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
|
|
9344
|
+
} else {
|
|
9345
|
+
this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
|
|
9346
|
+
}
|
|
8995
9347
|
} catch (err) {
|
|
8996
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
|
|
9348
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
|
|
8997
9349
|
this.endAttempt(task.id, "spawn-failed");
|
|
9350
|
+
if (attempt.repair) {
|
|
9351
|
+
await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
9352
|
+
this.emit("onTaskChanged");
|
|
9353
|
+
return this.tick();
|
|
9354
|
+
}
|
|
8998
9355
|
await this.releaseWorktree(task.id, { keep: false });
|
|
8999
9356
|
this.store.markPending(task.id);
|
|
9000
9357
|
this.onHold.add(task.id);
|
|
@@ -9008,17 +9365,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9008
9365
|
* and take back the claim it made. Only the claim — whatever ended the
|
|
9009
9366
|
* attempt may have decided the task since (mark complete, cancel, retry).
|
|
9010
9367
|
*/
|
|
9011
|
-
abandonSpawn(task, session) {
|
|
9368
|
+
abandonSpawn(task, attempt, session) {
|
|
9012
9369
|
session?.kill();
|
|
9013
9370
|
if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
|
|
9014
|
-
this.store.
|
|
9371
|
+
if (attempt.repair) this.store.markAwaitingUser(task.id);
|
|
9372
|
+
else this.store.markPending(task.id);
|
|
9015
9373
|
this.emit("onTaskChanged");
|
|
9016
9374
|
}
|
|
9375
|
+
repairPrompt(task) {
|
|
9376
|
+
const run = this.requireRun();
|
|
9377
|
+
const record = run.tasks[task.id];
|
|
9378
|
+
const conflict = {
|
|
9379
|
+
branch: record?.branch ?? "",
|
|
9380
|
+
repos: Object.keys(record?.repairBase ?? {}),
|
|
9381
|
+
...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
|
|
9382
|
+
...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
|
|
9383
|
+
};
|
|
9384
|
+
return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
|
|
9385
|
+
}
|
|
9017
9386
|
/**
|
|
9018
9387
|
* The one place an attempt's working directory is decided. Async so a
|
|
9019
9388
|
* per-attempt workspace (worktree isolation, #12) can be prepared here.
|
|
9020
9389
|
*/
|
|
9021
9390
|
async resolveAttemptCwd(task, attempt) {
|
|
9391
|
+
if (attempt.repair && this.isolationRun) {
|
|
9392
|
+
const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
|
|
9393
|
+
attempt.worktree = true;
|
|
9394
|
+
this.emit("onIsolationChanged");
|
|
9395
|
+
return cwd2;
|
|
9396
|
+
}
|
|
9022
9397
|
if (this.runMode !== "isolated" || !this.isolationRun) {
|
|
9023
9398
|
await this.releaseWorktree(task.id, { keep: false });
|
|
9024
9399
|
return this.workspaceRootFn();
|
|
@@ -9106,8 +9481,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9106
9481
|
}
|
|
9107
9482
|
}
|
|
9108
9483
|
this.runMode = "isolated";
|
|
9484
|
+
await this.sweep();
|
|
9109
9485
|
return true;
|
|
9110
9486
|
}
|
|
9487
|
+
/** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
|
|
9488
|
+
async sweep() {
|
|
9489
|
+
if (!this.isolationRun) return;
|
|
9490
|
+
try {
|
|
9491
|
+
await this.isolation.sweep(this.isolationRun);
|
|
9492
|
+
} catch (err) {
|
|
9493
|
+
this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
|
|
9494
|
+
}
|
|
9495
|
+
}
|
|
9111
9496
|
/**
|
|
9112
9497
|
* The plan's run carries on while anything has landed on its integration
|
|
9113
9498
|
* branch: a resumed plan's dependents must start from a tip that holds their
|
|
@@ -9120,14 +9505,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9120
9505
|
/**
|
|
9121
9506
|
* A run with nothing landed holds only superseded attempts, so it goes whole.
|
|
9122
9507
|
* One that cannot be continued for another reason — it ran from a different
|
|
9123
|
-
* workspace path — keeps its integration branch
|
|
9124
|
-
* work up.
|
|
9508
|
+
* workspace path — keeps its integration branch in each repo that has not
|
|
9509
|
+
* merged it: only the user gives landed work up.
|
|
9125
9510
|
*/
|
|
9126
9511
|
async mintRun(root) {
|
|
9127
9512
|
const previous = this.isolationRun;
|
|
9128
9513
|
if (previous) {
|
|
9129
9514
|
const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
|
|
9130
|
-
await this.isolation.discard(previous, {
|
|
9515
|
+
await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
|
|
9131
9516
|
this.isolationRun = null;
|
|
9132
9517
|
}
|
|
9133
9518
|
this.isolationRun = await this.isolation.startRun(root);
|
|
@@ -9192,9 +9577,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
9192
9577
|
return attempt;
|
|
9193
9578
|
}
|
|
9194
9579
|
endAllAttempts(reason) {
|
|
9195
|
-
|
|
9580
|
+
const ended = [...this.attempts.values()];
|
|
9581
|
+
for (const { taskId } of ended) this.endAttempt(taskId, reason);
|
|
9196
9582
|
this.verifier.reset();
|
|
9197
9583
|
if (reason === "load") this.output.reset();
|
|
9584
|
+
return ended;
|
|
9198
9585
|
}
|
|
9199
9586
|
};
|
|
9200
9587
|
|
|
@@ -9324,7 +9711,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
|
|
|
9324
9711
|
};
|
|
9325
9712
|
|
|
9326
9713
|
// src/services/ModelDiscovery.ts
|
|
9327
|
-
import * as
|
|
9714
|
+
import * as path12 from "path";
|
|
9328
9715
|
import { exec as exec3, spawn } from "child_process";
|
|
9329
9716
|
import { promisify as promisify3 } from "util";
|
|
9330
9717
|
import * as fsSync3 from "fs";
|
|
@@ -9332,17 +9719,17 @@ import * as osMod from "os";
|
|
|
9332
9719
|
|
|
9333
9720
|
// src/utils/globalDataDir.ts
|
|
9334
9721
|
import * as fs5 from "fs";
|
|
9335
|
-
import * as
|
|
9722
|
+
import * as path10 from "path";
|
|
9336
9723
|
import * as os4 from "os";
|
|
9337
9724
|
function globalDataDir() {
|
|
9338
|
-
return
|
|
9725
|
+
return path10.join(os4.homedir(), ".ordewell");
|
|
9339
9726
|
}
|
|
9340
9727
|
function copyDirSync(src, dest) {
|
|
9341
9728
|
fs5.mkdirSync(dest, { recursive: true });
|
|
9342
9729
|
const entries = fs5.readdirSync(src, { withFileTypes: true });
|
|
9343
9730
|
for (const entry of entries) {
|
|
9344
|
-
const srcPath =
|
|
9345
|
-
const destPath =
|
|
9731
|
+
const srcPath = path10.join(src, entry.name);
|
|
9732
|
+
const destPath = path10.join(dest, entry.name);
|
|
9346
9733
|
if (entry.isDirectory()) {
|
|
9347
9734
|
copyDirSync(srcPath, destPath);
|
|
9348
9735
|
} else {
|
|
@@ -9354,12 +9741,12 @@ var migrated = false;
|
|
|
9354
9741
|
function migrateOldConfigDir() {
|
|
9355
9742
|
if (migrated) return;
|
|
9356
9743
|
migrated = true;
|
|
9357
|
-
const oldDir =
|
|
9744
|
+
const oldDir = path10.join(os4.homedir(), ".config", "ordewell");
|
|
9358
9745
|
if (!fs5.existsSync(oldDir)) return;
|
|
9359
9746
|
const newDir = globalDataDir();
|
|
9360
9747
|
const liftFile = (name) => {
|
|
9361
|
-
const oldFile =
|
|
9362
|
-
const newFile =
|
|
9748
|
+
const oldFile = path10.join(oldDir, name);
|
|
9749
|
+
const newFile = path10.join(newDir, name);
|
|
9363
9750
|
if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
|
|
9364
9751
|
fs5.mkdirSync(newDir, { recursive: true });
|
|
9365
9752
|
fs5.copyFileSync(oldFile, newFile);
|
|
@@ -9367,8 +9754,8 @@ function migrateOldConfigDir() {
|
|
|
9367
9754
|
};
|
|
9368
9755
|
liftFile("settings.json");
|
|
9369
9756
|
liftFile(".env");
|
|
9370
|
-
const oldPlugins =
|
|
9371
|
-
const newPlugins =
|
|
9757
|
+
const oldPlugins = path10.join(oldDir, "plugins");
|
|
9758
|
+
const newPlugins = path10.join(newDir, "plugins");
|
|
9372
9759
|
if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
|
|
9373
9760
|
copyDirSync(oldPlugins, newPlugins);
|
|
9374
9761
|
}
|
|
@@ -9376,7 +9763,7 @@ function migrateOldConfigDir() {
|
|
|
9376
9763
|
|
|
9377
9764
|
// src/utils/launch.ts
|
|
9378
9765
|
import * as fsSync2 from "fs";
|
|
9379
|
-
import * as
|
|
9766
|
+
import * as path11 from "path";
|
|
9380
9767
|
|
|
9381
9768
|
// src/utils/shell.ts
|
|
9382
9769
|
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
|
|
@@ -9405,7 +9792,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
|
|
|
9405
9792
|
var WINDOWS_MAX_COMMAND_LINE = 32767;
|
|
9406
9793
|
function defaultPowerShell() {
|
|
9407
9794
|
const root = process.env.SystemRoot || process.env.windir;
|
|
9408
|
-
return root ?
|
|
9795
|
+
return root ? path11.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
9409
9796
|
}
|
|
9410
9797
|
var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
|
|
9411
9798
|
var CommandLineTooLongError = class extends Error {
|
|
@@ -9457,7 +9844,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
|
|
|
9457
9844
|
};
|
|
9458
9845
|
function isResolvableOnPosixPath(command, PATH, exists) {
|
|
9459
9846
|
if (command.includes("/")) return exists(command);
|
|
9460
|
-
return PATH.split(
|
|
9847
|
+
return PATH.split(path11.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path11.join(dir, command)));
|
|
9461
9848
|
}
|
|
9462
9849
|
function isExecutableResolved(command, plan, PATH, deps = {}) {
|
|
9463
9850
|
if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
|
|
@@ -9497,9 +9884,9 @@ function cmdArgs(line) {
|
|
|
9497
9884
|
return ["/d", "/s", "/c", `"${line}"`];
|
|
9498
9885
|
}
|
|
9499
9886
|
function candidateExtensions(command, pathExt) {
|
|
9500
|
-
const declared = pathExt.split(
|
|
9887
|
+
const declared = pathExt.split(path11.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
9501
9888
|
const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
|
|
9502
|
-
const ext =
|
|
9889
|
+
const ext = path11.win32.extname(command).toLowerCase();
|
|
9503
9890
|
if (ext && known.has(ext)) {
|
|
9504
9891
|
const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
|
|
9505
9892
|
return { direct: [], cmd: [], powershell: [], [route]: [""] };
|
|
@@ -9520,13 +9907,13 @@ async function findWindowsExecutables(command, deps) {
|
|
|
9520
9907
|
const exists = deps.exists ?? defaultExists2;
|
|
9521
9908
|
const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
|
|
9522
9909
|
const byRoute = candidateExtensions(command, pathExt);
|
|
9523
|
-
const dirs =
|
|
9524
|
-
const base =
|
|
9910
|
+
const dirs = path11.win32.basename(command) !== command ? [path11.win32.dirname(path11.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path11.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
9911
|
+
const base = path11.win32.basename(command);
|
|
9525
9912
|
const found = [];
|
|
9526
9913
|
for (const route of ROUTES) {
|
|
9527
9914
|
outer: for (const ext of byRoute[route]) {
|
|
9528
9915
|
for (const dir of dirs) {
|
|
9529
|
-
const candidate =
|
|
9916
|
+
const candidate = path11.win32.join(dir, `${base}${ext}`);
|
|
9530
9917
|
if (exists(candidate)) {
|
|
9531
9918
|
found.push({ file: candidate, route });
|
|
9532
9919
|
break outer;
|
|
@@ -9621,7 +10008,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9621
10008
|
const PATH = await augmentedPath();
|
|
9622
10009
|
const launch = await planDirectLaunch(command, args).catch(() => null);
|
|
9623
10010
|
if (!launch) return null;
|
|
9624
|
-
return new Promise((
|
|
10011
|
+
return new Promise((resolve6) => {
|
|
9625
10012
|
let child;
|
|
9626
10013
|
try {
|
|
9627
10014
|
child = spawn(launch.file, launch.args, {
|
|
@@ -9630,7 +10017,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9630
10017
|
windowsVerbatimArguments: launch.verbatim
|
|
9631
10018
|
});
|
|
9632
10019
|
} catch {
|
|
9633
|
-
|
|
10020
|
+
resolve6(null);
|
|
9634
10021
|
return;
|
|
9635
10022
|
}
|
|
9636
10023
|
let settled = false;
|
|
@@ -9639,7 +10026,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
9639
10026
|
settled = true;
|
|
9640
10027
|
clearTimeout(timer);
|
|
9641
10028
|
killTree(child);
|
|
9642
|
-
|
|
10029
|
+
resolve6(value);
|
|
9643
10030
|
};
|
|
9644
10031
|
const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
|
|
9645
10032
|
child.on("error", () => finish(null));
|
|
@@ -9706,7 +10093,7 @@ function geminiDiscoveryCacheKey(apiKey) {
|
|
|
9706
10093
|
return {
|
|
9707
10094
|
key: `gemini-models:${hash}`,
|
|
9708
10095
|
dir,
|
|
9709
|
-
file:
|
|
10096
|
+
file: path12.join(dir, `gemini_models_${hash}.json`)
|
|
9710
10097
|
};
|
|
9711
10098
|
}
|
|
9712
10099
|
function readGeminiCache(apiKey) {
|
|
@@ -10230,7 +10617,7 @@ var ModelDiscovery = class {
|
|
|
10230
10617
|
|
|
10231
10618
|
// src/services/ModelCatalog.ts
|
|
10232
10619
|
import * as fs6 from "fs";
|
|
10233
|
-
import * as
|
|
10620
|
+
import * as path13 from "path";
|
|
10234
10621
|
var CACHE_KEY = "model-catalog";
|
|
10235
10622
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
10236
10623
|
function storageKey(baseUrl) {
|
|
@@ -10282,7 +10669,7 @@ var ModelCatalog = class {
|
|
|
10282
10669
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
10283
10670
|
const key = storageKey(url);
|
|
10284
10671
|
const cacheDir = globalCacheDir();
|
|
10285
|
-
const cacheFile =
|
|
10672
|
+
const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
10286
10673
|
if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
|
|
10287
10674
|
} catch {
|
|
10288
10675
|
}
|
|
@@ -10290,7 +10677,7 @@ var ModelCatalog = class {
|
|
|
10290
10677
|
static readCache(key) {
|
|
10291
10678
|
try {
|
|
10292
10679
|
const cacheDir = globalCacheDir();
|
|
10293
|
-
const cacheFile =
|
|
10680
|
+
const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
10294
10681
|
if (!fs6.existsSync(cacheFile)) return null;
|
|
10295
10682
|
const raw = fs6.readFileSync(cacheFile, "utf8");
|
|
10296
10683
|
const data = JSON.parse(raw);
|
|
@@ -10304,7 +10691,7 @@ var ModelCatalog = class {
|
|
|
10304
10691
|
try {
|
|
10305
10692
|
const cacheDir = globalCacheDir();
|
|
10306
10693
|
fs6.mkdirSync(cacheDir, { recursive: true });
|
|
10307
|
-
const cacheFile =
|
|
10694
|
+
const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
10308
10695
|
fs6.writeFileSync(
|
|
10309
10696
|
cacheFile,
|
|
10310
10697
|
JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
|
|
@@ -10688,7 +11075,8 @@ var ORDEWELL_SETTABLE_ENV = [
|
|
|
10688
11075
|
"ORDEWELL_SUBAGENT_MODEL",
|
|
10689
11076
|
"ORDEWELL_PLANNER_EFFORT",
|
|
10690
11077
|
"ORDEWELL_RESEARCH_ENABLED",
|
|
10691
|
-
"ORDEWELL_MAX_PARALLEL"
|
|
11078
|
+
"ORDEWELL_MAX_PARALLEL",
|
|
11079
|
+
"ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
|
|
10692
11080
|
];
|
|
10693
11081
|
var SETTINGS_ENV_REFUSED = [
|
|
10694
11082
|
// Approval posture and pre-approved scope.
|
|
@@ -10745,7 +11133,7 @@ import { spawn as nodeSpawn } from "child_process";
|
|
|
10745
11133
|
|
|
10746
11134
|
// src/utils/workspace.ts
|
|
10747
11135
|
import * as fs7 from "fs";
|
|
10748
|
-
import * as
|
|
11136
|
+
import * as path14 from "path";
|
|
10749
11137
|
var WorkspaceNotFoundError = class extends Error {
|
|
10750
11138
|
constructor(workspace) {
|
|
10751
11139
|
super(`Workspace "${workspace}" does not exist.`);
|
|
@@ -10801,7 +11189,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
|
|
|
10801
11189
|
throw new WorkspaceNotAProjectError(workspace);
|
|
10802
11190
|
}
|
|
10803
11191
|
const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
|
|
10804
|
-
(marker2) => exists(
|
|
11192
|
+
(marker2) => exists(path14.join(resolved, marker2))
|
|
10805
11193
|
);
|
|
10806
11194
|
if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
|
|
10807
11195
|
}
|
|
@@ -10869,8 +11257,8 @@ var StdioAgentAdapter = class {
|
|
|
10869
11257
|
}
|
|
10870
11258
|
async start(opts) {
|
|
10871
11259
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
10872
|
-
this.processEnded = new Promise((
|
|
10873
|
-
this.markEnded =
|
|
11260
|
+
this.processEnded = new Promise((resolve6) => {
|
|
11261
|
+
this.markEnded = resolve6;
|
|
10874
11262
|
});
|
|
10875
11263
|
const spec = this.spawnSpec(opts);
|
|
10876
11264
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
@@ -10921,7 +11309,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10921
11309
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
10922
11310
|
return;
|
|
10923
11311
|
}
|
|
10924
|
-
await new Promise((
|
|
11312
|
+
await new Promise((resolve6) => {
|
|
10925
11313
|
let settled = false;
|
|
10926
11314
|
const finish = () => {
|
|
10927
11315
|
if (settled) return;
|
|
@@ -10932,7 +11320,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10932
11320
|
signal?.removeEventListener("abort", onAbort);
|
|
10933
11321
|
this.process?.removeListener("exit", onExit);
|
|
10934
11322
|
this.process?.removeListener("error", onExit);
|
|
10935
|
-
|
|
11323
|
+
resolve6();
|
|
10936
11324
|
};
|
|
10937
11325
|
const emit = (event) => {
|
|
10938
11326
|
onEvent(event);
|
|
@@ -11147,14 +11535,14 @@ function codexSandboxUnavailableMessage() {
|
|
|
11147
11535
|
].join("\n");
|
|
11148
11536
|
}
|
|
11149
11537
|
function runProbe(deps, cwd, env, flags) {
|
|
11150
|
-
return new Promise((
|
|
11538
|
+
return new Promise((resolve6) => {
|
|
11151
11539
|
let output = "";
|
|
11152
11540
|
let settled = false;
|
|
11153
11541
|
const finish = (code) => {
|
|
11154
11542
|
if (settled) return;
|
|
11155
11543
|
settled = true;
|
|
11156
11544
|
clearTimeout(timer);
|
|
11157
|
-
|
|
11545
|
+
resolve6({ code, output });
|
|
11158
11546
|
};
|
|
11159
11547
|
const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
11160
11548
|
timer.unref?.();
|
|
@@ -11237,8 +11625,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11237
11625
|
async handshake(opts) {
|
|
11238
11626
|
this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
|
|
11239
11627
|
if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
|
|
11240
|
-
const ready = new Promise((
|
|
11241
|
-
this.settleHandshake =
|
|
11628
|
+
const ready = new Promise((resolve6) => {
|
|
11629
|
+
this.settleHandshake = resolve6;
|
|
11242
11630
|
});
|
|
11243
11631
|
this.writeLine({
|
|
11244
11632
|
jsonrpc: "2.0",
|
|
@@ -11251,8 +11639,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11251
11639
|
// A binary that rejects `app-server` outright dies immediately; waiting
|
|
11252
11640
|
// out the timeout would turn a one-line diagnostic into a 30s stall.
|
|
11253
11641
|
this.processEnded.then(() => false),
|
|
11254
|
-
new Promise((
|
|
11255
|
-
const t = setTimeout(() =>
|
|
11642
|
+
new Promise((resolve6) => {
|
|
11643
|
+
const t = setTimeout(() => resolve6(false), HANDSHAKE_TIMEOUT_MS);
|
|
11256
11644
|
t.unref?.();
|
|
11257
11645
|
})
|
|
11258
11646
|
]);
|
|
@@ -11529,12 +11917,12 @@ function describeError(err) {
|
|
|
11529
11917
|
return lines.join(": ");
|
|
11530
11918
|
}
|
|
11531
11919
|
function delay(ms, signal) {
|
|
11532
|
-
return new Promise((
|
|
11533
|
-
const timer = setTimeout(
|
|
11920
|
+
return new Promise((resolve6) => {
|
|
11921
|
+
const timer = setTimeout(resolve6, ms);
|
|
11534
11922
|
timer.unref?.();
|
|
11535
11923
|
signal?.addEventListener("abort", () => {
|
|
11536
11924
|
clearTimeout(timer);
|
|
11537
|
-
|
|
11925
|
+
resolve6();
|
|
11538
11926
|
}, { once: true });
|
|
11539
11927
|
});
|
|
11540
11928
|
}
|
|
@@ -11573,12 +11961,12 @@ var OpenCodeAdapter = class {
|
|
|
11573
11961
|
cwd: opts.cwd,
|
|
11574
11962
|
windowsVerbatimArguments: launch.verbatim
|
|
11575
11963
|
});
|
|
11576
|
-
const banner = new Promise((
|
|
11964
|
+
const banner = new Promise((resolve6) => {
|
|
11577
11965
|
let seen = "";
|
|
11578
11966
|
const scan = (chunk) => {
|
|
11579
11967
|
seen += chunk.toString();
|
|
11580
11968
|
const match = seen.match(/https?:\/\/[^\s]+/);
|
|
11581
|
-
if (match)
|
|
11969
|
+
if (match) resolve6(match[0].replace(/[.,)]$/, ""));
|
|
11582
11970
|
};
|
|
11583
11971
|
this.process.stdout?.on("data", scan);
|
|
11584
11972
|
this.process.stderr?.on("data", (chunk) => {
|
|
@@ -11587,15 +11975,15 @@ var OpenCodeAdapter = class {
|
|
|
11587
11975
|
});
|
|
11588
11976
|
this.process.on("exit", () => {
|
|
11589
11977
|
this.exited = true;
|
|
11590
|
-
|
|
11978
|
+
resolve6(null);
|
|
11591
11979
|
});
|
|
11592
11980
|
this.process.on("error", (err) => {
|
|
11593
11981
|
this.exited = true;
|
|
11594
11982
|
this.stderrTail = (this.stderrTail + `
|
|
11595
11983
|
${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
11596
|
-
|
|
11984
|
+
resolve6(null);
|
|
11597
11985
|
});
|
|
11598
|
-
const timer = setTimeout(() =>
|
|
11986
|
+
const timer = setTimeout(() => resolve6(null), SERVER_READY_TIMEOUT_MS);
|
|
11599
11987
|
timer.unref?.();
|
|
11600
11988
|
});
|
|
11601
11989
|
this.baseUrl = await banner;
|
|
@@ -11628,8 +12016,8 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11628
12016
|
const streamAbort = new AbortController();
|
|
11629
12017
|
let connected = () => {
|
|
11630
12018
|
};
|
|
11631
|
-
const streamReady = new Promise((
|
|
11632
|
-
connected =
|
|
12019
|
+
const streamReady = new Promise((resolve6) => {
|
|
12020
|
+
connected = resolve6;
|
|
11633
12021
|
});
|
|
11634
12022
|
const live = this.streamEvents(
|
|
11635
12023
|
streamAbort.signal,
|
|
@@ -11833,14 +12221,14 @@ ${part.text}` : part.text });
|
|
|
11833
12221
|
}
|
|
11834
12222
|
}
|
|
11835
12223
|
}
|
|
11836
|
-
async json(method,
|
|
11837
|
-
const response = await this.deps.fetch(`${this.baseUrl}${
|
|
12224
|
+
async json(method, path23, body, signal) {
|
|
12225
|
+
const response = await this.deps.fetch(`${this.baseUrl}${path23}`, {
|
|
11838
12226
|
method,
|
|
11839
12227
|
headers: { "content-type": "application/json" },
|
|
11840
12228
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
11841
12229
|
...signal ? { signal } : {}
|
|
11842
12230
|
});
|
|
11843
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
12231
|
+
if (!response.ok) throw new Error(`${method} ${path23} failed: ${response.status} ${response.statusText}`);
|
|
11844
12232
|
return await response.json().catch(() => null);
|
|
11845
12233
|
}
|
|
11846
12234
|
nativeSessionId() {
|
|
@@ -12509,33 +12897,9 @@ var PlannerConversation = class {
|
|
|
12509
12897
|
if (m.role !== "user" || index === 0) return [];
|
|
12510
12898
|
const line = m.content.split("\n")[0];
|
|
12511
12899
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
12512
|
-
return [{ index, preview, timestamp: m.timestamp }];
|
|
12900
|
+
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
12513
12901
|
});
|
|
12514
12902
|
}
|
|
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
12903
|
/**
|
|
12540
12904
|
* Replace the transcript with a summary of it, keeping the last two
|
|
12541
12905
|
* exchanges as they were, and drop the live context so the next message
|
|
@@ -12611,6 +12975,28 @@ var PlannerConversation = class {
|
|
|
12611
12975
|
researchLog: plan?.researchLog ?? []
|
|
12612
12976
|
});
|
|
12613
12977
|
}
|
|
12978
|
+
/**
|
|
12979
|
+
* A copy of the dialogue as it stood just before the user message at
|
|
12980
|
+
* `index` (a position from {@link rewindTargets}) — what a rewind forks
|
|
12981
|
+
* from — plus that message's full text, for a surface to offer back. This
|
|
12982
|
+
* conversation is not touched. The research trace is cut at the same point
|
|
12983
|
+
* by time: its entries carry no link to the message that caused them.
|
|
12984
|
+
*/
|
|
12985
|
+
cloneBefore(index) {
|
|
12986
|
+
this.assertIdle("rewind the conversation");
|
|
12987
|
+
if (!this.rewindTargets().some((t) => t.index === index)) {
|
|
12988
|
+
if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
|
|
12989
|
+
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.");
|
|
12990
|
+
}
|
|
12991
|
+
const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
|
|
12992
|
+
return {
|
|
12993
|
+
dialogue: structuredClone({
|
|
12994
|
+
conversationHistory: this.transcript.slice(0, index),
|
|
12995
|
+
researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
|
|
12996
|
+
}),
|
|
12997
|
+
rewoundMessage
|
|
12998
|
+
};
|
|
12999
|
+
}
|
|
12614
13000
|
/** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
|
|
12615
13001
|
recordModification(request, requestedAt, taskCount) {
|
|
12616
13002
|
this.append("user", request, { timestamp: requestedAt });
|
|
@@ -13197,17 +13583,17 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
13197
13583
|
|
|
13198
13584
|
// src/services/SkillsService.ts
|
|
13199
13585
|
import * as fs8 from "fs";
|
|
13200
|
-
import * as
|
|
13586
|
+
import * as path16 from "path";
|
|
13201
13587
|
|
|
13202
13588
|
// src/services/builtinSkills.ts
|
|
13203
|
-
import * as
|
|
13589
|
+
import * as path15 from "path";
|
|
13204
13590
|
import { fileURLToPath } from "url";
|
|
13205
13591
|
function builtinSkillsDir() {
|
|
13206
|
-
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname :
|
|
13592
|
+
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path15.dirname(fileURLToPath(
|
|
13207
13593
|
// @ts-expect-error - import.meta is only valid under an ESM module setting
|
|
13208
13594
|
import.meta.url
|
|
13209
13595
|
));
|
|
13210
|
-
return
|
|
13596
|
+
return path15.join(dir, "..", "skills");
|
|
13211
13597
|
}
|
|
13212
13598
|
|
|
13213
13599
|
// src/services/SkillsService.ts
|
|
@@ -13246,8 +13632,8 @@ function copyDirSync2(src, dest) {
|
|
|
13246
13632
|
fs8.mkdirSync(dest, { recursive: true });
|
|
13247
13633
|
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
13248
13634
|
for (const entry of entries) {
|
|
13249
|
-
const srcPath =
|
|
13250
|
-
const destPath =
|
|
13635
|
+
const srcPath = path16.join(src, entry.name);
|
|
13636
|
+
const destPath = path16.join(dest, entry.name);
|
|
13251
13637
|
if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
|
|
13252
13638
|
else fs8.copyFileSync(srcPath, destPath);
|
|
13253
13639
|
}
|
|
@@ -13258,7 +13644,7 @@ function readDir(dir, source) {
|
|
|
13258
13644
|
const skills = [];
|
|
13259
13645
|
for (const entry of entries) {
|
|
13260
13646
|
if (!entry.isDirectory()) continue;
|
|
13261
|
-
const skillFile =
|
|
13647
|
+
const skillFile = path16.join(dir, entry.name, "SKILL.md");
|
|
13262
13648
|
if (!fs8.existsSync(skillFile)) continue;
|
|
13263
13649
|
const parsed = parseSkillFile(skillFile, entry.name, source);
|
|
13264
13650
|
if (parsed) skills.push(parsed);
|
|
@@ -13271,17 +13657,17 @@ var SkillsService = class {
|
|
|
13271
13657
|
}
|
|
13272
13658
|
workspaceRoot;
|
|
13273
13659
|
localDir() {
|
|
13274
|
-
return this.workspaceRoot ?
|
|
13660
|
+
return this.workspaceRoot ? path16.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
|
|
13275
13661
|
}
|
|
13276
13662
|
globalDir() {
|
|
13277
|
-
return
|
|
13663
|
+
return path16.join(globalDataDir(), "skills");
|
|
13278
13664
|
}
|
|
13279
13665
|
seed(name) {
|
|
13280
|
-
const dest =
|
|
13666
|
+
const dest = path16.join(this.globalDir(), name);
|
|
13281
13667
|
if (fs8.existsSync(dest)) return false;
|
|
13282
13668
|
let srcDir;
|
|
13283
13669
|
try {
|
|
13284
|
-
srcDir =
|
|
13670
|
+
srcDir = path16.join(builtinSkillsDir(), name);
|
|
13285
13671
|
} catch (err) {
|
|
13286
13672
|
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
13287
13673
|
return false;
|
|
@@ -13307,7 +13693,7 @@ var SkillsService = class {
|
|
|
13307
13693
|
if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
|
|
13308
13694
|
let raw;
|
|
13309
13695
|
try {
|
|
13310
|
-
raw = fs8.readFileSync(
|
|
13696
|
+
raw = fs8.readFileSync(path16.join(dir, "SKILL.md"), "utf8");
|
|
13311
13697
|
} catch {
|
|
13312
13698
|
return false;
|
|
13313
13699
|
}
|
|
@@ -13324,7 +13710,7 @@ var SkillsService = class {
|
|
|
13324
13710
|
}
|
|
13325
13711
|
pruneRetired() {
|
|
13326
13712
|
for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
|
|
13327
|
-
const dest =
|
|
13713
|
+
const dest = path16.join(this.globalDir(), name);
|
|
13328
13714
|
if (!fs8.existsSync(dest)) continue;
|
|
13329
13715
|
if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
|
|
13330
13716
|
try {
|
|
@@ -13339,10 +13725,10 @@ var SkillsService = class {
|
|
|
13339
13725
|
if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
|
|
13340
13726
|
const localDir = this.localDir();
|
|
13341
13727
|
if (localDir) {
|
|
13342
|
-
const localFile =
|
|
13728
|
+
const localFile = path16.join(localDir, name, "SKILL.md");
|
|
13343
13729
|
if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
|
|
13344
13730
|
}
|
|
13345
|
-
const globalFile =
|
|
13731
|
+
const globalFile = path16.join(this.globalDir(), name, "SKILL.md");
|
|
13346
13732
|
if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
|
|
13347
13733
|
return void 0;
|
|
13348
13734
|
}
|
|
@@ -13371,7 +13757,7 @@ function createSkillsService(workspaceRoot) {
|
|
|
13371
13757
|
|
|
13372
13758
|
// src/utils/sessionStore.ts
|
|
13373
13759
|
import * as fs9 from "fs";
|
|
13374
|
-
import * as
|
|
13760
|
+
import * as path17 from "path";
|
|
13375
13761
|
|
|
13376
13762
|
// src/utils/sessionId.ts
|
|
13377
13763
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -13382,7 +13768,7 @@ function mintSessionId() {
|
|
|
13382
13768
|
// src/utils/sessionStore.ts
|
|
13383
13769
|
var SESSIONS_SUBDIR = "sessions";
|
|
13384
13770
|
function getSessionsDir(baseDir) {
|
|
13385
|
-
return
|
|
13771
|
+
return path17.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
13386
13772
|
}
|
|
13387
13773
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
13388
13774
|
function hasScalarRunner(parsed) {
|
|
@@ -13404,8 +13790,8 @@ function legacySessionFilename(session) {
|
|
|
13404
13790
|
return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
|
|
13405
13791
|
}
|
|
13406
13792
|
function dropLegacyFile(sessionsDir, meta) {
|
|
13407
|
-
const legacy =
|
|
13408
|
-
if (legacy ===
|
|
13793
|
+
const legacy = path17.join(sessionsDir, legacySessionFilename(meta));
|
|
13794
|
+
if (legacy === path17.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
|
|
13409
13795
|
try {
|
|
13410
13796
|
const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
|
|
13411
13797
|
if (held === meta.id) fs9.unlinkSync(legacy);
|
|
@@ -13431,7 +13817,7 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
13431
13817
|
plan: JSON.parse(JSON.stringify(plan))
|
|
13432
13818
|
};
|
|
13433
13819
|
const filename = sessionFilename(meta);
|
|
13434
|
-
const filePath =
|
|
13820
|
+
const filePath = path17.join(sessionsDir, filename);
|
|
13435
13821
|
fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
13436
13822
|
dropLegacyFile(sessionsDir, meta);
|
|
13437
13823
|
return meta;
|
|
@@ -13442,7 +13828,7 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
13442
13828
|
const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
13443
13829
|
return files.map((f) => {
|
|
13444
13830
|
try {
|
|
13445
|
-
const raw = fs9.readFileSync(
|
|
13831
|
+
const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
|
|
13446
13832
|
const parsed = JSON.parse(raw);
|
|
13447
13833
|
if (hasScalarRunner(parsed)) {
|
|
13448
13834
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -13460,7 +13846,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
13460
13846
|
const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
13461
13847
|
for (const f of files) {
|
|
13462
13848
|
try {
|
|
13463
|
-
const raw = fs9.readFileSync(
|
|
13849
|
+
const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
|
|
13464
13850
|
const session = JSON.parse(raw);
|
|
13465
13851
|
if (session.meta.id === sessionId) return f;
|
|
13466
13852
|
} catch (err) {
|
|
@@ -13475,7 +13861,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
13475
13861
|
try {
|
|
13476
13862
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
13477
13863
|
if (file) {
|
|
13478
|
-
const raw = fs9.readFileSync(
|
|
13864
|
+
const raw = fs9.readFileSync(path17.join(sessionsDir, file), "utf-8");
|
|
13479
13865
|
const parsed = JSON.parse(raw);
|
|
13480
13866
|
if (hasScalarRunner(parsed)) {
|
|
13481
13867
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -13510,7 +13896,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
13510
13896
|
try {
|
|
13511
13897
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
13512
13898
|
if (file) {
|
|
13513
|
-
fs9.unlinkSync(
|
|
13899
|
+
fs9.unlinkSync(path17.join(sessionsDir, file));
|
|
13514
13900
|
return true;
|
|
13515
13901
|
}
|
|
13516
13902
|
} catch (err) {
|
|
@@ -14042,26 +14428,30 @@ var Session = class {
|
|
|
14042
14428
|
*/
|
|
14043
14429
|
forkConversation() {
|
|
14044
14430
|
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 };
|
|
14431
|
+
return this.saveFork(this.conversation.clone());
|
|
14050
14432
|
}
|
|
14051
14433
|
/**
|
|
14052
|
-
*
|
|
14053
|
-
* position from {@link rewindTargets})
|
|
14054
|
-
*
|
|
14055
|
-
*
|
|
14434
|
+
* Fork the conversation from just before a user message (a transcript
|
|
14435
|
+
* position from {@link rewindTargets}): the fork holds everything said
|
|
14436
|
+
* before it and the current task list, like {@link forkConversation}, and
|
|
14437
|
+
* this session keeps its whole history. The message's full text comes back
|
|
14438
|
+
* so a surface can offer it for resending.
|
|
14056
14439
|
*/
|
|
14057
14440
|
rewindConversation(userMessageIndex) {
|
|
14058
14441
|
if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
|
|
14059
|
-
|
|
14442
|
+
const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
|
|
14443
|
+
return { ...this.saveFork(dialogue), rewoundMessage };
|
|
14444
|
+
}
|
|
14445
|
+
saveFork(dialogue) {
|
|
14446
|
+
const sessionId = mintSessionId();
|
|
14447
|
+
const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
|
|
14448
|
+
saveSession(plan, this.goal, this.workspace, sessionId);
|
|
14449
|
+
return { sessionId, goal: this.goal, workspace: this.workspace };
|
|
14060
14450
|
}
|
|
14061
14451
|
/**
|
|
14062
14452
|
* Condense the conversation on the user's say-so: a hidden planner turn
|
|
14063
14453
|
* summarises it, and the summary replaces everything but the last two
|
|
14064
|
-
* exchanges. Conversation only
|
|
14454
|
+
* exchanges. Conversation only — the tasks, and any run
|
|
14065
14455
|
* executing them, are untouched. Atomic: a failed or stopped turn changes
|
|
14066
14456
|
* nothing.
|
|
14067
14457
|
*/
|
|
@@ -14334,8 +14724,9 @@ var Session = class {
|
|
|
14334
14724
|
* The opt-in way through a merge conflict: add an AI task, on the conflicted
|
|
14335
14725
|
* task's own runner and model, that merges its branch by hand in a worktree
|
|
14336
14726
|
* of its own. When that task lands, the conflicted one lands through it (see
|
|
14337
|
-
* {@link TaskOrchestrator.linkConflictResolver}).
|
|
14338
|
-
*
|
|
14727
|
+
* {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
|
|
14728
|
+
* conflict is a repair, a new attempt of the same task (ADR-0015); a task is
|
|
14729
|
+
* only ever added to the plan by this call.
|
|
14339
14730
|
*/
|
|
14340
14731
|
async resolveConflictAsTask(taskId) {
|
|
14341
14732
|
if (!this.plan) return null;
|
|
@@ -15088,8 +15479,8 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
15088
15479
|
import { execFile as execFile3 } from "child_process";
|
|
15089
15480
|
import { promisify as promisify5 } from "util";
|
|
15090
15481
|
import { tmpdir } from "os";
|
|
15091
|
-
import { join as
|
|
15092
|
-
import { existsSync as
|
|
15482
|
+
import { join as join16 } from "path";
|
|
15483
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
15093
15484
|
|
|
15094
15485
|
// src/utils/tmux.ts
|
|
15095
15486
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -15196,7 +15587,7 @@ var TmuxSession = class extends AbstractTerminalSession {
|
|
|
15196
15587
|
if (this.exited) return;
|
|
15197
15588
|
let content;
|
|
15198
15589
|
try {
|
|
15199
|
-
content =
|
|
15590
|
+
content = existsSync11(this.logPath) ? readFileSync7(this.logPath, "utf8") : "";
|
|
15200
15591
|
} catch {
|
|
15201
15592
|
return;
|
|
15202
15593
|
}
|
|
@@ -15375,7 +15766,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
15375
15766
|
windowName,
|
|
15376
15767
|
this.execFileImpl,
|
|
15377
15768
|
this.pollIntervalMs,
|
|
15378
|
-
|
|
15769
|
+
join16(this.logDir, `${id}.log`),
|
|
15379
15770
|
this.socket
|
|
15380
15771
|
);
|
|
15381
15772
|
invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
|
|
@@ -15388,8 +15779,8 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
15388
15779
|
|
|
15389
15780
|
// src/utils/daemonToken.ts
|
|
15390
15781
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
15391
|
-
import { closeSync, mkdirSync as
|
|
15392
|
-
import { join as
|
|
15782
|
+
import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeSync } from "fs";
|
|
15783
|
+
import { join as join17 } from "path";
|
|
15393
15784
|
var BEARER_PREFIX = "bearer ";
|
|
15394
15785
|
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
15395
15786
|
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
@@ -15397,12 +15788,12 @@ function configDir() {
|
|
|
15397
15788
|
return globalDataDir();
|
|
15398
15789
|
}
|
|
15399
15790
|
function daemonTokenPath(port) {
|
|
15400
|
-
return
|
|
15791
|
+
return join17(configDir(), `server-${port}.token`);
|
|
15401
15792
|
}
|
|
15402
15793
|
function mintDaemonToken(port) {
|
|
15403
15794
|
const token = randomBytes3(32).toString("base64url");
|
|
15404
15795
|
const file = daemonTokenPath(port);
|
|
15405
|
-
|
|
15796
|
+
mkdirSync9(configDir(), { recursive: true });
|
|
15406
15797
|
try {
|
|
15407
15798
|
unlinkSync3(file);
|
|
15408
15799
|
} catch {
|
|
@@ -15417,7 +15808,7 @@ function mintDaemonToken(port) {
|
|
|
15417
15808
|
}
|
|
15418
15809
|
function readDaemonToken(port) {
|
|
15419
15810
|
try {
|
|
15420
|
-
const token =
|
|
15811
|
+
const token = readFileSync8(daemonTokenPath(port), "utf8").trim();
|
|
15421
15812
|
return token === "" ? void 0 : token;
|
|
15422
15813
|
} catch {
|
|
15423
15814
|
return void 0;
|
|
@@ -15459,17 +15850,17 @@ function tokensMatch(presented, expected) {
|
|
|
15459
15850
|
|
|
15460
15851
|
// src/plugins/RunnerRegistry.ts
|
|
15461
15852
|
import * as fs11 from "fs";
|
|
15462
|
-
import * as
|
|
15853
|
+
import * as path20 from "path";
|
|
15463
15854
|
import * as os5 from "os";
|
|
15464
15855
|
import { execFileSync } from "child_process";
|
|
15465
15856
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
15466
15857
|
|
|
15467
15858
|
// src/plugins/FsPluginStore.ts
|
|
15468
15859
|
import * as fs10 from "fs";
|
|
15469
|
-
import * as
|
|
15860
|
+
import * as path19 from "path";
|
|
15470
15861
|
|
|
15471
15862
|
// src/plugins/pluginNames.ts
|
|
15472
|
-
import * as
|
|
15863
|
+
import * as path18 from "path";
|
|
15473
15864
|
var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
15474
15865
|
var MAX_PLUGIN_NAME_LENGTH = 64;
|
|
15475
15866
|
function isPlainPluginName(name) {
|
|
@@ -15484,10 +15875,10 @@ function assertPlainPluginName(name) {
|
|
|
15484
15875
|
}
|
|
15485
15876
|
function resolvePluginInstallDir(pluginsDir, name) {
|
|
15486
15877
|
assertPlainPluginName(name);
|
|
15487
|
-
const root =
|
|
15488
|
-
const dest =
|
|
15489
|
-
const
|
|
15490
|
-
if (
|
|
15878
|
+
const root = path18.resolve(pluginsDir);
|
|
15879
|
+
const dest = path18.resolve(root, name);
|
|
15880
|
+
const relative5 = path18.relative(root, dest);
|
|
15881
|
+
if (relative5 !== name || relative5 === "" || path18.isAbsolute(relative5)) {
|
|
15491
15882
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
15492
15883
|
}
|
|
15493
15884
|
return dest;
|
|
@@ -15502,10 +15893,10 @@ function isValidManifest(obj) {
|
|
|
15502
15893
|
|
|
15503
15894
|
// src/plugins/FsPluginStore.ts
|
|
15504
15895
|
function userPluginsDir() {
|
|
15505
|
-
return
|
|
15896
|
+
return path19.join(globalDataDir(), "plugins");
|
|
15506
15897
|
}
|
|
15507
15898
|
function loadManifestFromDir(dir) {
|
|
15508
|
-
const manifestPath =
|
|
15899
|
+
const manifestPath = path19.join(dir, "manifest.json");
|
|
15509
15900
|
if (!fs10.existsSync(manifestPath)) return null;
|
|
15510
15901
|
try {
|
|
15511
15902
|
const raw = fs10.readFileSync(manifestPath, "utf8");
|
|
@@ -15520,8 +15911,8 @@ function copyDirSync3(src, dest) {
|
|
|
15520
15911
|
ensureDir(dest);
|
|
15521
15912
|
const entries = fs10.readdirSync(src, { withFileTypes: true });
|
|
15522
15913
|
for (const entry of entries) {
|
|
15523
|
-
const srcPath =
|
|
15524
|
-
const destPath =
|
|
15914
|
+
const srcPath = path19.join(src, entry.name);
|
|
15915
|
+
const destPath = path19.join(dest, entry.name);
|
|
15525
15916
|
if (entry.isDirectory()) {
|
|
15526
15917
|
copyDirSync3(srcPath, destPath);
|
|
15527
15918
|
} else {
|
|
@@ -15533,7 +15924,7 @@ function rmDirSync(dir) {
|
|
|
15533
15924
|
if (!fs10.existsSync(dir)) return;
|
|
15534
15925
|
const entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
15535
15926
|
for (const entry of entries) {
|
|
15536
|
-
const p =
|
|
15927
|
+
const p = path19.join(dir, entry.name);
|
|
15537
15928
|
if (entry.isDirectory()) {
|
|
15538
15929
|
rmDirSync(p);
|
|
15539
15930
|
} else {
|
|
@@ -15586,11 +15977,11 @@ var FsPluginStore = class {
|
|
|
15586
15977
|
return null;
|
|
15587
15978
|
}
|
|
15588
15979
|
}
|
|
15589
|
-
dirExists(
|
|
15590
|
-
return fs10.existsSync(
|
|
15980
|
+
dirExists(path23) {
|
|
15981
|
+
return fs10.existsSync(path23) && fs10.statSync(path23).isDirectory();
|
|
15591
15982
|
}
|
|
15592
|
-
exists(
|
|
15593
|
-
return fs10.existsSync(
|
|
15983
|
+
exists(path23) {
|
|
15984
|
+
return fs10.existsSync(path23);
|
|
15594
15985
|
}
|
|
15595
15986
|
};
|
|
15596
15987
|
|
|
@@ -16022,7 +16413,7 @@ var RunnerRegistry = class {
|
|
|
16022
16413
|
return entry?.source === "builtin";
|
|
16023
16414
|
}
|
|
16024
16415
|
installFromPath(sourcePath) {
|
|
16025
|
-
const absPath =
|
|
16416
|
+
const absPath = path20.resolve(sourcePath);
|
|
16026
16417
|
if (!this.store.dirExists(absPath)) {
|
|
16027
16418
|
throw new Error(`Plugin source must be a directory: ${absPath}`);
|
|
16028
16419
|
}
|
|
@@ -16034,7 +16425,7 @@ var RunnerRegistry = class {
|
|
|
16034
16425
|
}
|
|
16035
16426
|
installFromGit(url) {
|
|
16036
16427
|
const validated = assertInstallablePluginUrl(url);
|
|
16037
|
-
const tmpDir =
|
|
16428
|
+
const tmpDir = path20.join(os5.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
|
|
16038
16429
|
this.store.ensureDir(tmpDir);
|
|
16039
16430
|
try {
|
|
16040
16431
|
try {
|
|
@@ -16047,7 +16438,7 @@ var RunnerRegistry = class {
|
|
|
16047
16438
|
const hasManifest = contents.some((f) => f === "manifest.json");
|
|
16048
16439
|
let sourceDir = tmpDir;
|
|
16049
16440
|
if (!hasManifest) {
|
|
16050
|
-
const onlyEntry = contents.length === 1 ?
|
|
16441
|
+
const onlyEntry = contents.length === 1 ? path20.join(tmpDir, contents[0]) : void 0;
|
|
16051
16442
|
if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
|
|
16052
16443
|
throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
|
|
16053
16444
|
}
|
|
@@ -16095,7 +16486,7 @@ var RunnerRegistry = class {
|
|
|
16095
16486
|
}
|
|
16096
16487
|
createSkeleton(name, outputDir) {
|
|
16097
16488
|
assertPlainPluginName(name);
|
|
16098
|
-
const dir =
|
|
16489
|
+
const dir = path20.resolve(outputDir, name);
|
|
16099
16490
|
this.store.ensureDir(dir);
|
|
16100
16491
|
const manifest = {
|
|
16101
16492
|
name,
|
|
@@ -16127,8 +16518,8 @@ var RunnerRegistry = class {
|
|
|
16127
16518
|
]
|
|
16128
16519
|
}
|
|
16129
16520
|
};
|
|
16130
|
-
this.store.writeFile(
|
|
16131
|
-
const runSh =
|
|
16521
|
+
this.store.writeFile(path20.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
16522
|
+
const runSh = path20.join(dir, "run.sh");
|
|
16132
16523
|
const scriptContent = `#!/usr/bin/env bash
|
|
16133
16524
|
# Custom runner script for ${name}
|
|
16134
16525
|
# Receives:
|
|
@@ -16161,14 +16552,14 @@ function fsChmodSync(fpath, mode) {
|
|
|
16161
16552
|
|
|
16162
16553
|
// src/utils/stateStore.ts
|
|
16163
16554
|
import * as fs12 from "fs";
|
|
16164
|
-
import * as
|
|
16555
|
+
import * as path21 from "path";
|
|
16165
16556
|
var STATE_FILE = "state.json";
|
|
16166
16557
|
function getStatePath(baseDir) {
|
|
16167
|
-
return
|
|
16558
|
+
return path21.join(getStateDir(baseDir), STATE_FILE);
|
|
16168
16559
|
}
|
|
16169
16560
|
function saveState(plan, baseDir) {
|
|
16170
16561
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
16171
|
-
ensureDir(
|
|
16562
|
+
ensureDir(path21.dirname(statePath));
|
|
16172
16563
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
16173
16564
|
fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
16174
16565
|
}
|
|
@@ -16203,7 +16594,7 @@ function stateExists(baseDir) {
|
|
|
16203
16594
|
|
|
16204
16595
|
// src/services/SettingsService.ts
|
|
16205
16596
|
import * as fs13 from "fs";
|
|
16206
|
-
import * as
|
|
16597
|
+
import * as path22 from "path";
|
|
16207
16598
|
var DEFAULTS = {
|
|
16208
16599
|
tdd: { enabled: true },
|
|
16209
16600
|
verification: { enabled: false }
|
|
@@ -16211,7 +16602,7 @@ var DEFAULTS = {
|
|
|
16211
16602
|
function getSettingsPath() {
|
|
16212
16603
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
16213
16604
|
if (override && override.trim()) return override.trim();
|
|
16214
|
-
return
|
|
16605
|
+
return path22.join(globalDataDir(), "settings.json");
|
|
16215
16606
|
}
|
|
16216
16607
|
function readPlannerModels(raw) {
|
|
16217
16608
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -16339,7 +16730,7 @@ var SettingsService = class {
|
|
|
16339
16730
|
return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
|
|
16340
16731
|
}
|
|
16341
16732
|
persist() {
|
|
16342
|
-
const dir =
|
|
16733
|
+
const dir = path22.dirname(this.filePath);
|
|
16343
16734
|
if (!fs13.existsSync(dir)) {
|
|
16344
16735
|
fs13.mkdirSync(dir, { recursive: true });
|
|
16345
16736
|
}
|
|
@@ -16482,6 +16873,7 @@ export {
|
|
|
16482
16873
|
augmentPromptWithPriorOutputs,
|
|
16483
16874
|
augmentedPath,
|
|
16484
16875
|
bearerHeaderValue,
|
|
16876
|
+
buildConflictRepairPrompt,
|
|
16485
16877
|
buildConflictResolutionPrompt,
|
|
16486
16878
|
buildConversationSystemPrompt,
|
|
16487
16879
|
buildFallbackGrepArgs,
|
|
@@ -16501,6 +16893,7 @@ export {
|
|
|
16501
16893
|
canMergeTasks,
|
|
16502
16894
|
canSetDependencies,
|
|
16503
16895
|
canSplitTask,
|
|
16896
|
+
capConflictFiles,
|
|
16504
16897
|
checkDepsResolve,
|
|
16505
16898
|
checkImmutableLog,
|
|
16506
16899
|
checkInProgress,
|