@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.js
CHANGED
|
@@ -129,6 +129,7 @@ __export(index_exports, {
|
|
|
129
129
|
augmentPromptWithPriorOutputs: () => augmentPromptWithPriorOutputs,
|
|
130
130
|
augmentedPath: () => augmentedPath,
|
|
131
131
|
bearerHeaderValue: () => bearerHeaderValue,
|
|
132
|
+
buildConflictRepairPrompt: () => buildConflictRepairPrompt,
|
|
132
133
|
buildConflictResolutionPrompt: () => buildConflictResolutionPrompt,
|
|
133
134
|
buildConversationSystemPrompt: () => buildConversationSystemPrompt,
|
|
134
135
|
buildFallbackGrepArgs: () => buildFallbackGrepArgs,
|
|
@@ -148,6 +149,7 @@ __export(index_exports, {
|
|
|
148
149
|
canMergeTasks: () => canMergeTasks,
|
|
149
150
|
canSetDependencies: () => canSetDependencies,
|
|
150
151
|
canSplitTask: () => canSplitTask,
|
|
152
|
+
capConflictFiles: () => capConflictFiles,
|
|
151
153
|
checkDepsResolve: () => checkDepsResolve,
|
|
152
154
|
checkImmutableLog: () => checkImmutableLog,
|
|
153
155
|
checkInProgress: () => checkInProgress,
|
|
@@ -3323,14 +3325,14 @@ var PendingApprovals = class {
|
|
|
3323
3325
|
ask(request) {
|
|
3324
3326
|
const id = `ap-${Date.now()}-${counter++}`;
|
|
3325
3327
|
const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3326
|
-
return new Promise((
|
|
3328
|
+
return new Promise((resolve6) => {
|
|
3327
3329
|
const settle = (granted) => {
|
|
3328
3330
|
const entry = this.entries.get(id);
|
|
3329
3331
|
if (!entry) return;
|
|
3330
3332
|
clearTimeout(entry.timer);
|
|
3331
3333
|
this.entries.delete(id);
|
|
3332
3334
|
this.opts.onSettled?.(id, granted);
|
|
3333
|
-
|
|
3335
|
+
resolve6(granted);
|
|
3334
3336
|
};
|
|
3335
3337
|
const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
3336
3338
|
timer.unref?.();
|
|
@@ -4028,6 +4030,10 @@ function normalizeGeminiModel(id) {
|
|
|
4028
4030
|
function commaList(raw) {
|
|
4029
4031
|
return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
4030
4032
|
}
|
|
4033
|
+
function nonNegativeInt(raw, fallback) {
|
|
4034
|
+
const trimmed = (raw ?? "").trim();
|
|
4035
|
+
return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
|
|
4036
|
+
}
|
|
4031
4037
|
var BaseConfig = class {
|
|
4032
4038
|
// --- Legacy named getters (backward compat) ---
|
|
4033
4039
|
get openAiBaseUrl() {
|
|
@@ -4250,6 +4256,9 @@ var BaseConfig = class {
|
|
|
4250
4256
|
get worktreeLinks() {
|
|
4251
4257
|
return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
|
|
4252
4258
|
}
|
|
4259
|
+
get conflictRepairAttempts() {
|
|
4260
|
+
return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
|
|
4261
|
+
}
|
|
4253
4262
|
get approvalMode() {
|
|
4254
4263
|
const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
|
|
4255
4264
|
return raw === "allow" || raw === "deny" ? raw : "ask";
|
|
@@ -4309,21 +4318,33 @@ var ISOLATION_STATE = {
|
|
|
4309
4318
|
active: "active",
|
|
4310
4319
|
merged: "integrated",
|
|
4311
4320
|
conflict: "conflict",
|
|
4321
|
+
repairing: "repairing",
|
|
4312
4322
|
kept: "kept",
|
|
4313
4323
|
failed: "kept"
|
|
4314
4324
|
};
|
|
4315
|
-
function taskIsolationOf(record) {
|
|
4325
|
+
function taskIsolationOf(record, repairLimit) {
|
|
4316
4326
|
return {
|
|
4317
4327
|
state: ISOLATION_STATE[record.status],
|
|
4318
4328
|
branch: record.branch,
|
|
4319
4329
|
worktree: record.workspace,
|
|
4320
4330
|
repos: Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath),
|
|
4321
|
-
...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {}
|
|
4331
|
+
...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
|
|
4332
|
+
...record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {},
|
|
4333
|
+
...record.repairs ? { repair: { attempt: record.repairs, limit: repairLimit } } : {},
|
|
4334
|
+
...record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}
|
|
4322
4335
|
};
|
|
4323
4336
|
}
|
|
4337
|
+
function capConflictFiles(files, max = 5) {
|
|
4338
|
+
return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
|
|
4339
|
+
}
|
|
4324
4340
|
function handoffOf(run) {
|
|
4325
4341
|
const merged = Object.values(run.tasks).filter((r) => r.status === "merged").sort((a, b) => a.order - b.order);
|
|
4326
|
-
const entry = (r) => ({
|
|
4342
|
+
const entry = (r) => ({
|
|
4343
|
+
taskId: r.taskId,
|
|
4344
|
+
order: r.order,
|
|
4345
|
+
title: r.title,
|
|
4346
|
+
...r.repairedFiles?.length ? { repairedFiles: r.repairedFiles } : {}
|
|
4347
|
+
});
|
|
4327
4348
|
return {
|
|
4328
4349
|
repos: run.repos.map((repo) => ({
|
|
4329
4350
|
path: repo.path,
|
|
@@ -4387,13 +4408,17 @@ function landedNotice(landed) {
|
|
|
4387
4408
|
if (landed.length === 0) return "Nothing was merged.";
|
|
4388
4409
|
return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
|
|
4389
4410
|
}
|
|
4390
|
-
function
|
|
4411
|
+
function repairedNotice(repaired) {
|
|
4412
|
+
const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
|
|
4413
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
4414
|
+
return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
4415
|
+
}
|
|
4416
|
+
function describeMergeResult(result, branch, group, repaired = []) {
|
|
4391
4417
|
switch (result.outcome) {
|
|
4392
|
-
case "merged":
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
};
|
|
4418
|
+
case "merged": {
|
|
4419
|
+
const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
|
|
4420
|
+
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
4421
|
+
}
|
|
4397
4422
|
case "blocked":
|
|
4398
4423
|
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
|
|
4399
4424
|
case "conflict":
|
|
@@ -4414,7 +4439,7 @@ var import_child_process2 = require("child_process");
|
|
|
4414
4439
|
var import_util2 = require("util");
|
|
4415
4440
|
var import_crypto = require("crypto");
|
|
4416
4441
|
var fs4 = __toESM(require("fs"));
|
|
4417
|
-
var
|
|
4442
|
+
var path9 = __toESM(require("path"));
|
|
4418
4443
|
|
|
4419
4444
|
// src/utils/shellPath.ts
|
|
4420
4445
|
var import_child_process = require("child_process");
|
|
@@ -4443,28 +4468,28 @@ function posixBinDirs() {
|
|
|
4443
4468
|
];
|
|
4444
4469
|
}
|
|
4445
4470
|
function windowsBinDirs(env, home) {
|
|
4446
|
-
const
|
|
4447
|
-
const under = (base, ...parts) => base ? [
|
|
4471
|
+
const join23 = (...parts) => path5.win32.join(...parts);
|
|
4472
|
+
const under = (base, ...parts) => base ? [join23(base, ...parts)] : [];
|
|
4448
4473
|
const appData = env.APPDATA;
|
|
4449
4474
|
const localAppData = env.LOCALAPPDATA;
|
|
4450
4475
|
const programData = env.ProgramData;
|
|
4451
4476
|
return [
|
|
4452
4477
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4478
|
+
join23(home, ".local", "bin"),
|
|
4479
|
+
join23(home, ".opencode", "bin"),
|
|
4480
|
+
join23(home, ".claude", "bin"),
|
|
4481
|
+
join23(home, ".codex", "bin"),
|
|
4457
4482
|
...under(localAppData, "Programs", "claude"),
|
|
4458
4483
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
4459
4484
|
// literal spelling is kept as a fallback for a host that does not export
|
|
4460
4485
|
// APPDATA (a service account, a stripped CI container).
|
|
4461
4486
|
...under(appData, "npm"),
|
|
4462
|
-
|
|
4487
|
+
join23(home, "AppData", "Roaming", "npm"),
|
|
4463
4488
|
...under(localAppData, "pnpm"),
|
|
4464
4489
|
...under(localAppData, "Yarn", "bin"),
|
|
4465
|
-
|
|
4490
|
+
join23(home, ".bun", "bin"),
|
|
4466
4491
|
// Windows package managers.
|
|
4467
|
-
|
|
4492
|
+
join23(home, "scoop", "shims"),
|
|
4468
4493
|
...under(env.SCOOP, "shims"),
|
|
4469
4494
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
4470
4495
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -4474,7 +4499,7 @@ function windowsBinDirs(env, home) {
|
|
|
4474
4499
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
4475
4500
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
4476
4501
|
...under(localAppData, "Volta", "bin"),
|
|
4477
|
-
|
|
4502
|
+
join23(home, ".volta", "bin")
|
|
4478
4503
|
];
|
|
4479
4504
|
}
|
|
4480
4505
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -4578,6 +4603,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
|
|
|
4578
4603
|
|
|
4579
4604
|
// src/services/worktreeLink.ts
|
|
4580
4605
|
var fs3 = __toESM(require("fs"));
|
|
4606
|
+
var path8 = __toESM(require("path"));
|
|
4581
4607
|
function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
4582
4608
|
const isDir = fs3.statSync(source).isDirectory();
|
|
4583
4609
|
if (platform !== "win32") {
|
|
@@ -4596,6 +4622,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
|
|
|
4596
4622
|
return "copy";
|
|
4597
4623
|
}
|
|
4598
4624
|
}
|
|
4625
|
+
function mirrorDir(source, target, opts) {
|
|
4626
|
+
const copied = [];
|
|
4627
|
+
const links = [];
|
|
4628
|
+
const mirror = (from, to, prefix, top) => {
|
|
4629
|
+
fs3.mkdirSync(to, { recursive: true });
|
|
4630
|
+
for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
|
|
4631
|
+
const name = path8.join(prefix, entry.name);
|
|
4632
|
+
const at = { from: path8.join(from, entry.name), to: path8.join(to, entry.name), name };
|
|
4633
|
+
if (entry.isSymbolicLink()) links.push(at);
|
|
4634
|
+
else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
|
|
4635
|
+
else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
|
|
4636
|
+
}
|
|
4637
|
+
};
|
|
4638
|
+
mirror(source, target, "", true);
|
|
4639
|
+
let pending = links;
|
|
4640
|
+
for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
|
|
4641
|
+
before = pending.length;
|
|
4642
|
+
pending = pending.filter((link) => {
|
|
4643
|
+
const kind = relink(link.from, link.to, opts);
|
|
4644
|
+
if (kind === "copy") copied.push(link.name);
|
|
4645
|
+
return kind === null;
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4648
|
+
return copied;
|
|
4649
|
+
}
|
|
4650
|
+
function relink(from, to, opts) {
|
|
4651
|
+
const text = fs3.readlinkSync(from);
|
|
4652
|
+
const pointee = path8.isAbsolute(text) ? rebased(text, opts) : text;
|
|
4653
|
+
if (opts.platform !== "win32") {
|
|
4654
|
+
if (!fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) && fs3.existsSync(path8.resolve(path8.dirname(from), pointee))) {
|
|
4655
|
+
fs3.symlinkSync(rebased(path8.resolve(path8.dirname(from), pointee), opts), to);
|
|
4656
|
+
return "symlink";
|
|
4657
|
+
}
|
|
4658
|
+
fs3.symlinkSync(pointee, to);
|
|
4659
|
+
return "symlink";
|
|
4660
|
+
}
|
|
4661
|
+
const resolved = fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) ? path8.resolve(path8.dirname(to), pointee) : rebased(path8.resolve(path8.dirname(from), pointee), opts);
|
|
4662
|
+
try {
|
|
4663
|
+
fs3.statSync(resolved);
|
|
4664
|
+
} catch {
|
|
4665
|
+
return null;
|
|
4666
|
+
}
|
|
4667
|
+
return linkPath(resolved, to, opts.platform, opts.hardLink);
|
|
4668
|
+
}
|
|
4669
|
+
function rebased(pointee, opts) {
|
|
4670
|
+
const rel = path8.relative(opts.from, pointee);
|
|
4671
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
4672
|
+
return inside && !rel.split(/[\\/]/).includes("node_modules") ? path8.join(opts.to, rel) : pointee;
|
|
4673
|
+
}
|
|
4599
4674
|
|
|
4600
4675
|
// src/services/GitWorktreeIsolation.ts
|
|
4601
4676
|
var execFileAsync = (0, import_util2.promisify)(import_child_process2.execFile);
|
|
@@ -4610,15 +4685,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
|
|
|
4610
4685
|
var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
|
|
4611
4686
|
var isEnvFile = (name) => name.startsWith(".env");
|
|
4612
4687
|
var INTEGRATION_DIR = "integration";
|
|
4688
|
+
var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
|
|
4689
|
+
var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
|
|
4613
4690
|
var NESTED_REPO_DEPTH = 2;
|
|
4614
4691
|
var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
|
|
4615
4692
|
var GITLINK_MODE = "160000";
|
|
4616
4693
|
function isInside(parent, child) {
|
|
4617
|
-
const rel =
|
|
4618
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4694
|
+
const rel = path9.relative(parent, child);
|
|
4695
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
4619
4696
|
}
|
|
4620
4697
|
function hasGitEntry(dir) {
|
|
4621
|
-
return fs4.existsSync(
|
|
4698
|
+
return fs4.existsSync(path9.join(dir, ".git"));
|
|
4622
4699
|
}
|
|
4623
4700
|
function firstLine(text) {
|
|
4624
4701
|
return text.split(/\r?\n/, 1)[0].trim();
|
|
@@ -4634,12 +4711,20 @@ function statusPaths(porcelain) {
|
|
|
4634
4711
|
}
|
|
4635
4712
|
return paths;
|
|
4636
4713
|
}
|
|
4714
|
+
function leftoverMarkerFiles(check) {
|
|
4715
|
+
const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
|
|
4716
|
+
return [...new Set(files)];
|
|
4717
|
+
}
|
|
4718
|
+
function settleStatus(record, status) {
|
|
4719
|
+
record.status = status;
|
|
4720
|
+
delete record.repairBase;
|
|
4721
|
+
}
|
|
4637
4722
|
function sameDir(a, b) {
|
|
4638
4723
|
const real = (p) => {
|
|
4639
4724
|
try {
|
|
4640
4725
|
return fs4.realpathSync.native(p);
|
|
4641
4726
|
} catch {
|
|
4642
|
-
return
|
|
4727
|
+
return path9.resolve(p);
|
|
4643
4728
|
}
|
|
4644
4729
|
};
|
|
4645
4730
|
return real(a) === real(b);
|
|
@@ -4673,8 +4758,8 @@ function inactive(reason, repos) {
|
|
|
4673
4758
|
}
|
|
4674
4759
|
function groupPathOf(listed) {
|
|
4675
4760
|
const slashed = listed.trim().replace(/\\/g, "/");
|
|
4676
|
-
if (
|
|
4677
|
-
const rel =
|
|
4761
|
+
if (path9.posix.isAbsolute(slashed) || path9.win32.isAbsolute(slashed)) return null;
|
|
4762
|
+
const rel = path9.posix.normalize(slashed).replace(/\/+$/, "");
|
|
4678
4763
|
return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
|
|
4679
4764
|
}
|
|
4680
4765
|
function matchLinks(root, patterns) {
|
|
@@ -4690,9 +4775,26 @@ function matchLinks(root, patterns) {
|
|
|
4690
4775
|
}
|
|
4691
4776
|
function segmentMatches(root, base, segment) {
|
|
4692
4777
|
const under = (name) => base ? `${base}/${name}` : name;
|
|
4693
|
-
if (!/[*?]/.test(segment)) return lexists(
|
|
4778
|
+
if (!/[*?]/.test(segment)) return lexists(path9.join(root, base, segment)) ? [under(segment)] : [];
|
|
4694
4779
|
const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
|
|
4695
|
-
return listDir(
|
|
4780
|
+
return listDir(path9.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
|
|
4781
|
+
}
|
|
4782
|
+
function installDirs(root) {
|
|
4783
|
+
const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
|
|
4784
|
+
return ["node_modules", ...packages].filter((dir) => lexists(path9.join(root, dir)));
|
|
4785
|
+
}
|
|
4786
|
+
function workspaceGlobs(root) {
|
|
4787
|
+
let manifest;
|
|
4788
|
+
try {
|
|
4789
|
+
manifest = JSON.parse(fs4.readFileSync(path9.join(root, "package.json"), "utf8"));
|
|
4790
|
+
} catch {
|
|
4791
|
+
return [];
|
|
4792
|
+
}
|
|
4793
|
+
if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
|
|
4794
|
+
const { workspaces } = manifest;
|
|
4795
|
+
const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
|
|
4796
|
+
if (!Array.isArray(listed)) return [];
|
|
4797
|
+
return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
|
|
4696
4798
|
}
|
|
4697
4799
|
var GitWorktreeIsolation = class {
|
|
4698
4800
|
constructor(deps) {
|
|
@@ -4746,7 +4848,7 @@ var GitWorktreeIsolation = class {
|
|
|
4746
4848
|
groupPaths(workspaceRoot) {
|
|
4747
4849
|
const listed = this.deps.config.workspaceRepos;
|
|
4748
4850
|
if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
|
|
4749
|
-
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(
|
|
4851
|
+
const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path9.join(workspaceRoot, rel)));
|
|
4750
4852
|
return [...new Set(paths)].sort();
|
|
4751
4853
|
}
|
|
4752
4854
|
async committedRepos(workspaceRoot, paths) {
|
|
@@ -4762,7 +4864,7 @@ var GitWorktreeIsolation = class {
|
|
|
4762
4864
|
return status.ok ? status.stdout.trim() !== "" : null;
|
|
4763
4865
|
}
|
|
4764
4866
|
reposDirectlyIn(dir) {
|
|
4765
|
-
return this.childDirs(dir).filter((name) => hasGitEntry(
|
|
4867
|
+
return this.childDirs(dir).filter((name) => hasGitEntry(path9.join(dir, name)));
|
|
4766
4868
|
}
|
|
4767
4869
|
childDirs(dir) {
|
|
4768
4870
|
try {
|
|
@@ -4779,9 +4881,9 @@ var GitWorktreeIsolation = class {
|
|
|
4779
4881
|
async nestedRepos(workspaceRoot, toplevel) {
|
|
4780
4882
|
const found = [];
|
|
4781
4883
|
const walk = (rel, depth) => {
|
|
4782
|
-
for (const name of this.childDirs(
|
|
4884
|
+
for (const name of this.childDirs(path9.join(workspaceRoot, rel))) {
|
|
4783
4885
|
const child = rel ? `${rel}/${name}` : name;
|
|
4784
|
-
if (hasGitEntry(
|
|
4886
|
+
if (hasGitEntry(path9.join(workspaceRoot, child))) found.push(child);
|
|
4785
4887
|
else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
|
|
4786
4888
|
}
|
|
4787
4889
|
};
|
|
@@ -4802,7 +4904,7 @@ var GitWorktreeIsolation = class {
|
|
|
4802
4904
|
const [meta, file] = entry.split(" ");
|
|
4803
4905
|
if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
|
|
4804
4906
|
}
|
|
4805
|
-
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f",
|
|
4907
|
+
const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path9.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
|
|
4806
4908
|
for (const entry of declared.stdout.split("\0")) {
|
|
4807
4909
|
const value = entry.split("\n")[1];
|
|
4808
4910
|
if (value) paths.add(value.replace(/\/+$/, ""));
|
|
@@ -4858,7 +4960,7 @@ var GitWorktreeIsolation = class {
|
|
|
4858
4960
|
async acceptsWorktree(run, repo) {
|
|
4859
4961
|
const dir = this.integrationDir(run, repo);
|
|
4860
4962
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
4861
|
-
fs4.mkdirSync(
|
|
4963
|
+
fs4.mkdirSync(path9.dirname(dir), { recursive: true });
|
|
4862
4964
|
const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
|
|
4863
4965
|
await this.removeWorktreeDir(run, repo, dir, []);
|
|
4864
4966
|
return added.ok;
|
|
@@ -4873,12 +4975,12 @@ var GitWorktreeIsolation = class {
|
|
|
4873
4975
|
if (isolated.includes(SELF_REPO)) return [];
|
|
4874
4976
|
const shared = [];
|
|
4875
4977
|
const walk = (rel) => {
|
|
4876
|
-
for (const name of listDir(
|
|
4978
|
+
for (const name of listDir(path9.join(workspaceRoot, rel))) {
|
|
4877
4979
|
if (!rel && (name === STATE_DIR || name === ".git")) continue;
|
|
4878
4980
|
const child = rel ? `${rel}/${name}` : name;
|
|
4879
4981
|
if (isolated.includes(child)) continue;
|
|
4880
4982
|
if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
|
|
4881
|
-
else if (resolves(
|
|
4983
|
+
else if (resolves(path9.join(workspaceRoot, child))) shared.push(child);
|
|
4882
4984
|
}
|
|
4883
4985
|
};
|
|
4884
4986
|
walk("");
|
|
@@ -4895,17 +4997,17 @@ var GitWorktreeIsolation = class {
|
|
|
4895
4997
|
const copied = [];
|
|
4896
4998
|
try {
|
|
4897
4999
|
for (const repo of run.repos) {
|
|
4898
|
-
const worktree =
|
|
5000
|
+
const worktree = path9.join(dir, repo.path);
|
|
4899
5001
|
const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
|
|
4900
|
-
fs4.mkdirSync(
|
|
5002
|
+
fs4.mkdirSync(path9.dirname(worktree), { recursive: true });
|
|
4901
5003
|
await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
|
|
4902
5004
|
record.repos[repo.path] = { worktree, linked: [] };
|
|
4903
5005
|
}
|
|
4904
5006
|
run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
|
|
4905
5007
|
for (const rel of run.shared) {
|
|
4906
|
-
const target =
|
|
4907
|
-
fs4.mkdirSync(
|
|
4908
|
-
if (linkPath(
|
|
5008
|
+
const target = path9.join(dir, rel);
|
|
5009
|
+
fs4.mkdirSync(path9.dirname(target), { recursive: true });
|
|
5010
|
+
if (linkPath(path9.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
|
|
4909
5011
|
}
|
|
4910
5012
|
for (const repo of run.repos) {
|
|
4911
5013
|
const entry = record.repos[repo.path];
|
|
@@ -4913,7 +5015,7 @@ var GitWorktreeIsolation = class {
|
|
|
4913
5015
|
if (repo.path === SELF_REPO) cwd = inRepo;
|
|
4914
5016
|
const boot = await this.bootstrap(repo, inRepo);
|
|
4915
5017
|
entry.linked = boot.linked;
|
|
4916
|
-
copied.push(...boot.copied.map((name) =>
|
|
5018
|
+
copied.push(...boot.copied.map((name) => path9.posix.join(repo.path, name)));
|
|
4917
5019
|
}
|
|
4918
5020
|
} catch (err) {
|
|
4919
5021
|
record.status = "failed";
|
|
@@ -4924,6 +5026,53 @@ var GitWorktreeIsolation = class {
|
|
|
4924
5026
|
return { cwd, branch, copied };
|
|
4925
5027
|
});
|
|
4926
5028
|
}
|
|
5029
|
+
reopen(task, run) {
|
|
5030
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
5031
|
+
const record = run.tasks[task.id];
|
|
5032
|
+
if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
|
|
5033
|
+
let cwd = record.workspace;
|
|
5034
|
+
const repairBase = {};
|
|
5035
|
+
for (const repo of run.repos) {
|
|
5036
|
+
const entry = record.repos[repo.path];
|
|
5037
|
+
if (!entry) continue;
|
|
5038
|
+
if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
|
|
5039
|
+
if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
|
|
5040
|
+
if (entry.changed || repo.path === record.conflictRepo) {
|
|
5041
|
+
repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
|
|
5045
|
+
record.repairs = (record.repairs ?? 0) + 1;
|
|
5046
|
+
record.repairBase = repairBase;
|
|
5047
|
+
record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
|
|
5048
|
+
record.status = "repairing";
|
|
5049
|
+
return { cwd, branch: record.branch, copied: [] };
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
async verifyRepair(task, run) {
|
|
5053
|
+
const record = run.tasks[task.id];
|
|
5054
|
+
if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
|
|
5055
|
+
for (const repo of run.repos) {
|
|
5056
|
+
const entry = record.repos[repo.path];
|
|
5057
|
+
if (!entry) continue;
|
|
5058
|
+
try {
|
|
5059
|
+
await this.commitWorktree(repo, record, entry);
|
|
5060
|
+
} catch {
|
|
5061
|
+
return { ok: false, reason: "failed", repo: repo.path };
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
for (const repo of run.repos) {
|
|
5065
|
+
const base = record.repairBase[repo.path];
|
|
5066
|
+
if (base === void 0) continue;
|
|
5067
|
+
const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
|
|
5068
|
+
if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
|
|
5069
|
+
const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
|
|
5070
|
+
if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
|
|
5071
|
+
const files = leftoverMarkerFiles(check.stdout);
|
|
5072
|
+
if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
|
|
5073
|
+
}
|
|
5074
|
+
return { ok: true };
|
|
5075
|
+
}
|
|
4927
5076
|
/**
|
|
4928
5077
|
* The place in a repo's worktree that matches where the workspace sits in
|
|
4929
5078
|
* the real repo: the worktree itself, or a subdirectory of it when the
|
|
@@ -4931,7 +5080,7 @@ var GitWorktreeIsolation = class {
|
|
|
4931
5080
|
*/
|
|
4932
5081
|
async inWorkspacePlace(repo, worktree) {
|
|
4933
5082
|
const prefix = await this.prefixOf(repo);
|
|
4934
|
-
return prefix ?
|
|
5083
|
+
return prefix ? path9.join(worktree, prefix) : worktree;
|
|
4935
5084
|
}
|
|
4936
5085
|
async prefixOf(repo) {
|
|
4937
5086
|
return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
|
|
@@ -4951,6 +5100,7 @@ var GitWorktreeIsolation = class {
|
|
|
4951
5100
|
if (!record) return;
|
|
4952
5101
|
if (opts.keep) {
|
|
4953
5102
|
if (record.status === "active") record.status = "kept";
|
|
5103
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4954
5104
|
return;
|
|
4955
5105
|
}
|
|
4956
5106
|
await this.removeTask(run, record, { dropRecord: true });
|
|
@@ -4970,6 +5120,7 @@ var GitWorktreeIsolation = class {
|
|
|
4970
5120
|
for (const record of Object.values(run.tasks)) {
|
|
4971
5121
|
if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
|
|
4972
5122
|
else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
|
|
5123
|
+
else if (record.status === "repairing") settleStatus(record, "conflict");
|
|
4973
5124
|
}
|
|
4974
5125
|
await this.removeUnowned(run);
|
|
4975
5126
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
@@ -5064,14 +5215,59 @@ var GitWorktreeIsolation = class {
|
|
|
5064
5215
|
await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
|
|
5065
5216
|
}
|
|
5066
5217
|
await this.removeUnowned(run);
|
|
5067
|
-
if (
|
|
5068
|
-
for (const repo of run.repos)
|
|
5218
|
+
if (opts.integration !== "keep") {
|
|
5219
|
+
for (const repo of run.repos) {
|
|
5220
|
+
if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
|
|
5221
|
+
else await this.deleteIfMerged(repo.root, repo.integrationBranch);
|
|
5222
|
+
}
|
|
5069
5223
|
run.tasks = {};
|
|
5070
5224
|
this.removeIfEmpty(this.runRoot(run));
|
|
5071
5225
|
}
|
|
5072
5226
|
for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
5073
5227
|
});
|
|
5074
5228
|
}
|
|
5229
|
+
sweep(run) {
|
|
5230
|
+
return this.admin(run.workspaceRoot, async () => {
|
|
5231
|
+
const failed = [];
|
|
5232
|
+
for (const repo of run.repos) {
|
|
5233
|
+
if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
|
|
5234
|
+
}
|
|
5235
|
+
if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
/** False when git failed anywhere; the other branches are still tried. */
|
|
5239
|
+
async sweepRepo(run, repo) {
|
|
5240
|
+
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
5241
|
+
const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
|
|
5242
|
+
const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
|
|
5243
|
+
if (!worktrees.ok || !refs.ok) return false;
|
|
5244
|
+
const checkedOut = /* @__PURE__ */ new Set();
|
|
5245
|
+
const liveRuns = /* @__PURE__ */ new Set();
|
|
5246
|
+
for (const line of worktrees.stdout.split(/\r?\n/)) {
|
|
5247
|
+
if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
|
|
5248
|
+
const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
|
|
5249
|
+
if (runId) liveRuns.add(runId);
|
|
5250
|
+
}
|
|
5251
|
+
let ok = true;
|
|
5252
|
+
for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
5253
|
+
const branch = ref.slice("refs/heads/".length);
|
|
5254
|
+
const runId = RUN_BRANCH.exec(branch)?.[1];
|
|
5255
|
+
if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
|
|
5256
|
+
if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
|
|
5257
|
+
}
|
|
5258
|
+
return ok;
|
|
5259
|
+
}
|
|
5260
|
+
/**
|
|
5261
|
+
* Delete `branch` only if the repo's checked-out HEAD already contains it —
|
|
5262
|
+
* HEAD, not an upstream: what the user has checked out is what they merged
|
|
5263
|
+
* into. `-d` rather than `-D`, so git also refuses a branch a worktree has
|
|
5264
|
+
* checked out.
|
|
5265
|
+
*/
|
|
5266
|
+
async deleteIfMerged(root, branch) {
|
|
5267
|
+
const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
|
|
5268
|
+
if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
|
|
5269
|
+
return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
|
|
5270
|
+
}
|
|
5075
5271
|
async drain() {
|
|
5076
5272
|
try {
|
|
5077
5273
|
while (this.waiting.length > 0) {
|
|
@@ -5124,21 +5320,24 @@ var GitWorktreeIsolation = class {
|
|
|
5124
5320
|
return this.stopLanding(record, "failed");
|
|
5125
5321
|
}
|
|
5126
5322
|
for (const repo of changed) {
|
|
5127
|
-
const outcome = await this.mergeTask(run, repo, record);
|
|
5323
|
+
const { outcome, files } = await this.mergeTask(run, repo, record);
|
|
5128
5324
|
if (outcome === "merged") continue;
|
|
5129
|
-
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
|
|
5325
|
+
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
|
|
5130
5326
|
}
|
|
5131
5327
|
}
|
|
5132
5328
|
delete run.landing;
|
|
5133
|
-
record
|
|
5329
|
+
settleStatus(record, "merged");
|
|
5134
5330
|
delete record.conflictRepo;
|
|
5331
|
+
delete record.conflictFiles;
|
|
5135
5332
|
await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
|
|
5136
5333
|
return "merged";
|
|
5137
5334
|
}
|
|
5138
|
-
stopLanding(record, outcome, repo) {
|
|
5139
|
-
record
|
|
5335
|
+
stopLanding(record, outcome, repo, files) {
|
|
5336
|
+
settleStatus(record, outcome);
|
|
5140
5337
|
if (repo) record.conflictRepo = repo.path;
|
|
5141
5338
|
else delete record.conflictRepo;
|
|
5339
|
+
if (files && files.length > 0) record.conflictFiles = files;
|
|
5340
|
+
else delete record.conflictFiles;
|
|
5142
5341
|
return outcome;
|
|
5143
5342
|
}
|
|
5144
5343
|
/** Merge the task branch into one repo's integration branch. A merge that does not complete is aborted: it is Ordewell's own. */
|
|
@@ -5147,13 +5346,13 @@ var GitWorktreeIsolation = class {
|
|
|
5147
5346
|
try {
|
|
5148
5347
|
dir = await this.ensureIntegrationWorktree(run, repo);
|
|
5149
5348
|
} catch {
|
|
5150
|
-
return "failed";
|
|
5349
|
+
return { outcome: "failed", files: [] };
|
|
5151
5350
|
}
|
|
5152
5351
|
const merge = await this.tryGit(dir, ["merge", "--no-ff", "--no-edit", "-m", `Merge task ${record.order}: ${firstLine(record.title)}`, record.branch]);
|
|
5153
|
-
if (merge.ok) return "merged";
|
|
5154
|
-
const
|
|
5352
|
+
if (merge.ok) return { outcome: "merged", files: [] };
|
|
5353
|
+
const files = await this.unmergedPaths(dir);
|
|
5155
5354
|
if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
|
|
5156
|
-
return
|
|
5355
|
+
return { outcome: files.length > 0 ? "conflict" : "failed", files };
|
|
5157
5356
|
}
|
|
5158
5357
|
/**
|
|
5159
5358
|
* Return every repo of the landing in flight to its recorded tip, then
|
|
@@ -5221,15 +5420,15 @@ var GitWorktreeIsolation = class {
|
|
|
5221
5420
|
await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
|
|
5222
5421
|
}
|
|
5223
5422
|
integrationDir(run, repo) {
|
|
5224
|
-
return
|
|
5423
|
+
return path9.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
|
|
5225
5424
|
}
|
|
5226
5425
|
async ensureIntegrationWorktree(run, repo) {
|
|
5227
5426
|
return this.admin(run.workspaceRoot, async () => {
|
|
5228
5427
|
const dir = this.integrationDir(run, repo);
|
|
5229
|
-
if (fs4.existsSync(
|
|
5428
|
+
if (fs4.existsSync(path9.join(dir, ".git"))) return dir;
|
|
5230
5429
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
5231
5430
|
ensureStateDirIgnored(run.workspaceRoot);
|
|
5232
|
-
fs4.mkdirSync(
|
|
5431
|
+
fs4.mkdirSync(path9.dirname(dir), { recursive: true });
|
|
5233
5432
|
await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
|
|
5234
5433
|
return dir;
|
|
5235
5434
|
});
|
|
@@ -5264,20 +5463,20 @@ var GitWorktreeIsolation = class {
|
|
|
5264
5463
|
return;
|
|
5265
5464
|
}
|
|
5266
5465
|
for (const entry of entries) {
|
|
5267
|
-
const target =
|
|
5466
|
+
const target = path9.join(dir, entry.name);
|
|
5268
5467
|
if (entry.isSymbolicLink()) this.unlinkIfLink(target);
|
|
5269
5468
|
else if (entry.isDirectory()) this.unlinkLinksUnder(target);
|
|
5270
5469
|
}
|
|
5271
5470
|
}
|
|
5272
5471
|
/** Task workspaces and branches under this run that no record accounts for. */
|
|
5273
5472
|
async removeUnowned(run) {
|
|
5274
|
-
const owned = new Set(Object.values(run.tasks).map((r) =>
|
|
5473
|
+
const owned = new Set(Object.values(run.tasks).map((r) => path9.resolve(r.workspace)));
|
|
5275
5474
|
const root = this.runRoot(run);
|
|
5276
5475
|
if (fs4.existsSync(root)) {
|
|
5277
5476
|
for (const entry of fs4.readdirSync(root)) {
|
|
5278
|
-
const dir =
|
|
5279
|
-
if (entry === INTEGRATION_DIR || owned.has(
|
|
5280
|
-
for (const repo of run.repos) await this.removeWorktreeDir(run, repo,
|
|
5477
|
+
const dir = path9.join(root, entry);
|
|
5478
|
+
if (entry === INTEGRATION_DIR || owned.has(path9.resolve(dir))) continue;
|
|
5479
|
+
for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path9.join(dir, repo.path), []);
|
|
5281
5480
|
this.removeTaskWorkspace(run, dir);
|
|
5282
5481
|
}
|
|
5283
5482
|
}
|
|
@@ -5291,14 +5490,18 @@ var GitWorktreeIsolation = class {
|
|
|
5291
5490
|
}
|
|
5292
5491
|
}
|
|
5293
5492
|
async removeWorktreeDir(run, repo, dir, linked) {
|
|
5294
|
-
for (const name of linked
|
|
5493
|
+
for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
|
|
5494
|
+
const target = path9.join(dir, name);
|
|
5495
|
+
this.unlinkIfLink(target);
|
|
5496
|
+
this.unlinkLinksUnder(target);
|
|
5497
|
+
}
|
|
5295
5498
|
for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
|
|
5296
|
-
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(
|
|
5499
|
+
if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path9.join(dir, name));
|
|
5297
5500
|
}
|
|
5298
5501
|
await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
|
|
5299
5502
|
if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
|
|
5300
5503
|
await this.tryGit(repo.root, ["worktree", "prune"]);
|
|
5301
|
-
for (let parent =
|
|
5504
|
+
for (let parent = path9.dirname(dir); isInside(this.runRoot(run), parent); parent = path9.dirname(parent)) this.removeIfEmpty(parent);
|
|
5302
5505
|
}
|
|
5303
5506
|
unlinkIfLink(target) {
|
|
5304
5507
|
try {
|
|
@@ -5313,14 +5516,14 @@ var GitWorktreeIsolation = class {
|
|
|
5313
5516
|
}
|
|
5314
5517
|
}
|
|
5315
5518
|
runRoot(run) {
|
|
5316
|
-
return
|
|
5519
|
+
return path9.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
|
|
5317
5520
|
}
|
|
5318
5521
|
namesFor(run, task) {
|
|
5319
5522
|
const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
|
|
5320
5523
|
let name = `${task.order}-${slug2}`;
|
|
5321
5524
|
const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
|
|
5322
5525
|
if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
|
|
5323
|
-
return { branch: `ordewell/${run.id}/${name}`, dir:
|
|
5526
|
+
return { branch: `ordewell/${run.id}/${name}`, dir: path9.join(this.runRoot(run), name) };
|
|
5324
5527
|
}
|
|
5325
5528
|
/**
|
|
5326
5529
|
* Make one repo's worktree runnable: the default artifacts, unless a setup
|
|
@@ -5333,16 +5536,26 @@ var GitWorktreeIsolation = class {
|
|
|
5333
5536
|
const linked = [];
|
|
5334
5537
|
const copied = [];
|
|
5335
5538
|
const link = (name) => {
|
|
5336
|
-
const target =
|
|
5539
|
+
const target = path9.join(cwd, name);
|
|
5337
5540
|
if (lexists(target)) return;
|
|
5338
|
-
fs4.mkdirSync(
|
|
5339
|
-
if (linkPath(
|
|
5541
|
+
fs4.mkdirSync(path9.dirname(target), { recursive: true });
|
|
5542
|
+
if (linkPath(path9.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
|
|
5543
|
+
linked.push(name);
|
|
5544
|
+
};
|
|
5545
|
+
const mirror = (name) => {
|
|
5546
|
+
const target = path9.join(cwd, name);
|
|
5547
|
+
if (lexists(target) || !fs4.existsSync(path9.dirname(target))) return;
|
|
5548
|
+
const source = path9.join(repo.root, name);
|
|
5549
|
+
if (!fs4.lstatSync(source).isDirectory()) return link(name);
|
|
5550
|
+
const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
|
|
5551
|
+
copied.push(...mirrored.map((entry) => path9.join(name, entry)));
|
|
5340
5552
|
linked.push(name);
|
|
5341
5553
|
};
|
|
5342
5554
|
if (!setup) {
|
|
5343
5555
|
for (const name of fs4.readdirSync(repo.root)) {
|
|
5344
|
-
if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
5556
|
+
if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
|
|
5345
5557
|
}
|
|
5558
|
+
for (const dir of installDirs(repo.root)) mirror(dir);
|
|
5346
5559
|
}
|
|
5347
5560
|
for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
|
|
5348
5561
|
if (setup) await this.runSetup(setup, repo, cwd);
|
|
@@ -6004,6 +6217,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
6004
6217
|
}
|
|
6005
6218
|
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.";
|
|
6006
6219
|
var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
|
|
6220
|
+
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.";
|
|
6007
6221
|
function repoGroupSection(isolated) {
|
|
6008
6222
|
if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
|
|
6009
6223
|
return [
|
|
@@ -6095,6 +6309,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
6095
6309
|
"DEPENDENCY & PARALLELISM:",
|
|
6096
6310
|
"- Independent slices should have NO dependencies \u2014 they run in parallel.",
|
|
6097
6311
|
"- Only add dependencies when a slice truly depends on artifacts another slice creates.",
|
|
6312
|
+
SHARED_APPEND_FILE_RULE,
|
|
6098
6313
|
...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
|
|
6099
6314
|
...repoGroupSection(variant.isolatedExecution),
|
|
6100
6315
|
"",
|
|
@@ -6196,6 +6411,7 @@ function corePlannerPrompt(isolatedExecution) {
|
|
|
6196
6411
|
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.",
|
|
6197
6412
|
"- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
|
|
6198
6413
|
"- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
|
|
6414
|
+
SHARED_APPEND_FILE_RULE,
|
|
6199
6415
|
isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
|
|
6200
6416
|
...repoGroupSection(isolatedExecution),
|
|
6201
6417
|
"",
|
|
@@ -6473,6 +6689,31 @@ ${task.prompt ?? task.description}`;
|
|
|
6473
6689
|
asked
|
|
6474
6690
|
].join("\n");
|
|
6475
6691
|
}
|
|
6692
|
+
function buildConflictRepairPrompt(task, conflict, integrationBranch) {
|
|
6693
|
+
const { branch } = conflict;
|
|
6694
|
+
const files = conflict.conflictFiles ?? [];
|
|
6695
|
+
const asked = `What the task was asked to do:
|
|
6696
|
+
${task.prompt ?? task.description}`;
|
|
6697
|
+
const resolve6 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
|
|
6698
|
+
const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
|
|
6699
|
+
if (repos.length === 0) {
|
|
6700
|
+
return [
|
|
6701
|
+
`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.`,
|
|
6702
|
+
`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.`,
|
|
6703
|
+
"Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
|
|
6704
|
+
"",
|
|
6705
|
+
asked
|
|
6706
|
+
].join("\n");
|
|
6707
|
+
}
|
|
6708
|
+
const stoppedIn = conflict.conflictRepo ?? repos[0];
|
|
6709
|
+
return [
|
|
6710
|
+
`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.`,
|
|
6711
|
+
`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.`,
|
|
6712
|
+
"Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
|
|
6713
|
+
"",
|
|
6714
|
+
asked
|
|
6715
|
+
].join("\n");
|
|
6716
|
+
}
|
|
6476
6717
|
|
|
6477
6718
|
// src/services/PlanValidator.ts
|
|
6478
6719
|
function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
|
|
@@ -7936,9 +8177,9 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
|
7936
8177
|
const scope = args.include ? ` in ${args.include}` : "";
|
|
7937
8178
|
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
7938
8179
|
}
|
|
7939
|
-
const
|
|
7940
|
-
if (
|
|
7941
|
-
const basename = String(
|
|
8180
|
+
const path24 = args.filePath || args.file_path || args.path;
|
|
8181
|
+
if (path24) {
|
|
8182
|
+
const basename = String(path24).split("/").pop() || String(path24);
|
|
7942
8183
|
return `${display} ${basename}`;
|
|
7943
8184
|
}
|
|
7944
8185
|
if (args.pattern) {
|
|
@@ -9578,7 +9819,7 @@ ${line}
|
|
|
9578
9819
|
// src/services/transcriptCapture.ts
|
|
9579
9820
|
var import_fs = require("fs");
|
|
9580
9821
|
var os3 = __toESM(require("os"));
|
|
9581
|
-
var
|
|
9822
|
+
var path10 = __toESM(require("path"));
|
|
9582
9823
|
var HomeTranscriptReader = class {
|
|
9583
9824
|
home;
|
|
9584
9825
|
constructor(opts = {}) {
|
|
@@ -9597,19 +9838,19 @@ var HomeTranscriptReader = class {
|
|
|
9597
9838
|
};
|
|
9598
9839
|
var CLAUDE_PROJECT_NAME_MAX = 200;
|
|
9599
9840
|
function claudeProjectDirs(home, cwd) {
|
|
9600
|
-
const projects =
|
|
9841
|
+
const projects = path10.join(home, ".claude", "projects");
|
|
9601
9842
|
const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
9602
9843
|
if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
|
|
9603
|
-
const dir =
|
|
9844
|
+
const dir = path10.join(projects, munged);
|
|
9604
9845
|
return (0, import_fs.existsSync)(dir) ? [dir] : [];
|
|
9605
9846
|
}
|
|
9606
9847
|
if (!(0, import_fs.existsSync)(projects)) return [];
|
|
9607
9848
|
const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
|
|
9608
|
-
return (0, import_fs.readdirSync)(projects).filter((name) => name.startsWith(prefix)).map((name) =>
|
|
9849
|
+
return (0, import_fs.readdirSync)(projects).filter((name) => name.startsWith(prefix)).map((name) => path10.join(projects, name));
|
|
9609
9850
|
}
|
|
9610
9851
|
function claudeFinal(home, query, maxChars) {
|
|
9611
9852
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
9612
|
-
const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
9853
|
+
const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path10.join(dir, f))).filter((f) => cutoff ? (0, import_fs.statSync)(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => (0, import_fs.statSync)(b).mtimeMs - (0, import_fs.statSync)(a).mtimeMs);
|
|
9613
9854
|
for (const file of candidates) {
|
|
9614
9855
|
const raw = (0, import_fs.readFileSync)(file, "utf8");
|
|
9615
9856
|
if (!raw.includes(query.marker)) continue;
|
|
@@ -9645,7 +9886,7 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
9645
9886
|
} catch {
|
|
9646
9887
|
return null;
|
|
9647
9888
|
}
|
|
9648
|
-
const dbPath =
|
|
9889
|
+
const dbPath = path10.join(home, ".local", "share", "opencode", "opencode.db");
|
|
9649
9890
|
if (!(0, import_fs.existsSync)(dbPath)) return null;
|
|
9650
9891
|
let db;
|
|
9651
9892
|
try {
|
|
@@ -9698,7 +9939,7 @@ async function opencodeFinal(home, query, maxChars) {
|
|
|
9698
9939
|
}
|
|
9699
9940
|
}
|
|
9700
9941
|
function codexFinal(home, query, maxChars) {
|
|
9701
|
-
const root =
|
|
9942
|
+
const root = path10.join(home, ".codex", "sessions");
|
|
9702
9943
|
if (!(0, import_fs.existsSync)(root)) return null;
|
|
9703
9944
|
const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
|
|
9704
9945
|
const files = [];
|
|
@@ -9712,7 +9953,7 @@ function codexFinal(home, query, maxChars) {
|
|
|
9712
9953
|
continue;
|
|
9713
9954
|
}
|
|
9714
9955
|
for (const e of entries) {
|
|
9715
|
-
const full =
|
|
9956
|
+
const full = path10.join(dir, e);
|
|
9716
9957
|
let st;
|
|
9717
9958
|
try {
|
|
9718
9959
|
st = (0, import_fs.statSync)(full);
|
|
@@ -10350,7 +10591,7 @@ var TaskOrchestrator = class {
|
|
|
10350
10591
|
if (!this.isolationRun) return null;
|
|
10351
10592
|
const record = this.isolationRun.tasks[taskId];
|
|
10352
10593
|
if (!record) return { state: "none" };
|
|
10353
|
-
return taskIsolationOf(record);
|
|
10594
|
+
return taskIsolationOf(record, this.config.conflictRepairAttempts);
|
|
10354
10595
|
}
|
|
10355
10596
|
/**
|
|
10356
10597
|
* The plan's isolation as a surface shows it, for one with no stream to have
|
|
@@ -10359,7 +10600,7 @@ var TaskOrchestrator = class {
|
|
|
10359
10600
|
isolationView() {
|
|
10360
10601
|
const run = this.isolationRun;
|
|
10361
10602
|
if (!run) return null;
|
|
10362
|
-
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
|
|
10603
|
+
const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
|
|
10363
10604
|
return { tasks, handoff: handoffOf(run) };
|
|
10364
10605
|
}
|
|
10365
10606
|
getAttemptSession(taskId) {
|
|
@@ -10395,24 +10636,43 @@ var TaskOrchestrator = class {
|
|
|
10395
10636
|
async reviewRunDiff() {
|
|
10396
10637
|
return this.isolation.reviewDiff(this.requireRun());
|
|
10397
10638
|
}
|
|
10398
|
-
/**
|
|
10639
|
+
/**
|
|
10640
|
+
* "Merge all": the run's integration branches into whatever the user has
|
|
10641
|
+
* checked out, in every repo or none. Once everything merged, the run has
|
|
10642
|
+
* nothing left to hand over, so it is cleared up and forgotten; a branch
|
|
10643
|
+
* the user's HEAD somehow does not contain stays for the next run's sweep.
|
|
10644
|
+
*/
|
|
10399
10645
|
async mergeRun() {
|
|
10400
10646
|
const run = this.requireRun();
|
|
10401
10647
|
const result = await this.isolation.mergeIntoCheckedOut(run);
|
|
10402
10648
|
const branch = integrationBranchNameOf(run);
|
|
10403
10649
|
const group = run.repos.some((r) => r.path !== SELF_REPO);
|
|
10404
|
-
const
|
|
10650
|
+
const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
|
|
10651
|
+
const { level, message } = describeMergeResult(result, branch, group, repaired);
|
|
10405
10652
|
this.notifications[level](message);
|
|
10653
|
+
if (result.outcome === "merged") await this.clearMergedRun(run);
|
|
10406
10654
|
return result;
|
|
10407
10655
|
}
|
|
10656
|
+
async clearMergedRun(run) {
|
|
10657
|
+
try {
|
|
10658
|
+
await this.isolation.discard(run, { integration: "delete-merged" });
|
|
10659
|
+
} catch (err) {
|
|
10660
|
+
this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
10661
|
+
return;
|
|
10662
|
+
}
|
|
10663
|
+
this.forgetRun();
|
|
10664
|
+
}
|
|
10408
10665
|
/** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
|
|
10409
10666
|
async cleanupRun() {
|
|
10410
|
-
await this.isolation.discard(this.requireRun(), {
|
|
10667
|
+
await this.isolation.discard(this.requireRun(), { integration: "keep" });
|
|
10411
10668
|
this.emit("onIsolationChanged");
|
|
10412
10669
|
}
|
|
10413
10670
|
/** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
|
|
10414
10671
|
async discardRun() {
|
|
10415
|
-
await this.isolation.discard(this.requireRun(), {
|
|
10672
|
+
await this.isolation.discard(this.requireRun(), { integration: "delete" });
|
|
10673
|
+
this.forgetRun();
|
|
10674
|
+
}
|
|
10675
|
+
forgetRun() {
|
|
10416
10676
|
this.isolationRun = null;
|
|
10417
10677
|
this.resolvers = {};
|
|
10418
10678
|
this.emit("onIsolationChanged");
|
|
@@ -10448,7 +10708,8 @@ var TaskOrchestrator = class {
|
|
|
10448
10708
|
}
|
|
10449
10709
|
loadPlan(tasks, planRunners = ["claude-code"]) {
|
|
10450
10710
|
this.store.load(tasks, planRunners);
|
|
10451
|
-
this.endAllAttempts("load");
|
|
10711
|
+
const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
|
|
10712
|
+
for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
10452
10713
|
if (!this.running) this.runMode = null;
|
|
10453
10714
|
this.blockedStart = null;
|
|
10454
10715
|
this.planStatus = "approved";
|
|
@@ -10506,9 +10767,10 @@ var TaskOrchestrator = class {
|
|
|
10506
10767
|
this.running = false;
|
|
10507
10768
|
this.planStatus = "approved";
|
|
10508
10769
|
this.terminalRunner.stopAll();
|
|
10509
|
-
const
|
|
10510
|
-
|
|
10511
|
-
|
|
10770
|
+
for (const a of this.endAllAttempts("stop")) {
|
|
10771
|
+
if (a.repair) this.store.markAwaitingUser(a.taskId);
|
|
10772
|
+
if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
|
|
10773
|
+
}
|
|
10512
10774
|
this.runMode = null;
|
|
10513
10775
|
this.blockedStart = null;
|
|
10514
10776
|
this.onHold.clear();
|
|
@@ -10524,6 +10786,7 @@ var TaskOrchestrator = class {
|
|
|
10524
10786
|
console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
|
|
10525
10787
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
10526
10788
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
10789
|
+
if (attempt.repair) return this.settleRepair(task, attempt, verdict);
|
|
10527
10790
|
const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
|
|
10528
10791
|
const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
|
|
10529
10792
|
if (this.attempts.get(taskId) !== attempt) return;
|
|
@@ -10544,6 +10807,9 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10544
10807
|
}
|
|
10545
10808
|
this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
|
|
10546
10809
|
this.logAndArchive(task, verdict);
|
|
10810
|
+
await this.afterVerdict();
|
|
10811
|
+
}
|
|
10812
|
+
async afterVerdict() {
|
|
10547
10813
|
this.emit("onTaskChanged");
|
|
10548
10814
|
if (!this.running) {
|
|
10549
10815
|
if (this.attempts.size === 0) {
|
|
@@ -10556,6 +10822,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10556
10822
|
}
|
|
10557
10823
|
await this.tick();
|
|
10558
10824
|
}
|
|
10825
|
+
/**
|
|
10826
|
+
* A repair's verdict decides only whether its work may try to land. It never
|
|
10827
|
+
* replaces the verdict the task's own work earned, and a repair that does not
|
|
10828
|
+
* land leaves the task waiting on the user — never a failed task, so never a
|
|
10829
|
+
* halted run.
|
|
10830
|
+
*/
|
|
10831
|
+
async settleRepair(task, attempt, verdict) {
|
|
10832
|
+
const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
|
|
10833
|
+
if (this.attempts.get(task.id) !== attempt) return;
|
|
10834
|
+
this.endAttempt(task.id, "verdict");
|
|
10835
|
+
if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
|
|
10836
|
+
else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
|
|
10837
|
+
else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
|
|
10838
|
+
else this.landUnmerged(task, landed.outcome);
|
|
10839
|
+
await this.afterVerdict();
|
|
10840
|
+
}
|
|
10841
|
+
/**
|
|
10842
|
+
* Evidence before the queue: the repair's work is committed and checked, and
|
|
10843
|
+
* only then merged, through the same serialized landing as any other task —
|
|
10844
|
+
* so a repair the tip has moved past again is a fresh conflict, not a pass.
|
|
10845
|
+
*/
|
|
10846
|
+
async landRepair(task, attempt) {
|
|
10847
|
+
const run = this.isolationRun;
|
|
10848
|
+
let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
|
|
10849
|
+
if (!run) return { evidence, outcome: "failed" };
|
|
10850
|
+
attempt.phase = "integrating";
|
|
10851
|
+
attempt.integration = (async () => {
|
|
10852
|
+
evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
|
|
10853
|
+
if (evidence.ok) return this.integrateWork(task);
|
|
10854
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
10855
|
+
return "conflict";
|
|
10856
|
+
})();
|
|
10857
|
+
const outcome = await attempt.integration;
|
|
10858
|
+
return { evidence, outcome };
|
|
10859
|
+
}
|
|
10860
|
+
async landedRepair(task, verdict) {
|
|
10861
|
+
const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
|
|
10862
|
+
this.store.markCompleted(task.id);
|
|
10863
|
+
this.store.unblockDependents(task.id);
|
|
10864
|
+
this.logAndArchive(task, task.verdict ?? verdict);
|
|
10865
|
+
this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
|
|
10866
|
+
await this.landResolved(task.id);
|
|
10867
|
+
}
|
|
10868
|
+
/** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
|
|
10869
|
+
async unrepaired(task, why) {
|
|
10870
|
+
await this.releaseWorktree(task.id, { keep: true });
|
|
10871
|
+
this.store.markAwaitingUser(task.id);
|
|
10872
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
10873
|
+
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.`);
|
|
10874
|
+
}
|
|
10875
|
+
describeEvidence(evidence) {
|
|
10876
|
+
const run = this.isolationRun;
|
|
10877
|
+
const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
|
|
10878
|
+
const branch = run ? integrationBranchNameOf(run) : "the integration branch";
|
|
10879
|
+
switch (evidence.reason) {
|
|
10880
|
+
case "not-merged":
|
|
10881
|
+
return `finished, but its branch${inRepo} does not contain ${branch}`;
|
|
10882
|
+
case "conflict-markers": {
|
|
10883
|
+
const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
|
|
10884
|
+
return `finished, but left conflict markers in ${capConflictFiles(files)}`;
|
|
10885
|
+
}
|
|
10886
|
+
case "failed":
|
|
10887
|
+
return `finished, but git could not check its work${inRepo}`;
|
|
10888
|
+
}
|
|
10889
|
+
}
|
|
10559
10890
|
/**
|
|
10560
10891
|
* Merge a passed attempt's worktree. The attempt stays live while it waits on
|
|
10561
10892
|
* the module's merge queue, so a cancel, retry or stop in that window still
|
|
@@ -10589,11 +10920,25 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10589
10920
|
}
|
|
10590
10921
|
landUnmerged(task, landing) {
|
|
10591
10922
|
const branch = this.isolationRun ? integrationBranchNameOf(this.isolationRun) : "the integration branch";
|
|
10592
|
-
const
|
|
10593
|
-
const inRepo =
|
|
10923
|
+
const record = this.isolationRun?.tasks[task.id];
|
|
10924
|
+
const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
|
|
10594
10925
|
if (landing === "conflict") {
|
|
10926
|
+
const repair = this.nextRepair(task.id);
|
|
10595
10927
|
this.store.markAwaitingUser(task.id);
|
|
10596
|
-
|
|
10928
|
+
const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
|
|
10929
|
+
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}.`;
|
|
10930
|
+
if (repair) {
|
|
10931
|
+
this.notifications.warn(conflicted);
|
|
10932
|
+
if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
|
|
10933
|
+
else {
|
|
10934
|
+
this.store.markPending(task.id);
|
|
10935
|
+
this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
|
|
10936
|
+
}
|
|
10937
|
+
return;
|
|
10938
|
+
}
|
|
10939
|
+
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.`);
|
|
10940
|
+
const whyNot = this.noRepairReason(task);
|
|
10941
|
+
if (whyNot) this.tell("info", whyNot);
|
|
10597
10942
|
} else {
|
|
10598
10943
|
this.store.markFailed(task.id);
|
|
10599
10944
|
this.running = false;
|
|
@@ -10601,6 +10946,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10601
10946
|
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.`);
|
|
10602
10947
|
}
|
|
10603
10948
|
}
|
|
10949
|
+
/** 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. */
|
|
10950
|
+
nextRepair(taskId) {
|
|
10951
|
+
const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
|
|
10952
|
+
const limit = this.config.conflictRepairAttempts;
|
|
10953
|
+
const spent = record?.repairs ?? 0;
|
|
10954
|
+
return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
|
|
10955
|
+
}
|
|
10956
|
+
noRepairReason(task) {
|
|
10957
|
+
const limit = this.config.conflictRepairAttempts;
|
|
10958
|
+
if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
|
|
10959
|
+
const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
|
|
10960
|
+
return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
|
|
10961
|
+
}
|
|
10604
10962
|
/**
|
|
10605
10963
|
* Mark which added task resolves which conflict. The resolver merges the
|
|
10606
10964
|
* conflicted task's branch by hand in its own worktree; once that lands, the
|
|
@@ -10890,6 +11248,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10890
11248
|
cwd: null,
|
|
10891
11249
|
worktree: false,
|
|
10892
11250
|
integration: null,
|
|
11251
|
+
repair: this.nextRepair(task.id),
|
|
10893
11252
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10894
11253
|
};
|
|
10895
11254
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -10900,12 +11259,13 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10900
11259
|
const cwd = await this.resolveAttemptCwd(task, attempt);
|
|
10901
11260
|
attempt.cwd = cwd;
|
|
10902
11261
|
if (this.attempts.get(task.id) !== attempt) {
|
|
10903
|
-
if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep:
|
|
10904
|
-
return this.abandonSpawn(task);
|
|
11262
|
+
if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
|
|
11263
|
+
return this.abandonSpawn(task, attempt);
|
|
10905
11264
|
}
|
|
10906
|
-
const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
|
|
11265
|
+
const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
|
|
10907
11266
|
planMapEnabled: this.config.planMapEnabled,
|
|
10908
|
-
|
|
11267
|
+
// A merge to resolve is not new behaviour to drive test-first.
|
|
11268
|
+
tddEnabled: !attempt.repair && this.tddEnabled()
|
|
10909
11269
|
});
|
|
10910
11270
|
const session = await this.terminalRunner.spawn({
|
|
10911
11271
|
taskId: task.id,
|
|
@@ -10920,16 +11280,26 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10920
11280
|
order: task.order,
|
|
10921
11281
|
title: task.title
|
|
10922
11282
|
});
|
|
10923
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
|
|
11283
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
|
|
10924
11284
|
attempt.phase = "running";
|
|
10925
11285
|
attempt.session = session;
|
|
10926
11286
|
this.output.attach(task.id, session);
|
|
10927
11287
|
this.verifier.watch(task, session);
|
|
10928
11288
|
this.emit("onTaskChanged");
|
|
10929
|
-
|
|
11289
|
+
if (attempt.repair) {
|
|
11290
|
+
const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
|
|
11291
|
+
this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
|
|
11292
|
+
} else {
|
|
11293
|
+
this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
|
|
11294
|
+
}
|
|
10930
11295
|
} catch (err) {
|
|
10931
|
-
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
|
|
11296
|
+
if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
|
|
10932
11297
|
this.endAttempt(task.id, "spawn-failed");
|
|
11298
|
+
if (attempt.repair) {
|
|
11299
|
+
await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
11300
|
+
this.emit("onTaskChanged");
|
|
11301
|
+
return this.tick();
|
|
11302
|
+
}
|
|
10933
11303
|
await this.releaseWorktree(task.id, { keep: false });
|
|
10934
11304
|
this.store.markPending(task.id);
|
|
10935
11305
|
this.onHold.add(task.id);
|
|
@@ -10943,17 +11313,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
10943
11313
|
* and take back the claim it made. Only the claim — whatever ended the
|
|
10944
11314
|
* attempt may have decided the task since (mark complete, cancel, retry).
|
|
10945
11315
|
*/
|
|
10946
|
-
abandonSpawn(task, session) {
|
|
11316
|
+
abandonSpawn(task, attempt, session) {
|
|
10947
11317
|
session?.kill();
|
|
10948
11318
|
if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
|
|
10949
|
-
this.store.
|
|
11319
|
+
if (attempt.repair) this.store.markAwaitingUser(task.id);
|
|
11320
|
+
else this.store.markPending(task.id);
|
|
10950
11321
|
this.emit("onTaskChanged");
|
|
10951
11322
|
}
|
|
11323
|
+
repairPrompt(task) {
|
|
11324
|
+
const run = this.requireRun();
|
|
11325
|
+
const record = run.tasks[task.id];
|
|
11326
|
+
const conflict = {
|
|
11327
|
+
branch: record?.branch ?? "",
|
|
11328
|
+
repos: Object.keys(record?.repairBase ?? {}),
|
|
11329
|
+
...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
|
|
11330
|
+
...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
|
|
11331
|
+
};
|
|
11332
|
+
return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
|
|
11333
|
+
}
|
|
10952
11334
|
/**
|
|
10953
11335
|
* The one place an attempt's working directory is decided. Async so a
|
|
10954
11336
|
* per-attempt workspace (worktree isolation, #12) can be prepared here.
|
|
10955
11337
|
*/
|
|
10956
11338
|
async resolveAttemptCwd(task, attempt) {
|
|
11339
|
+
if (attempt.repair && this.isolationRun) {
|
|
11340
|
+
const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
|
|
11341
|
+
attempt.worktree = true;
|
|
11342
|
+
this.emit("onIsolationChanged");
|
|
11343
|
+
return cwd2;
|
|
11344
|
+
}
|
|
10957
11345
|
if (this.runMode !== "isolated" || !this.isolationRun) {
|
|
10958
11346
|
await this.releaseWorktree(task.id, { keep: false });
|
|
10959
11347
|
return this.workspaceRootFn();
|
|
@@ -11041,8 +11429,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
11041
11429
|
}
|
|
11042
11430
|
}
|
|
11043
11431
|
this.runMode = "isolated";
|
|
11432
|
+
await this.sweep();
|
|
11044
11433
|
return true;
|
|
11045
11434
|
}
|
|
11435
|
+
/** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
|
|
11436
|
+
async sweep() {
|
|
11437
|
+
if (!this.isolationRun) return;
|
|
11438
|
+
try {
|
|
11439
|
+
await this.isolation.sweep(this.isolationRun);
|
|
11440
|
+
} catch (err) {
|
|
11441
|
+
this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
|
|
11442
|
+
}
|
|
11443
|
+
}
|
|
11046
11444
|
/**
|
|
11047
11445
|
* The plan's run carries on while anything has landed on its integration
|
|
11048
11446
|
* branch: a resumed plan's dependents must start from a tip that holds their
|
|
@@ -11055,14 +11453,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
11055
11453
|
/**
|
|
11056
11454
|
* A run with nothing landed holds only superseded attempts, so it goes whole.
|
|
11057
11455
|
* One that cannot be continued for another reason — it ran from a different
|
|
11058
|
-
* workspace path — keeps its integration branch
|
|
11059
|
-
* work up.
|
|
11456
|
+
* workspace path — keeps its integration branch in each repo that has not
|
|
11457
|
+
* merged it: only the user gives landed work up.
|
|
11060
11458
|
*/
|
|
11061
11459
|
async mintRun(root) {
|
|
11062
11460
|
const previous = this.isolationRun;
|
|
11063
11461
|
if (previous) {
|
|
11064
11462
|
const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
|
|
11065
|
-
await this.isolation.discard(previous, {
|
|
11463
|
+
await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
|
|
11066
11464
|
this.isolationRun = null;
|
|
11067
11465
|
}
|
|
11068
11466
|
this.isolationRun = await this.isolation.startRun(root);
|
|
@@ -11127,9 +11525,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
11127
11525
|
return attempt;
|
|
11128
11526
|
}
|
|
11129
11527
|
endAllAttempts(reason) {
|
|
11130
|
-
|
|
11528
|
+
const ended = [...this.attempts.values()];
|
|
11529
|
+
for (const { taskId } of ended) this.endAttempt(taskId, reason);
|
|
11131
11530
|
this.verifier.reset();
|
|
11132
11531
|
if (reason === "load") this.output.reset();
|
|
11532
|
+
return ended;
|
|
11133
11533
|
}
|
|
11134
11534
|
};
|
|
11135
11535
|
|
|
@@ -11259,7 +11659,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
|
|
|
11259
11659
|
};
|
|
11260
11660
|
|
|
11261
11661
|
// src/services/ModelDiscovery.ts
|
|
11262
|
-
var
|
|
11662
|
+
var path13 = __toESM(require("path"));
|
|
11263
11663
|
var import_child_process4 = require("child_process");
|
|
11264
11664
|
var import_util3 = require("util");
|
|
11265
11665
|
var fsSync3 = __toESM(require("fs"));
|
|
@@ -11267,17 +11667,17 @@ var osMod = __toESM(require("os"));
|
|
|
11267
11667
|
|
|
11268
11668
|
// src/utils/globalDataDir.ts
|
|
11269
11669
|
var fs5 = __toESM(require("fs"));
|
|
11270
|
-
var
|
|
11670
|
+
var path11 = __toESM(require("path"));
|
|
11271
11671
|
var os4 = __toESM(require("os"));
|
|
11272
11672
|
function globalDataDir() {
|
|
11273
|
-
return
|
|
11673
|
+
return path11.join(os4.homedir(), ".ordewell");
|
|
11274
11674
|
}
|
|
11275
11675
|
function copyDirSync(src, dest) {
|
|
11276
11676
|
fs5.mkdirSync(dest, { recursive: true });
|
|
11277
11677
|
const entries = fs5.readdirSync(src, { withFileTypes: true });
|
|
11278
11678
|
for (const entry of entries) {
|
|
11279
|
-
const srcPath =
|
|
11280
|
-
const destPath =
|
|
11679
|
+
const srcPath = path11.join(src, entry.name);
|
|
11680
|
+
const destPath = path11.join(dest, entry.name);
|
|
11281
11681
|
if (entry.isDirectory()) {
|
|
11282
11682
|
copyDirSync(srcPath, destPath);
|
|
11283
11683
|
} else {
|
|
@@ -11289,12 +11689,12 @@ var migrated = false;
|
|
|
11289
11689
|
function migrateOldConfigDir() {
|
|
11290
11690
|
if (migrated) return;
|
|
11291
11691
|
migrated = true;
|
|
11292
|
-
const oldDir =
|
|
11692
|
+
const oldDir = path11.join(os4.homedir(), ".config", "ordewell");
|
|
11293
11693
|
if (!fs5.existsSync(oldDir)) return;
|
|
11294
11694
|
const newDir = globalDataDir();
|
|
11295
11695
|
const liftFile = (name) => {
|
|
11296
|
-
const oldFile =
|
|
11297
|
-
const newFile =
|
|
11696
|
+
const oldFile = path11.join(oldDir, name);
|
|
11697
|
+
const newFile = path11.join(newDir, name);
|
|
11298
11698
|
if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
|
|
11299
11699
|
fs5.mkdirSync(newDir, { recursive: true });
|
|
11300
11700
|
fs5.copyFileSync(oldFile, newFile);
|
|
@@ -11302,8 +11702,8 @@ function migrateOldConfigDir() {
|
|
|
11302
11702
|
};
|
|
11303
11703
|
liftFile("settings.json");
|
|
11304
11704
|
liftFile(".env");
|
|
11305
|
-
const oldPlugins =
|
|
11306
|
-
const newPlugins =
|
|
11705
|
+
const oldPlugins = path11.join(oldDir, "plugins");
|
|
11706
|
+
const newPlugins = path11.join(newDir, "plugins");
|
|
11307
11707
|
if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
|
|
11308
11708
|
copyDirSync(oldPlugins, newPlugins);
|
|
11309
11709
|
}
|
|
@@ -11311,7 +11711,7 @@ function migrateOldConfigDir() {
|
|
|
11311
11711
|
|
|
11312
11712
|
// src/utils/launch.ts
|
|
11313
11713
|
var fsSync2 = __toESM(require("fs"));
|
|
11314
|
-
var
|
|
11714
|
+
var path12 = __toESM(require("path"));
|
|
11315
11715
|
|
|
11316
11716
|
// src/utils/shell.ts
|
|
11317
11717
|
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
|
|
@@ -11340,7 +11740,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
|
|
|
11340
11740
|
var WINDOWS_MAX_COMMAND_LINE = 32767;
|
|
11341
11741
|
function defaultPowerShell() {
|
|
11342
11742
|
const root = process.env.SystemRoot || process.env.windir;
|
|
11343
|
-
return root ?
|
|
11743
|
+
return root ? path12.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
11344
11744
|
}
|
|
11345
11745
|
var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
|
|
11346
11746
|
var CommandLineTooLongError = class extends Error {
|
|
@@ -11392,7 +11792,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
|
|
|
11392
11792
|
};
|
|
11393
11793
|
function isResolvableOnPosixPath(command, PATH, exists) {
|
|
11394
11794
|
if (command.includes("/")) return exists(command);
|
|
11395
|
-
return PATH.split(
|
|
11795
|
+
return PATH.split(path12.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path12.join(dir, command)));
|
|
11396
11796
|
}
|
|
11397
11797
|
function isExecutableResolved(command, plan, PATH, deps = {}) {
|
|
11398
11798
|
if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
|
|
@@ -11432,9 +11832,9 @@ function cmdArgs(line) {
|
|
|
11432
11832
|
return ["/d", "/s", "/c", `"${line}"`];
|
|
11433
11833
|
}
|
|
11434
11834
|
function candidateExtensions(command, pathExt) {
|
|
11435
|
-
const declared = pathExt.split(
|
|
11835
|
+
const declared = pathExt.split(path12.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
11436
11836
|
const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
|
|
11437
|
-
const ext =
|
|
11837
|
+
const ext = path12.win32.extname(command).toLowerCase();
|
|
11438
11838
|
if (ext && known.has(ext)) {
|
|
11439
11839
|
const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
|
|
11440
11840
|
return { direct: [], cmd: [], powershell: [], [route]: [""] };
|
|
@@ -11455,13 +11855,13 @@ async function findWindowsExecutables(command, deps) {
|
|
|
11455
11855
|
const exists = deps.exists ?? defaultExists2;
|
|
11456
11856
|
const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
|
|
11457
11857
|
const byRoute = candidateExtensions(command, pathExt);
|
|
11458
|
-
const dirs =
|
|
11459
|
-
const base =
|
|
11858
|
+
const dirs = path12.win32.basename(command) !== command ? [path12.win32.dirname(path12.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path12.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
11859
|
+
const base = path12.win32.basename(command);
|
|
11460
11860
|
const found = [];
|
|
11461
11861
|
for (const route of ROUTES) {
|
|
11462
11862
|
outer: for (const ext of byRoute[route]) {
|
|
11463
11863
|
for (const dir of dirs) {
|
|
11464
|
-
const candidate =
|
|
11864
|
+
const candidate = path12.win32.join(dir, `${base}${ext}`);
|
|
11465
11865
|
if (exists(candidate)) {
|
|
11466
11866
|
found.push({ file: candidate, route });
|
|
11467
11867
|
break outer;
|
|
@@ -11556,7 +11956,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
11556
11956
|
const PATH = await augmentedPath();
|
|
11557
11957
|
const launch = await planDirectLaunch(command, args).catch(() => null);
|
|
11558
11958
|
if (!launch) return null;
|
|
11559
|
-
return new Promise((
|
|
11959
|
+
return new Promise((resolve6) => {
|
|
11560
11960
|
let child;
|
|
11561
11961
|
try {
|
|
11562
11962
|
child = (0, import_child_process4.spawn)(launch.file, launch.args, {
|
|
@@ -11565,7 +11965,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
11565
11965
|
windowsVerbatimArguments: launch.verbatim
|
|
11566
11966
|
});
|
|
11567
11967
|
} catch {
|
|
11568
|
-
|
|
11968
|
+
resolve6(null);
|
|
11569
11969
|
return;
|
|
11570
11970
|
}
|
|
11571
11971
|
let settled = false;
|
|
@@ -11574,7 +11974,7 @@ var defaultAppServerClient = async (command, args, options) => {
|
|
|
11574
11974
|
settled = true;
|
|
11575
11975
|
clearTimeout(timer);
|
|
11576
11976
|
killTree(child);
|
|
11577
|
-
|
|
11977
|
+
resolve6(value);
|
|
11578
11978
|
};
|
|
11579
11979
|
const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
|
|
11580
11980
|
child.on("error", () => finish(null));
|
|
@@ -11641,7 +12041,7 @@ function geminiDiscoveryCacheKey(apiKey) {
|
|
|
11641
12041
|
return {
|
|
11642
12042
|
key: `gemini-models:${hash}`,
|
|
11643
12043
|
dir,
|
|
11644
|
-
file:
|
|
12044
|
+
file: path13.join(dir, `gemini_models_${hash}.json`)
|
|
11645
12045
|
};
|
|
11646
12046
|
}
|
|
11647
12047
|
function readGeminiCache(apiKey) {
|
|
@@ -12165,7 +12565,7 @@ var ModelDiscovery = class {
|
|
|
12165
12565
|
|
|
12166
12566
|
// src/services/ModelCatalog.ts
|
|
12167
12567
|
var fs6 = __toESM(require("fs"));
|
|
12168
|
-
var
|
|
12568
|
+
var path14 = __toESM(require("path"));
|
|
12169
12569
|
var CACHE_KEY = "model-catalog";
|
|
12170
12570
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
12171
12571
|
function storageKey(baseUrl) {
|
|
@@ -12217,7 +12617,7 @@ var ModelCatalog = class {
|
|
|
12217
12617
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
12218
12618
|
const key = storageKey(url);
|
|
12219
12619
|
const cacheDir = globalCacheDir();
|
|
12220
|
-
const cacheFile =
|
|
12620
|
+
const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
12221
12621
|
if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
|
|
12222
12622
|
} catch {
|
|
12223
12623
|
}
|
|
@@ -12225,7 +12625,7 @@ var ModelCatalog = class {
|
|
|
12225
12625
|
static readCache(key) {
|
|
12226
12626
|
try {
|
|
12227
12627
|
const cacheDir = globalCacheDir();
|
|
12228
|
-
const cacheFile =
|
|
12628
|
+
const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
12229
12629
|
if (!fs6.existsSync(cacheFile)) return null;
|
|
12230
12630
|
const raw = fs6.readFileSync(cacheFile, "utf8");
|
|
12231
12631
|
const data = JSON.parse(raw);
|
|
@@ -12239,7 +12639,7 @@ var ModelCatalog = class {
|
|
|
12239
12639
|
try {
|
|
12240
12640
|
const cacheDir = globalCacheDir();
|
|
12241
12641
|
fs6.mkdirSync(cacheDir, { recursive: true });
|
|
12242
|
-
const cacheFile =
|
|
12642
|
+
const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
12243
12643
|
fs6.writeFileSync(
|
|
12244
12644
|
cacheFile,
|
|
12245
12645
|
JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
|
|
@@ -12623,7 +13023,8 @@ var ORDEWELL_SETTABLE_ENV = [
|
|
|
12623
13023
|
"ORDEWELL_SUBAGENT_MODEL",
|
|
12624
13024
|
"ORDEWELL_PLANNER_EFFORT",
|
|
12625
13025
|
"ORDEWELL_RESEARCH_ENABLED",
|
|
12626
|
-
"ORDEWELL_MAX_PARALLEL"
|
|
13026
|
+
"ORDEWELL_MAX_PARALLEL",
|
|
13027
|
+
"ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
|
|
12627
13028
|
];
|
|
12628
13029
|
var SETTINGS_ENV_REFUSED = [
|
|
12629
13030
|
// Approval posture and pre-approved scope.
|
|
@@ -12680,7 +13081,7 @@ var import_child_process6 = require("child_process");
|
|
|
12680
13081
|
|
|
12681
13082
|
// src/utils/workspace.ts
|
|
12682
13083
|
var fs7 = __toESM(require("fs"));
|
|
12683
|
-
var
|
|
13084
|
+
var path15 = __toESM(require("path"));
|
|
12684
13085
|
var WorkspaceNotFoundError = class extends Error {
|
|
12685
13086
|
constructor(workspace) {
|
|
12686
13087
|
super(`Workspace "${workspace}" does not exist.`);
|
|
@@ -12736,7 +13137,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
|
|
|
12736
13137
|
throw new WorkspaceNotAProjectError(workspace);
|
|
12737
13138
|
}
|
|
12738
13139
|
const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
|
|
12739
|
-
(marker2) => exists(
|
|
13140
|
+
(marker2) => exists(path15.join(resolved, marker2))
|
|
12740
13141
|
);
|
|
12741
13142
|
if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
|
|
12742
13143
|
}
|
|
@@ -12804,8 +13205,8 @@ var StdioAgentAdapter = class {
|
|
|
12804
13205
|
}
|
|
12805
13206
|
async start(opts) {
|
|
12806
13207
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
12807
|
-
this.processEnded = new Promise((
|
|
12808
|
-
this.markEnded =
|
|
13208
|
+
this.processEnded = new Promise((resolve6) => {
|
|
13209
|
+
this.markEnded = resolve6;
|
|
12809
13210
|
});
|
|
12810
13211
|
const spec = this.spawnSpec(opts);
|
|
12811
13212
|
const resolvePath = this.deps.resolvePath ?? augmentedPath;
|
|
@@ -12856,7 +13257,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
12856
13257
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
12857
13258
|
return;
|
|
12858
13259
|
}
|
|
12859
|
-
await new Promise((
|
|
13260
|
+
await new Promise((resolve6) => {
|
|
12860
13261
|
let settled = false;
|
|
12861
13262
|
const finish = () => {
|
|
12862
13263
|
if (settled) return;
|
|
@@ -12867,7 +13268,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
12867
13268
|
signal?.removeEventListener("abort", onAbort);
|
|
12868
13269
|
this.process?.removeListener("exit", onExit);
|
|
12869
13270
|
this.process?.removeListener("error", onExit);
|
|
12870
|
-
|
|
13271
|
+
resolve6();
|
|
12871
13272
|
};
|
|
12872
13273
|
const emit = (event) => {
|
|
12873
13274
|
onEvent(event);
|
|
@@ -13082,14 +13483,14 @@ function codexSandboxUnavailableMessage() {
|
|
|
13082
13483
|
].join("\n");
|
|
13083
13484
|
}
|
|
13084
13485
|
function runProbe(deps, cwd, env, flags) {
|
|
13085
|
-
return new Promise((
|
|
13486
|
+
return new Promise((resolve6) => {
|
|
13086
13487
|
let output = "";
|
|
13087
13488
|
let settled = false;
|
|
13088
13489
|
const finish = (code) => {
|
|
13089
13490
|
if (settled) return;
|
|
13090
13491
|
settled = true;
|
|
13091
13492
|
clearTimeout(timer);
|
|
13092
|
-
|
|
13493
|
+
resolve6({ code, output });
|
|
13093
13494
|
};
|
|
13094
13495
|
const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
13095
13496
|
timer.unref?.();
|
|
@@ -13172,8 +13573,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
13172
13573
|
async handshake(opts) {
|
|
13173
13574
|
this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
|
|
13174
13575
|
if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
|
|
13175
|
-
const ready = new Promise((
|
|
13176
|
-
this.settleHandshake =
|
|
13576
|
+
const ready = new Promise((resolve6) => {
|
|
13577
|
+
this.settleHandshake = resolve6;
|
|
13177
13578
|
});
|
|
13178
13579
|
this.writeLine({
|
|
13179
13580
|
jsonrpc: "2.0",
|
|
@@ -13186,8 +13587,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
13186
13587
|
// A binary that rejects `app-server` outright dies immediately; waiting
|
|
13187
13588
|
// out the timeout would turn a one-line diagnostic into a 30s stall.
|
|
13188
13589
|
this.processEnded.then(() => false),
|
|
13189
|
-
new Promise((
|
|
13190
|
-
const t = setTimeout(() =>
|
|
13590
|
+
new Promise((resolve6) => {
|
|
13591
|
+
const t = setTimeout(() => resolve6(false), HANDSHAKE_TIMEOUT_MS);
|
|
13191
13592
|
t.unref?.();
|
|
13192
13593
|
})
|
|
13193
13594
|
]);
|
|
@@ -13464,12 +13865,12 @@ function describeError(err) {
|
|
|
13464
13865
|
return lines.join(": ");
|
|
13465
13866
|
}
|
|
13466
13867
|
function delay(ms, signal) {
|
|
13467
|
-
return new Promise((
|
|
13468
|
-
const timer = setTimeout(
|
|
13868
|
+
return new Promise((resolve6) => {
|
|
13869
|
+
const timer = setTimeout(resolve6, ms);
|
|
13469
13870
|
timer.unref?.();
|
|
13470
13871
|
signal?.addEventListener("abort", () => {
|
|
13471
13872
|
clearTimeout(timer);
|
|
13472
|
-
|
|
13873
|
+
resolve6();
|
|
13473
13874
|
}, { once: true });
|
|
13474
13875
|
});
|
|
13475
13876
|
}
|
|
@@ -13508,12 +13909,12 @@ var OpenCodeAdapter = class {
|
|
|
13508
13909
|
cwd: opts.cwd,
|
|
13509
13910
|
windowsVerbatimArguments: launch.verbatim
|
|
13510
13911
|
});
|
|
13511
|
-
const banner = new Promise((
|
|
13912
|
+
const banner = new Promise((resolve6) => {
|
|
13512
13913
|
let seen = "";
|
|
13513
13914
|
const scan = (chunk) => {
|
|
13514
13915
|
seen += chunk.toString();
|
|
13515
13916
|
const match = seen.match(/https?:\/\/[^\s]+/);
|
|
13516
|
-
if (match)
|
|
13917
|
+
if (match) resolve6(match[0].replace(/[.,)]$/, ""));
|
|
13517
13918
|
};
|
|
13518
13919
|
this.process.stdout?.on("data", scan);
|
|
13519
13920
|
this.process.stderr?.on("data", (chunk) => {
|
|
@@ -13522,15 +13923,15 @@ var OpenCodeAdapter = class {
|
|
|
13522
13923
|
});
|
|
13523
13924
|
this.process.on("exit", () => {
|
|
13524
13925
|
this.exited = true;
|
|
13525
|
-
|
|
13926
|
+
resolve6(null);
|
|
13526
13927
|
});
|
|
13527
13928
|
this.process.on("error", (err) => {
|
|
13528
13929
|
this.exited = true;
|
|
13529
13930
|
this.stderrTail = (this.stderrTail + `
|
|
13530
13931
|
${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
13531
|
-
|
|
13932
|
+
resolve6(null);
|
|
13532
13933
|
});
|
|
13533
|
-
const timer = setTimeout(() =>
|
|
13934
|
+
const timer = setTimeout(() => resolve6(null), SERVER_READY_TIMEOUT_MS);
|
|
13534
13935
|
timer.unref?.();
|
|
13535
13936
|
});
|
|
13536
13937
|
this.baseUrl = await banner;
|
|
@@ -13563,8 +13964,8 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13563
13964
|
const streamAbort = new AbortController();
|
|
13564
13965
|
let connected = () => {
|
|
13565
13966
|
};
|
|
13566
|
-
const streamReady = new Promise((
|
|
13567
|
-
connected =
|
|
13967
|
+
const streamReady = new Promise((resolve6) => {
|
|
13968
|
+
connected = resolve6;
|
|
13568
13969
|
});
|
|
13569
13970
|
const live = this.streamEvents(
|
|
13570
13971
|
streamAbort.signal,
|
|
@@ -13768,14 +14169,14 @@ ${part.text}` : part.text });
|
|
|
13768
14169
|
}
|
|
13769
14170
|
}
|
|
13770
14171
|
}
|
|
13771
|
-
async json(method,
|
|
13772
|
-
const response = await this.deps.fetch(`${this.baseUrl}${
|
|
14172
|
+
async json(method, path24, body, signal) {
|
|
14173
|
+
const response = await this.deps.fetch(`${this.baseUrl}${path24}`, {
|
|
13773
14174
|
method,
|
|
13774
14175
|
headers: { "content-type": "application/json" },
|
|
13775
14176
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
13776
14177
|
...signal ? { signal } : {}
|
|
13777
14178
|
});
|
|
13778
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
14179
|
+
if (!response.ok) throw new Error(`${method} ${path24} failed: ${response.status} ${response.statusText}`);
|
|
13779
14180
|
return await response.json().catch(() => null);
|
|
13780
14181
|
}
|
|
13781
14182
|
nativeSessionId() {
|
|
@@ -14444,33 +14845,9 @@ var PlannerConversation = class {
|
|
|
14444
14845
|
if (m.role !== "user" || index === 0) return [];
|
|
14445
14846
|
const line = m.content.split("\n")[0];
|
|
14446
14847
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
14447
|
-
return [{ index, preview, timestamp: m.timestamp }];
|
|
14848
|
+
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
14448
14849
|
});
|
|
14449
14850
|
}
|
|
14450
|
-
/**
|
|
14451
|
-
* Cut the dialogue back to just before the user message at `index` (a
|
|
14452
|
-
* position in the transcript), discarding it and everything after. The task
|
|
14453
|
-
* list is the host's and rides along untouched — rewinding moves where the
|
|
14454
|
-
* conversation resumes, not what the plan is. The planner's research trace
|
|
14455
|
-
* goes back to the same point, by time: its entries carry no link to the
|
|
14456
|
-
* message that caused them.
|
|
14457
|
-
*/
|
|
14458
|
-
rewind(index) {
|
|
14459
|
-
this.assertIdle("rewind the conversation");
|
|
14460
|
-
const plan = this.requirePlan();
|
|
14461
|
-
if (!this.rewindTargets().some((t) => t.index === index)) {
|
|
14462
|
-
if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
|
|
14463
|
-
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.");
|
|
14464
|
-
}
|
|
14465
|
-
const cutoff = this.transcript[index].timestamp;
|
|
14466
|
-
const rewound = this.host.mutate(() => {
|
|
14467
|
-
plan.conversationHistory = this.transcript.slice(0, index);
|
|
14468
|
-
plan.researchLog = (plan.researchLog ?? []).filter((e) => e.timestamp < cutoff);
|
|
14469
|
-
return true;
|
|
14470
|
-
}, () => this.host.broadcastPlan());
|
|
14471
|
-
this.reset();
|
|
14472
|
-
return rewound;
|
|
14473
|
-
}
|
|
14474
14851
|
/**
|
|
14475
14852
|
* Replace the transcript with a summary of it, keeping the last two
|
|
14476
14853
|
* exchanges as they were, and drop the live context so the next message
|
|
@@ -14546,6 +14923,28 @@ var PlannerConversation = class {
|
|
|
14546
14923
|
researchLog: plan?.researchLog ?? []
|
|
14547
14924
|
});
|
|
14548
14925
|
}
|
|
14926
|
+
/**
|
|
14927
|
+
* A copy of the dialogue as it stood just before the user message at
|
|
14928
|
+
* `index` (a position from {@link rewindTargets}) — what a rewind forks
|
|
14929
|
+
* from — plus that message's full text, for a surface to offer back. This
|
|
14930
|
+
* conversation is not touched. The research trace is cut at the same point
|
|
14931
|
+
* by time: its entries carry no link to the message that caused them.
|
|
14932
|
+
*/
|
|
14933
|
+
cloneBefore(index) {
|
|
14934
|
+
this.assertIdle("rewind the conversation");
|
|
14935
|
+
if (!this.rewindTargets().some((t) => t.index === index)) {
|
|
14936
|
+
if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
|
|
14937
|
+
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.");
|
|
14938
|
+
}
|
|
14939
|
+
const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
|
|
14940
|
+
return {
|
|
14941
|
+
dialogue: structuredClone({
|
|
14942
|
+
conversationHistory: this.transcript.slice(0, index),
|
|
14943
|
+
researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
|
|
14944
|
+
}),
|
|
14945
|
+
rewoundMessage
|
|
14946
|
+
};
|
|
14947
|
+
}
|
|
14549
14948
|
/** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
|
|
14550
14949
|
recordModification(request, requestedAt, taskCount) {
|
|
14551
14950
|
this.append("user", request, { timestamp: requestedAt });
|
|
@@ -15132,18 +15531,18 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
15132
15531
|
|
|
15133
15532
|
// src/services/SkillsService.ts
|
|
15134
15533
|
var fs8 = __toESM(require("fs"));
|
|
15135
|
-
var
|
|
15534
|
+
var path17 = __toESM(require("path"));
|
|
15136
15535
|
|
|
15137
15536
|
// src/services/builtinSkills.ts
|
|
15138
|
-
var
|
|
15537
|
+
var path16 = __toESM(require("path"));
|
|
15139
15538
|
var import_url = require("url");
|
|
15140
15539
|
var import_meta = {};
|
|
15141
15540
|
function builtinSkillsDir() {
|
|
15142
|
-
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname :
|
|
15541
|
+
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path16.dirname((0, import_url.fileURLToPath)(
|
|
15143
15542
|
// @ts-expect-error - import.meta is only valid under an ESM module setting
|
|
15144
15543
|
import_meta.url
|
|
15145
15544
|
));
|
|
15146
|
-
return
|
|
15545
|
+
return path16.join(dir, "..", "skills");
|
|
15147
15546
|
}
|
|
15148
15547
|
|
|
15149
15548
|
// src/services/SkillsService.ts
|
|
@@ -15182,8 +15581,8 @@ function copyDirSync2(src, dest) {
|
|
|
15182
15581
|
fs8.mkdirSync(dest, { recursive: true });
|
|
15183
15582
|
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
15184
15583
|
for (const entry of entries) {
|
|
15185
|
-
const srcPath =
|
|
15186
|
-
const destPath =
|
|
15584
|
+
const srcPath = path17.join(src, entry.name);
|
|
15585
|
+
const destPath = path17.join(dest, entry.name);
|
|
15187
15586
|
if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
|
|
15188
15587
|
else fs8.copyFileSync(srcPath, destPath);
|
|
15189
15588
|
}
|
|
@@ -15194,7 +15593,7 @@ function readDir(dir, source) {
|
|
|
15194
15593
|
const skills = [];
|
|
15195
15594
|
for (const entry of entries) {
|
|
15196
15595
|
if (!entry.isDirectory()) continue;
|
|
15197
|
-
const skillFile =
|
|
15596
|
+
const skillFile = path17.join(dir, entry.name, "SKILL.md");
|
|
15198
15597
|
if (!fs8.existsSync(skillFile)) continue;
|
|
15199
15598
|
const parsed = parseSkillFile(skillFile, entry.name, source);
|
|
15200
15599
|
if (parsed) skills.push(parsed);
|
|
@@ -15207,17 +15606,17 @@ var SkillsService = class {
|
|
|
15207
15606
|
}
|
|
15208
15607
|
workspaceRoot;
|
|
15209
15608
|
localDir() {
|
|
15210
|
-
return this.workspaceRoot ?
|
|
15609
|
+
return this.workspaceRoot ? path17.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
|
|
15211
15610
|
}
|
|
15212
15611
|
globalDir() {
|
|
15213
|
-
return
|
|
15612
|
+
return path17.join(globalDataDir(), "skills");
|
|
15214
15613
|
}
|
|
15215
15614
|
seed(name) {
|
|
15216
|
-
const dest =
|
|
15615
|
+
const dest = path17.join(this.globalDir(), name);
|
|
15217
15616
|
if (fs8.existsSync(dest)) return false;
|
|
15218
15617
|
let srcDir;
|
|
15219
15618
|
try {
|
|
15220
|
-
srcDir =
|
|
15619
|
+
srcDir = path17.join(builtinSkillsDir(), name);
|
|
15221
15620
|
} catch (err) {
|
|
15222
15621
|
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
15223
15622
|
return false;
|
|
@@ -15243,7 +15642,7 @@ var SkillsService = class {
|
|
|
15243
15642
|
if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
|
|
15244
15643
|
let raw;
|
|
15245
15644
|
try {
|
|
15246
|
-
raw = fs8.readFileSync(
|
|
15645
|
+
raw = fs8.readFileSync(path17.join(dir, "SKILL.md"), "utf8");
|
|
15247
15646
|
} catch {
|
|
15248
15647
|
return false;
|
|
15249
15648
|
}
|
|
@@ -15260,7 +15659,7 @@ var SkillsService = class {
|
|
|
15260
15659
|
}
|
|
15261
15660
|
pruneRetired() {
|
|
15262
15661
|
for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
|
|
15263
|
-
const dest =
|
|
15662
|
+
const dest = path17.join(this.globalDir(), name);
|
|
15264
15663
|
if (!fs8.existsSync(dest)) continue;
|
|
15265
15664
|
if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
|
|
15266
15665
|
try {
|
|
@@ -15275,10 +15674,10 @@ var SkillsService = class {
|
|
|
15275
15674
|
if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
|
|
15276
15675
|
const localDir = this.localDir();
|
|
15277
15676
|
if (localDir) {
|
|
15278
|
-
const localFile =
|
|
15677
|
+
const localFile = path17.join(localDir, name, "SKILL.md");
|
|
15279
15678
|
if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
|
|
15280
15679
|
}
|
|
15281
|
-
const globalFile =
|
|
15680
|
+
const globalFile = path17.join(this.globalDir(), name, "SKILL.md");
|
|
15282
15681
|
if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
|
|
15283
15682
|
return void 0;
|
|
15284
15683
|
}
|
|
@@ -15361,7 +15760,7 @@ function truncateCheckpointSummary(summary) {
|
|
|
15361
15760
|
|
|
15362
15761
|
// src/utils/sessionStore.ts
|
|
15363
15762
|
var fs9 = __toESM(require("fs"));
|
|
15364
|
-
var
|
|
15763
|
+
var path18 = __toESM(require("path"));
|
|
15365
15764
|
|
|
15366
15765
|
// src/utils/sessionId.ts
|
|
15367
15766
|
var import_crypto2 = require("crypto");
|
|
@@ -15372,7 +15771,7 @@ function mintSessionId() {
|
|
|
15372
15771
|
// src/utils/sessionStore.ts
|
|
15373
15772
|
var SESSIONS_SUBDIR = "sessions";
|
|
15374
15773
|
function getSessionsDir(baseDir) {
|
|
15375
|
-
return
|
|
15774
|
+
return path18.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
15376
15775
|
}
|
|
15377
15776
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
15378
15777
|
function hasScalarRunner(parsed) {
|
|
@@ -15394,8 +15793,8 @@ function legacySessionFilename(session) {
|
|
|
15394
15793
|
return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
|
|
15395
15794
|
}
|
|
15396
15795
|
function dropLegacyFile(sessionsDir, meta) {
|
|
15397
|
-
const legacy =
|
|
15398
|
-
if (legacy ===
|
|
15796
|
+
const legacy = path18.join(sessionsDir, legacySessionFilename(meta));
|
|
15797
|
+
if (legacy === path18.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
|
|
15399
15798
|
try {
|
|
15400
15799
|
const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
|
|
15401
15800
|
if (held === meta.id) fs9.unlinkSync(legacy);
|
|
@@ -15421,7 +15820,7 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
15421
15820
|
plan: JSON.parse(JSON.stringify(plan))
|
|
15422
15821
|
};
|
|
15423
15822
|
const filename = sessionFilename(meta);
|
|
15424
|
-
const filePath =
|
|
15823
|
+
const filePath = path18.join(sessionsDir, filename);
|
|
15425
15824
|
fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
15426
15825
|
dropLegacyFile(sessionsDir, meta);
|
|
15427
15826
|
return meta;
|
|
@@ -15432,7 +15831,7 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
15432
15831
|
const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
15433
15832
|
return files.map((f) => {
|
|
15434
15833
|
try {
|
|
15435
|
-
const raw = fs9.readFileSync(
|
|
15834
|
+
const raw = fs9.readFileSync(path18.join(sessionsDir, f), "utf-8");
|
|
15436
15835
|
const parsed = JSON.parse(raw);
|
|
15437
15836
|
if (hasScalarRunner(parsed)) {
|
|
15438
15837
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -15450,7 +15849,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
15450
15849
|
const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
15451
15850
|
for (const f of files) {
|
|
15452
15851
|
try {
|
|
15453
|
-
const raw = fs9.readFileSync(
|
|
15852
|
+
const raw = fs9.readFileSync(path18.join(sessionsDir, f), "utf-8");
|
|
15454
15853
|
const session = JSON.parse(raw);
|
|
15455
15854
|
if (session.meta.id === sessionId) return f;
|
|
15456
15855
|
} catch (err) {
|
|
@@ -15465,7 +15864,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
15465
15864
|
try {
|
|
15466
15865
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
15467
15866
|
if (file) {
|
|
15468
|
-
const raw = fs9.readFileSync(
|
|
15867
|
+
const raw = fs9.readFileSync(path18.join(sessionsDir, file), "utf-8");
|
|
15469
15868
|
const parsed = JSON.parse(raw);
|
|
15470
15869
|
if (hasScalarRunner(parsed)) {
|
|
15471
15870
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -15500,7 +15899,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
15500
15899
|
try {
|
|
15501
15900
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
15502
15901
|
if (file) {
|
|
15503
|
-
fs9.unlinkSync(
|
|
15902
|
+
fs9.unlinkSync(path18.join(sessionsDir, file));
|
|
15504
15903
|
return true;
|
|
15505
15904
|
}
|
|
15506
15905
|
} catch (err) {
|
|
@@ -16032,26 +16431,30 @@ var Session = class {
|
|
|
16032
16431
|
*/
|
|
16033
16432
|
forkConversation() {
|
|
16034
16433
|
if (!this.plan) throw new ConversationEditError("No planning conversation to fork");
|
|
16035
|
-
|
|
16036
|
-
const sessionId = mintSessionId();
|
|
16037
|
-
const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
|
|
16038
|
-
saveSession(plan, this.goal, this.workspace, sessionId);
|
|
16039
|
-
return { sessionId, goal: this.goal, workspace: this.workspace };
|
|
16434
|
+
return this.saveFork(this.conversation.clone());
|
|
16040
16435
|
}
|
|
16041
16436
|
/**
|
|
16042
|
-
*
|
|
16043
|
-
* position from {@link rewindTargets})
|
|
16044
|
-
*
|
|
16045
|
-
*
|
|
16437
|
+
* Fork the conversation from just before a user message (a transcript
|
|
16438
|
+
* position from {@link rewindTargets}): the fork holds everything said
|
|
16439
|
+
* before it and the current task list, like {@link forkConversation}, and
|
|
16440
|
+
* this session keeps its whole history. The message's full text comes back
|
|
16441
|
+
* so a surface can offer it for resending.
|
|
16046
16442
|
*/
|
|
16047
16443
|
rewindConversation(userMessageIndex) {
|
|
16048
16444
|
if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
|
|
16049
|
-
|
|
16445
|
+
const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
|
|
16446
|
+
return { ...this.saveFork(dialogue), rewoundMessage };
|
|
16447
|
+
}
|
|
16448
|
+
saveFork(dialogue) {
|
|
16449
|
+
const sessionId = mintSessionId();
|
|
16450
|
+
const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
|
|
16451
|
+
saveSession(plan, this.goal, this.workspace, sessionId);
|
|
16452
|
+
return { sessionId, goal: this.goal, workspace: this.workspace };
|
|
16050
16453
|
}
|
|
16051
16454
|
/**
|
|
16052
16455
|
* Condense the conversation on the user's say-so: a hidden planner turn
|
|
16053
16456
|
* summarises it, and the summary replaces everything but the last two
|
|
16054
|
-
* exchanges. Conversation only
|
|
16457
|
+
* exchanges. Conversation only — the tasks, and any run
|
|
16055
16458
|
* executing them, are untouched. Atomic: a failed or stopped turn changes
|
|
16056
16459
|
* nothing.
|
|
16057
16460
|
*/
|
|
@@ -16324,8 +16727,9 @@ var Session = class {
|
|
|
16324
16727
|
* The opt-in way through a merge conflict: add an AI task, on the conflicted
|
|
16325
16728
|
* task's own runner and model, that merges its branch by hand in a worktree
|
|
16326
16729
|
* of its own. When that task lands, the conflicted one lands through it (see
|
|
16327
|
-
* {@link TaskOrchestrator.linkConflictResolver}).
|
|
16328
|
-
*
|
|
16730
|
+
* {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
|
|
16731
|
+
* conflict is a repair, a new attempt of the same task (ADR-0015); a task is
|
|
16732
|
+
* only ever added to the plan by this call.
|
|
16329
16733
|
*/
|
|
16330
16734
|
async resolveConflictAsTask(taskId) {
|
|
16331
16735
|
if (!this.plan) return null;
|
|
@@ -17511,17 +17915,17 @@ function tokensMatch(presented, expected) {
|
|
|
17511
17915
|
|
|
17512
17916
|
// src/plugins/RunnerRegistry.ts
|
|
17513
17917
|
var fs11 = __toESM(require("fs"));
|
|
17514
|
-
var
|
|
17918
|
+
var path21 = __toESM(require("path"));
|
|
17515
17919
|
var os5 = __toESM(require("os"));
|
|
17516
17920
|
var import_child_process10 = require("child_process");
|
|
17517
17921
|
var import_crypto4 = require("crypto");
|
|
17518
17922
|
|
|
17519
17923
|
// src/plugins/FsPluginStore.ts
|
|
17520
17924
|
var fs10 = __toESM(require("fs"));
|
|
17521
|
-
var
|
|
17925
|
+
var path20 = __toESM(require("path"));
|
|
17522
17926
|
|
|
17523
17927
|
// src/plugins/pluginNames.ts
|
|
17524
|
-
var
|
|
17928
|
+
var path19 = __toESM(require("path"));
|
|
17525
17929
|
var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
17526
17930
|
var MAX_PLUGIN_NAME_LENGTH = 64;
|
|
17527
17931
|
function isPlainPluginName(name) {
|
|
@@ -17536,10 +17940,10 @@ function assertPlainPluginName(name) {
|
|
|
17536
17940
|
}
|
|
17537
17941
|
function resolvePluginInstallDir(pluginsDir, name) {
|
|
17538
17942
|
assertPlainPluginName(name);
|
|
17539
|
-
const root =
|
|
17540
|
-
const dest =
|
|
17541
|
-
const
|
|
17542
|
-
if (
|
|
17943
|
+
const root = path19.resolve(pluginsDir);
|
|
17944
|
+
const dest = path19.resolve(root, name);
|
|
17945
|
+
const relative5 = path19.relative(root, dest);
|
|
17946
|
+
if (relative5 !== name || relative5 === "" || path19.isAbsolute(relative5)) {
|
|
17543
17947
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
17544
17948
|
}
|
|
17545
17949
|
return dest;
|
|
@@ -17554,10 +17958,10 @@ function isValidManifest(obj) {
|
|
|
17554
17958
|
|
|
17555
17959
|
// src/plugins/FsPluginStore.ts
|
|
17556
17960
|
function userPluginsDir() {
|
|
17557
|
-
return
|
|
17961
|
+
return path20.join(globalDataDir(), "plugins");
|
|
17558
17962
|
}
|
|
17559
17963
|
function loadManifestFromDir(dir) {
|
|
17560
|
-
const manifestPath =
|
|
17964
|
+
const manifestPath = path20.join(dir, "manifest.json");
|
|
17561
17965
|
if (!fs10.existsSync(manifestPath)) return null;
|
|
17562
17966
|
try {
|
|
17563
17967
|
const raw = fs10.readFileSync(manifestPath, "utf8");
|
|
@@ -17572,8 +17976,8 @@ function copyDirSync3(src, dest) {
|
|
|
17572
17976
|
ensureDir(dest);
|
|
17573
17977
|
const entries = fs10.readdirSync(src, { withFileTypes: true });
|
|
17574
17978
|
for (const entry of entries) {
|
|
17575
|
-
const srcPath =
|
|
17576
|
-
const destPath =
|
|
17979
|
+
const srcPath = path20.join(src, entry.name);
|
|
17980
|
+
const destPath = path20.join(dest, entry.name);
|
|
17577
17981
|
if (entry.isDirectory()) {
|
|
17578
17982
|
copyDirSync3(srcPath, destPath);
|
|
17579
17983
|
} else {
|
|
@@ -17585,7 +17989,7 @@ function rmDirSync(dir) {
|
|
|
17585
17989
|
if (!fs10.existsSync(dir)) return;
|
|
17586
17990
|
const entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
17587
17991
|
for (const entry of entries) {
|
|
17588
|
-
const p =
|
|
17992
|
+
const p = path20.join(dir, entry.name);
|
|
17589
17993
|
if (entry.isDirectory()) {
|
|
17590
17994
|
rmDirSync(p);
|
|
17591
17995
|
} else {
|
|
@@ -17638,11 +18042,11 @@ var FsPluginStore = class {
|
|
|
17638
18042
|
return null;
|
|
17639
18043
|
}
|
|
17640
18044
|
}
|
|
17641
|
-
dirExists(
|
|
17642
|
-
return fs10.existsSync(
|
|
18045
|
+
dirExists(path24) {
|
|
18046
|
+
return fs10.existsSync(path24) && fs10.statSync(path24).isDirectory();
|
|
17643
18047
|
}
|
|
17644
|
-
exists(
|
|
17645
|
-
return fs10.existsSync(
|
|
18048
|
+
exists(path24) {
|
|
18049
|
+
return fs10.existsSync(path24);
|
|
17646
18050
|
}
|
|
17647
18051
|
};
|
|
17648
18052
|
|
|
@@ -18074,7 +18478,7 @@ var RunnerRegistry = class {
|
|
|
18074
18478
|
return entry?.source === "builtin";
|
|
18075
18479
|
}
|
|
18076
18480
|
installFromPath(sourcePath) {
|
|
18077
|
-
const absPath =
|
|
18481
|
+
const absPath = path21.resolve(sourcePath);
|
|
18078
18482
|
if (!this.store.dirExists(absPath)) {
|
|
18079
18483
|
throw new Error(`Plugin source must be a directory: ${absPath}`);
|
|
18080
18484
|
}
|
|
@@ -18086,7 +18490,7 @@ var RunnerRegistry = class {
|
|
|
18086
18490
|
}
|
|
18087
18491
|
installFromGit(url) {
|
|
18088
18492
|
const validated = assertInstallablePluginUrl(url);
|
|
18089
|
-
const tmpDir =
|
|
18493
|
+
const tmpDir = path21.join(os5.tmpdir(), `ordewell-plugin-${(0, import_crypto4.randomBytes)(12).toString("hex")}`);
|
|
18090
18494
|
this.store.ensureDir(tmpDir);
|
|
18091
18495
|
try {
|
|
18092
18496
|
try {
|
|
@@ -18099,7 +18503,7 @@ var RunnerRegistry = class {
|
|
|
18099
18503
|
const hasManifest = contents.some((f) => f === "manifest.json");
|
|
18100
18504
|
let sourceDir = tmpDir;
|
|
18101
18505
|
if (!hasManifest) {
|
|
18102
|
-
const onlyEntry = contents.length === 1 ?
|
|
18506
|
+
const onlyEntry = contents.length === 1 ? path21.join(tmpDir, contents[0]) : void 0;
|
|
18103
18507
|
if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
|
|
18104
18508
|
throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
|
|
18105
18509
|
}
|
|
@@ -18147,7 +18551,7 @@ var RunnerRegistry = class {
|
|
|
18147
18551
|
}
|
|
18148
18552
|
createSkeleton(name, outputDir) {
|
|
18149
18553
|
assertPlainPluginName(name);
|
|
18150
|
-
const dir =
|
|
18554
|
+
const dir = path21.resolve(outputDir, name);
|
|
18151
18555
|
this.store.ensureDir(dir);
|
|
18152
18556
|
const manifest = {
|
|
18153
18557
|
name,
|
|
@@ -18179,8 +18583,8 @@ var RunnerRegistry = class {
|
|
|
18179
18583
|
]
|
|
18180
18584
|
}
|
|
18181
18585
|
};
|
|
18182
|
-
this.store.writeFile(
|
|
18183
|
-
const runSh =
|
|
18586
|
+
this.store.writeFile(path21.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
18587
|
+
const runSh = path21.join(dir, "run.sh");
|
|
18184
18588
|
const scriptContent = `#!/usr/bin/env bash
|
|
18185
18589
|
# Custom runner script for ${name}
|
|
18186
18590
|
# Receives:
|
|
@@ -18213,14 +18617,14 @@ function fsChmodSync(fpath, mode) {
|
|
|
18213
18617
|
|
|
18214
18618
|
// src/utils/stateStore.ts
|
|
18215
18619
|
var fs12 = __toESM(require("fs"));
|
|
18216
|
-
var
|
|
18620
|
+
var path22 = __toESM(require("path"));
|
|
18217
18621
|
var STATE_FILE = "state.json";
|
|
18218
18622
|
function getStatePath(baseDir) {
|
|
18219
|
-
return
|
|
18623
|
+
return path22.join(getStateDir(baseDir), STATE_FILE);
|
|
18220
18624
|
}
|
|
18221
18625
|
function saveState(plan, baseDir) {
|
|
18222
18626
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
18223
|
-
ensureDir(
|
|
18627
|
+
ensureDir(path22.dirname(statePath));
|
|
18224
18628
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
18225
18629
|
fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
18226
18630
|
}
|
|
@@ -18255,7 +18659,7 @@ function stateExists(baseDir) {
|
|
|
18255
18659
|
|
|
18256
18660
|
// src/services/SettingsService.ts
|
|
18257
18661
|
var fs13 = __toESM(require("fs"));
|
|
18258
|
-
var
|
|
18662
|
+
var path23 = __toESM(require("path"));
|
|
18259
18663
|
var DEFAULTS = {
|
|
18260
18664
|
tdd: { enabled: true },
|
|
18261
18665
|
verification: { enabled: false }
|
|
@@ -18263,7 +18667,7 @@ var DEFAULTS = {
|
|
|
18263
18667
|
function getSettingsPath() {
|
|
18264
18668
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
18265
18669
|
if (override && override.trim()) return override.trim();
|
|
18266
|
-
return
|
|
18670
|
+
return path23.join(globalDataDir(), "settings.json");
|
|
18267
18671
|
}
|
|
18268
18672
|
function readPlannerModels(raw) {
|
|
18269
18673
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -18391,7 +18795,7 @@ var SettingsService = class {
|
|
|
18391
18795
|
return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
|
|
18392
18796
|
}
|
|
18393
18797
|
persist() {
|
|
18394
|
-
const dir =
|
|
18798
|
+
const dir = path23.dirname(this.filePath);
|
|
18395
18799
|
if (!fs13.existsSync(dir)) {
|
|
18396
18800
|
fs13.mkdirSync(dir, { recursive: true });
|
|
18397
18801
|
}
|
|
@@ -18535,6 +18939,7 @@ var PlannerModelMemory = class {
|
|
|
18535
18939
|
augmentPromptWithPriorOutputs,
|
|
18536
18940
|
augmentedPath,
|
|
18537
18941
|
bearerHeaderValue,
|
|
18942
|
+
buildConflictRepairPrompt,
|
|
18538
18943
|
buildConflictResolutionPrompt,
|
|
18539
18944
|
buildConversationSystemPrompt,
|
|
18540
18945
|
buildFallbackGrepArgs,
|
|
@@ -18554,6 +18959,7 @@ var PlannerModelMemory = class {
|
|
|
18554
18959
|
canMergeTasks,
|
|
18555
18960
|
canSetDependencies,
|
|
18556
18961
|
canSplitTask,
|
|
18962
|
+
capConflictFiles,
|
|
18557
18963
|
checkDepsResolve,
|
|
18558
18964
|
checkImmutableLog,
|
|
18559
18965
|
checkInProgress,
|