@brainervirus/workit-claude-code 7.4.0 → 7.5.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/workit.js +1121 -116
- package/package.json +3 -3
- package/skills/fanout/SKILL.md +27 -27
- package/skills/fanout/references/brief.md +5 -1
package/dist/workit.js
CHANGED
|
@@ -65,7 +65,7 @@ var package_default;
|
|
|
65
65
|
var init_package = __esm(() => {
|
|
66
66
|
package_default = {
|
|
67
67
|
name: "@brainervirus/workit-cli",
|
|
68
|
-
version: "7.
|
|
68
|
+
version: "7.5.0",
|
|
69
69
|
private: false,
|
|
70
70
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
71
71
|
keywords: [
|
|
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
|
|
|
103
103
|
},
|
|
104
104
|
dependencies: {
|
|
105
105
|
"@babel/parser": "7.29.9",
|
|
106
|
-
"@brainervirus/workit-core": "^7.
|
|
107
|
-
"@brainervirus/workit-cursor": "^7.
|
|
108
|
-
"@brainervirus/workit-opencode": "^7.
|
|
106
|
+
"@brainervirus/workit-core": "^7.5.0",
|
|
107
|
+
"@brainervirus/workit-cursor": "^7.5.0",
|
|
108
|
+
"@brainervirus/workit-opencode": "^7.5.0",
|
|
109
109
|
"@inkjs/ui": "2.0.0",
|
|
110
110
|
ink: "7.1.1",
|
|
111
111
|
react: "19.2.8"
|
|
@@ -73255,6 +73255,33 @@ var init_resolve = __esm(() => {
|
|
|
73255
73255
|
init_gitlab();
|
|
73256
73256
|
});
|
|
73257
73257
|
|
|
73258
|
+
// packages/workit-core/src/duration.ts
|
|
73259
|
+
function parseDuration2(value) {
|
|
73260
|
+
const text = value.trim().toLowerCase();
|
|
73261
|
+
if (/^\d+(?:\.\d+)?$/u.test(text))
|
|
73262
|
+
return Math.round(Number(text) * 1000);
|
|
73263
|
+
const parts = [...text.matchAll(/(\d+(?:\.\d+)?)\s*([a-z]+)\s*/gu)];
|
|
73264
|
+
if (parts.length === 0 || parts.map((part) => part[0]).join("") !== text)
|
|
73265
|
+
return null;
|
|
73266
|
+
let total = 0;
|
|
73267
|
+
for (const [, amount, unit] of parts) {
|
|
73268
|
+
const factor = UNITS.find(([pattern]) => pattern.test(unit))?.[1];
|
|
73269
|
+
if (factor === undefined)
|
|
73270
|
+
return null;
|
|
73271
|
+
total += Number(amount) * factor;
|
|
73272
|
+
}
|
|
73273
|
+
return Math.round(total);
|
|
73274
|
+
}
|
|
73275
|
+
var UNITS;
|
|
73276
|
+
var init_duration = __esm(() => {
|
|
73277
|
+
UNITS = [
|
|
73278
|
+
[/^(?:ms|milliseconds?)$/u, 1],
|
|
73279
|
+
[/^(?:s|secs?|seconds?)$/u, 1000],
|
|
73280
|
+
[/^(?:m|mins?|minutes?)$/u, 60000],
|
|
73281
|
+
[/^(?:h|hrs?|hours?)$/u, 3600000]
|
|
73282
|
+
];
|
|
73283
|
+
});
|
|
73284
|
+
|
|
73258
73285
|
// packages/workit-cli/src/verbs/forge-common.ts
|
|
73259
73286
|
function parseFlags(argv, spec) {
|
|
73260
73287
|
const parsed = { values: {}, booleans: new Set, lists: {}, positionals: [] };
|
|
@@ -73292,14 +73319,6 @@ function positiveInt(value, flag) {
|
|
|
73292
73319
|
return null;
|
|
73293
73320
|
return /^[1-9]\d{0,8}$/u.test(value) ? Number(value) : `${flag} must be a positive integer`;
|
|
73294
73321
|
}
|
|
73295
|
-
function parseDuration2(value) {
|
|
73296
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/u.exec(value.trim());
|
|
73297
|
-
if (!match)
|
|
73298
|
-
return null;
|
|
73299
|
-
const unit = match[2] ?? "s";
|
|
73300
|
-
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
|
|
73301
|
-
return Math.round(Number(match[1]) * factor);
|
|
73302
|
-
}
|
|
73303
73322
|
function releaseTrunk(io, track, branch) {
|
|
73304
73323
|
const resolved = vcsConfig("resolve", io.cwd, {
|
|
73305
73324
|
track,
|
|
@@ -73382,6 +73401,7 @@ var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", me
|
|
|
73382
73401
|
var init_forge_common = __esm(() => {
|
|
73383
73402
|
init_resolve();
|
|
73384
73403
|
init_vcs_config();
|
|
73404
|
+
init_duration();
|
|
73385
73405
|
init_output();
|
|
73386
73406
|
forgeDeps = {
|
|
73387
73407
|
runner: undefined,
|
|
@@ -75694,9 +75714,34 @@ var init_stack2 = __esm(() => {
|
|
|
75694
75714
|
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
75695
75715
|
import fs23 from "node:fs";
|
|
75696
75716
|
import path43 from "node:path";
|
|
75717
|
+
function expandBraces(pattern, limit = 64) {
|
|
75718
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
75719
|
+
if (pattern[index] === "\\") {
|
|
75720
|
+
index += 1;
|
|
75721
|
+
continue;
|
|
75722
|
+
}
|
|
75723
|
+
const close = pattern.indexOf("}", index);
|
|
75724
|
+
if (pattern[index] !== "{" || close < index)
|
|
75725
|
+
continue;
|
|
75726
|
+
const head = pattern.slice(0, index);
|
|
75727
|
+
const tail = pattern.slice(close + 1);
|
|
75728
|
+
const out = [];
|
|
75729
|
+
for (const alternative of pattern.slice(index + 1, close).split(","))
|
|
75730
|
+
for (const rest of expandBraces(tail, limit)) {
|
|
75731
|
+
if (out.length >= limit)
|
|
75732
|
+
return out;
|
|
75733
|
+
out.push(`${head}${alternative}${rest}`);
|
|
75734
|
+
}
|
|
75735
|
+
return out;
|
|
75736
|
+
}
|
|
75737
|
+
return [pattern];
|
|
75738
|
+
}
|
|
75697
75739
|
function scopeSamples(pattern) {
|
|
75698
75740
|
if (!hasWildcard(pattern))
|
|
75699
75741
|
return [unescape2(pattern)];
|
|
75742
|
+
return [...new Set(expandBraces(pattern).map(sampleOne))];
|
|
75743
|
+
}
|
|
75744
|
+
function sampleOne(pattern) {
|
|
75700
75745
|
let out = "";
|
|
75701
75746
|
for (let index = 0;index < pattern.length; index += 1) {
|
|
75702
75747
|
const char = pattern[index];
|
|
@@ -75715,14 +75760,10 @@ function scopeSamples(pattern) {
|
|
|
75715
75760
|
out += "<any>";
|
|
75716
75761
|
else if (char === "?")
|
|
75717
75762
|
out += "x";
|
|
75718
|
-
else
|
|
75719
|
-
const close = pattern.indexOf("}", index);
|
|
75720
|
-
out += pattern.slice(index + 1, close).split(",")[0];
|
|
75721
|
-
index = close;
|
|
75722
|
-
} else
|
|
75763
|
+
else
|
|
75723
75764
|
out += char;
|
|
75724
75765
|
}
|
|
75725
|
-
return
|
|
75766
|
+
return out;
|
|
75726
75767
|
}
|
|
75727
75768
|
function globSource(pattern) {
|
|
75728
75769
|
let out = "";
|
|
@@ -75763,7 +75804,20 @@ function matchesScope(pattern, file) {
|
|
|
75763
75804
|
return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
|
|
75764
75805
|
}
|
|
75765
75806
|
function normalizePattern(raw) {
|
|
75766
|
-
let pattern = raw.trim()
|
|
75807
|
+
let pattern = raw.trim();
|
|
75808
|
+
if (pattern.includes("\\")) {
|
|
75809
|
+
if (/\\[*?{}]/u.test(pattern))
|
|
75810
|
+
return {
|
|
75811
|
+
error: "has \\*, \\?, \\{ or \\}: only \\[ and \\] are escapes; use / as the separator (src/**/*.ts)"
|
|
75812
|
+
};
|
|
75813
|
+
const brackets = /\\[[\]]/u.test(pattern);
|
|
75814
|
+
if (brackets && pattern.replace(/\\[[\]]/gu, "").includes("\\"))
|
|
75815
|
+
return {
|
|
75816
|
+
error: "mixes \\[ or \\] escapes with other backslashes, which is ambiguous: use / as the separator (app/\\[id\\]/page.tsx)"
|
|
75817
|
+
};
|
|
75818
|
+
if (!brackets)
|
|
75819
|
+
pattern = pattern.replaceAll("\\", "/");
|
|
75820
|
+
}
|
|
75767
75821
|
while (pattern.startsWith("./"))
|
|
75768
75822
|
pattern = pattern.slice(2);
|
|
75769
75823
|
if (pattern === "." || pattern === "")
|
|
@@ -75890,7 +75944,7 @@ function parsePlan(raw, defaults, name) {
|
|
|
75890
75944
|
id,
|
|
75891
75945
|
branch,
|
|
75892
75946
|
base: optional("base") ?? "",
|
|
75893
|
-
worktree: optional("worktree") ?? path43.join(defaults.worktreeRoot, id),
|
|
75947
|
+
worktree: path43.resolve(defaults.root, optional("worktree") ?? path43.join(defaults.worktreeRoot, id)),
|
|
75894
75948
|
tier,
|
|
75895
75949
|
dependsOn: [...new Set(dependsOn ?? [])],
|
|
75896
75950
|
scope,
|
|
@@ -76066,6 +76120,30 @@ function analyzeOverlap(plan, candidates) {
|
|
|
76066
76120
|
});
|
|
76067
76121
|
return { overlaps, conflicts };
|
|
76068
76122
|
}
|
|
76123
|
+
function caseIndex(files) {
|
|
76124
|
+
const index = new Map;
|
|
76125
|
+
for (const file of files) {
|
|
76126
|
+
const parts = file.split("/");
|
|
76127
|
+
for (let end = 1;end <= parts.length; end += 1) {
|
|
76128
|
+
const prefix = parts.slice(0, end).join("/");
|
|
76129
|
+
const key = prefix.toLowerCase();
|
|
76130
|
+
(index.get(key) ?? index.set(key, new Set).get(key))?.add(prefix);
|
|
76131
|
+
}
|
|
76132
|
+
}
|
|
76133
|
+
return index;
|
|
76134
|
+
}
|
|
76135
|
+
function caseClash(index, file) {
|
|
76136
|
+
const parts = file.split("/");
|
|
76137
|
+
for (let end = 1;end <= parts.length; end += 1) {
|
|
76138
|
+
const prefix = parts.slice(0, end).join("/");
|
|
76139
|
+
if (prefix.includes("<any>"))
|
|
76140
|
+
return null;
|
|
76141
|
+
const spellings = index.get(prefix.toLowerCase());
|
|
76142
|
+
if (spellings && !spellings.has(prefix))
|
|
76143
|
+
return { path: prefix, trunk: [...spellings].toSorted()[0] };
|
|
76144
|
+
}
|
|
76145
|
+
return null;
|
|
76146
|
+
}
|
|
76069
76147
|
function branchRef(cwd, branch) {
|
|
76070
76148
|
if (!branch || branch.startsWith("-"))
|
|
76071
76149
|
return null;
|
|
@@ -76088,9 +76166,12 @@ function defaultTrunk(cwd) {
|
|
|
76088
76166
|
const base = defaultBase(cwd);
|
|
76089
76167
|
return base ? base.replace(/^origin\//u, "") : "main";
|
|
76090
76168
|
}
|
|
76091
|
-
function
|
|
76169
|
+
function mainCheckout(cwd) {
|
|
76092
76170
|
const common = gitCommonDir(cwd);
|
|
76093
|
-
|
|
76171
|
+
return common && path43.basename(common) === ".git" ? path43.dirname(common) : path43.resolve(cwd);
|
|
76172
|
+
}
|
|
76173
|
+
function worktreeRoot(cwd) {
|
|
76174
|
+
const main = mainCheckout(cwd);
|
|
76094
76175
|
return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
|
|
76095
76176
|
}
|
|
76096
76177
|
function fanoutsDir(cwd) {
|
|
@@ -76226,7 +76307,11 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76226
76307
|
const dir = fanoutsDir(cwd);
|
|
76227
76308
|
if (!dir.ok)
|
|
76228
76309
|
return dir;
|
|
76229
|
-
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
|
|
76310
|
+
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
|
|
76311
|
+
trunk: input.trunk ?? defaultTrunk(cwd),
|
|
76312
|
+
root: mainCheckout(cwd),
|
|
76313
|
+
worktreeRoot: worktreeRoot(cwd)
|
|
76314
|
+
}, input.name);
|
|
76230
76315
|
if (!parsed.ok)
|
|
76231
76316
|
return fanoutFail("invalid_input", `the plan is incomplete: ${parsed.problems[0]}${parsed.problems.length > 1 ? ` (+${parsed.problems.length - 1} more)` : ""}`, "fill every listed field, then run workit fanout plan again", { problems: parsed.problems });
|
|
76232
76317
|
const plan = parsed.plan;
|
|
@@ -76241,6 +76326,20 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76241
76326
|
const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
|
|
76242
76327
|
if (conflicts.length)
|
|
76243
76328
|
return fanoutFail("blocked", `${conflicts.length} file-scope overlap${conflicts.length === 1 ? "" : "s"} between slices: ${conflicts[0].slices.join(" and ")} on ${conflicts[0].paths[0]}${conflicts[0].paths.length > 1 ? ` (+${conflicts[0].paths.length - 1})` : ""}`, conflicts[0].suggestion.text, { name: plan.name, conflicts, overlaps });
|
|
76329
|
+
const trunkCase = caseIndex(tracked);
|
|
76330
|
+
const clashes = [];
|
|
76331
|
+
for (const slice of plan.slices) {
|
|
76332
|
+
const seen = new Set;
|
|
76333
|
+
for (const sample of [...slice.scope, ...slice.owns].flatMap(scopeSamples)) {
|
|
76334
|
+
const clash = caseClash(trunkCase, sample);
|
|
76335
|
+
if (clash && !seen.has(clash.path)) {
|
|
76336
|
+
seen.add(clash.path);
|
|
76337
|
+
clashes.push({ slice: slice.id, ...clash });
|
|
76338
|
+
}
|
|
76339
|
+
}
|
|
76340
|
+
}
|
|
76341
|
+
if (clashes.length)
|
|
76342
|
+
return fanoutFail("blocked", `slice ${clashes[0].slice} names ${clashes[0].path}, which differs only in case from ${plan.trunk}'s ${clashes[0].trunk} (one path on macOS and Windows)`, `use ${plan.trunk}'s spelling ${clashes[0].trunk} in slice ${clashes[0].slice}'s scope, then workit fanout plan again`, { name: plan.name, caseCollisions: clashes, overlaps });
|
|
76244
76343
|
const existing = readFanout(cwd, plan.name);
|
|
76245
76344
|
const previous = existing.ok ? existing.data : null;
|
|
76246
76345
|
plan.createdAt = previous?.createdAt || now.toISOString();
|
|
@@ -76315,11 +76414,11 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
|
76315
76414
|
edges,
|
|
76316
76415
|
text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
|
|
76317
76416
|
};
|
|
76318
|
-
}, gitRun4 = (cwd, args) => {
|
|
76417
|
+
}, gitRun4 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
76319
76418
|
const run = spawnSync21("git", args, {
|
|
76320
76419
|
cwd,
|
|
76321
76420
|
encoding: "utf8",
|
|
76322
|
-
timeout:
|
|
76421
|
+
timeout: timeoutMs,
|
|
76323
76422
|
killSignal: "SIGKILL",
|
|
76324
76423
|
windowsHide: true,
|
|
76325
76424
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -76352,6 +76451,200 @@ var init_fanout = __esm(() => {
|
|
|
76352
76451
|
ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
76353
76452
|
});
|
|
76354
76453
|
|
|
76454
|
+
// packages/workit-core/src/fanout-landed.ts
|
|
76455
|
+
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
76456
|
+
function forgeBreaker(resolved) {
|
|
76457
|
+
if (!resolved)
|
|
76458
|
+
return { forge: null, down: () => null };
|
|
76459
|
+
let tripped = null;
|
|
76460
|
+
const guard = (call) => (...args) => {
|
|
76461
|
+
if (tripped)
|
|
76462
|
+
return { ok: false, code: "unavailable", error: tripped };
|
|
76463
|
+
const result = call(...args);
|
|
76464
|
+
if (!result.ok && result.code === "unavailable")
|
|
76465
|
+
tripped = result.error;
|
|
76466
|
+
return result;
|
|
76467
|
+
};
|
|
76468
|
+
const inner = resolved.forge;
|
|
76469
|
+
const forge = Object.create(inner);
|
|
76470
|
+
forge.findPr = guard(inner.findPr.bind(inner));
|
|
76471
|
+
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
76472
|
+
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
76473
|
+
}
|
|
76474
|
+
function branchCreatedAt(cwd, branch) {
|
|
76475
|
+
if (!branch || branch.startsWith("-"))
|
|
76476
|
+
return null;
|
|
76477
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
76478
|
+
if (!log.ok)
|
|
76479
|
+
return null;
|
|
76480
|
+
return log.stdout.trim().split(`
|
|
76481
|
+
`).findLast(Boolean) ?? null;
|
|
76482
|
+
}
|
|
76483
|
+
function hasOwnCommits2(cwd, branch, start, head) {
|
|
76484
|
+
if (!branch || branch.startsWith("-"))
|
|
76485
|
+
return false;
|
|
76486
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
76487
|
+
if (!log.ok)
|
|
76488
|
+
return false;
|
|
76489
|
+
for (const line of log.stdout.split(`
|
|
76490
|
+
`)) {
|
|
76491
|
+
const [sha, subject = ""] = line.split("\x00");
|
|
76492
|
+
if (!sha)
|
|
76493
|
+
continue;
|
|
76494
|
+
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
76495
|
+
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
76496
|
+
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
76497
|
+
continue;
|
|
76498
|
+
if (sha !== head && !isAncestor2(cwd, sha, head))
|
|
76499
|
+
continue;
|
|
76500
|
+
if (start && (sha === start || isAncestor2(cwd, sha, start)))
|
|
76501
|
+
continue;
|
|
76502
|
+
return true;
|
|
76503
|
+
}
|
|
76504
|
+
return false;
|
|
76505
|
+
}
|
|
76506
|
+
function reverted(cwd, trunk, landedHead, start) {
|
|
76507
|
+
const fork = mergeBase(cwd, landedHead, trunk);
|
|
76508
|
+
if (fork && fork !== landedHead)
|
|
76509
|
+
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
76510
|
+
const before = start && start !== landedHead && isAncestor2(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
76511
|
+
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
76512
|
+
}
|
|
76513
|
+
function unchangedSince(cwd, before, after, trunk) {
|
|
76514
|
+
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
76515
|
+
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
76516
|
+
if (files.length === 0)
|
|
76517
|
+
return false;
|
|
76518
|
+
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
76519
|
+
}
|
|
76520
|
+
function trunkPatchIds(ctx, fork) {
|
|
76521
|
+
const cache = ctx.cache ??= new Map;
|
|
76522
|
+
const known = cache.get(fork);
|
|
76523
|
+
if (known)
|
|
76524
|
+
return known;
|
|
76525
|
+
const ids = new Set;
|
|
76526
|
+
const log = gitRun4(ctx.cwd, [
|
|
76527
|
+
"log",
|
|
76528
|
+
"--no-merges",
|
|
76529
|
+
`--max-count=${TRUNK_SCAN}`,
|
|
76530
|
+
"-p",
|
|
76531
|
+
"--format=commit %H",
|
|
76532
|
+
"-U3",
|
|
76533
|
+
"--diff-algorithm=myers",
|
|
76534
|
+
"--indent-heuristic",
|
|
76535
|
+
"--no-color",
|
|
76536
|
+
"--no-ext-diff",
|
|
76537
|
+
"--no-textconv",
|
|
76538
|
+
"--no-renames",
|
|
76539
|
+
"--full-index",
|
|
76540
|
+
"--src-prefix=a/",
|
|
76541
|
+
"--dst-prefix=b/",
|
|
76542
|
+
`${fork}..${ctx.trunk}`,
|
|
76543
|
+
"--"
|
|
76544
|
+
]);
|
|
76545
|
+
if (log.ok && log.stdout.trim()) {
|
|
76546
|
+
const run = spawnSync22("git", ["patch-id", "--stable"], {
|
|
76547
|
+
cwd: ctx.cwd,
|
|
76548
|
+
input: log.stdout,
|
|
76549
|
+
encoding: "utf8",
|
|
76550
|
+
timeout: GIT_TIMEOUTS.local,
|
|
76551
|
+
windowsHide: true,
|
|
76552
|
+
maxBuffer: 64 * 1024 * 1024
|
|
76553
|
+
});
|
|
76554
|
+
for (const line of (run.stdout ?? "").split(`
|
|
76555
|
+
`)) {
|
|
76556
|
+
const id = line.trim().split(/\s+/u)[0];
|
|
76557
|
+
if (id)
|
|
76558
|
+
ids.add(id);
|
|
76559
|
+
}
|
|
76560
|
+
}
|
|
76561
|
+
cache.set(fork, ids);
|
|
76562
|
+
return ids;
|
|
76563
|
+
}
|
|
76564
|
+
function linked(rows, input, slice, head) {
|
|
76565
|
+
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
76566
|
+
return rows.some((row) => (Number.isNaN(since) || Date.parse(String(row.at)) >= since) && (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id || head !== null && row.branch === slice.branch && row.head === head));
|
|
76567
|
+
}
|
|
76568
|
+
function detectLanding(ctx, slice, input) {
|
|
76569
|
+
const out = {
|
|
76570
|
+
landed: false,
|
|
76571
|
+
how: null,
|
|
76572
|
+
pr: null,
|
|
76573
|
+
landedHead: null,
|
|
76574
|
+
forgeError: null,
|
|
76575
|
+
note: null
|
|
76576
|
+
};
|
|
76577
|
+
const landed = (how, landedHead) => {
|
|
76578
|
+
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
76579
|
+
return { ...out, landed: true, how, landedHead };
|
|
76580
|
+
return {
|
|
76581
|
+
...out,
|
|
76582
|
+
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
76583
|
+
};
|
|
76584
|
+
};
|
|
76585
|
+
const { head } = input;
|
|
76586
|
+
if (ctx.forge) {
|
|
76587
|
+
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
76588
|
+
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
76589
|
+
projectId: ctx.forge.headProjectId,
|
|
76590
|
+
sha: head
|
|
76591
|
+
});
|
|
76592
|
+
if (!found.ok)
|
|
76593
|
+
out.forgeError = found.error;
|
|
76594
|
+
else if (found.data) {
|
|
76595
|
+
out.pr = found.data;
|
|
76596
|
+
if (found.data.state === "merged") {
|
|
76597
|
+
const merged = found.data.headSha;
|
|
76598
|
+
const label = `#${found.data.number}`;
|
|
76599
|
+
if (!head) {
|
|
76600
|
+
if (!linked(input.rows, input, slice, merged)) {
|
|
76601
|
+
out.pr = null;
|
|
76602
|
+
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
76603
|
+
return out;
|
|
76604
|
+
}
|
|
76605
|
+
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
76606
|
+
}
|
|
76607
|
+
if (!merged || head === merged || isAncestor2(ctx.cwd, head, merged))
|
|
76608
|
+
return landed("pr_merged", merged ?? head);
|
|
76609
|
+
out.note = isAncestor2(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
|
|
76610
|
+
return out;
|
|
76611
|
+
}
|
|
76612
|
+
}
|
|
76613
|
+
}
|
|
76614
|
+
if (!head)
|
|
76615
|
+
return out;
|
|
76616
|
+
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
76617
|
+
if (fork === head) {
|
|
76618
|
+
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
76619
|
+
}
|
|
76620
|
+
if (!fork)
|
|
76621
|
+
return out;
|
|
76622
|
+
const trunkIds = trunkPatchIds(ctx, fork);
|
|
76623
|
+
if (trunkIds.size === 0)
|
|
76624
|
+
return out;
|
|
76625
|
+
const froms = [fork];
|
|
76626
|
+
if (input.parentHead && input.parentHead !== head && isAncestor2(ctx.cwd, input.parentHead, head))
|
|
76627
|
+
froms.push(input.parentHead);
|
|
76628
|
+
for (const from of froms) {
|
|
76629
|
+
const id = patchId(ctx.cwd, from, head);
|
|
76630
|
+
if (id && trunkIds.has(id))
|
|
76631
|
+
return landed("squash_patch", head);
|
|
76632
|
+
}
|
|
76633
|
+
return out;
|
|
76634
|
+
}
|
|
76635
|
+
function sliceStart(cwd, rows, fanout, slice) {
|
|
76636
|
+
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
76637
|
+
if (reflog)
|
|
76638
|
+
return reflog;
|
|
76639
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string");
|
|
76640
|
+
return row?.baseSha ?? null;
|
|
76641
|
+
}
|
|
76642
|
+
var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
76643
|
+
var init_fanout_landed = __esm(() => {
|
|
76644
|
+
init_rev();
|
|
76645
|
+
init_fanout();
|
|
76646
|
+
});
|
|
76647
|
+
|
|
76355
76648
|
// packages/workit-core/src/fanout-check.ts
|
|
76356
76649
|
function mergeConflicts(cwd, a, b) {
|
|
76357
76650
|
const run = gitRun4(cwd, [
|
|
@@ -76385,10 +76678,15 @@ function checkFanout(cwd, plan, options = {}) {
|
|
|
76385
76678
|
const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
|
|
76386
76679
|
const ledger = readLedger(cwd);
|
|
76387
76680
|
const rows = ledger.ok ? ledger.value.rows : [];
|
|
76681
|
+
const breaker = forgeBreaker(options.forge ?? null);
|
|
76682
|
+
const landings = sliceLandings(cwd, plan, trunk, tips, rows, breaker.forge);
|
|
76683
|
+
const trunkCase = caseIndex(nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", trunk]).stdout));
|
|
76388
76684
|
const checks = new Map;
|
|
76389
76685
|
const changedBy = new Map;
|
|
76390
|
-
for (const slice of plan.slices)
|
|
76391
|
-
|
|
76686
|
+
for (const slice of plan.slices) {
|
|
76687
|
+
const landing = landings.get(slice.id);
|
|
76688
|
+
checks.set(slice.id, landing.landed ? landedCheck(slice, tips.get(slice.id) ?? null, landing) : inspect(cwd, plan, slice, { trunk, trunkCase, tips, landings, rows }, changedBy));
|
|
76689
|
+
}
|
|
76392
76690
|
const conflicts = [];
|
|
76393
76691
|
for (let i = 0;i < order.length; i += 1)
|
|
76394
76692
|
for (let j = i + 1;j < order.length; j += 1) {
|
|
@@ -76396,7 +76694,9 @@ function checkFanout(cwd, plan, options = {}) {
|
|
|
76396
76694
|
const second = order[j];
|
|
76397
76695
|
const a = tips.get(first);
|
|
76398
76696
|
const b = tips.get(second);
|
|
76399
|
-
if (
|
|
76697
|
+
if (landings.get(first)?.landed || landings.get(second)?.landed)
|
|
76698
|
+
continue;
|
|
76699
|
+
if (!a || !b || isAncestor3(cwd, a, b) || isAncestor3(cwd, b, a))
|
|
76400
76700
|
continue;
|
|
76401
76701
|
const check = checks.get(second);
|
|
76402
76702
|
const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
|
|
@@ -76434,19 +76734,29 @@ function checkFanout(cwd, plan, options = {}) {
|
|
|
76434
76734
|
}
|
|
76435
76735
|
for (const id of order) {
|
|
76436
76736
|
const check = checks.get(id);
|
|
76437
|
-
|
|
76737
|
+
if (check.status === "landed")
|
|
76738
|
+
continue;
|
|
76739
|
+
check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready" && checks.get(dep)?.status !== "landed");
|
|
76438
76740
|
if (check.waitsFor.length)
|
|
76439
76741
|
check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
|
|
76440
76742
|
check.status = check.reasons.length ? "blocked" : "ready";
|
|
76441
76743
|
}
|
|
76442
76744
|
const selected = options.only?.length ? new Set(options.only) : null;
|
|
76443
76745
|
const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
|
|
76444
|
-
const ready = slices.every((check) => check.status
|
|
76746
|
+
const ready = slices.every((check) => check.status !== "blocked");
|
|
76445
76747
|
const blocked = slices.find((check) => check.status === "blocked");
|
|
76446
76748
|
const notes = plan.v === FANOUT_VERSION ? [] : [
|
|
76447
76749
|
`plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
|
|
76448
76750
|
];
|
|
76449
|
-
const
|
|
76751
|
+
const forgeErrors = [...landings.values()].flatMap((landing) => landing.forgeError ? [landing.forgeError] : []);
|
|
76752
|
+
if (!options.forge)
|
|
76753
|
+
notes.push("landed slices detected with git only (no forge): a squash merge may be missed");
|
|
76754
|
+
else if (breaker.down())
|
|
76755
|
+
notes.push(`forge went down (${breaker.down()}); git alone decided the remaining slices`);
|
|
76756
|
+
else if (forgeErrors.length)
|
|
76757
|
+
notes.push(`forge lookups failed (${forgeErrors[0]}); git alone decided those slices`);
|
|
76758
|
+
const toLand = slices.filter((check) => check.status === "ready").map((check) => check.id);
|
|
76759
|
+
const next = blocked ? unblockFor(blocked, plan) : toLand.length ? `land in this order: ${toLand.join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)` : "every slice has landed";
|
|
76450
76760
|
return {
|
|
76451
76761
|
ok: true,
|
|
76452
76762
|
data: {
|
|
@@ -76462,28 +76772,61 @@ function checkFanout(cwd, plan, options = {}) {
|
|
|
76462
76772
|
}
|
|
76463
76773
|
};
|
|
76464
76774
|
}
|
|
76465
|
-
function
|
|
76466
|
-
const
|
|
76467
|
-
const
|
|
76468
|
-
|
|
76469
|
-
|
|
76470
|
-
|
|
76471
|
-
|
|
76472
|
-
|
|
76473
|
-
|
|
76474
|
-
|
|
76475
|
-
|
|
76476
|
-
|
|
76477
|
-
|
|
76478
|
-
|
|
76479
|
-
|
|
76775
|
+
function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
|
|
76776
|
+
const ctx = { cwd, trunk, forge };
|
|
76777
|
+
const out = new Map;
|
|
76778
|
+
const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
|
|
76779
|
+
for (const id of landingOrder(plan.slices)) {
|
|
76780
|
+
const slice = byId.get(id);
|
|
76781
|
+
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
76782
|
+
const parentHead = parent ? tips.get(parent.id) ?? out.get(parent.id)?.landedHead ?? null : null;
|
|
76783
|
+
out.set(id, detectLanding(ctx, slice, {
|
|
76784
|
+
head: tips.get(id) ?? null,
|
|
76785
|
+
createdFrom: sliceStart(cwd, rows, plan.name, slice),
|
|
76786
|
+
parentHead,
|
|
76787
|
+
fanout: plan.name,
|
|
76788
|
+
since: plan.createdAt || null,
|
|
76789
|
+
rows
|
|
76790
|
+
}));
|
|
76791
|
+
}
|
|
76792
|
+
return out;
|
|
76793
|
+
}
|
|
76794
|
+
function landedCheck(slice, head, landing) {
|
|
76795
|
+
return {
|
|
76796
|
+
...blank2(slice, head),
|
|
76797
|
+
status: "landed",
|
|
76798
|
+
landed: { how: landing.how ?? "pr_merged", pr: landing.pr?.number ?? null }
|
|
76480
76799
|
};
|
|
76800
|
+
}
|
|
76801
|
+
function trunkCaseCollisions(cwd, ctx, head, changed) {
|
|
76802
|
+
const headFiles = treeFiles(cwd, head);
|
|
76803
|
+
const forkFiles = treeFiles(cwd, gitRun4(cwd, ["merge-base", head, ctx.trunk]).stdout.trim() || null);
|
|
76804
|
+
if (!headFiles)
|
|
76805
|
+
return [];
|
|
76806
|
+
const present = (files, prefix) => Boolean(files && (files.has(prefix) || [...files].some((file) => file.startsWith(`${prefix}/`))));
|
|
76807
|
+
const out = new Set;
|
|
76808
|
+
for (const file of changed) {
|
|
76809
|
+
if (!headFiles.has(file))
|
|
76810
|
+
continue;
|
|
76811
|
+
const clash = caseClash(ctx.trunkCase, file);
|
|
76812
|
+
if (!clash)
|
|
76813
|
+
continue;
|
|
76814
|
+
if (present(forkFiles, clash.trunk) && !present(headFiles, clash.trunk))
|
|
76815
|
+
continue;
|
|
76816
|
+
out.add(`${clash.path} ~ ${clash.trunk}`);
|
|
76817
|
+
}
|
|
76818
|
+
return [...out].toSorted();
|
|
76819
|
+
}
|
|
76820
|
+
function inspect(cwd, plan, slice, ctx, changedBy) {
|
|
76821
|
+
const { trunk, tips, rows } = ctx;
|
|
76822
|
+
const head = tips.get(slice.id) ?? null;
|
|
76823
|
+
const check = blank2(slice, head);
|
|
76481
76824
|
if (!head) {
|
|
76482
|
-
check.reasons.push(`branch ${slice.branch} not found (not started,
|
|
76825
|
+
check.reasons.push(`branch ${slice.branch} not found (not started, or not fetched)`);
|
|
76483
76826
|
return check;
|
|
76484
76827
|
}
|
|
76485
76828
|
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
76486
|
-
const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
|
|
76829
|
+
const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : branchRef(cwd, slice.base);
|
|
76487
76830
|
if (!SAFE(baseRef)) {
|
|
76488
76831
|
check.reasons.push(`base ${slice.base} not found`);
|
|
76489
76832
|
return check;
|
|
@@ -76501,6 +76844,9 @@ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
|
|
|
76501
76844
|
check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
|
|
76502
76845
|
if (check.outOfScope.length)
|
|
76503
76846
|
check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
|
|
76847
|
+
check.caseCollisions = trunkCaseCollisions(cwd, ctx, head, changed);
|
|
76848
|
+
if (check.caseCollisions.length)
|
|
76849
|
+
check.reasons.push(`differs only in case from ${plan.trunk}: ${check.caseCollisions.join(", ")}`);
|
|
76504
76850
|
const trunkResult = mergeConflicts(cwd, trunk, head);
|
|
76505
76851
|
if (Array.isArray(trunkResult)) {
|
|
76506
76852
|
check.trunkConflicts = trunkResult;
|
|
@@ -76514,9 +76860,11 @@ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
|
|
|
76514
76860
|
}
|
|
76515
76861
|
function unblockFor(check, plan) {
|
|
76516
76862
|
if (!check.head)
|
|
76517
|
-
return `start or fetch ${check.branch}, then workit fanout check
|
|
76863
|
+
return `start or fetch ${check.branch}, then workit fanout check (a landed slice is detected through its merged PR when the forge is reachable)`;
|
|
76518
76864
|
if (check.outOfScope.length)
|
|
76519
76865
|
return `move ${check.outOfScope.join(", ")} off ${check.branch} (a follow-up slice), or widen slice ${check.id}'s scope with workit fanout plan, then workit fanout check`;
|
|
76866
|
+
if (check.caseCollisions.length)
|
|
76867
|
+
return `rename to ${plan.trunk}'s spelling on ${check.branch} (git mv), then workit fanout check`;
|
|
76520
76868
|
if (check.trunkConflicts.length)
|
|
76521
76869
|
return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
|
|
76522
76870
|
if (check.siblingConflicts.length) {
|
|
@@ -76527,10 +76875,552 @@ function unblockFor(check, plan) {
|
|
|
76527
76875
|
return `unblock ${check.waitsFor.join(", ")} first`;
|
|
76528
76876
|
return check.reasons[0] ?? "workit fanout check";
|
|
76529
76877
|
}
|
|
76530
|
-
var SAFE = (value) => Boolean(value) && !value?.startsWith("-"),
|
|
76878
|
+
var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
|
|
76879
|
+
id: slice.id,
|
|
76880
|
+
branch: slice.branch,
|
|
76881
|
+
head,
|
|
76882
|
+
base: slice.base,
|
|
76883
|
+
status: "blocked",
|
|
76884
|
+
changed: 0,
|
|
76885
|
+
outOfScope: [],
|
|
76886
|
+
caseCollisions: [],
|
|
76887
|
+
trunkConflicts: [],
|
|
76888
|
+
siblingConflicts: [],
|
|
76889
|
+
waitsFor: [],
|
|
76890
|
+
verdict: { accepted: false, basis: "missing" },
|
|
76891
|
+
landed: null,
|
|
76892
|
+
reasons: []
|
|
76893
|
+
}), treeFiles = (cwd, rev) => {
|
|
76894
|
+
if (!rev)
|
|
76895
|
+
return null;
|
|
76896
|
+
const run = gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", rev]);
|
|
76897
|
+
return run.ok ? new Set(nulList2(run.stdout)) : null;
|
|
76898
|
+
};
|
|
76531
76899
|
var init_fanout_check = __esm(() => {
|
|
76532
76900
|
init_ledger();
|
|
76533
76901
|
init_fanout();
|
|
76902
|
+
init_fanout_landed();
|
|
76903
|
+
});
|
|
76904
|
+
|
|
76905
|
+
// packages/workit-core/src/fanout-status.ts
|
|
76906
|
+
function timeboxMs(text) {
|
|
76907
|
+
if (!text || /^\s*\d+(?:\.\d+)?\s*$/u.test(text))
|
|
76908
|
+
return null;
|
|
76909
|
+
const ms = parseDuration2(text);
|
|
76910
|
+
return ms && ms > 0 ? ms : null;
|
|
76911
|
+
}
|
|
76912
|
+
function activity(cwd, plan, slice, rows) {
|
|
76913
|
+
const head = branchRef(cwd, slice.branch);
|
|
76914
|
+
const commit = head ? isoOf(gitRun4(cwd, ["log", "-1", "--format=%ct", head, "--"]).stdout) : null;
|
|
76915
|
+
const moved = /@\{(\d+)\}/u.exec(gitRun4(cwd, [
|
|
76916
|
+
"log",
|
|
76917
|
+
"-g",
|
|
76918
|
+
"-1",
|
|
76919
|
+
"--date=unix",
|
|
76920
|
+
"--format=%gd",
|
|
76921
|
+
`refs/heads/${slice.branch}`,
|
|
76922
|
+
"--"
|
|
76923
|
+
]).stdout);
|
|
76924
|
+
const reflog = moved ? isoOf(moved[1]) : null;
|
|
76925
|
+
const ledger = rows.filter((row) => row.branch === slice.branch || row.fanout === plan.name && row.slice === slice.id).map((row) => row.at);
|
|
76926
|
+
return {
|
|
76927
|
+
head,
|
|
76928
|
+
commit,
|
|
76929
|
+
last: newest(commit, reflog, ...ledger),
|
|
76930
|
+
worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id)
|
|
76931
|
+
};
|
|
76932
|
+
}
|
|
76933
|
+
function fanoutStatus(cwd, plan, options) {
|
|
76934
|
+
const now = options.now ?? new Date;
|
|
76935
|
+
const breaker = forgeBreaker(options.forge);
|
|
76936
|
+
const forge = breaker.forge;
|
|
76937
|
+
const trunk = trunkRef(cwd, plan.trunk);
|
|
76938
|
+
if (!trunk)
|
|
76939
|
+
return fanoutFail("unavailable", `trunk ${plan.trunk} does not resolve here`, `git fetch origin ${plan.trunk}`);
|
|
76940
|
+
const ledger = readLedger(cwd);
|
|
76941
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
76942
|
+
const notes = [];
|
|
76943
|
+
if (!ledger.ok)
|
|
76944
|
+
notes.push(`ledger unreadable (${ledger.error}); verdicts read as missing`);
|
|
76945
|
+
if (!forge)
|
|
76946
|
+
notes.push(`forge unavailable${options.forgeError ? ` (${options.forgeError})` : ""}: PR and CI not shown; landed detection uses git only, so a squash merge may be missed`);
|
|
76947
|
+
const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
|
|
76948
|
+
const landings = sliceLandings(cwd, plan, trunk, tips, rows, forge);
|
|
76949
|
+
const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
|
|
76950
|
+
const order = landingOrder(plan.slices);
|
|
76951
|
+
const out = new Map;
|
|
76952
|
+
for (const id of order) {
|
|
76953
|
+
const slice = byId.get(id);
|
|
76954
|
+
const landing = landings.get(id);
|
|
76955
|
+
const seen = activity(cwd, plan, slice, rows);
|
|
76956
|
+
const threshold = options.stuckAfterMs ?? timeboxMs(slice.timebox) ?? DEFAULT_STUCK_MS;
|
|
76957
|
+
const idle = seen.last ? Math.max(0, now.getTime() - Date.parse(seen.last)) : null;
|
|
76958
|
+
const verdict = seen.head ? checkVerdicts(cwd, slice.branch, rows) : null;
|
|
76959
|
+
const row = {
|
|
76960
|
+
id,
|
|
76961
|
+
branch: slice.branch,
|
|
76962
|
+
base: slice.base,
|
|
76963
|
+
worktree: slice.worktree,
|
|
76964
|
+
dependsOn: slice.dependsOn,
|
|
76965
|
+
branchExists: seen.head !== null,
|
|
76966
|
+
head: seen.head,
|
|
76967
|
+
lastCommitAt: seen.commit,
|
|
76968
|
+
lastActivityAt: seen.last,
|
|
76969
|
+
idleMinutes: idle === null ? null : Math.floor(idle / 60000),
|
|
76970
|
+
stuckAfterMinutes: Math.round(threshold / 60000),
|
|
76971
|
+
pr: landing?.pr ? { number: landing.pr.number, url: landing.pr.url, state: landing.pr.state } : null,
|
|
76972
|
+
ci: "unknown",
|
|
76973
|
+
verdict: {
|
|
76974
|
+
accepted: verdict?.accepted.accepted ?? false,
|
|
76975
|
+
basis: verdict?.current.basis ?? "none",
|
|
76976
|
+
review: verdict?.review ?? "unreviewed"
|
|
76977
|
+
},
|
|
76978
|
+
landed: landing?.landed && landing.how ? { how: landing.how, pr: landing.pr?.number ?? null } : null,
|
|
76979
|
+
stuck: false,
|
|
76980
|
+
waitsFor: [],
|
|
76981
|
+
state: "not_started",
|
|
76982
|
+
reasons: [],
|
|
76983
|
+
notes: [
|
|
76984
|
+
...landing?.note ? [landing.note] : [],
|
|
76985
|
+
...landing?.forgeError ? [`forge: ${landing.forgeError}`] : []
|
|
76986
|
+
]
|
|
76987
|
+
};
|
|
76988
|
+
const answered = forge !== null && !landing?.forgeError;
|
|
76989
|
+
if (answered && landing?.pr && landing.pr.state === "open" && !row.landed) {
|
|
76990
|
+
const status = forge.forge.prStatus(landing.pr.number);
|
|
76991
|
+
if (status.ok)
|
|
76992
|
+
row.ci = checksState(gatingChecks(status.data).checks, missingRequired(status.data));
|
|
76993
|
+
else
|
|
76994
|
+
row.notes.push(`forge: ${status.error}`);
|
|
76995
|
+
}
|
|
76996
|
+
row.waitsFor = slice.dependsOn.filter((dep) => !out.get(dep)?.landed);
|
|
76997
|
+
out.set(id, row);
|
|
76998
|
+
if (row.landed) {
|
|
76999
|
+
row.state = "landed";
|
|
77000
|
+
continue;
|
|
77001
|
+
}
|
|
77002
|
+
const started = row.branchExists || seen.worktreeCreated;
|
|
77003
|
+
if (!row.branchExists)
|
|
77004
|
+
row.reasons.push("no branch yet");
|
|
77005
|
+
if (!row.verdict.accepted)
|
|
77006
|
+
row.reasons.push(`verdict ${row.verdict.basis === "none" ? "missing" : row.verdict.basis} on the current head`);
|
|
77007
|
+
if (answered && row.branchExists) {
|
|
77008
|
+
if (!row.pr)
|
|
77009
|
+
row.reasons.push("no PR");
|
|
77010
|
+
else if (row.pr.state !== "open")
|
|
77011
|
+
row.reasons.push(`PR ${row.pr.state}`);
|
|
77012
|
+
else if (row.ci === "failing" || row.ci === "pending")
|
|
77013
|
+
row.reasons.push(`CI ${row.ci}`);
|
|
77014
|
+
}
|
|
77015
|
+
row.stuck = started && !row.verdict.accepted && idle !== null && idle > threshold;
|
|
77016
|
+
if (row.stuck)
|
|
77017
|
+
row.reasons.push(`no activity for ${row.idleMinutes} min`);
|
|
77018
|
+
}
|
|
77019
|
+
const suggested = [];
|
|
77020
|
+
for (const id of order) {
|
|
77021
|
+
const row = out.get(id);
|
|
77022
|
+
if (row.landed || row.reasons.length)
|
|
77023
|
+
continue;
|
|
77024
|
+
if (row.waitsFor.every((dep) => suggested.includes(dep)))
|
|
77025
|
+
suggested.push(id);
|
|
77026
|
+
}
|
|
77027
|
+
const spawnable = [];
|
|
77028
|
+
for (const id of order) {
|
|
77029
|
+
const row = out.get(id);
|
|
77030
|
+
if (row.landed)
|
|
77031
|
+
continue;
|
|
77032
|
+
const parent = plan.slices.find((other) => other.branch === row.base);
|
|
77033
|
+
if (suggested.includes(id))
|
|
77034
|
+
row.state = "ready";
|
|
77035
|
+
else if (row.stuck)
|
|
77036
|
+
row.state = "stuck";
|
|
77037
|
+
else if (row.branchExists)
|
|
77038
|
+
row.state = "active";
|
|
77039
|
+
else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
|
|
77040
|
+
const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
|
|
77041
|
+
row.state = created ? "active" : "not_started";
|
|
77042
|
+
if (!created)
|
|
77043
|
+
spawnable.push(id);
|
|
77044
|
+
} else
|
|
77045
|
+
row.state = "waiting";
|
|
77046
|
+
if (row.waitsFor.length && !suggested.includes(id))
|
|
77047
|
+
row.reasons.push(`waits for ${row.waitsFor.join(", ")}`);
|
|
77048
|
+
}
|
|
77049
|
+
const stuck = order.filter((id) => out.get(id)?.stuck);
|
|
77050
|
+
const down = breaker.down();
|
|
77051
|
+
if (down)
|
|
77052
|
+
notes.push(`forge went down during this run (${down}): later slices show no PR or CI, and git alone decided whether they landed`);
|
|
77053
|
+
const online = forge !== null && !down;
|
|
77054
|
+
const noPr = order.filter((id) => {
|
|
77055
|
+
const row = out.get(id);
|
|
77056
|
+
return !row.landed && row.reasons.length === 1 && row.reasons[0] === "no PR";
|
|
77057
|
+
});
|
|
77058
|
+
const next = stuck.length ? `replace ${stuck.join(", ")}: stop the worker, then workit fanout worktree release ${stuck[0]} (records git status first) and respawn it in MODE: resume` : suggested.length ? online ? `land in this order: ${suggested.join(", ")} (workit fanout check first; workit pr merge each)` : `land in this order: ${suggested.join(", ")} once each has a PR (offline: PRs unchecked; open a missing one with workit git push && workit pr create --fill, then workit fanout status online)` : noPr.length ? `open PRs for ${noPr.join(", ")}: workit git push && workit pr create --fill on each branch` : spawnable.length ? `spawn ${spawnable.join(", ")} (workit fanout worktree create <slice> on hosts without native worktrees)` : order.every((id) => out.get(id)?.landed) ? "every slice has landed" : "wait for workers, then workit fanout status again";
|
|
77059
|
+
return {
|
|
77060
|
+
ok: true,
|
|
77061
|
+
data: {
|
|
77062
|
+
name: plan.name,
|
|
77063
|
+
trunk: plan.trunk,
|
|
77064
|
+
trunkRef: trunk,
|
|
77065
|
+
forge: {
|
|
77066
|
+
available: online,
|
|
77067
|
+
error: online ? null : down ?? options.forgeError ?? null
|
|
77068
|
+
},
|
|
77069
|
+
slices: order.map((id) => out.get(id)),
|
|
77070
|
+
landingOrder: suggested,
|
|
77071
|
+
spawnable,
|
|
77072
|
+
stuck,
|
|
77073
|
+
next,
|
|
77074
|
+
notes
|
|
77075
|
+
}
|
|
77076
|
+
};
|
|
77077
|
+
}
|
|
77078
|
+
var DEFAULT_STUCK_MS, isoOf = (seconds) => {
|
|
77079
|
+
const value = Number(seconds.trim());
|
|
77080
|
+
return Number.isFinite(value) && value > 0 ? new Date(value * 1000).toISOString() : null;
|
|
77081
|
+
}, newest = (...values) => values.filter((value) => Boolean(value) && !Number.isNaN(Date.parse(value ?? ""))).reduce((best, value) => best === null || Date.parse(value) > Date.parse(best) ? value : best, null);
|
|
77082
|
+
var init_fanout_status = __esm(() => {
|
|
77083
|
+
init_duration();
|
|
77084
|
+
init_report();
|
|
77085
|
+
init_ledger();
|
|
77086
|
+
init_fanout_check();
|
|
77087
|
+
init_fanout_landed();
|
|
77088
|
+
init_fanout();
|
|
77089
|
+
DEFAULT_STUCK_MS = 30 * 60000;
|
|
77090
|
+
});
|
|
77091
|
+
|
|
77092
|
+
// packages/workit-core/src/fanout-worktree.ts
|
|
77093
|
+
import fs24 from "node:fs";
|
|
77094
|
+
import path44 from "node:path";
|
|
77095
|
+
function rowSample(lines) {
|
|
77096
|
+
const out = [];
|
|
77097
|
+
let bytes = 0;
|
|
77098
|
+
for (const line of lines) {
|
|
77099
|
+
const entry = line.slice(0, 200);
|
|
77100
|
+
bytes += Buffer.byteLength(entry) + 3;
|
|
77101
|
+
if (bytes > DIRTY_ROW_BYTES)
|
|
77102
|
+
break;
|
|
77103
|
+
out.push(entry);
|
|
77104
|
+
}
|
|
77105
|
+
return out;
|
|
77106
|
+
}
|
|
77107
|
+
function listWorktrees(cwd) {
|
|
77108
|
+
const out = [];
|
|
77109
|
+
let current = null;
|
|
77110
|
+
for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
|
|
77111
|
+
if (field.startsWith("worktree ")) {
|
|
77112
|
+
current = { path: field.slice(9), head: null, branch: null, locked: false };
|
|
77113
|
+
out.push(current);
|
|
77114
|
+
} else if (!current)
|
|
77115
|
+
continue;
|
|
77116
|
+
else if (field.startsWith("HEAD "))
|
|
77117
|
+
current.head = field.slice(5);
|
|
77118
|
+
else if (field.startsWith("branch "))
|
|
77119
|
+
current.branch = field.slice(7);
|
|
77120
|
+
else if (field === "locked" || field.startsWith("locked "))
|
|
77121
|
+
current.locked = true;
|
|
77122
|
+
}
|
|
77123
|
+
return out;
|
|
77124
|
+
}
|
|
77125
|
+
function createdRow(cwd, plan, slice, target) {
|
|
77126
|
+
const ledger = readLedger(cwd);
|
|
77127
|
+
if (!ledger.ok)
|
|
77128
|
+
return null;
|
|
77129
|
+
const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
|
|
77130
|
+
const rows = ledger.value.rows;
|
|
77131
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
|
|
77132
|
+
if (!row)
|
|
77133
|
+
return null;
|
|
77134
|
+
const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
|
|
77135
|
+
if (removedSince)
|
|
77136
|
+
return null;
|
|
77137
|
+
const made = adminDirTime(target);
|
|
77138
|
+
if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
|
|
77139
|
+
return null;
|
|
77140
|
+
return row;
|
|
77141
|
+
}
|
|
77142
|
+
function adminDirTime(worktree) {
|
|
77143
|
+
const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
|
|
77144
|
+
if (!dir.ok)
|
|
77145
|
+
return null;
|
|
77146
|
+
try {
|
|
77147
|
+
const stat = fs24.statSync(dir.stdout.trim());
|
|
77148
|
+
return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
|
|
77149
|
+
} catch {
|
|
77150
|
+
return null;
|
|
77151
|
+
}
|
|
77152
|
+
}
|
|
77153
|
+
function findSlice(plan, id) {
|
|
77154
|
+
const slice = plan.slices.find((candidate) => candidate.id === id);
|
|
77155
|
+
return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
|
|
77156
|
+
}
|
|
77157
|
+
function excludeScratch(cwd) {
|
|
77158
|
+
const common = gitCommonDir(cwd);
|
|
77159
|
+
if (!common)
|
|
77160
|
+
return;
|
|
77161
|
+
const file = path44.join(common, "info", "exclude");
|
|
77162
|
+
let text = "";
|
|
77163
|
+
try {
|
|
77164
|
+
text = fs24.readFileSync(file, "utf8");
|
|
77165
|
+
} catch {
|
|
77166
|
+
text = "";
|
|
77167
|
+
}
|
|
77168
|
+
if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
|
|
77169
|
+
return;
|
|
77170
|
+
fs24.mkdirSync(path44.dirname(file), { recursive: true });
|
|
77171
|
+
fs24.appendFileSync(file, `${text && !text.endsWith(`
|
|
77172
|
+
`) ? `
|
|
77173
|
+
` : ""}# workit fanout: per-worker scratch dirs
|
|
77174
|
+
${EXCLUDE_LINE}
|
|
77175
|
+
`);
|
|
77176
|
+
}
|
|
77177
|
+
function ensureScratch(worktree) {
|
|
77178
|
+
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77179
|
+
fs24.mkdirSync(scratch, { recursive: true });
|
|
77180
|
+
return scratch;
|
|
77181
|
+
}
|
|
77182
|
+
function createSliceWorktree(cwd, plan, input) {
|
|
77183
|
+
const found = findSlice(plan, input.slice);
|
|
77184
|
+
if (!found.ok)
|
|
77185
|
+
return found;
|
|
77186
|
+
const slice = found.data;
|
|
77187
|
+
if (!slice.worktree)
|
|
77188
|
+
return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
|
|
77189
|
+
const target = slicePath(cwd, slice);
|
|
77190
|
+
const worktrees = listWorktrees(cwd);
|
|
77191
|
+
const localRef = `refs/heads/${slice.branch}`;
|
|
77192
|
+
const existing = worktrees.find((entry) => samePath(entry.path, target));
|
|
77193
|
+
const base = {
|
|
77194
|
+
fanout: plan.name,
|
|
77195
|
+
slice: slice.id,
|
|
77196
|
+
branch: slice.branch,
|
|
77197
|
+
base: slice.base,
|
|
77198
|
+
path: target
|
|
77199
|
+
};
|
|
77200
|
+
if (existing) {
|
|
77201
|
+
if (existing.branch !== null && existing.branch !== localRef)
|
|
77202
|
+
return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
|
|
77203
|
+
if (!createdRow(cwd, plan, slice, target))
|
|
77204
|
+
return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
|
|
77205
|
+
excludeScratch(cwd);
|
|
77206
|
+
return {
|
|
77207
|
+
ok: true,
|
|
77208
|
+
data: {
|
|
77209
|
+
...base,
|
|
77210
|
+
scratch: ensureScratch(target),
|
|
77211
|
+
head: existing.head ?? "",
|
|
77212
|
+
mode: resolveRef(cwd, localRef) ? "resume" : "new",
|
|
77213
|
+
created: false,
|
|
77214
|
+
notes: ["the worktree already exists; nothing was changed"]
|
|
77215
|
+
}
|
|
77216
|
+
};
|
|
77217
|
+
}
|
|
77218
|
+
const elsewhere = worktrees.find((entry) => entry.branch === localRef);
|
|
77219
|
+
if (elsewhere)
|
|
77220
|
+
return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
|
|
77221
|
+
let dirExisted = false;
|
|
77222
|
+
try {
|
|
77223
|
+
const stat = fs24.statSync(target);
|
|
77224
|
+
if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
|
|
77225
|
+
return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
|
|
77226
|
+
dirExisted = true;
|
|
77227
|
+
} catch (error) {
|
|
77228
|
+
if (error.code !== "ENOENT")
|
|
77229
|
+
return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
|
|
77230
|
+
}
|
|
77231
|
+
const notes = [];
|
|
77232
|
+
let mode = "resume";
|
|
77233
|
+
if (resolveRef(cwd, localRef)) {
|
|
77234
|
+
const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
|
|
77235
|
+
if (!added.ok)
|
|
77236
|
+
return worktreeFailed(added.stderr);
|
|
77237
|
+
} else {
|
|
77238
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77239
|
+
const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
|
|
77240
|
+
if (resolveRef(cwd, remoteRef)) {
|
|
77241
|
+
const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
|
|
77242
|
+
if (!added.ok)
|
|
77243
|
+
return worktreeFailed(added.stderr);
|
|
77244
|
+
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
77245
|
+
} else {
|
|
77246
|
+
mode = "new";
|
|
77247
|
+
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
77248
|
+
const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
77249
|
+
if (!from)
|
|
77250
|
+
return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
|
|
77251
|
+
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
77252
|
+
if (!added.ok)
|
|
77253
|
+
return worktreeFailed(added.stderr);
|
|
77254
|
+
const branched = gitBranch(target, { name: slice.branch, base: slice.base });
|
|
77255
|
+
if (!branched.ok) {
|
|
77256
|
+
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
77257
|
+
return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
|
|
77258
|
+
}
|
|
77259
|
+
notes.push(...branched.data.notes);
|
|
77260
|
+
}
|
|
77261
|
+
}
|
|
77262
|
+
excludeScratch(cwd);
|
|
77263
|
+
const scratch = ensureScratch(target);
|
|
77264
|
+
const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
|
|
77265
|
+
const baseSha = mode === "new" ? head : null;
|
|
77266
|
+
appendObserved(cwd, {
|
|
77267
|
+
type: "fanout.worktree.created",
|
|
77268
|
+
actor: input.actor,
|
|
77269
|
+
fanout: plan.name,
|
|
77270
|
+
slice: slice.id,
|
|
77271
|
+
branch: slice.branch,
|
|
77272
|
+
path: target,
|
|
77273
|
+
scratch,
|
|
77274
|
+
mode,
|
|
77275
|
+
head,
|
|
77276
|
+
dirExisted,
|
|
77277
|
+
planCreatedAt: plan.createdAt,
|
|
77278
|
+
...baseSha ? { baseSha } : {}
|
|
77279
|
+
});
|
|
77280
|
+
return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
|
|
77281
|
+
}
|
|
77282
|
+
function releaseSliceWorktree(cwd, plan, input) {
|
|
77283
|
+
const found = findSlice(plan, input.slice);
|
|
77284
|
+
if (!found.ok)
|
|
77285
|
+
return found;
|
|
77286
|
+
const slice = found.data;
|
|
77287
|
+
const target = slicePath(cwd, slice);
|
|
77288
|
+
const worktrees = listWorktrees(cwd);
|
|
77289
|
+
const index = worktrees.findIndex((entry) => samePath(entry.path, target));
|
|
77290
|
+
const out = {
|
|
77291
|
+
fanout: plan.name,
|
|
77292
|
+
slice: slice.id,
|
|
77293
|
+
branch: slice.branch,
|
|
77294
|
+
path: target,
|
|
77295
|
+
head: null,
|
|
77296
|
+
dirty: [],
|
|
77297
|
+
forced: input.force,
|
|
77298
|
+
removed: false,
|
|
77299
|
+
unpushed: null,
|
|
77300
|
+
notes: []
|
|
77301
|
+
};
|
|
77302
|
+
if (index < 0) {
|
|
77303
|
+
if (fs24.existsSync(target))
|
|
77304
|
+
return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
|
|
77305
|
+
out.notes.push(`no worktree at ${target}; nothing to release`);
|
|
77306
|
+
return { ok: true, data: out };
|
|
77307
|
+
}
|
|
77308
|
+
const entry = worktrees[index];
|
|
77309
|
+
if (index === 0)
|
|
77310
|
+
return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
|
|
77311
|
+
if (inside2(cwd, entry.path))
|
|
77312
|
+
return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
|
|
77313
|
+
if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
|
|
77314
|
+
return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
|
|
77315
|
+
const made = createdRow(cwd, plan, slice, entry.path);
|
|
77316
|
+
if (!made)
|
|
77317
|
+
return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
|
|
77318
|
+
out.head = entry.head;
|
|
77319
|
+
const status = gitRun4(entry.path, [
|
|
77320
|
+
"status",
|
|
77321
|
+
"--porcelain=v1",
|
|
77322
|
+
"-z",
|
|
77323
|
+
"--untracked-files=all",
|
|
77324
|
+
"--ignore-submodules=none"
|
|
77325
|
+
]);
|
|
77326
|
+
if (!status.ok)
|
|
77327
|
+
return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
|
|
77328
|
+
`)[0]}`, "nothing was removed; inspect the worktree by hand");
|
|
77329
|
+
const entries = porcelainEntries(status.stdout);
|
|
77330
|
+
const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
|
|
77331
|
+
out.dirty = entries.filter((line) => !inScratch(line));
|
|
77332
|
+
out.unpushed = unpushedCount(cwd, slice.branch);
|
|
77333
|
+
const refused = out.dirty.length > 0 && !input.force;
|
|
77334
|
+
const recorded = appendObserved(cwd, {
|
|
77335
|
+
type: "fanout.worktree.released",
|
|
77336
|
+
actor: input.actor,
|
|
77337
|
+
fanout: plan.name,
|
|
77338
|
+
slice: slice.id,
|
|
77339
|
+
branch: slice.branch,
|
|
77340
|
+
path: entry.path,
|
|
77341
|
+
head: entry.head,
|
|
77342
|
+
uncommittedCount: out.dirty.length,
|
|
77343
|
+
uncommitted: rowSample(out.dirty),
|
|
77344
|
+
forced: input.force,
|
|
77345
|
+
outcome: refused ? "refused" : "removed"
|
|
77346
|
+
});
|
|
77347
|
+
if (!recorded.ok)
|
|
77348
|
+
return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
|
|
77349
|
+
if (refused)
|
|
77350
|
+
return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
|
|
77351
|
+
const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
|
|
77352
|
+
let removed = gitRun4(cwd, args, SLOW);
|
|
77353
|
+
if (!removed.ok && entries.some(inScratch)) {
|
|
77354
|
+
removeScratch(entry.path);
|
|
77355
|
+
removed = gitRun4(cwd, args, SLOW);
|
|
77356
|
+
}
|
|
77357
|
+
if (!removed.ok)
|
|
77358
|
+
return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
|
|
77359
|
+
`)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
|
|
77360
|
+
out.removed = true;
|
|
77361
|
+
if (made.dirExisted === true)
|
|
77362
|
+
fs24.mkdirSync(entry.path, { recursive: true });
|
|
77363
|
+
if (out.unpushed)
|
|
77364
|
+
out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
|
|
77365
|
+
return { ok: true, data: out };
|
|
77366
|
+
}
|
|
77367
|
+
function porcelainEntries(stdout) {
|
|
77368
|
+
const fields = nulList2(stdout);
|
|
77369
|
+
const out = [];
|
|
77370
|
+
for (let index = 0;index < fields.length; index += 1) {
|
|
77371
|
+
const entry = fields[index];
|
|
77372
|
+
out.push(entry);
|
|
77373
|
+
if (entry[0] === "R" || entry[0] === "C")
|
|
77374
|
+
index += 1;
|
|
77375
|
+
}
|
|
77376
|
+
return out;
|
|
77377
|
+
}
|
|
77378
|
+
function unpushedCount(cwd, branch) {
|
|
77379
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77380
|
+
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
77381
|
+
if (!local)
|
|
77382
|
+
return null;
|
|
77383
|
+
const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
77384
|
+
if (!tracking)
|
|
77385
|
+
return null;
|
|
77386
|
+
const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
|
|
77387
|
+
return count.ok ? Number(count.stdout.trim()) : null;
|
|
77388
|
+
}
|
|
77389
|
+
function removeScratch(worktree) {
|
|
77390
|
+
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77391
|
+
let stat;
|
|
77392
|
+
try {
|
|
77393
|
+
stat = fs24.lstatSync(scratch);
|
|
77394
|
+
} catch {
|
|
77395
|
+
return;
|
|
77396
|
+
}
|
|
77397
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
77398
|
+
fs24.unlinkSync(scratch);
|
|
77399
|
+
else
|
|
77400
|
+
fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
|
|
77401
|
+
}
|
|
77402
|
+
var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
|
|
77403
|
+
try {
|
|
77404
|
+
return fs24.realpathSync.native(target);
|
|
77405
|
+
} catch {
|
|
77406
|
+
return path44.resolve(target);
|
|
77407
|
+
}
|
|
77408
|
+
}, samePath = (a, b) => {
|
|
77409
|
+
const left = canonical2(a);
|
|
77410
|
+
const right = canonical2(b);
|
|
77411
|
+
return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
77412
|
+
}, inside2 = (child, parent) => {
|
|
77413
|
+
const relative = path44.relative(canonical2(parent), canonical2(child));
|
|
77414
|
+
return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
|
|
77415
|
+
}, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
77416
|
+
`)[0] || "unknown error"}`);
|
|
77417
|
+
var init_fanout_worktree = __esm(() => {
|
|
77418
|
+
init_ops();
|
|
77419
|
+
init_rev();
|
|
77420
|
+
init_ledger();
|
|
77421
|
+
init_fanout();
|
|
77422
|
+
SLOW = GIT_TIMEOUTS.worktree;
|
|
77423
|
+
EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
|
|
76534
77424
|
});
|
|
76535
77425
|
|
|
76536
77426
|
// packages/workit-cli/src/verbs/fanout.ts
|
|
@@ -76538,8 +77428,14 @@ var exports_fanout = {};
|
|
|
76538
77428
|
__export(exports_fanout, {
|
|
76539
77429
|
run: () => run15
|
|
76540
77430
|
});
|
|
76541
|
-
import
|
|
76542
|
-
import
|
|
77431
|
+
import fs25 from "node:fs";
|
|
77432
|
+
import path45 from "node:path";
|
|
77433
|
+
function forgeFor(io, offline) {
|
|
77434
|
+
if (offline)
|
|
77435
|
+
return { forge: null, error: "--offline" };
|
|
77436
|
+
const connected = connect(io, null);
|
|
77437
|
+
return connected.ok ? { forge: connected.data, error: null } : { forge: null, error: connected.error };
|
|
77438
|
+
}
|
|
76543
77439
|
function renderBlockedPlan(io, result) {
|
|
76544
77440
|
if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
|
|
76545
77441
|
return;
|
|
@@ -76547,6 +77443,11 @@ function renderBlockedPlan(io, result) {
|
|
|
76547
77443
|
if (Array.isArray(problems))
|
|
76548
77444
|
for (const problem of problems)
|
|
76549
77445
|
io.stderr(` - ${problem}
|
|
77446
|
+
`);
|
|
77447
|
+
const clashes = result.data?.caseCollisions;
|
|
77448
|
+
if (Array.isArray(clashes))
|
|
77449
|
+
for (const clash of clashes)
|
|
77450
|
+
io.stderr(` - ${clash.slice}: ${clash.path} ~ trunk's ${clash.trunk}
|
|
76550
77451
|
`);
|
|
76551
77452
|
const conflicts = result.data?.conflicts;
|
|
76552
77453
|
if (Array.isArray(conflicts))
|
|
@@ -76584,10 +77485,10 @@ function plan2(argv, io) {
|
|
|
76584
77485
|
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
|
|
76585
77486
|
if (flags.positionals.length !== 1)
|
|
76586
77487
|
return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
|
|
76587
|
-
const file =
|
|
77488
|
+
const file = path45.resolve(io.cwd, flags.positionals[0]);
|
|
76588
77489
|
let raw;
|
|
76589
77490
|
try {
|
|
76590
|
-
raw = JSON.parse(
|
|
77491
|
+
raw = JSON.parse(fs25.readFileSync(file, "utf8"));
|
|
76591
77492
|
} catch (error) {
|
|
76592
77493
|
return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
|
|
76593
77494
|
}
|
|
@@ -76616,7 +77517,7 @@ function plan2(argv, io) {
|
|
|
76616
77517
|
]);
|
|
76617
77518
|
}
|
|
76618
77519
|
function check2(argv, io) {
|
|
76619
|
-
const flags = parseFlags(argv, { name: "value", base: "value" });
|
|
77520
|
+
const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
|
|
76620
77521
|
if (typeof flags === "string")
|
|
76621
77522
|
return usage3(io, flags, CHECK_USAGE);
|
|
76622
77523
|
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
@@ -76624,7 +77525,8 @@ function check2(argv, io) {
|
|
|
76624
77525
|
return failed(io, selected);
|
|
76625
77526
|
const result = checkFanout(io.cwd, selected.data, {
|
|
76626
77527
|
only: flags.positionals,
|
|
76627
|
-
trunkRef: flags.values.base ?? null
|
|
77528
|
+
trunkRef: flags.values.base ?? null,
|
|
77529
|
+
forge: forgeFor(io, flags.booleans.has("offline")).forge
|
|
76628
77530
|
});
|
|
76629
77531
|
if (!result.ok)
|
|
76630
77532
|
return failed(io, result);
|
|
@@ -76640,17 +77542,92 @@ function check2(argv, io) {
|
|
|
76640
77542
|
unblock: result.data.next
|
|
76641
77543
|
}));
|
|
76642
77544
|
}
|
|
77545
|
+
function status3(argv, io) {
|
|
77546
|
+
const flags = parseFlags(argv, { name: "value", "stuck-after": "value", offline: "boolean" });
|
|
77547
|
+
if (typeof flags === "string")
|
|
77548
|
+
return usage3(io, flags, STATUS_USAGE3);
|
|
77549
|
+
if (flags.positionals.length)
|
|
77550
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE3);
|
|
77551
|
+
const raw = flags.values["stuck-after"];
|
|
77552
|
+
const stuckAfterMs = raw === undefined ? null : parseDuration2(raw);
|
|
77553
|
+
if (stuckAfterMs === 0 || raw !== undefined && stuckAfterMs === null)
|
|
77554
|
+
return usage3(io, "--stuck-after takes a duration such as 30m, 45m or 2h", STATUS_USAGE3);
|
|
77555
|
+
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
77556
|
+
if (!selected.ok)
|
|
77557
|
+
return failed(io, selected);
|
|
77558
|
+
const forge = forgeFor(io, flags.booleans.has("offline"));
|
|
77559
|
+
const result = fanoutStatus(io.cwd, selected.data, {
|
|
77560
|
+
forge: forge.forge,
|
|
77561
|
+
forgeError: forge.error,
|
|
77562
|
+
stuckAfterMs
|
|
77563
|
+
});
|
|
77564
|
+
if (!result.ok)
|
|
77565
|
+
return failed(io, result);
|
|
77566
|
+
return emit(io, ok(result.data), renderStatus2);
|
|
77567
|
+
}
|
|
77568
|
+
function worktree(argv, io) {
|
|
77569
|
+
const [action, ...rest] = argv;
|
|
77570
|
+
const flags = parseFlags(rest, { name: "value", force: "boolean" });
|
|
77571
|
+
if (typeof flags === "string")
|
|
77572
|
+
return usage3(io, flags, WORKTREE_USAGE);
|
|
77573
|
+
if (action !== "create" && action !== "release")
|
|
77574
|
+
return usage3(io, action ? `unknown worktree action "${action}"` : "missing create or release", WORKTREE_USAGE);
|
|
77575
|
+
if (flags.positionals.length !== 1)
|
|
77576
|
+
return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <slice>", WORKTREE_USAGE);
|
|
77577
|
+
if (action === "create" && flags.booleans.has("force"))
|
|
77578
|
+
return usage3(io, "--force applies to release only", WORKTREE_USAGE);
|
|
77579
|
+
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
77580
|
+
if (!selected.ok)
|
|
77581
|
+
return failed(io, selected);
|
|
77582
|
+
const actor = actorFromEnv(io.env);
|
|
77583
|
+
if (action === "create") {
|
|
77584
|
+
const result = createSliceWorktree(io.cwd, selected.data, {
|
|
77585
|
+
slice: flags.positionals[0],
|
|
77586
|
+
actor
|
|
77587
|
+
});
|
|
77588
|
+
if (!result.ok)
|
|
77589
|
+
return failed(io, result);
|
|
77590
|
+
return emit(io, ok(result.data), (data) => [
|
|
77591
|
+
`${data.created ? "created" : "kept"} worktree for slice ${data.slice} (${data.mode}): ${data.path}`,
|
|
77592
|
+
` branch ${data.branch} <- ${data.base} at ${short5(data.head)}`,
|
|
77593
|
+
` scratch ${data.scratch} (put it in the worker brief: temp files go here)`,
|
|
77594
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
77595
|
+
]);
|
|
77596
|
+
}
|
|
77597
|
+
const result = releaseSliceWorktree(io.cwd, selected.data, {
|
|
77598
|
+
slice: flags.positionals[0],
|
|
77599
|
+
force: flags.booleans.has("force"),
|
|
77600
|
+
actor
|
|
77601
|
+
});
|
|
77602
|
+
if (!result.ok) {
|
|
77603
|
+
const code = failed(io, result);
|
|
77604
|
+
const dirty = result.data?.dirty;
|
|
77605
|
+
if (!io.json && Array.isArray(dirty))
|
|
77606
|
+
for (const line of dirty)
|
|
77607
|
+
io.stderr(` ${line}
|
|
77608
|
+
`);
|
|
77609
|
+
return code;
|
|
77610
|
+
}
|
|
77611
|
+
return emit(io, ok(result.data), (data) => [
|
|
77612
|
+
data.removed ? `released slice ${data.slice}: removed ${data.path}${data.dirty.length ? ` (dropped ${data.dirty.length} uncommitted change${data.dirty.length === 1 ? "" : "s"}, recorded in the ledger)` : ""}; branch ${data.branch} kept` : `slice ${data.slice}: nothing released`,
|
|
77613
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
77614
|
+
]);
|
|
77615
|
+
}
|
|
76643
77616
|
async function run15(argv, io) {
|
|
76644
77617
|
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
76645
77618
|
if (sub === "plan")
|
|
76646
77619
|
return plan2(rest, io);
|
|
76647
77620
|
if (sub === "check")
|
|
76648
77621
|
return check2(rest, io);
|
|
77622
|
+
if (sub === "status")
|
|
77623
|
+
return status3(rest, io);
|
|
77624
|
+
if (sub === "worktree")
|
|
77625
|
+
return worktree(rest, io);
|
|
76649
77626
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
|
|
76650
77627
|
}
|
|
76651
|
-
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]", USAGE8 = "workit fanout plan|check ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
77628
|
+
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
76652
77629
|
`fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
|
|
76653
|
-
...data.slices.map((slice) => ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
|
|
77630
|
+
...data.slices.map((slice) => slice.status === "landed" ? ` -- ${slice.id} (${slice.branch}) landed (${slice.landed?.how}${slice.landed?.pr ? ` #${slice.landed.pr}` : ""})` : ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
|
|
76654
77631
|
${slice.reasons.join(`
|
|
76655
77632
|
`)}` : ""}`),
|
|
76656
77633
|
...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
|
|
@@ -76658,9 +77635,37 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
|
|
|
76658
77635
|
`landing order: ${data.landingOrder.join(", ")}`,
|
|
76659
77636
|
`next: ${data.next}`,
|
|
76660
77637
|
...data.notes.map((note) => `note: ${note}`)
|
|
76661
|
-
]
|
|
77638
|
+
], short5 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
|
|
77639
|
+
const counts = new Map;
|
|
77640
|
+
for (const slice of data.slices)
|
|
77641
|
+
counts.set(slice.state, (counts.get(slice.state) ?? 0) + 1);
|
|
77642
|
+
return [
|
|
77643
|
+
`fanout ${data.name} on ${data.trunk}: ${[...counts].map(([state, count]) => `${count} ${state.replace("_", " ")}`).join(", ")} · forge ${data.forge.available ? "on" : "off"}`,
|
|
77644
|
+
...data.slices.map((slice) => {
|
|
77645
|
+
const parts = [
|
|
77646
|
+
slice.branchExists ? short5(slice.head) : "no branch",
|
|
77647
|
+
slice.lastCommitAt && slice.idleMinutes !== null ? `idle ${slice.idleMinutes} min` : null,
|
|
77648
|
+
slice.pr ? `PR #${slice.pr.number} ${slice.pr.state}` : null,
|
|
77649
|
+
slice.ci === "unknown" ? null : `CI ${slice.ci}`,
|
|
77650
|
+
`verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis === "none" ? "missing" : slice.verdict.basis}`
|
|
77651
|
+
].filter(Boolean);
|
|
77652
|
+
const label = slice.state === "stuck" ? "STUCK" : slice.state.replace("_", " ");
|
|
77653
|
+
const detail = slice.landed ? `landed (${slice.landed.how}${slice.landed.pr ? ` #${slice.landed.pr}` : ""})` : parts.join(" · ");
|
|
77654
|
+
const why = slice.state === "landed" ? [] : [...slice.reasons, ...slice.notes];
|
|
77655
|
+
return ` ${label.padEnd(11)} ${slice.id} (${slice.branch}) ${detail}${why.length ? `
|
|
77656
|
+
${why.join(`
|
|
77657
|
+
`)}` : ""}`;
|
|
77658
|
+
}),
|
|
77659
|
+
`landing order: ${data.landingOrder.length ? data.landingOrder.join(", ") : "(none ready)"}`,
|
|
77660
|
+
...data.spawnable.length ? [`spawnable: ${data.spawnable.join(", ")}`] : [],
|
|
77661
|
+
`next: ${data.next}`,
|
|
77662
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
77663
|
+
];
|
|
77664
|
+
};
|
|
76662
77665
|
var init_fanout2 = __esm(() => {
|
|
76663
77666
|
init_fanout_check();
|
|
77667
|
+
init_fanout_status();
|
|
77668
|
+
init_fanout_worktree();
|
|
76664
77669
|
init_fanout();
|
|
76665
77670
|
init_rev();
|
|
76666
77671
|
init_ledger();
|
|
@@ -91815,7 +92820,7 @@ var init_ast = __esm(() => {
|
|
|
91815
92820
|
|
|
91816
92821
|
// packages/workit-cli/src/test-audit/rules.ts
|
|
91817
92822
|
import { builtinModules } from "node:module";
|
|
91818
|
-
import
|
|
92823
|
+
import path46 from "node:path";
|
|
91819
92824
|
function testCallee(call) {
|
|
91820
92825
|
let callee = call.callee;
|
|
91821
92826
|
if (callee.type === "CallExpression")
|
|
@@ -92308,12 +93313,12 @@ function checkConstantsOnly(ctx, emit) {
|
|
|
92308
93313
|
}
|
|
92309
93314
|
function checkOverMocking(ctx, emit) {
|
|
92310
93315
|
const { program } = ctx.parsed;
|
|
92311
|
-
const unit =
|
|
93316
|
+
const unit = path46.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
|
|
92312
93317
|
const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
|
|
92313
93318
|
const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
|
|
92314
93319
|
for (const call of mocked) {
|
|
92315
93320
|
const spec = call.arguments[0].value;
|
|
92316
|
-
const base =
|
|
93321
|
+
const base = path46.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
92317
93322
|
if (base === unit)
|
|
92318
93323
|
emit({
|
|
92319
93324
|
rule: "over-mocking",
|
|
@@ -92701,11 +93706,11 @@ var init_rules = __esm(() => {
|
|
|
92701
93706
|
});
|
|
92702
93707
|
|
|
92703
93708
|
// packages/workit-cli/src/test-audit/audit.ts
|
|
92704
|
-
import { spawnSync as
|
|
93709
|
+
import { spawnSync as spawnSync23 } from "node:child_process";
|
|
92705
93710
|
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
|
|
92706
|
-
import
|
|
93711
|
+
import path47 from "node:path";
|
|
92707
93712
|
function git6(cwd, args) {
|
|
92708
|
-
const run =
|
|
93713
|
+
const run = spawnSync23("git", ["-c", "core.quotepath=off", ...args], {
|
|
92709
93714
|
cwd,
|
|
92710
93715
|
encoding: "utf8",
|
|
92711
93716
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -92716,7 +93721,7 @@ function walkTests(dir, out) {
|
|
|
92716
93721
|
for (const entry of readdirSync8(dir, { withFileTypes: true })) {
|
|
92717
93722
|
if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
|
|
92718
93723
|
continue;
|
|
92719
|
-
const full =
|
|
93724
|
+
const full = path47.join(dir, entry.name);
|
|
92720
93725
|
if (entry.isDirectory()) {
|
|
92721
93726
|
if (!SKIP_DIRS.has(entry.name))
|
|
92722
93727
|
walkTests(full, out);
|
|
@@ -92728,7 +93733,7 @@ function expandPaths(cwd, inputs) {
|
|
|
92728
93733
|
const files = [];
|
|
92729
93734
|
const missing = [];
|
|
92730
93735
|
for (const input of inputs.length > 0 ? inputs : ["."]) {
|
|
92731
|
-
const full =
|
|
93736
|
+
const full = path47.resolve(cwd, input);
|
|
92732
93737
|
if (!existsSync18(full))
|
|
92733
93738
|
missing.push(input);
|
|
92734
93739
|
else if (statSync7(full).isDirectory())
|
|
@@ -92765,11 +93770,11 @@ function changedLines(cwd, base) {
|
|
|
92765
93770
|
for (let offset = 0;offset < count; offset += 1)
|
|
92766
93771
|
lines.add(start + offset);
|
|
92767
93772
|
}
|
|
92768
|
-
changed.set(
|
|
93773
|
+
changed.set(path47.join(root, file), lines);
|
|
92769
93774
|
}
|
|
92770
93775
|
const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
92771
93776
|
for (const file of untracked.stdout.split("\x00").filter(Boolean))
|
|
92772
|
-
changed.set(
|
|
93777
|
+
changed.set(path47.join(root, file), "all");
|
|
92773
93778
|
return changed;
|
|
92774
93779
|
}
|
|
92775
93780
|
function auditFiles(files, display) {
|
|
@@ -92831,7 +93836,7 @@ function auditFiles(files, display) {
|
|
|
92831
93836
|
durationMs: Math.round(performance.now() - started)
|
|
92832
93837
|
};
|
|
92833
93838
|
}
|
|
92834
|
-
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(
|
|
93839
|
+
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path47.sep).join("/"), SEVERITY_ORDER, ruleIds;
|
|
92835
93840
|
var init_audit = __esm(() => {
|
|
92836
93841
|
init_ast();
|
|
92837
93842
|
init_rules();
|
|
@@ -92861,7 +93866,7 @@ import {
|
|
|
92861
93866
|
copyFileSync as copyFileSync6
|
|
92862
93867
|
} from "node:fs";
|
|
92863
93868
|
import os14 from "node:os";
|
|
92864
|
-
import
|
|
93869
|
+
import path48 from "node:path";
|
|
92865
93870
|
function mutantsFor(source, file, lines) {
|
|
92866
93871
|
const parsed = parseSource(source, file);
|
|
92867
93872
|
const out = [];
|
|
@@ -92934,7 +93939,7 @@ function relatedTests(source, testFiles) {
|
|
|
92934
93939
|
for (const match of text.matchAll(SPECIFIER)) {
|
|
92935
93940
|
const spec = match[1];
|
|
92936
93941
|
if (spec.startsWith(".")) {
|
|
92937
|
-
if (stripExt(
|
|
93942
|
+
if (stripExt(path48.posix.join(path48.posix.dirname(file), spec)) === target)
|
|
92938
93943
|
return true;
|
|
92939
93944
|
} else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
|
|
92940
93945
|
return true;
|
|
@@ -92954,12 +93959,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
92954
93959
|
for (const name of names) {
|
|
92955
93960
|
if (!name.startsWith(TEMP_PREFIX))
|
|
92956
93961
|
continue;
|
|
92957
|
-
const dir =
|
|
93962
|
+
const dir = path48.join(tmp, name);
|
|
92958
93963
|
try {
|
|
92959
93964
|
const stat = lstatSync7(dir);
|
|
92960
93965
|
if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
|
|
92961
93966
|
continue;
|
|
92962
|
-
const pid = Number(readFileSync20(
|
|
93967
|
+
const pid = Number(readFileSync20(path48.join(dir, PID_FILE), "utf8"));
|
|
92963
93968
|
if (Number.isInteger(pid) && pid > 0 && alive(pid))
|
|
92964
93969
|
continue;
|
|
92965
93970
|
} catch (error) {
|
|
@@ -92976,8 +93981,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
92976
93981
|
function linkNodeModules(origin, target, repo, copy) {
|
|
92977
93982
|
mkdirSync9(target, { recursive: true });
|
|
92978
93983
|
for (const entry of readdirSync9(origin, { withFileTypes: true })) {
|
|
92979
|
-
const from =
|
|
92980
|
-
const to =
|
|
93984
|
+
const from = path48.join(origin, entry.name);
|
|
93985
|
+
const to = path48.join(target, entry.name);
|
|
92981
93986
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
92982
93987
|
linkNodeModules(from, to, repo, copy);
|
|
92983
93988
|
continue;
|
|
@@ -92993,9 +93998,9 @@ function linkNodeModules(origin, target, repo, copy) {
|
|
|
92993
93998
|
} catch {
|
|
92994
93999
|
continue;
|
|
92995
94000
|
}
|
|
92996
|
-
const relative =
|
|
92997
|
-
if (!relative.startsWith("..") && !
|
|
92998
|
-
symlinkSync(
|
|
94001
|
+
const relative = path48.relative(repo, real);
|
|
94002
|
+
if (!relative.startsWith("..") && !path48.isAbsolute(relative) && !relative.split(path48.sep).includes("node_modules")) {
|
|
94003
|
+
symlinkSync(path48.join(copy, relative), to, "junction");
|
|
92999
94004
|
continue;
|
|
93000
94005
|
}
|
|
93001
94006
|
}
|
|
@@ -93010,8 +94015,8 @@ function copySymlink(repo, tree, from, to) {
|
|
|
93010
94015
|
real = null;
|
|
93011
94016
|
}
|
|
93012
94017
|
const realRepo = realpathSync9(repo);
|
|
93013
|
-
if (real &&
|
|
93014
|
-
symlinkSync(
|
|
94018
|
+
if (real && inside3(realRepo, real)) {
|
|
94019
|
+
symlinkSync(path48.relative(path48.dirname(to), path48.join(tree, path48.relative(realRepo, real))), to);
|
|
93015
94020
|
return;
|
|
93016
94021
|
}
|
|
93017
94022
|
if (real && statSync8(real).isFile())
|
|
@@ -93020,10 +94025,10 @@ function copySymlink(repo, tree, from, to) {
|
|
|
93020
94025
|
symlinkSync(readlinkSync3(from), to);
|
|
93021
94026
|
}
|
|
93022
94027
|
function writeInside(copy, file, content) {
|
|
93023
|
-
const target =
|
|
94028
|
+
const target = path48.join(copy, file);
|
|
93024
94029
|
try {
|
|
93025
94030
|
const stat = lstatSync7(target);
|
|
93026
|
-
if (!stat.isFile() || !
|
|
94031
|
+
if (!stat.isFile() || !inside3(realpathSync9(copy), realpathSync9(path48.dirname(target))))
|
|
93027
94032
|
return false;
|
|
93028
94033
|
} catch {
|
|
93029
94034
|
return false;
|
|
@@ -93034,9 +94039,9 @@ function writeInside(copy, file, content) {
|
|
|
93034
94039
|
return true;
|
|
93035
94040
|
}
|
|
93036
94041
|
function copyWorkingTree(repo) {
|
|
93037
|
-
const dir = mkdtempSync4(
|
|
93038
|
-
writeFileSync9(
|
|
93039
|
-
const tree =
|
|
94042
|
+
const dir = mkdtempSync4(path48.join(os14.tmpdir(), TEMP_PREFIX));
|
|
94043
|
+
writeFileSync9(path48.join(dir, PID_FILE), String(process.pid));
|
|
94044
|
+
const tree = path48.join(dir, "tree");
|
|
93040
94045
|
const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
|
|
93041
94046
|
if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
|
|
93042
94047
|
const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
|
|
@@ -93053,29 +94058,29 @@ function copyWorkingTree(repo) {
|
|
|
93053
94058
|
for (const relative of listed) {
|
|
93054
94059
|
if (!relative)
|
|
93055
94060
|
continue;
|
|
93056
|
-
const from =
|
|
94061
|
+
const from = path48.join(repo, relative);
|
|
93057
94062
|
let stat;
|
|
93058
94063
|
try {
|
|
93059
94064
|
stat = lstatSync7(from);
|
|
93060
94065
|
} catch {
|
|
93061
94066
|
continue;
|
|
93062
94067
|
}
|
|
93063
|
-
const to =
|
|
93064
|
-
mkdirSync9(
|
|
94068
|
+
const to = path48.join(tree, relative);
|
|
94069
|
+
mkdirSync9(path48.dirname(to), { recursive: true });
|
|
93065
94070
|
if (stat.isSymbolicLink())
|
|
93066
94071
|
copySymlink(repo, tree, from, to);
|
|
93067
94072
|
else if (stat.isFile())
|
|
93068
94073
|
copyFileSync6(from, to);
|
|
93069
|
-
let parent =
|
|
94074
|
+
let parent = path48.dirname(relative);
|
|
93070
94075
|
while (parent !== "." && !dirs.has(parent)) {
|
|
93071
94076
|
dirs.add(parent);
|
|
93072
|
-
parent =
|
|
94077
|
+
parent = path48.dirname(parent);
|
|
93073
94078
|
}
|
|
93074
94079
|
}
|
|
93075
94080
|
for (const parent of dirs) {
|
|
93076
|
-
const modules =
|
|
93077
|
-
if (existsSync19(modules) && !existsSync19(
|
|
93078
|
-
linkNodeModules(modules,
|
|
94081
|
+
const modules = path48.join(repo, parent, "node_modules");
|
|
94082
|
+
if (existsSync19(modules) && !existsSync19(path48.join(tree, parent, "node_modules")))
|
|
94083
|
+
linkNodeModules(modules, path48.join(tree, parent, "node_modules"), repo, tree);
|
|
93079
94084
|
}
|
|
93080
94085
|
return { dir, tree };
|
|
93081
94086
|
}
|
|
@@ -93114,14 +94119,14 @@ async function runMutation(options) {
|
|
|
93114
94119
|
const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
|
|
93115
94120
|
const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
|
|
93116
94121
|
try {
|
|
93117
|
-
return { file, text: readFileSync20(
|
|
94122
|
+
return { file, text: readFileSync20(path48.join(repo, file), "utf8") };
|
|
93118
94123
|
} catch {
|
|
93119
94124
|
return { file, text: "" };
|
|
93120
94125
|
}
|
|
93121
94126
|
});
|
|
93122
94127
|
const candidates = [];
|
|
93123
94128
|
for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
93124
|
-
const relative = toPosix(
|
|
94129
|
+
const relative = toPosix(path48.relative(repo, absolute));
|
|
93125
94130
|
if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
|
|
93126
94131
|
continue;
|
|
93127
94132
|
if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
|
|
@@ -93135,9 +94140,9 @@ async function runMutation(options) {
|
|
|
93135
94140
|
for (const mutant of mutants)
|
|
93136
94141
|
if (!related.has(mutant.file))
|
|
93137
94142
|
related.set(mutant.file, relatedTests(mutant.file, testFiles));
|
|
93138
|
-
const cwdRelative =
|
|
94143
|
+
const cwdRelative = path48.relative(repo, options.cwd);
|
|
93139
94144
|
const asArg = (file) => {
|
|
93140
|
-
const relative = toPosix(
|
|
94145
|
+
const relative = toPosix(path48.relative(cwdRelative, file));
|
|
93141
94146
|
return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
|
|
93142
94147
|
};
|
|
93143
94148
|
const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
|
|
@@ -93172,7 +94177,7 @@ async function runMutation(options) {
|
|
|
93172
94177
|
return finish2(options, 0, results, counts, started);
|
|
93173
94178
|
removeStaleCopies();
|
|
93174
94179
|
const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
|
|
93175
|
-
const copyCwd =
|
|
94180
|
+
const copyCwd = path48.join(copy, cwdRelative);
|
|
93176
94181
|
let current = null;
|
|
93177
94182
|
const restore = () => {
|
|
93178
94183
|
if (current)
|
|
@@ -93207,7 +94212,7 @@ async function runMutation(options) {
|
|
|
93207
94212
|
record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
|
|
93208
94213
|
continue;
|
|
93209
94214
|
}
|
|
93210
|
-
const original = readFileSync20(
|
|
94215
|
+
const original = readFileSync20(path48.join(copy, mutant.file), "utf8");
|
|
93211
94216
|
const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
|
|
93212
94217
|
current = { file: mutant.file, original };
|
|
93213
94218
|
if (!writeInside(copy, mutant.file, mutated)) {
|
|
@@ -93263,9 +94268,9 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
|
|
|
93263
94268
|
} catch (error) {
|
|
93264
94269
|
return error.code === "EPERM";
|
|
93265
94270
|
}
|
|
93266
|
-
},
|
|
93267
|
-
const relative =
|
|
93268
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
94271
|
+
}, inside3 = (root, file) => {
|
|
94272
|
+
const relative = path48.relative(root, file);
|
|
94273
|
+
return relative === "" || !relative.startsWith("..") && !path48.isAbsolute(relative);
|
|
93269
94274
|
}, active, killTree = (child) => {
|
|
93270
94275
|
try {
|
|
93271
94276
|
if (child.pid && process.platform !== "win32")
|
|
@@ -93314,7 +94319,7 @@ __export(exports_test_audit, {
|
|
|
93314
94319
|
run: () => run17
|
|
93315
94320
|
});
|
|
93316
94321
|
import { existsSync as existsSync20 } from "node:fs";
|
|
93317
|
-
import
|
|
94322
|
+
import path49 from "node:path";
|
|
93318
94323
|
function parse7(argv) {
|
|
93319
94324
|
const options = {
|
|
93320
94325
|
paths: [],
|
|
@@ -93414,8 +94419,8 @@ async function run17(argv, io) {
|
|
|
93414
94419
|
return emit(io, fail("invalid_input", `${options}
|
|
93415
94420
|
usage: ${USAGE10}`));
|
|
93416
94421
|
const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
|
|
93417
|
-
const repo = top.ok ?
|
|
93418
|
-
const display = (file) => toPosix(
|
|
94422
|
+
const repo = top.ok ? path49.resolve(top.stdout.trim()) : null;
|
|
94423
|
+
const display = (file) => toPosix(path49.relative(io.cwd, file) || file);
|
|
93419
94424
|
let base = null;
|
|
93420
94425
|
let changed = null;
|
|
93421
94426
|
if (options.diff || options.mutate) {
|
|
@@ -93658,7 +94663,7 @@ __export(exports_youtrack, {
|
|
|
93658
94663
|
run: () => run19
|
|
93659
94664
|
});
|
|
93660
94665
|
import { readFileSync as readFileSync21 } from "node:fs";
|
|
93661
|
-
import
|
|
94666
|
+
import path50 from "node:path";
|
|
93662
94667
|
async function run19(argv, io) {
|
|
93663
94668
|
const [sub, ...rest] = argv;
|
|
93664
94669
|
if (sub !== "note" && sub !== "time" && sub !== "meeting")
|
|
@@ -93704,7 +94709,7 @@ async function run19(argv, io) {
|
|
|
93704
94709
|
let markdown = flags.values.markdown ?? "";
|
|
93705
94710
|
if (flags.values.file)
|
|
93706
94711
|
try {
|
|
93707
|
-
markdown = readFileSync21(
|
|
94712
|
+
markdown = readFileSync21(path50.resolve(io.cwd, flags.values.file), "utf8");
|
|
93708
94713
|
} catch (error) {
|
|
93709
94714
|
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
|
|
93710
94715
|
}
|
|
@@ -93773,7 +94778,7 @@ __export(exports_changelog, {
|
|
|
93773
94778
|
run: () => run20
|
|
93774
94779
|
});
|
|
93775
94780
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
93776
|
-
import
|
|
94781
|
+
import path51 from "node:path";
|
|
93777
94782
|
async function run20(argv, io) {
|
|
93778
94783
|
const [sub, ...rest] = argv;
|
|
93779
94784
|
if (sub !== "apply")
|
|
@@ -93819,7 +94824,7 @@ async function run20(argv, io) {
|
|
|
93819
94824
|
]);
|
|
93820
94825
|
}
|
|
93821
94826
|
var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
|
|
93822
|
-
const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(
|
|
94827
|
+
const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path51.resolve(cwd, raw.slice(1)), "utf8") : raw;
|
|
93823
94828
|
return JSON.parse(text);
|
|
93824
94829
|
};
|
|
93825
94830
|
var init_changelog2 = __esm(() => {
|
|
@@ -93836,7 +94841,7 @@ __export(exports_handoff, {
|
|
|
93836
94841
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
93837
94842
|
function human2(brief) {
|
|
93838
94843
|
const lines = [
|
|
93839
|
-
`branch: ${brief.branch ?? "(detached)"} @ ${
|
|
94844
|
+
`branch: ${brief.branch ?? "(detached)"} @ ${short6(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
|
|
93840
94845
|
];
|
|
93841
94846
|
if (brief.base)
|
|
93842
94847
|
lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
|
|
@@ -93907,7 +94912,7 @@ async function run21(argv, io) {
|
|
|
93907
94912
|
await Promise.resolve().then(() => init_task());
|
|
93908
94913
|
return runTaskCommand(["handoff", ...argv]);
|
|
93909
94914
|
}
|
|
93910
|
-
var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]",
|
|
94915
|
+
var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short6 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
93911
94916
|
var init_handoff = __esm(() => {
|
|
93912
94917
|
init_ledger();
|
|
93913
94918
|
init_output();
|
|
@@ -93916,7 +94921,7 @@ var init_handoff = __esm(() => {
|
|
|
93916
94921
|
// packages/workit-cli/src/main.ts
|
|
93917
94922
|
init_package();
|
|
93918
94923
|
init_output();
|
|
93919
|
-
import
|
|
94924
|
+
import path52 from "node:path";
|
|
93920
94925
|
|
|
93921
94926
|
// packages/workit-cli/src/verbs/registry.ts
|
|
93922
94927
|
var TASK_FAMILY_NAMES = [
|
|
@@ -94031,8 +95036,8 @@ var VERBS = [
|
|
|
94031
95036
|
{
|
|
94032
95037
|
name: "fanout",
|
|
94033
95038
|
group: "delivery",
|
|
94034
|
-
usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
|
|
94035
|
-
summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners),
|
|
95039
|
+
usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
|
|
95040
|
+
summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
|
|
94036
95041
|
load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
|
|
94037
95042
|
},
|
|
94038
95043
|
{
|
|
@@ -94160,7 +95165,7 @@ var defaultIo = (json) => ({
|
|
|
94160
95165
|
});
|
|
94161
95166
|
async function main(argv, overrides = {}, options = {}) {
|
|
94162
95167
|
const parsed = parseGlobals(argv);
|
|
94163
|
-
const cwd =
|
|
95168
|
+
const cwd = path52.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
94164
95169
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
94165
95170
|
if (parsed.error)
|
|
94166
95171
|
return emit(io, fail("invalid_input", parsed.error));
|