@brainervirus/workit-claude-code 7.2.0 → 7.4.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-hook.js +29 -30
- package/dist/workit.js +1502 -191
- package/package.json +3 -3
- package/skills/fanout/SKILL.md +29 -20
- package/skills/fanout/references/brief.md +45 -0
- package/skills/retro/SKILL.md +72 -0
- package/skills/retro/agents/openai.yaml +3 -0
- package/skills/retro/references/sources.md +35 -0
- package/skills/retro/references/steering.md +39 -0
- package/skills/review/SKILL.md +3 -2
- package/skills/shape/references/knowledge.md +3 -0
- package/skills/ship/SKILL.md +2 -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.4.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.4.0",
|
|
107
|
+
"@brainervirus/workit-cursor": "^7.4.0",
|
|
108
|
+
"@brainervirus/workit-opencode": "^7.4.0",
|
|
109
109
|
"@inkjs/ui": "2.0.0",
|
|
110
110
|
ink: "7.1.1",
|
|
111
111
|
react: "19.2.8"
|
|
@@ -63144,7 +63144,8 @@ var init_skill_manifests = __esm(() => {
|
|
|
63144
63144
|
"workit-test-audit",
|
|
63145
63145
|
"workit-deslop",
|
|
63146
63146
|
"workit-fanout",
|
|
63147
|
-
"workit-verify-app"
|
|
63147
|
+
"workit-verify-app",
|
|
63148
|
+
"workit-retro"
|
|
63148
63149
|
];
|
|
63149
63150
|
if (false) {}
|
|
63150
63151
|
});
|
|
@@ -69916,6 +69917,270 @@ var init_launch = __esm(() => {
|
|
|
69916
69917
|
init_upgrade();
|
|
69917
69918
|
});
|
|
69918
69919
|
|
|
69920
|
+
// packages/workit-core/src/knowledge.ts
|
|
69921
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
69922
|
+
import fs17 from "node:fs";
|
|
69923
|
+
import path36 from "node:path";
|
|
69924
|
+
function lintKnowledge(root) {
|
|
69925
|
+
const seen = new Set;
|
|
69926
|
+
const unreadable = [];
|
|
69927
|
+
const load = (file) => {
|
|
69928
|
+
const doc = read(root, file);
|
|
69929
|
+
if (doc === "unreadable")
|
|
69930
|
+
unreadable.push(file);
|
|
69931
|
+
if (doc === null || doc === "unreadable" || seen.has(doc.real))
|
|
69932
|
+
return null;
|
|
69933
|
+
seen.add(doc.real);
|
|
69934
|
+
return doc;
|
|
69935
|
+
};
|
|
69936
|
+
const steering = STEERING_FILES.map(load).filter((doc) => doc !== null);
|
|
69937
|
+
const needBased = NEED_BASED_FILES.map(load).filter((doc) => doc !== null);
|
|
69938
|
+
const findings = [];
|
|
69939
|
+
for (const doc of steering) {
|
|
69940
|
+
const bytes = Buffer.byteLength(doc.text);
|
|
69941
|
+
if (bytes > AGENTS_BYTE_BUDGET)
|
|
69942
|
+
findings.push({
|
|
69943
|
+
rule: "agents-budget",
|
|
69944
|
+
file: doc.file,
|
|
69945
|
+
line: null,
|
|
69946
|
+
message: `${bytes} bytes, over the ${AGENTS_BYTE_BUDGET}-byte budget; move detail behind a pointer or into a check, and name what each addition removes`
|
|
69947
|
+
});
|
|
69948
|
+
}
|
|
69949
|
+
for (const doc of needBased)
|
|
69950
|
+
if (scaffoldOnly(doc))
|
|
69951
|
+
findings.push({
|
|
69952
|
+
rule: "scaffold-file",
|
|
69953
|
+
file: doc.file,
|
|
69954
|
+
line: null,
|
|
69955
|
+
message: "holds no entry (headings, comments or placeholders only); delete it, or create it in the same edit as its first real entry"
|
|
69956
|
+
});
|
|
69957
|
+
const tracked = trackedTopLevel(root);
|
|
69958
|
+
for (const doc of [...steering, ...needBased])
|
|
69959
|
+
findings.push(...brokenLinks(root, doc, tracked));
|
|
69960
|
+
const standards = needBased.find((doc) => doc.file === STANDARDS_FILE);
|
|
69961
|
+
if (standards)
|
|
69962
|
+
findings.push(...duplicateRules(steering, standards));
|
|
69963
|
+
return {
|
|
69964
|
+
root,
|
|
69965
|
+
budget: AGENTS_BYTE_BUDGET,
|
|
69966
|
+
files: [...steering, ...needBased].map((doc) => ({
|
|
69967
|
+
file: doc.file,
|
|
69968
|
+
bytes: Buffer.byteLength(doc.text)
|
|
69969
|
+
})),
|
|
69970
|
+
findings,
|
|
69971
|
+
unreadable
|
|
69972
|
+
};
|
|
69973
|
+
}
|
|
69974
|
+
var AGENTS_BYTE_BUDGET, STEERING_FILES, NEED_BASED_FILES, STANDARDS_FILE = "CODING_STANDARDS.md", MIN_RULE_WORDS = 6, FENCE, HEADING, TABLE_SEPARATOR, LIST_ITEM, PLACEHOLDER2, INDENTED_CODE, blank = (line) => (line ?? "").trim() === "", proseLines = (lines) => {
|
|
69975
|
+
const out = [];
|
|
69976
|
+
let fence = null;
|
|
69977
|
+
let comment = false;
|
|
69978
|
+
let indentedCode = false;
|
|
69979
|
+
let inList = false;
|
|
69980
|
+
lines.forEach((raw, index) => {
|
|
69981
|
+
if (fence) {
|
|
69982
|
+
const close = FENCE.exec(raw);
|
|
69983
|
+
if (close && close[1][0] === fence[0] && close[1].length >= fence.length && !close[2].trim())
|
|
69984
|
+
fence = null;
|
|
69985
|
+
return;
|
|
69986
|
+
}
|
|
69987
|
+
if (!comment) {
|
|
69988
|
+
const open = FENCE.exec(raw);
|
|
69989
|
+
if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
|
|
69990
|
+
fence = open[1];
|
|
69991
|
+
indentedCode = false;
|
|
69992
|
+
return;
|
|
69993
|
+
}
|
|
69994
|
+
if (INDENTED_CODE.test(raw) && !blank(raw)) {
|
|
69995
|
+
if (indentedCode || !inList && blank(lines[index - 1])) {
|
|
69996
|
+
indentedCode = true;
|
|
69997
|
+
return;
|
|
69998
|
+
}
|
|
69999
|
+
} else if (!blank(raw)) {
|
|
70000
|
+
indentedCode = false;
|
|
70001
|
+
inList = LIST_ITEM.test(raw) || inList && /^\s/.test(raw);
|
|
70002
|
+
}
|
|
70003
|
+
}
|
|
70004
|
+
let text = raw;
|
|
70005
|
+
if (comment) {
|
|
70006
|
+
const end = text.indexOf("-->");
|
|
70007
|
+
if (end === -1)
|
|
70008
|
+
return;
|
|
70009
|
+
comment = false;
|
|
70010
|
+
text = text.slice(end + 3);
|
|
70011
|
+
}
|
|
70012
|
+
text = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
70013
|
+
const open = text.indexOf("<!--");
|
|
70014
|
+
if (open !== -1) {
|
|
70015
|
+
comment = true;
|
|
70016
|
+
text = text.slice(0, open);
|
|
70017
|
+
}
|
|
70018
|
+
out.push({ index, text });
|
|
70019
|
+
});
|
|
70020
|
+
return out;
|
|
70021
|
+
}, withoutFrontMatter = (lines) => {
|
|
70022
|
+
if (lines[0]?.trim() !== "---")
|
|
70023
|
+
return lines;
|
|
70024
|
+
const end = lines.indexOf("---", 1);
|
|
70025
|
+
return end === -1 ? lines : [...lines.slice(0, end + 1).map(() => ""), ...lines.slice(end + 1)];
|
|
70026
|
+
}, read = (root, file) => {
|
|
70027
|
+
const full = path36.join(root, file);
|
|
70028
|
+
try {
|
|
70029
|
+
fs17.lstatSync(full);
|
|
70030
|
+
} catch {
|
|
70031
|
+
return null;
|
|
70032
|
+
}
|
|
70033
|
+
try {
|
|
70034
|
+
if (!fs17.statSync(full).isFile())
|
|
70035
|
+
return null;
|
|
70036
|
+
const text = fs17.readFileSync(full, "utf8");
|
|
70037
|
+
return { file, text, lines: text.split(/\r?\n/), real: fs17.realpathSync(full) };
|
|
70038
|
+
} catch {
|
|
70039
|
+
return "unreadable";
|
|
70040
|
+
}
|
|
70041
|
+
}, trackedTopLevel = (root) => {
|
|
70042
|
+
const run = spawnSync14("git", ["ls-files", "-z", "--cached"], {
|
|
70043
|
+
cwd: root,
|
|
70044
|
+
encoding: "utf8",
|
|
70045
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
70046
|
+
timeout: 20000,
|
|
70047
|
+
windowsHide: true
|
|
70048
|
+
});
|
|
70049
|
+
if (run.status !== 0)
|
|
70050
|
+
return null;
|
|
70051
|
+
return new Set(run.stdout.split("\x00").filter(Boolean).map((file) => file.split("/")[0]));
|
|
70052
|
+
}, exists = (target) => {
|
|
70053
|
+
try {
|
|
70054
|
+
fs17.statSync(target);
|
|
70055
|
+
return true;
|
|
70056
|
+
} catch {
|
|
70057
|
+
return false;
|
|
70058
|
+
}
|
|
70059
|
+
}, LINK, REFERENCE_DEFINITION, CODE_SPAN, PATH_LIKE, isExternal = (target) => /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("//"), localTarget = (target) => {
|
|
70060
|
+
const bare = target.replace(/[#?].*$/, "");
|
|
70061
|
+
try {
|
|
70062
|
+
return decodeURIComponent(bare);
|
|
70063
|
+
} catch {
|
|
70064
|
+
return bare;
|
|
70065
|
+
}
|
|
70066
|
+
}, brokenLinks = (root, doc, tracked) => {
|
|
70067
|
+
const findings = [];
|
|
70068
|
+
const base = path36.dirname(path36.join(root, doc.file));
|
|
70069
|
+
const missing = (target, line, how) => findings.push({
|
|
70070
|
+
rule: "broken-link",
|
|
70071
|
+
file: doc.file,
|
|
70072
|
+
line,
|
|
70073
|
+
message: `${how} \`${target}\` points at a missing file`
|
|
70074
|
+
});
|
|
70075
|
+
for (const { index, text } of proseLines(doc.lines)) {
|
|
70076
|
+
const prose = text.replace(CODE_SPAN, (span) => " ".repeat(span.length));
|
|
70077
|
+
const targets = [...prose.matchAll(LINK)].map((match) => match[1] ?? match[2]);
|
|
70078
|
+
const definition = REFERENCE_DEFINITION.exec(prose)?.[1];
|
|
70079
|
+
if (definition)
|
|
70080
|
+
targets.push(definition);
|
|
70081
|
+
for (const target of targets) {
|
|
70082
|
+
if (isExternal(target))
|
|
70083
|
+
continue;
|
|
70084
|
+
const local = localTarget(target);
|
|
70085
|
+
if (!local)
|
|
70086
|
+
continue;
|
|
70087
|
+
const resolved = local.startsWith("/") ? path36.join(root, local) : path36.resolve(base, local);
|
|
70088
|
+
if (!exists(resolved))
|
|
70089
|
+
missing(target, index + 1, "link");
|
|
70090
|
+
}
|
|
70091
|
+
for (const [, span] of text.matchAll(CODE_SPAN)) {
|
|
70092
|
+
const candidate = span.replace(/:\d+(?::\d+)?$/, "");
|
|
70093
|
+
if (!PATH_LIKE.test(candidate))
|
|
70094
|
+
continue;
|
|
70095
|
+
const relative = /^\.\.?\//.test(candidate);
|
|
70096
|
+
const first = candidate.split("/")[0];
|
|
70097
|
+
if (!relative && !(tracked ? tracked.has(first) : exists(path36.join(root, first))))
|
|
70098
|
+
continue;
|
|
70099
|
+
const resolved = relative ? path36.resolve(base, candidate) : path36.join(root, candidate);
|
|
70100
|
+
if (!exists(resolved))
|
|
70101
|
+
missing(span, index + 1, "pointer");
|
|
70102
|
+
}
|
|
70103
|
+
}
|
|
70104
|
+
return findings;
|
|
70105
|
+
}, isPlaceholder2 = (text) => PLACEHOLDER2.test(text.replace(LIST_ITEM, "").replace(/[*_`]/g, "").replace(/^\[\s?\]\s*/, "").trim()), scaffoldOnly = (doc) => {
|
|
70106
|
+
const lines = proseLines(withoutFrontMatter(doc.lines));
|
|
70107
|
+
return lines.every(({ text }, position) => {
|
|
70108
|
+
const trimmed = text.trim();
|
|
70109
|
+
if (trimmed === "" || HEADING.test(text) || /^([-*_])(\s*\1){2,}$/.test(trimmed))
|
|
70110
|
+
return true;
|
|
70111
|
+
if (TABLE_SEPARATOR.test(text))
|
|
70112
|
+
return true;
|
|
70113
|
+
const next = lines[position + 1]?.text ?? "";
|
|
70114
|
+
if (trimmed.startsWith("|") && TABLE_SEPARATOR.test(next))
|
|
70115
|
+
return true;
|
|
70116
|
+
if (trimmed.startsWith("|"))
|
|
70117
|
+
return trimmed.split("|").map((cell) => cell.trim()).every((cell) => cell === "" || isPlaceholder2(cell));
|
|
70118
|
+
return isPlaceholder2(trimmed);
|
|
70119
|
+
});
|
|
70120
|
+
}, ruleUnits = (doc) => {
|
|
70121
|
+
const units = [];
|
|
70122
|
+
let current = null;
|
|
70123
|
+
const flush = () => {
|
|
70124
|
+
if (current)
|
|
70125
|
+
units.push(current);
|
|
70126
|
+
current = null;
|
|
70127
|
+
};
|
|
70128
|
+
for (const { index, text } of proseLines(doc.lines)) {
|
|
70129
|
+
const trimmed = text.trim();
|
|
70130
|
+
if (trimmed === "" || HEADING.test(text) || trimmed.startsWith("|")) {
|
|
70131
|
+
flush();
|
|
70132
|
+
continue;
|
|
70133
|
+
}
|
|
70134
|
+
if (LIST_ITEM.test(text) || !current) {
|
|
70135
|
+
flush();
|
|
70136
|
+
current = { line: index + 1, text: trimmed.replace(LIST_ITEM, "") };
|
|
70137
|
+
} else
|
|
70138
|
+
current.text += ` ${trimmed}`;
|
|
70139
|
+
}
|
|
70140
|
+
flush();
|
|
70141
|
+
return units;
|
|
70142
|
+
}, normalize = (sentence) => sentence.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`]/g, "").toLowerCase().replace(/\s+/g, " ").replace(/[\s.,;:!?]+$/, "").trim(), sentences = (doc) => {
|
|
70143
|
+
const out = new Map;
|
|
70144
|
+
for (const unit of ruleUnits(doc))
|
|
70145
|
+
for (const sentence of unit.text.split(/(?<=[.!?])\s+/)) {
|
|
70146
|
+
const key = normalize(sentence);
|
|
70147
|
+
if (key.split(" ").length >= MIN_RULE_WORDS && !out.has(key))
|
|
70148
|
+
out.set(key, unit.line);
|
|
70149
|
+
}
|
|
70150
|
+
return out;
|
|
70151
|
+
}, duplicateRules = (steering, standards) => {
|
|
70152
|
+
const findings = [];
|
|
70153
|
+
const rules = sentences(standards);
|
|
70154
|
+
for (const doc of steering)
|
|
70155
|
+
for (const [key, line] of sentences(doc)) {
|
|
70156
|
+
const standardsLine = rules.get(key);
|
|
70157
|
+
if (standardsLine === undefined)
|
|
70158
|
+
continue;
|
|
70159
|
+
findings.push({
|
|
70160
|
+
rule: "duplicate-rule",
|
|
70161
|
+
file: doc.file,
|
|
70162
|
+
line,
|
|
70163
|
+
message: `repeats ${standards.file}:${standardsLine} ("${key.slice(0, 60)}${key.length > 60 ? "…" : ""}"); keep the rule in one file`
|
|
70164
|
+
});
|
|
70165
|
+
}
|
|
70166
|
+
return findings;
|
|
70167
|
+
};
|
|
70168
|
+
var init_knowledge = __esm(() => {
|
|
70169
|
+
AGENTS_BYTE_BUDGET = 8 * 1024;
|
|
70170
|
+
STEERING_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
70171
|
+
NEED_BASED_FILES = ["CODING_STANDARDS.md", "GLOSSARY.md"];
|
|
70172
|
+
FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
70173
|
+
HEADING = /^\s{0,3}#{1,6}(\s|$)/;
|
|
70174
|
+
TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
70175
|
+
LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
70176
|
+
PLACEHOLDER2 = /^(?:(?:tbd|todo|tba|fixme)\b.*|n\/a|none|none yet|coming soon|placeholder|\.\.\.|…|-)?[.:]?$/i;
|
|
70177
|
+
INDENTED_CODE = /^(?: {4}|\t)/;
|
|
70178
|
+
LINK = /!?\[[^\]]*\]\(\s*(?:<([^>\n]+)>|((?:[^()\s<>]|\([^()\s]*\))+))(?:\s+["'(][^)]*)?\)/g;
|
|
70179
|
+
REFERENCE_DEFINITION = /^\s{0,3}\[[^\]]+\]:\s*<?(\S+?)>?(?:\s|$)/;
|
|
70180
|
+
CODE_SPAN = /`([^`\n]+)`/g;
|
|
70181
|
+
PATH_LIKE = /^(?:\.{1,2}\/)?[\w@.-]+(?:\/[\w@.-]+)+\/?$/;
|
|
70182
|
+
});
|
|
70183
|
+
|
|
69919
70184
|
// packages/workit-cli/src/prompt.ts
|
|
69920
70185
|
import { createInterface } from "node:readline/promises";
|
|
69921
70186
|
async function askLine(question, output = process.stdout) {
|
|
@@ -69999,8 +70264,23 @@ async function run5(argv, io) {
|
|
|
69999
70264
|
fixLock = clearStaleMetadataLock(root, { force });
|
|
70000
70265
|
}
|
|
70001
70266
|
const report = runDoctor({ host: "cli", cwd: io.cwd, workspaceRoot: root });
|
|
70267
|
+
let knowledge = null;
|
|
70268
|
+
try {
|
|
70269
|
+
knowledge = lintKnowledge(root);
|
|
70270
|
+
if (knowledge.unreadable.length > 0)
|
|
70271
|
+
knowledge = null;
|
|
70272
|
+
} catch {
|
|
70273
|
+
knowledge = null;
|
|
70274
|
+
}
|
|
70275
|
+
const agents = knowledge?.files.find((file) => file.file === "AGENTS.md");
|
|
70276
|
+
const knowledgeLine = knowledge ? `info knowledge — ${agents ? `AGENTS.md ${agents.bytes} of ${knowledge.budget} bytes` : "no AGENTS.md"}; ${knowledge.findings.length} finding(s) (workit knowledge lint)` : "info knowledge — unavailable";
|
|
70002
70277
|
if (argv.includes("--json")) {
|
|
70003
|
-
|
|
70278
|
+
const summary = knowledge ? {
|
|
70279
|
+
agentsBytes: agents?.bytes ?? null,
|
|
70280
|
+
budget: knowledge.budget,
|
|
70281
|
+
findings: knowledge.findings.length
|
|
70282
|
+
} : { unavailable: true };
|
|
70283
|
+
io.stdout(`${JSON.stringify({ ...report, ...fixLock ? { fixLock } : {}, knowledge: summary }, null, 2)}
|
|
70004
70284
|
`);
|
|
70005
70285
|
return report.exitCode;
|
|
70006
70286
|
}
|
|
@@ -70019,12 +70299,15 @@ async function run5(argv, io) {
|
|
|
70019
70299
|
io.stdout(` fix: ${check.fix}
|
|
70020
70300
|
`);
|
|
70021
70301
|
}
|
|
70302
|
+
io.stdout(`${knowledgeLine}
|
|
70303
|
+
`);
|
|
70022
70304
|
io.stdout(`passed ${report.summary.passed} / warned ${report.summary.warned} / failed ${report.summary.failed}
|
|
70023
70305
|
`);
|
|
70024
70306
|
return report.exitCode;
|
|
70025
70307
|
}
|
|
70026
70308
|
var init_doctor2 = __esm(() => {
|
|
70027
70309
|
init_doctor();
|
|
70310
|
+
init_knowledge();
|
|
70028
70311
|
init_store_lock();
|
|
70029
70312
|
init_output();
|
|
70030
70313
|
init_prompt();
|
|
@@ -70135,11 +70418,11 @@ var init_redact = __esm(() => {
|
|
|
70135
70418
|
});
|
|
70136
70419
|
|
|
70137
70420
|
// packages/workit-core/src/checks.ts
|
|
70138
|
-
import { spawn, spawnSync as
|
|
70421
|
+
import { spawn, spawnSync as spawnSync15 } from "node:child_process";
|
|
70139
70422
|
import { createHash as createHash14 } from "node:crypto";
|
|
70140
|
-
import
|
|
70423
|
+
import fs18 from "node:fs";
|
|
70141
70424
|
import os13 from "node:os";
|
|
70142
|
-
import
|
|
70425
|
+
import path37 from "node:path";
|
|
70143
70426
|
import { StringDecoder } from "node:string_decoder";
|
|
70144
70427
|
function tailLines(text, lines = TAIL_LINES) {
|
|
70145
70428
|
const all = text.replace(/\r\n?/gu, `
|
|
@@ -70259,10 +70542,10 @@ function planSpawn(argv, options) {
|
|
|
70259
70542
|
return { file: argv[0], args: [], verbatim: false, shell: true };
|
|
70260
70543
|
if (options.platform !== "win32")
|
|
70261
70544
|
return { file: argv[0], args: argv.slice(1), verbatim: false, shell: false };
|
|
70262
|
-
const win =
|
|
70545
|
+
const win = path37.win32;
|
|
70263
70546
|
const isFile = options.isFile ?? ((file) => {
|
|
70264
70547
|
try {
|
|
70265
|
-
return
|
|
70548
|
+
return fs18.statSync(file).isFile();
|
|
70266
70549
|
} catch {
|
|
70267
70550
|
return false;
|
|
70268
70551
|
}
|
|
@@ -70314,7 +70597,7 @@ function runCheckCommand(argv, options) {
|
|
|
70314
70597
|
if (!child?.pid)
|
|
70315
70598
|
return;
|
|
70316
70599
|
if (windows)
|
|
70317
|
-
|
|
70600
|
+
spawnSync15("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
70318
70601
|
stdio: "ignore",
|
|
70319
70602
|
windowsHide: true
|
|
70320
70603
|
});
|
|
@@ -70416,13 +70699,13 @@ function runCheckCommand(argv, options) {
|
|
|
70416
70699
|
function storeLog(storeRoot, log) {
|
|
70417
70700
|
const digest = logDigest(log);
|
|
70418
70701
|
const ref = `blobs/logs/${digest.slice("sha256:".length)}.log`;
|
|
70419
|
-
const file =
|
|
70702
|
+
const file = path37.join(storeRoot, ref);
|
|
70420
70703
|
try {
|
|
70421
|
-
if (!
|
|
70422
|
-
|
|
70704
|
+
if (!fs18.existsSync(file)) {
|
|
70705
|
+
fs18.mkdirSync(path37.dirname(file), { recursive: true });
|
|
70423
70706
|
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
70424
|
-
|
|
70425
|
-
|
|
70707
|
+
fs18.writeFileSync(temp, log, { mode: 384 });
|
|
70708
|
+
fs18.renameSync(temp, file);
|
|
70426
70709
|
}
|
|
70427
70710
|
return { digest, ref };
|
|
70428
70711
|
} catch {
|
|
@@ -70432,17 +70715,17 @@ function storeLog(storeRoot, log) {
|
|
|
70432
70715
|
function pruneCheckLogs(storeRoot, options = {}) {
|
|
70433
70716
|
const limits = { ...LOG_RETENTION, ...options.retention };
|
|
70434
70717
|
const now = options.now ?? Date.now();
|
|
70435
|
-
const dir =
|
|
70718
|
+
const dir = path37.join(storeRoot, "blobs", "logs");
|
|
70436
70719
|
const report = { removed: 0, removedBytes: 0, kept: 0 };
|
|
70437
70720
|
let names;
|
|
70438
70721
|
try {
|
|
70439
|
-
names =
|
|
70722
|
+
names = fs18.readdirSync(dir);
|
|
70440
70723
|
} catch {
|
|
70441
70724
|
return report;
|
|
70442
70725
|
}
|
|
70443
70726
|
const files = names.flatMap((name) => {
|
|
70444
70727
|
try {
|
|
70445
|
-
const stat =
|
|
70728
|
+
const stat = fs18.statSync(path37.join(dir, name));
|
|
70446
70729
|
return stat.isFile() ? [{ name, size: stat.size, mtimeMs: stat.mtimeMs }] : [];
|
|
70447
70730
|
} catch {
|
|
70448
70731
|
return [];
|
|
@@ -70461,7 +70744,7 @@ function pruneCheckLogs(storeRoot, options = {}) {
|
|
|
70461
70744
|
continue;
|
|
70462
70745
|
if (!options.dryRun)
|
|
70463
70746
|
try {
|
|
70464
|
-
|
|
70747
|
+
fs18.rmSync(path37.join(dir, file.name), { force: true });
|
|
70465
70748
|
} catch {
|
|
70466
70749
|
continue;
|
|
70467
70750
|
}
|
|
@@ -70712,7 +70995,7 @@ var exports_check = {};
|
|
|
70712
70995
|
__export(exports_check, {
|
|
70713
70996
|
run: () => run9
|
|
70714
70997
|
});
|
|
70715
|
-
import
|
|
70998
|
+
import path38 from "node:path";
|
|
70716
70999
|
function parse5(argv) {
|
|
70717
71000
|
const split = argv.indexOf("--");
|
|
70718
71001
|
const head = split >= 0 ? argv.slice(0, split) : [...argv];
|
|
@@ -70895,7 +71178,7 @@ async function run9(argv, io) {
|
|
|
70895
71178
|
if (options.shell && command.length !== 1)
|
|
70896
71179
|
return emit(io, fail("invalid_input", '--shell takes the command as one string: workit check --shell -- "<cmd>"'));
|
|
70897
71180
|
}
|
|
70898
|
-
const relCwd = posix(
|
|
71181
|
+
const relCwd = posix(path38.relative(top, runCwd));
|
|
70899
71182
|
const configured = !options.shell && matchesNamedCheck(config, name, command, relCwd);
|
|
70900
71183
|
const actor = actorFromEnv(io.env);
|
|
70901
71184
|
const branchKey = codeKey(io.cwd, { base: options.base });
|
|
@@ -71019,7 +71302,7 @@ async function run9(argv, io) {
|
|
|
71019
71302
|
return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
|
|
71020
71303
|
return 0;
|
|
71021
71304
|
}
|
|
71022
|
-
var USAGE3 = "workit check <name> | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(
|
|
71305
|
+
var USAGE3 = "workit check <name> | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path38.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
|
|
71023
71306
|
var init_check = __esm(() => {
|
|
71024
71307
|
init_check_config();
|
|
71025
71308
|
init_checks3();
|
|
@@ -71040,7 +71323,7 @@ var init_check = __esm(() => {
|
|
|
71040
71323
|
var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
|
|
71041
71324
|
|
|
71042
71325
|
// packages/workit-core/src/git/ops.ts
|
|
71043
|
-
import { spawnSync as
|
|
71326
|
+
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
71044
71327
|
function dirtEntries(cwd) {
|
|
71045
71328
|
const run = git5(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
71046
71329
|
if (!run.ok)
|
|
@@ -71384,7 +71667,7 @@ ${run.stdout}`;
|
|
|
71384
71667
|
return /stale info|cannot lock ref|fetch first|non-fast-forward/iu.test(text) ? { ok: false, lease: true, error: "the remote branch tip changed before deletion" } : { ok: false, lease: false, error: gitError(run) };
|
|
71385
71668
|
}
|
|
71386
71669
|
var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}) => {
|
|
71387
|
-
const result =
|
|
71670
|
+
const result = spawnSync16("git", args, {
|
|
71388
71671
|
cwd,
|
|
71389
71672
|
encoding: "utf8",
|
|
71390
71673
|
env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
@@ -71420,11 +71703,11 @@ var init_ops = __esm(() => {
|
|
|
71420
71703
|
});
|
|
71421
71704
|
|
|
71422
71705
|
// packages/workit-core/src/forge/reruns.ts
|
|
71423
|
-
import
|
|
71424
|
-
import
|
|
71706
|
+
import fs19 from "node:fs";
|
|
71707
|
+
import path39 from "node:path";
|
|
71425
71708
|
function rerunLogPath(cwd) {
|
|
71426
71709
|
const common = gitCommonDir(cwd);
|
|
71427
|
-
return common ?
|
|
71710
|
+
return common ? path39.join(common, "workit", "ci-reruns.jsonl") : null;
|
|
71428
71711
|
}
|
|
71429
71712
|
function rerunCounts(cwd, key) {
|
|
71430
71713
|
const counts = new Map;
|
|
@@ -71433,7 +71716,7 @@ function rerunCounts(cwd, key) {
|
|
|
71433
71716
|
return counts;
|
|
71434
71717
|
let text;
|
|
71435
71718
|
try {
|
|
71436
|
-
text =
|
|
71719
|
+
text = fs19.readFileSync(file, "utf8");
|
|
71437
71720
|
} catch {
|
|
71438
71721
|
return counts;
|
|
71439
71722
|
}
|
|
@@ -71459,8 +71742,8 @@ function recordReruns(cwd, rows) {
|
|
|
71459
71742
|
if (!file)
|
|
71460
71743
|
return false;
|
|
71461
71744
|
try {
|
|
71462
|
-
|
|
71463
|
-
|
|
71745
|
+
fs19.mkdirSync(path39.dirname(file), { recursive: true });
|
|
71746
|
+
fs19.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
|
|
71464
71747
|
`).join(""));
|
|
71465
71748
|
return true;
|
|
71466
71749
|
} catch {
|
|
@@ -71472,18 +71755,18 @@ function withRerunLock(cwd, fn) {
|
|
|
71472
71755
|
if (!file)
|
|
71473
71756
|
return fn();
|
|
71474
71757
|
const lock = `${file.slice(0, -".jsonl".length)}.lock`;
|
|
71475
|
-
|
|
71758
|
+
fs19.mkdirSync(path39.dirname(lock), { recursive: true });
|
|
71476
71759
|
const until = Date.now() + LOCK_WAIT_MS2;
|
|
71477
71760
|
for (;; ) {
|
|
71478
71761
|
try {
|
|
71479
|
-
|
|
71762
|
+
fs19.closeSync(fs19.openSync(lock, "wx"));
|
|
71480
71763
|
break;
|
|
71481
71764
|
} catch (error) {
|
|
71482
71765
|
if (error.code !== "EEXIST")
|
|
71483
71766
|
throw error;
|
|
71484
71767
|
try {
|
|
71485
|
-
if (Date.now() -
|
|
71486
|
-
|
|
71768
|
+
if (Date.now() - fs19.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
|
|
71769
|
+
fs19.rmSync(lock, { force: true });
|
|
71487
71770
|
continue;
|
|
71488
71771
|
}
|
|
71489
71772
|
} catch {
|
|
@@ -71497,7 +71780,7 @@ function withRerunLock(cwd, fn) {
|
|
|
71497
71780
|
try {
|
|
71498
71781
|
return fn();
|
|
71499
71782
|
} finally {
|
|
71500
|
-
|
|
71783
|
+
fs19.rmSync(lock, { force: true });
|
|
71501
71784
|
}
|
|
71502
71785
|
}
|
|
71503
71786
|
var isRow = (value) => {
|
|
@@ -71865,7 +72148,7 @@ var init_report = __esm(() => {
|
|
|
71865
72148
|
});
|
|
71866
72149
|
|
|
71867
72150
|
// packages/workit-core/src/forge/pr-ops.ts
|
|
71868
|
-
import { spawnSync as
|
|
72151
|
+
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
71869
72152
|
function fillFromCommits(cwd, resolved, base) {
|
|
71870
72153
|
const tracking = `refs/remotes/${resolved.baseRemote}/${base}`;
|
|
71871
72154
|
if (!base.startsWith("-"))
|
|
@@ -71873,7 +72156,7 @@ function fillFromCommits(cwd, resolved, base) {
|
|
|
71873
72156
|
const from = resolveRef(cwd, tracking) ?? resolveRef(cwd, `refs/heads/${base}`);
|
|
71874
72157
|
if (!from)
|
|
71875
72158
|
return failure3("not_found", `base ${base} is not available locally`, `git fetch ${resolved.baseRemote} ${base}`);
|
|
71876
|
-
const log =
|
|
72159
|
+
const log = spawnSync17("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
|
|
71877
72160
|
cwd,
|
|
71878
72161
|
encoding: "utf8",
|
|
71879
72162
|
timeout: GIT_TIMEOUTS.local,
|
|
@@ -72148,17 +72431,17 @@ var init_pr_ops = __esm(() => {
|
|
|
72148
72431
|
});
|
|
72149
72432
|
|
|
72150
72433
|
// packages/workit-core/src/forge/exec.ts
|
|
72151
|
-
import { spawnSync as
|
|
72152
|
-
import
|
|
72153
|
-
import
|
|
72434
|
+
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
72435
|
+
import fs20 from "node:fs";
|
|
72436
|
+
import path40 from "node:path";
|
|
72154
72437
|
function findExecutable(bin, env) {
|
|
72155
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
72438
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path40.delimiter).filter(Boolean);
|
|
72156
72439
|
const exts = process.platform === "win32" ? [".exe"] : [""];
|
|
72157
72440
|
for (const dir of dirs)
|
|
72158
72441
|
for (const ext of exts) {
|
|
72159
|
-
const candidate =
|
|
72442
|
+
const candidate = path40.join(dir, `${bin}${ext}`);
|
|
72160
72443
|
try {
|
|
72161
|
-
const stat =
|
|
72444
|
+
const stat = fs20.statSync(candidate);
|
|
72162
72445
|
if (stat.isFile())
|
|
72163
72446
|
return candidate;
|
|
72164
72447
|
} catch {}
|
|
@@ -72183,7 +72466,7 @@ function systemRunner(kind, apiHost, env = process.env) {
|
|
|
72183
72466
|
if (!exe)
|
|
72184
72467
|
return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
|
|
72185
72468
|
const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
|
|
72186
|
-
const result =
|
|
72469
|
+
const result = spawnSync18(exe, [...args], {
|
|
72187
72470
|
encoding: "utf8",
|
|
72188
72471
|
env: { ...childEnv, ...token },
|
|
72189
72472
|
maxBuffer: MAX_BUFFER2,
|
|
@@ -72853,7 +73136,7 @@ var init_gitlab = __esm(() => {
|
|
|
72853
73136
|
});
|
|
72854
73137
|
|
|
72855
73138
|
// packages/workit-core/src/forge/resolve.ts
|
|
72856
|
-
import
|
|
73139
|
+
import fs21 from "node:fs";
|
|
72857
73140
|
function resolveForge(cwd, options = {}) {
|
|
72858
73141
|
let workspace;
|
|
72859
73142
|
try {
|
|
@@ -72882,7 +73165,7 @@ function resolveForge(cwd, options = {}) {
|
|
|
72882
73165
|
if (derived.via === "host_name" && !explicit)
|
|
72883
73166
|
return failure3("blocked", `token_host_unverified: push host ${derived.host} only looks like ${kind}; workspace ${workspace?.name ?? "?"} vcs.tokenFile is not sent to it`, `if ${apiHost} is your ${kind} server, set ${kind}.host in ~/.config/workit/vcs.json (or vcs.host for the workspace); otherwise fix the push remote`);
|
|
72884
73167
|
try {
|
|
72885
|
-
token =
|
|
73168
|
+
token = fs21.readFileSync(tokenFile.trim(), "utf8").trim();
|
|
72886
73169
|
} catch {
|
|
72887
73170
|
return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
|
|
72888
73171
|
}
|
|
@@ -73017,6 +73300,20 @@ function parseDuration2(value) {
|
|
|
73017
73300
|
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
|
|
73018
73301
|
return Math.round(Number(match[1]) * factor);
|
|
73019
73302
|
}
|
|
73303
|
+
function releaseTrunk(io, track, branch) {
|
|
73304
|
+
const resolved = vcsConfig("resolve", io.cwd, {
|
|
73305
|
+
track,
|
|
73306
|
+
...branch ? { branch } : {}
|
|
73307
|
+
});
|
|
73308
|
+
if (resolved.ok === false)
|
|
73309
|
+
return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null, tracked: false };
|
|
73310
|
+
if (resolved.releaseTrack?.blocking)
|
|
73311
|
+
return { error: String(resolved.releaseTrack.blocking) };
|
|
73312
|
+
return {
|
|
73313
|
+
trunk: String(resolved.defaultTargetBranch ?? "") || null,
|
|
73314
|
+
tracked: Boolean(resolved.releaseTrack)
|
|
73315
|
+
};
|
|
73316
|
+
}
|
|
73020
73317
|
function connect(io, branch) {
|
|
73021
73318
|
const resolved = resolveForge(io.cwd, {
|
|
73022
73319
|
branch,
|
|
@@ -73084,6 +73381,7 @@ function renderStatus(doc) {
|
|
|
73084
73381
|
var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
|
|
73085
73382
|
var init_forge_common = __esm(() => {
|
|
73086
73383
|
init_resolve();
|
|
73384
|
+
init_vcs_config();
|
|
73087
73385
|
init_output();
|
|
73088
73386
|
forgeDeps = {
|
|
73089
73387
|
runner: undefined,
|
|
@@ -73099,7 +73397,7 @@ __export(exports_pr, {
|
|
|
73099
73397
|
run: () => run10
|
|
73100
73398
|
});
|
|
73101
73399
|
import { readFileSync as readFileSync18 } from "node:fs";
|
|
73102
|
-
import
|
|
73400
|
+
import path41 from "node:path";
|
|
73103
73401
|
async function status(argv, io) {
|
|
73104
73402
|
const flags = parseFlags(argv, { pr: "value", branch: "value", "log-lines": "value" });
|
|
73105
73403
|
if (typeof flags === "string")
|
|
@@ -73170,7 +73468,7 @@ async function create4(argv, io) {
|
|
|
73170
73468
|
let body = flags.values.body ?? null;
|
|
73171
73469
|
if (flags.values["body-file"] !== undefined) {
|
|
73172
73470
|
try {
|
|
73173
|
-
body = readFileSync18(
|
|
73471
|
+
body = readFileSync18(path41.resolve(io.cwd, flags.values["body-file"]), "utf8");
|
|
73174
73472
|
} catch {
|
|
73175
73473
|
return usage3(io, `cannot read --body-file ${flags.values["body-file"]}`, CREATE_USAGE);
|
|
73176
73474
|
}
|
|
@@ -73662,7 +73960,7 @@ var init_git2 = __esm(() => {
|
|
|
73662
73960
|
});
|
|
73663
73961
|
|
|
73664
73962
|
// packages/workit-core/src/forge/verify.ts
|
|
73665
|
-
import { spawnSync as
|
|
73963
|
+
import { spawnSync as spawnSync19 } from "node:child_process";
|
|
73666
73964
|
function finish(cwd, report, actor) {
|
|
73667
73965
|
const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
|
|
73668
73966
|
const out = { ...report, delivered };
|
|
@@ -73875,7 +74173,7 @@ var NPM_NAME, SEMVER, SAFE_ARG, systemNpm = (args) => {
|
|
|
73875
74173
|
};
|
|
73876
74174
|
const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
|
|
73877
74175
|
const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
|
|
73878
|
-
const result = process.platform === "win32" ?
|
|
74176
|
+
const result = process.platform === "win32" ? spawnSync19(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync19("npm", [...args], options);
|
|
73879
74177
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
73880
74178
|
}, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
|
|
73881
74179
|
try {
|
|
@@ -73982,10 +74280,10 @@ var init_verify_delivery = __esm(() => {
|
|
|
73982
74280
|
});
|
|
73983
74281
|
|
|
73984
74282
|
// packages/workit-core/src/stack.ts
|
|
73985
|
-
import { spawnSync as
|
|
74283
|
+
import { spawnSync as spawnSync20 } from "node:child_process";
|
|
73986
74284
|
import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
|
|
73987
|
-
import
|
|
73988
|
-
import
|
|
74285
|
+
import fs22 from "node:fs";
|
|
74286
|
+
import path42 from "node:path";
|
|
73989
74287
|
function worktreeOf(cwd, branch) {
|
|
73990
74288
|
const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
|
|
73991
74289
|
if (!list.ok)
|
|
@@ -74008,7 +74306,7 @@ function rebasingBranch2(dir) {
|
|
|
74008
74306
|
if (!found.ok)
|
|
74009
74307
|
continue;
|
|
74010
74308
|
try {
|
|
74011
|
-
return
|
|
74309
|
+
return fs22.readFileSync(found.stdout.trim(), "utf8").trim();
|
|
74012
74310
|
} catch {}
|
|
74013
74311
|
}
|
|
74014
74312
|
return null;
|
|
@@ -74016,7 +74314,7 @@ function rebasingBranch2(dir) {
|
|
|
74016
74314
|
function rebaseInProgress(dir) {
|
|
74017
74315
|
for (const name of ["rebase-merge", "rebase-apply"]) {
|
|
74018
74316
|
const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
|
|
74019
|
-
if (found.ok &&
|
|
74317
|
+
if (found.ok && fs22.existsSync(found.stdout.trim()))
|
|
74020
74318
|
return true;
|
|
74021
74319
|
}
|
|
74022
74320
|
return false;
|
|
@@ -74028,8 +74326,8 @@ function gitRestack(cwd, input) {
|
|
|
74028
74326
|
const scratch = scratchDir(cwd);
|
|
74029
74327
|
if (!scratch)
|
|
74030
74328
|
return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
|
|
74031
|
-
|
|
74032
|
-
dir =
|
|
74329
|
+
fs22.mkdirSync(scratch, { recursive: true });
|
|
74330
|
+
dir = path42.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
|
|
74033
74331
|
const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
|
|
74034
74332
|
if (!added.ok)
|
|
74035
74333
|
return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
|
|
@@ -74067,10 +74365,10 @@ function gitRestack(cwd, input) {
|
|
|
74067
74365
|
}
|
|
74068
74366
|
function pruneScratch(cwd) {
|
|
74069
74367
|
const scratch = scratchDir(cwd);
|
|
74070
|
-
if (!scratch || !
|
|
74368
|
+
if (!scratch || !fs22.existsSync(scratch))
|
|
74071
74369
|
return;
|
|
74072
|
-
for (const name of
|
|
74073
|
-
const dir =
|
|
74370
|
+
for (const name of fs22.readdirSync(scratch)) {
|
|
74371
|
+
const dir = path42.join(scratch, name);
|
|
74074
74372
|
if (!rebaseInProgress(dir) && !trackedDirt(dir))
|
|
74075
74373
|
gitRun3(cwd, ["worktree", "remove", "--force", dir]);
|
|
74076
74374
|
}
|
|
@@ -74080,7 +74378,7 @@ function stacksDir(cwd) {
|
|
|
74080
74378
|
const root = storeRoot(cwd);
|
|
74081
74379
|
if (!root.ok)
|
|
74082
74380
|
return stackFail(root.code, root.error, root.unblock);
|
|
74083
|
-
return { ok: true, data:
|
|
74381
|
+
return { ok: true, data: path42.join(root.value.root, "stacks") };
|
|
74084
74382
|
}
|
|
74085
74383
|
function parseStack(raw) {
|
|
74086
74384
|
if (!isRecord7(raw) || !Array.isArray(raw.branches))
|
|
@@ -74132,10 +74430,10 @@ function readStack(cwd, name) {
|
|
|
74132
74430
|
const dir = stacksDir(cwd);
|
|
74133
74431
|
if (!dir.ok)
|
|
74134
74432
|
return dir;
|
|
74135
|
-
const file =
|
|
74433
|
+
const file = path42.join(dir.data, fileFor(name));
|
|
74136
74434
|
let text;
|
|
74137
74435
|
try {
|
|
74138
|
-
text =
|
|
74436
|
+
text = fs22.readFileSync(file, "utf8");
|
|
74139
74437
|
} catch (error) {
|
|
74140
74438
|
if (error.code === "ENOENT")
|
|
74141
74439
|
return { ok: true, data: null };
|
|
@@ -74153,17 +74451,17 @@ function writeStack(cwd, stack, now = new Date) {
|
|
|
74153
74451
|
const dir = stacksDir(cwd);
|
|
74154
74452
|
if (!dir.ok)
|
|
74155
74453
|
return dir;
|
|
74156
|
-
const file =
|
|
74454
|
+
const file = path42.join(dir.data, fileFor(stack.name));
|
|
74157
74455
|
const temp = `${file}.${process.pid}.tmp`;
|
|
74158
74456
|
try {
|
|
74159
|
-
|
|
74457
|
+
fs22.mkdirSync(dir.data, { recursive: true });
|
|
74160
74458
|
stack.updatedAt = now.toISOString();
|
|
74161
|
-
|
|
74459
|
+
fs22.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
|
|
74162
74460
|
`);
|
|
74163
|
-
|
|
74461
|
+
fs22.renameSync(temp, file);
|
|
74164
74462
|
return { ok: true, data: undefined };
|
|
74165
74463
|
} catch (error) {
|
|
74166
|
-
|
|
74464
|
+
fs22.rmSync(temp, { force: true });
|
|
74167
74465
|
return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
|
|
74168
74466
|
}
|
|
74169
74467
|
}
|
|
@@ -74173,7 +74471,7 @@ function listStacks(cwd) {
|
|
|
74173
74471
|
return dir;
|
|
74174
74472
|
let names = [];
|
|
74175
74473
|
try {
|
|
74176
|
-
names =
|
|
74474
|
+
names = fs22.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
|
|
74177
74475
|
} catch {
|
|
74178
74476
|
return { ok: true, data: [] };
|
|
74179
74477
|
}
|
|
@@ -74181,7 +74479,7 @@ function listStacks(cwd) {
|
|
|
74181
74479
|
for (const name of names) {
|
|
74182
74480
|
let parsed = null;
|
|
74183
74481
|
try {
|
|
74184
|
-
parsed = parseStack(JSON.parse(
|
|
74482
|
+
parsed = parseStack(JSON.parse(fs22.readFileSync(path42.join(dir.data, name), "utf8")));
|
|
74185
74483
|
} catch {
|
|
74186
74484
|
parsed = null;
|
|
74187
74485
|
}
|
|
@@ -74228,7 +74526,7 @@ function stackLockStale(raw, age) {
|
|
|
74228
74526
|
}
|
|
74229
74527
|
function takeLock(lock) {
|
|
74230
74528
|
try {
|
|
74231
|
-
|
|
74529
|
+
fs22.mkdirSync(lock);
|
|
74232
74530
|
} catch (error) {
|
|
74233
74531
|
if (error.code === "EEXIST")
|
|
74234
74532
|
return null;
|
|
@@ -74240,24 +74538,24 @@ function takeLock(lock) {
|
|
|
74240
74538
|
host: localLockHost(),
|
|
74241
74539
|
nonce: randomBytes3(8).toString("hex")
|
|
74242
74540
|
};
|
|
74243
|
-
|
|
74541
|
+
fs22.writeFileSync(path42.join(lock, OWNER), JSON.stringify(owner));
|
|
74244
74542
|
return owner;
|
|
74245
74543
|
}
|
|
74246
74544
|
function reapLock(lock, observed) {
|
|
74247
74545
|
const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
|
|
74248
74546
|
try {
|
|
74249
|
-
|
|
74547
|
+
fs22.renameSync(lock, tomb);
|
|
74250
74548
|
} catch {
|
|
74251
74549
|
return false;
|
|
74252
74550
|
}
|
|
74253
74551
|
if (readOwner(tomb).raw === observed) {
|
|
74254
|
-
|
|
74552
|
+
fs22.rmSync(tomb, { recursive: true, force: true });
|
|
74255
74553
|
return true;
|
|
74256
74554
|
}
|
|
74257
74555
|
try {
|
|
74258
|
-
|
|
74556
|
+
fs22.renameSync(tomb, lock);
|
|
74259
74557
|
} catch {
|
|
74260
|
-
|
|
74558
|
+
fs22.rmSync(tomb, { recursive: true, force: true });
|
|
74261
74559
|
}
|
|
74262
74560
|
return false;
|
|
74263
74561
|
}
|
|
@@ -74267,8 +74565,8 @@ function releaseLock(lock, owner) {
|
|
|
74267
74565
|
return;
|
|
74268
74566
|
const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
|
|
74269
74567
|
try {
|
|
74270
|
-
|
|
74271
|
-
|
|
74568
|
+
fs22.renameSync(lock, tomb);
|
|
74569
|
+
fs22.rmSync(tomb, { recursive: true, force: true });
|
|
74272
74570
|
} catch {}
|
|
74273
74571
|
}
|
|
74274
74572
|
async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
|
|
@@ -74277,9 +74575,9 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
|
|
|
74277
74575
|
return dir;
|
|
74278
74576
|
if (!validName(name))
|
|
74279
74577
|
return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
|
|
74280
|
-
const lock =
|
|
74578
|
+
const lock = path42.join(dir.data, `${fileFor(name)}.lock`);
|
|
74281
74579
|
try {
|
|
74282
|
-
|
|
74580
|
+
fs22.mkdirSync(dir.data, { recursive: true });
|
|
74283
74581
|
} catch (error) {
|
|
74284
74582
|
return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
|
|
74285
74583
|
}
|
|
@@ -74307,7 +74605,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
|
|
|
74307
74605
|
const touch = () => {
|
|
74308
74606
|
try {
|
|
74309
74607
|
const now = new Date;
|
|
74310
|
-
|
|
74608
|
+
fs22.utimesSync(path42.join(lock, OWNER), now, now);
|
|
74311
74609
|
} catch {}
|
|
74312
74610
|
};
|
|
74313
74611
|
const beat = setInterval(touch, HEARTBEAT_MS);
|
|
@@ -74495,7 +74793,7 @@ function commitKeys(cwd, range) {
|
|
|
74495
74793
|
const patches = new Map;
|
|
74496
74794
|
const diff = gitRun3(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
|
|
74497
74795
|
if (diff.ok && diff.stdout.trim()) {
|
|
74498
|
-
const ids =
|
|
74796
|
+
const ids = spawnSync20("git", ["patch-id", "--stable"], {
|
|
74499
74797
|
cwd,
|
|
74500
74798
|
input: diff.stdout,
|
|
74501
74799
|
encoding: "utf8",
|
|
@@ -75103,7 +75401,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
|
|
|
75103
75401
|
...unblock ? { unblock } : {},
|
|
75104
75402
|
...data ? { data } : {}
|
|
75105
75403
|
}), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
75106
|
-
const run =
|
|
75404
|
+
const run = spawnSync20("git", args, {
|
|
75107
75405
|
cwd,
|
|
75108
75406
|
encoding: "utf8",
|
|
75109
75407
|
timeout: timeoutMs,
|
|
@@ -75131,7 +75429,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
|
|
|
75131
75429
|
return !status.ok || status.stdout.trim() !== "";
|
|
75132
75430
|
}, scratchDir = (cwd) => {
|
|
75133
75431
|
const dir = stacksDir(cwd);
|
|
75134
|
-
return dir.ok ?
|
|
75432
|
+
return dir.ok ? path42.join(dir.data, "worktrees") : null;
|
|
75135
75433
|
}, firstLine = (run) => (`${run.stderr}
|
|
75136
75434
|
${run.stdout}`.split(`
|
|
75137
75435
|
`).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
|
|
@@ -75146,14 +75444,14 @@ ${run.stdout}`.split(`
|
|
|
75146
75444
|
}, isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str3 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
|
|
75147
75445
|
let raw = null;
|
|
75148
75446
|
try {
|
|
75149
|
-
raw =
|
|
75447
|
+
raw = fs22.readFileSync(path42.join(lock, OWNER), "utf8");
|
|
75150
75448
|
} catch {
|
|
75151
75449
|
raw = null;
|
|
75152
75450
|
}
|
|
75153
75451
|
let age = null;
|
|
75154
|
-
for (const target of [
|
|
75452
|
+
for (const target of [path42.join(lock, OWNER), lock])
|
|
75155
75453
|
try {
|
|
75156
|
-
age = Date.now() -
|
|
75454
|
+
age = Date.now() - fs22.statSync(target).mtimeMs;
|
|
75157
75455
|
break;
|
|
75158
75456
|
} catch {}
|
|
75159
75457
|
return { raw, age };
|
|
@@ -75207,7 +75505,7 @@ async function plan(argv, io) {
|
|
|
75207
75505
|
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
|
|
75208
75506
|
let trunk = flags.values.trunk ?? existing?.trunk ?? null;
|
|
75209
75507
|
if (trunk === null || track !== null) {
|
|
75210
|
-
const derived =
|
|
75508
|
+
const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
|
|
75211
75509
|
if ("error" in derived)
|
|
75212
75510
|
return emit(io, fail("blocked", derived.error, {
|
|
75213
75511
|
unblock: "workit stack plan --track <name> … # or --trunk <branch>"
|
|
@@ -75375,17 +75673,7 @@ async function run14(argv, io) {
|
|
|
75375
75673
|
return land(rest, io);
|
|
75376
75674
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
|
|
75377
75675
|
}
|
|
75378
|
-
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })),
|
|
75379
|
-
const resolved = vcsConfig("resolve", io.cwd, {
|
|
75380
|
-
track,
|
|
75381
|
-
...branch ? { branch } : {}
|
|
75382
|
-
});
|
|
75383
|
-
if (resolved.ok === false)
|
|
75384
|
-
return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
|
|
75385
|
-
if (resolved.releaseTrack?.blocking)
|
|
75386
|
-
return { error: String(resolved.releaseTrack.blocking) };
|
|
75387
|
-
return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
|
|
75388
|
-
}, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
75676
|
+
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
75389
75677
|
`${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
|
|
75390
75678
|
...data.steps.map((step) => [
|
|
75391
75679
|
` ${step.branch} on ${step.parent}:`,
|
|
@@ -75397,16 +75685,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
75397
75685
|
var init_stack2 = __esm(() => {
|
|
75398
75686
|
init_ledger();
|
|
75399
75687
|
init_stack();
|
|
75400
|
-
init_vcs_config();
|
|
75401
75688
|
init_output();
|
|
75402
75689
|
init_forge_common();
|
|
75403
75690
|
METHODS3 = new Set(["squash", "merge", "rebase"]);
|
|
75404
75691
|
});
|
|
75405
75692
|
|
|
75693
|
+
// packages/workit-core/src/fanout.ts
|
|
75694
|
+
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
75695
|
+
import fs23 from "node:fs";
|
|
75696
|
+
import path43 from "node:path";
|
|
75697
|
+
function scopeSamples(pattern) {
|
|
75698
|
+
if (!hasWildcard(pattern))
|
|
75699
|
+
return [unescape2(pattern)];
|
|
75700
|
+
let out = "";
|
|
75701
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
75702
|
+
const char = pattern[index];
|
|
75703
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
75704
|
+
out += pattern[index + 1];
|
|
75705
|
+
index += 1;
|
|
75706
|
+
} else if (char === "*" && pattern[index + 1] === "*") {
|
|
75707
|
+
const atSegmentStart = index === 0 || pattern[index - 1] === "/";
|
|
75708
|
+
if (atSegmentStart && pattern[index + 2] === "/")
|
|
75709
|
+
index += 2;
|
|
75710
|
+
else {
|
|
75711
|
+
out += "<any>";
|
|
75712
|
+
index += 1;
|
|
75713
|
+
}
|
|
75714
|
+
} else if (char === "*")
|
|
75715
|
+
out += "<any>";
|
|
75716
|
+
else if (char === "?")
|
|
75717
|
+
out += "x";
|
|
75718
|
+
else if (char === "{" && pattern.indexOf("}", index) > index) {
|
|
75719
|
+
const close = pattern.indexOf("}", index);
|
|
75720
|
+
out += pattern.slice(index + 1, close).split(",")[0];
|
|
75721
|
+
index = close;
|
|
75722
|
+
} else
|
|
75723
|
+
out += char;
|
|
75724
|
+
}
|
|
75725
|
+
return [out];
|
|
75726
|
+
}
|
|
75727
|
+
function globSource(pattern) {
|
|
75728
|
+
let out = "";
|
|
75729
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
75730
|
+
const char = pattern[index];
|
|
75731
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
75732
|
+
out += escapeRegExp4(pattern[index + 1]);
|
|
75733
|
+
index += 1;
|
|
75734
|
+
} else if (char === "*" && pattern[index + 1] === "*") {
|
|
75735
|
+
const atSegmentStart = index === 0 || pattern[index - 1] === "/";
|
|
75736
|
+
if (atSegmentStart && pattern[index + 2] === "/") {
|
|
75737
|
+
out += "(?:[^/]*/)*";
|
|
75738
|
+
index += 2;
|
|
75739
|
+
} else {
|
|
75740
|
+
out += ".*";
|
|
75741
|
+
index += 1;
|
|
75742
|
+
}
|
|
75743
|
+
} else if (char === "*")
|
|
75744
|
+
out += "[^/]*";
|
|
75745
|
+
else if (char === "?")
|
|
75746
|
+
out += "[^/]";
|
|
75747
|
+
else if (char === "{" && pattern.indexOf("}", index) > index) {
|
|
75748
|
+
const close = pattern.indexOf("}", index);
|
|
75749
|
+
const alternatives = pattern.slice(index + 1, close).split(",").map(globSource);
|
|
75750
|
+
out += `(?:${alternatives.join("|")})`;
|
|
75751
|
+
index = close;
|
|
75752
|
+
} else
|
|
75753
|
+
out += escapeRegExp4(char);
|
|
75754
|
+
}
|
|
75755
|
+
return out;
|
|
75756
|
+
}
|
|
75757
|
+
function matchesScope(pattern, file) {
|
|
75758
|
+
let regex = compiled.get(pattern);
|
|
75759
|
+
if (!regex) {
|
|
75760
|
+
regex = new RegExp(`^${globSource(pattern)}$`, "u");
|
|
75761
|
+
compiled.set(pattern, regex);
|
|
75762
|
+
}
|
|
75763
|
+
return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
|
|
75764
|
+
}
|
|
75765
|
+
function normalizePattern(raw) {
|
|
75766
|
+
let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
|
|
75767
|
+
while (pattern.startsWith("./"))
|
|
75768
|
+
pattern = pattern.slice(2);
|
|
75769
|
+
if (pattern === "." || pattern === "")
|
|
75770
|
+
pattern = raw.trim() ? "**" : "";
|
|
75771
|
+
if (pattern.endsWith("/"))
|
|
75772
|
+
pattern = `${pattern}**`;
|
|
75773
|
+
if (!pattern)
|
|
75774
|
+
return { error: "is empty" };
|
|
75775
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
|
|
75776
|
+
return { error: "must be relative to the repository root" };
|
|
75777
|
+
if (pattern.split("/").includes(".."))
|
|
75778
|
+
return { error: "must not contain .." };
|
|
75779
|
+
const bare = pattern.replace(/\\./gu, "");
|
|
75780
|
+
if (/[[\]]/u.test(bare))
|
|
75781
|
+
return {
|
|
75782
|
+
error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
|
|
75783
|
+
};
|
|
75784
|
+
if (bare.startsWith("!"))
|
|
75785
|
+
return { error: "uses ! (negation is not supported)" };
|
|
75786
|
+
let depth = 0;
|
|
75787
|
+
for (const char of bare) {
|
|
75788
|
+
if (char === "{")
|
|
75789
|
+
depth += 1;
|
|
75790
|
+
if (char === "}")
|
|
75791
|
+
depth -= 1;
|
|
75792
|
+
if (depth < 0 || depth > 1)
|
|
75793
|
+
return { error: "has unbalanced or nested braces" };
|
|
75794
|
+
}
|
|
75795
|
+
if (depth !== 0)
|
|
75796
|
+
return { error: "has unbalanced or nested braces" };
|
|
75797
|
+
return { pattern };
|
|
75798
|
+
}
|
|
75799
|
+
function inScope(plan, slice, file) {
|
|
75800
|
+
if (matchesAny(slice.owns, file))
|
|
75801
|
+
return true;
|
|
75802
|
+
if (!matchesAny(slice.scope, file))
|
|
75803
|
+
return false;
|
|
75804
|
+
return !plan.slices.some((other) => other.id !== slice.id && matchesAny(other.owns, file));
|
|
75805
|
+
}
|
|
75806
|
+
function parsePlan(raw, defaults, name) {
|
|
75807
|
+
const problems = [];
|
|
75808
|
+
if (!isRecord8(raw))
|
|
75809
|
+
return { ok: false, problems: ["the plan must be a JSON object"] };
|
|
75810
|
+
const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
|
|
75811
|
+
if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
|
|
75812
|
+
problems.push('name: pass --name or set a 1-200 character "name"');
|
|
75813
|
+
const trunk = typeof raw.trunk === "string" && raw.trunk.trim() ? raw.trunk.trim() : defaults.trunk;
|
|
75814
|
+
const shared = [];
|
|
75815
|
+
for (const entry of textList(raw.shared ?? []) ?? ["<not a list>"]) {
|
|
75816
|
+
const normalized = normalizePattern(entry);
|
|
75817
|
+
if ("error" in normalized)
|
|
75818
|
+
problems.push(`shared "${entry}" ${normalized.error}`);
|
|
75819
|
+
else
|
|
75820
|
+
shared.push(normalized.pattern);
|
|
75821
|
+
}
|
|
75822
|
+
if (!Array.isArray(raw.slices) || raw.slices.length === 0)
|
|
75823
|
+
return { ok: false, problems: [...problems, "slices: list at least one slice"] };
|
|
75824
|
+
const slices = [];
|
|
75825
|
+
const seenIds = new Set;
|
|
75826
|
+
const seenBranches = new Map;
|
|
75827
|
+
raw.slices.forEach((item, index) => {
|
|
75828
|
+
const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
|
|
75829
|
+
if (!isRecord8(item)) {
|
|
75830
|
+
problems.push(`${where}: must be an object`);
|
|
75831
|
+
return;
|
|
75832
|
+
}
|
|
75833
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
75834
|
+
if (!ID.test(id))
|
|
75835
|
+
problems.push(`${where}: id must match ${ID.source}`);
|
|
75836
|
+
else if (seenIds.has(id))
|
|
75837
|
+
problems.push(`${where}: duplicate id`);
|
|
75838
|
+
seenIds.add(id);
|
|
75839
|
+
const branch = typeof item.branch === "string" ? item.branch.trim() : "";
|
|
75840
|
+
if (!branch)
|
|
75841
|
+
problems.push(`${where}: branch is empty`);
|
|
75842
|
+
else if (seenBranches.has(branch))
|
|
75843
|
+
problems.push(`${where}: branch ${branch} is also slice ${seenBranches.get(branch)}'s`);
|
|
75844
|
+
else
|
|
75845
|
+
seenBranches.set(branch, id);
|
|
75846
|
+
if (branch === trunk)
|
|
75847
|
+
problems.push(`${where}: branch must not be the trunk ${trunk}`);
|
|
75848
|
+
const goal = typeof item.goal === "string" ? item.goal.trim() : "";
|
|
75849
|
+
if (!filled(goal))
|
|
75850
|
+
problems.push(`${where}: goal is empty`);
|
|
75851
|
+
const brief = (field) => {
|
|
75852
|
+
const list = textList(item[field]);
|
|
75853
|
+
const kept = (list ?? []).map((entry) => entry.trim()).filter(filled);
|
|
75854
|
+
if (kept.length === 0)
|
|
75855
|
+
problems.push(`${where}: ${field} is empty`);
|
|
75856
|
+
return kept;
|
|
75857
|
+
};
|
|
75858
|
+
const acceptance = brief("acceptance");
|
|
75859
|
+
const verify = brief("verify");
|
|
75860
|
+
const forbidden = brief("forbidden");
|
|
75861
|
+
const scope = [];
|
|
75862
|
+
const owns = [];
|
|
75863
|
+
for (const [field, out] of [
|
|
75864
|
+
["scope", scope],
|
|
75865
|
+
["owns", owns]
|
|
75866
|
+
]) {
|
|
75867
|
+
const list = textList(item[field] ?? []);
|
|
75868
|
+
if (list === null) {
|
|
75869
|
+
problems.push(`${where}: ${field} must be a list of globs`);
|
|
75870
|
+
continue;
|
|
75871
|
+
}
|
|
75872
|
+
for (const entry of list.filter(filled)) {
|
|
75873
|
+
const normalized = normalizePattern(entry);
|
|
75874
|
+
if ("error" in normalized)
|
|
75875
|
+
problems.push(`${where}: ${field} "${entry}" ${normalized.error}`);
|
|
75876
|
+
else
|
|
75877
|
+
out.push(normalized.pattern);
|
|
75878
|
+
}
|
|
75879
|
+
}
|
|
75880
|
+
if (scope.length === 0 && textList(item.scope ?? []) !== null)
|
|
75881
|
+
problems.push(`${where}: scope is empty`);
|
|
75882
|
+
const tier = item.tier;
|
|
75883
|
+
if (typeof tier !== "string" || !TIERS.includes(tier))
|
|
75884
|
+
problems.push(`${where}: tier must be one of ${TIERS.join(", ")}`);
|
|
75885
|
+
const dependsOn = textList(item.dependsOn ?? []);
|
|
75886
|
+
if (dependsOn === null)
|
|
75887
|
+
problems.push(`${where}: dependsOn must be a list of slice ids`);
|
|
75888
|
+
const optional = (field) => typeof item[field] === "string" && filled(item[field]) ? item[field].trim() : null;
|
|
75889
|
+
slices.push({
|
|
75890
|
+
id,
|
|
75891
|
+
branch,
|
|
75892
|
+
base: optional("base") ?? "",
|
|
75893
|
+
worktree: optional("worktree") ?? path43.join(defaults.worktreeRoot, id),
|
|
75894
|
+
tier,
|
|
75895
|
+
dependsOn: [...new Set(dependsOn ?? [])],
|
|
75896
|
+
scope,
|
|
75897
|
+
owns,
|
|
75898
|
+
goal,
|
|
75899
|
+
acceptance,
|
|
75900
|
+
verify,
|
|
75901
|
+
forbidden,
|
|
75902
|
+
context: optional("context"),
|
|
75903
|
+
timebox: optional("timebox")
|
|
75904
|
+
});
|
|
75905
|
+
});
|
|
75906
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75907
|
+
const byBranch = new Map(slices.map((slice) => [slice.branch, slice]));
|
|
75908
|
+
for (const slice of slices) {
|
|
75909
|
+
for (const dep of slice.dependsOn) {
|
|
75910
|
+
if (dep === slice.id)
|
|
75911
|
+
problems.push(`slice ${slice.id}: depends on itself`);
|
|
75912
|
+
else if (!byId.has(dep))
|
|
75913
|
+
problems.push(`slice ${slice.id}: depends on unknown slice ${dep}`);
|
|
75914
|
+
}
|
|
75915
|
+
const explicitBase = slice.base;
|
|
75916
|
+
if (explicitBase) {
|
|
75917
|
+
const parent = byBranch.get(explicitBase);
|
|
75918
|
+
if (parent && !slice.dependsOn.includes(parent.id))
|
|
75919
|
+
problems.push(`slice ${slice.id}: base ${explicitBase} is slice ${parent.id}'s branch; add "${parent.id}" to its dependsOn`);
|
|
75920
|
+
} else {
|
|
75921
|
+
const single = slice.dependsOn.length === 1 ? byId.get(slice.dependsOn[0]) : undefined;
|
|
75922
|
+
slice.base = single ? single.branch : trunk;
|
|
75923
|
+
}
|
|
75924
|
+
}
|
|
75925
|
+
const cycle = findCycle(slices);
|
|
75926
|
+
if (cycle)
|
|
75927
|
+
problems.push(`dependency cycle: ${cycle.join(" -> ")}`);
|
|
75928
|
+
if (problems.length)
|
|
75929
|
+
return { ok: false, problems };
|
|
75930
|
+
return {
|
|
75931
|
+
ok: true,
|
|
75932
|
+
plan: {
|
|
75933
|
+
v: FANOUT_VERSION,
|
|
75934
|
+
name: planName,
|
|
75935
|
+
trunk,
|
|
75936
|
+
shared,
|
|
75937
|
+
slices,
|
|
75938
|
+
createdAt: "",
|
|
75939
|
+
updatedAt: ""
|
|
75940
|
+
}
|
|
75941
|
+
};
|
|
75942
|
+
}
|
|
75943
|
+
function findCycle(slices) {
|
|
75944
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75945
|
+
const state = new Map;
|
|
75946
|
+
const trail = [];
|
|
75947
|
+
const visit = (id) => {
|
|
75948
|
+
if (state.get(id) === "done")
|
|
75949
|
+
return null;
|
|
75950
|
+
if (state.get(id) === "open")
|
|
75951
|
+
return [...trail.slice(trail.indexOf(id)), id];
|
|
75952
|
+
state.set(id, "open");
|
|
75953
|
+
trail.push(id);
|
|
75954
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
75955
|
+
if (!byId.has(dep) || dep === id)
|
|
75956
|
+
continue;
|
|
75957
|
+
const found = visit(dep);
|
|
75958
|
+
if (found)
|
|
75959
|
+
return found;
|
|
75960
|
+
}
|
|
75961
|
+
trail.pop();
|
|
75962
|
+
state.set(id, "done");
|
|
75963
|
+
return null;
|
|
75964
|
+
};
|
|
75965
|
+
for (const slice of slices) {
|
|
75966
|
+
const found = visit(slice.id);
|
|
75967
|
+
if (found)
|
|
75968
|
+
return found;
|
|
75969
|
+
}
|
|
75970
|
+
return null;
|
|
75971
|
+
}
|
|
75972
|
+
function landingOrder(slices) {
|
|
75973
|
+
const order = [];
|
|
75974
|
+
const placed = new Set;
|
|
75975
|
+
const known = new Set(slices.map((slice) => slice.id));
|
|
75976
|
+
while (order.length < slices.length) {
|
|
75977
|
+
const next = slices.find((slice) => !placed.has(slice.id) && slice.dependsOn.every((dep) => placed.has(dep) || !known.has(dep)));
|
|
75978
|
+
if (!next)
|
|
75979
|
+
break;
|
|
75980
|
+
order.push(next.id);
|
|
75981
|
+
placed.add(next.id);
|
|
75982
|
+
}
|
|
75983
|
+
return order;
|
|
75984
|
+
}
|
|
75985
|
+
function waves(slices) {
|
|
75986
|
+
const level = new Map;
|
|
75987
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75988
|
+
for (const id of landingOrder(slices)) {
|
|
75989
|
+
const deps = byId.get(id)?.dependsOn ?? [];
|
|
75990
|
+
level.set(id, deps.length ? Math.max(...deps.map((dep) => level.get(dep) ?? 0)) + 1 : 0);
|
|
75991
|
+
}
|
|
75992
|
+
const out = [];
|
|
75993
|
+
for (const slice of slices)
|
|
75994
|
+
(out[level.get(slice.id) ?? 0] ??= []).push(slice.id);
|
|
75995
|
+
return out.filter(Boolean);
|
|
75996
|
+
}
|
|
75997
|
+
function ancestors(slices) {
|
|
75998
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75999
|
+
const out = new Map;
|
|
76000
|
+
for (const id of landingOrder(slices)) {
|
|
76001
|
+
const set = new Set;
|
|
76002
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
76003
|
+
set.add(dep);
|
|
76004
|
+
for (const deeper of out.get(dep) ?? [])
|
|
76005
|
+
set.add(deeper);
|
|
76006
|
+
}
|
|
76007
|
+
out.set(id, set);
|
|
76008
|
+
}
|
|
76009
|
+
return out;
|
|
76010
|
+
}
|
|
76011
|
+
function analyzeOverlap(plan, candidates) {
|
|
76012
|
+
const shared = [...DEFAULT_SHARED, ...plan.shared];
|
|
76013
|
+
const before = ancestors(plan.slices);
|
|
76014
|
+
const related = (a, b) => Boolean(before.get(a)?.has(b) || before.get(b)?.has(a));
|
|
76015
|
+
const overlaps = [];
|
|
76016
|
+
const groups = new Map;
|
|
76017
|
+
const byCase = new Map;
|
|
76018
|
+
for (const file of [...new Set(candidates)].toSorted())
|
|
76019
|
+
(byCase.get(file.toLowerCase()) ?? byCase.set(file.toLowerCase(), []).get(file.toLowerCase()))?.push(file);
|
|
76020
|
+
for (const spellings of byCase.values()) {
|
|
76021
|
+
const file = spellings.join(" ~ ");
|
|
76022
|
+
const any = (patterns) => spellings.some((spelling) => matchesAny(patterns, spelling));
|
|
76023
|
+
const matching = plan.slices.filter((slice) => any(slice.scope) || any(slice.owns));
|
|
76024
|
+
if (matching.length < 2)
|
|
76025
|
+
continue;
|
|
76026
|
+
const ids = matching.map((slice) => slice.id);
|
|
76027
|
+
const owners = matching.filter((slice) => any(slice.owns));
|
|
76028
|
+
if (owners.length === 1) {
|
|
76029
|
+
overlaps.push({ path: file, slices: ids, resolution: "owner", owner: owners[0].id });
|
|
76030
|
+
continue;
|
|
76031
|
+
}
|
|
76032
|
+
if (owners.length === 0 && matching.every((a, i) => matching.slice(i + 1).every((b) => related(a.id, b.id)))) {
|
|
76033
|
+
overlaps.push({ path: file, slices: ids, resolution: "serialized" });
|
|
76034
|
+
continue;
|
|
76035
|
+
}
|
|
76036
|
+
const members = owners.length > 1 ? owners : matching;
|
|
76037
|
+
const reason = owners.length > 1 ? "several_owners" : "overlap";
|
|
76038
|
+
const isShared = any(shared);
|
|
76039
|
+
const key = `${reason}|${isShared}|${members.map((slice) => slice.id).join(",")}`;
|
|
76040
|
+
const group = groups.get(key);
|
|
76041
|
+
if (group)
|
|
76042
|
+
group.paths.push(file);
|
|
76043
|
+
else
|
|
76044
|
+
groups.set(key, {
|
|
76045
|
+
slices: members.map((slice) => slice.id),
|
|
76046
|
+
paths: [file],
|
|
76047
|
+
shared: isShared,
|
|
76048
|
+
reason,
|
|
76049
|
+
members,
|
|
76050
|
+
suggestion: ownerSuggestion(members[0].id, [])
|
|
76051
|
+
});
|
|
76052
|
+
}
|
|
76053
|
+
const conflicts = [...groups.values()].map(({ members, ...group }) => {
|
|
76054
|
+
const literal = members.find((slice) => slice.scope.some((pattern) => group.paths.includes(pattern)));
|
|
76055
|
+
const owner = ownerSuggestion((literal ?? members[0]).id, group.paths);
|
|
76056
|
+
if (group.reason === "several_owners")
|
|
76057
|
+
return {
|
|
76058
|
+
...group,
|
|
76059
|
+
suggestion: {
|
|
76060
|
+
...owner,
|
|
76061
|
+
text: `keep ${quoteList(group.paths)} in "owns" of slice ${owner.slice} only`
|
|
76062
|
+
}
|
|
76063
|
+
};
|
|
76064
|
+
const serialize = serializeSuggestion(group.slices);
|
|
76065
|
+
return group.shared ? { ...group, suggestion: owner, alternative: serialize } : { ...group, suggestion: serialize, alternative: owner };
|
|
76066
|
+
});
|
|
76067
|
+
return { overlaps, conflicts };
|
|
76068
|
+
}
|
|
76069
|
+
function branchRef(cwd, branch) {
|
|
76070
|
+
if (!branch || branch.startsWith("-"))
|
|
76071
|
+
return null;
|
|
76072
|
+
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
76073
|
+
if (local)
|
|
76074
|
+
return local;
|
|
76075
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
76076
|
+
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
76077
|
+
}
|
|
76078
|
+
function trunkRef(cwd, trunk) {
|
|
76079
|
+
if (!trunk || trunk.startsWith("-"))
|
|
76080
|
+
return null;
|
|
76081
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
76082
|
+
for (const ref of [`refs/remotes/${remote}/${trunk}`, `refs/heads/${trunk}`, trunk])
|
|
76083
|
+
if (resolveRef(cwd, ref))
|
|
76084
|
+
return ref;
|
|
76085
|
+
return null;
|
|
76086
|
+
}
|
|
76087
|
+
function defaultTrunk(cwd) {
|
|
76088
|
+
const base = defaultBase(cwd);
|
|
76089
|
+
return base ? base.replace(/^origin\//u, "") : "main";
|
|
76090
|
+
}
|
|
76091
|
+
function worktreeRoot(cwd) {
|
|
76092
|
+
const common = gitCommonDir(cwd);
|
|
76093
|
+
const main = common && path43.basename(common) === ".git" ? path43.dirname(common) : cwd;
|
|
76094
|
+
return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
|
|
76095
|
+
}
|
|
76096
|
+
function fanoutsDir(cwd) {
|
|
76097
|
+
const root = storeRoot(cwd);
|
|
76098
|
+
if (!root.ok)
|
|
76099
|
+
return fanoutFail(root.code, root.error, root.unblock);
|
|
76100
|
+
return { ok: true, data: path43.join(root.value.root, "fanouts") };
|
|
76101
|
+
}
|
|
76102
|
+
function parseStored(raw) {
|
|
76103
|
+
if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
|
|
76104
|
+
return null;
|
|
76105
|
+
if (!Array.isArray(raw.slices))
|
|
76106
|
+
return null;
|
|
76107
|
+
const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
76108
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
76109
|
+
let globsValid = true;
|
|
76110
|
+
const globs = (value) => list(value).map((entry) => {
|
|
76111
|
+
const normalized = normalizePattern(entry);
|
|
76112
|
+
if ("error" in normalized) {
|
|
76113
|
+
globsValid = false;
|
|
76114
|
+
return entry;
|
|
76115
|
+
}
|
|
76116
|
+
return normalized.pattern;
|
|
76117
|
+
});
|
|
76118
|
+
const slices = [];
|
|
76119
|
+
for (const item of raw.slices) {
|
|
76120
|
+
if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
|
|
76121
|
+
return null;
|
|
76122
|
+
slices.push({
|
|
76123
|
+
id: item.id,
|
|
76124
|
+
branch: item.branch,
|
|
76125
|
+
base: text(item.base) || raw.trunk,
|
|
76126
|
+
worktree: text(item.worktree),
|
|
76127
|
+
tier: TIERS.includes(item.tier) ? item.tier : "standard",
|
|
76128
|
+
dependsOn: list(item.dependsOn),
|
|
76129
|
+
scope: globs(item.scope),
|
|
76130
|
+
owns: globs(item.owns),
|
|
76131
|
+
goal: text(item.goal),
|
|
76132
|
+
acceptance: list(item.acceptance),
|
|
76133
|
+
verify: list(item.verify),
|
|
76134
|
+
forbidden: list(item.forbidden),
|
|
76135
|
+
context: typeof item.context === "string" ? item.context : null,
|
|
76136
|
+
timebox: typeof item.timebox === "string" ? item.timebox : null
|
|
76137
|
+
});
|
|
76138
|
+
}
|
|
76139
|
+
const shared = globs(raw.shared);
|
|
76140
|
+
const ids = new Set(slices.map((slice) => slice.id));
|
|
76141
|
+
if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
|
|
76142
|
+
return null;
|
|
76143
|
+
return {
|
|
76144
|
+
v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
|
|
76145
|
+
name: raw.name,
|
|
76146
|
+
trunk: raw.trunk,
|
|
76147
|
+
shared,
|
|
76148
|
+
slices,
|
|
76149
|
+
createdAt: text(raw.createdAt),
|
|
76150
|
+
updatedAt: text(raw.updatedAt)
|
|
76151
|
+
};
|
|
76152
|
+
}
|
|
76153
|
+
function readFanout(cwd, name) {
|
|
76154
|
+
const dir = fanoutsDir(cwd);
|
|
76155
|
+
if (!dir.ok)
|
|
76156
|
+
return dir;
|
|
76157
|
+
const file = path43.join(dir.data, stackFileName(name));
|
|
76158
|
+
let parsed = null;
|
|
76159
|
+
try {
|
|
76160
|
+
parsed = parseStored(JSON.parse(fs23.readFileSync(file, "utf8")));
|
|
76161
|
+
} catch (error) {
|
|
76162
|
+
if (error.code === "ENOENT")
|
|
76163
|
+
return { ok: true, data: null };
|
|
76164
|
+
parsed = null;
|
|
76165
|
+
}
|
|
76166
|
+
return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
|
|
76167
|
+
}
|
|
76168
|
+
function listFanouts(cwd) {
|
|
76169
|
+
const dir = fanoutsDir(cwd);
|
|
76170
|
+
if (!dir.ok)
|
|
76171
|
+
return dir;
|
|
76172
|
+
let names = [];
|
|
76173
|
+
try {
|
|
76174
|
+
names = fs23.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
|
|
76175
|
+
} catch {
|
|
76176
|
+
return { ok: true, data: [] };
|
|
76177
|
+
}
|
|
76178
|
+
const out = [];
|
|
76179
|
+
for (const entry of names) {
|
|
76180
|
+
let parsed = null;
|
|
76181
|
+
try {
|
|
76182
|
+
parsed = parseStored(JSON.parse(fs23.readFileSync(path43.join(dir.data, entry), "utf8")));
|
|
76183
|
+
} catch {
|
|
76184
|
+
parsed = null;
|
|
76185
|
+
}
|
|
76186
|
+
if (parsed && stackFileName(parsed.name) === entry)
|
|
76187
|
+
out.push(parsed);
|
|
76188
|
+
}
|
|
76189
|
+
return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
|
|
76190
|
+
}
|
|
76191
|
+
function selectFanout(cwd, name, current) {
|
|
76192
|
+
if (name) {
|
|
76193
|
+
const read = readFanout(cwd, name);
|
|
76194
|
+
if (!read.ok)
|
|
76195
|
+
return read;
|
|
76196
|
+
return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
|
|
76197
|
+
}
|
|
76198
|
+
const all = listFanouts(cwd);
|
|
76199
|
+
if (!all.ok)
|
|
76200
|
+
return all;
|
|
76201
|
+
const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
|
|
76202
|
+
if (holding.length === 1)
|
|
76203
|
+
return { ok: true, data: holding[0] };
|
|
76204
|
+
if (all.data.length === 1)
|
|
76205
|
+
return { ok: true, data: all.data[0] };
|
|
76206
|
+
return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
|
|
76207
|
+
}
|
|
76208
|
+
function writeFanout(cwd, plan) {
|
|
76209
|
+
const dir = fanoutsDir(cwd);
|
|
76210
|
+
if (!dir.ok)
|
|
76211
|
+
return dir;
|
|
76212
|
+
const file = path43.join(dir.data, stackFileName(plan.name));
|
|
76213
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
76214
|
+
try {
|
|
76215
|
+
fs23.mkdirSync(dir.data, { recursive: true });
|
|
76216
|
+
fs23.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
|
|
76217
|
+
`);
|
|
76218
|
+
fs23.renameSync(temp, file);
|
|
76219
|
+
return { ok: true, data: file };
|
|
76220
|
+
} catch (error) {
|
|
76221
|
+
fs23.rmSync(temp, { force: true });
|
|
76222
|
+
return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
|
|
76223
|
+
}
|
|
76224
|
+
}
|
|
76225
|
+
function planFanout(cwd, input, now = new Date) {
|
|
76226
|
+
const dir = fanoutsDir(cwd);
|
|
76227
|
+
if (!dir.ok)
|
|
76228
|
+
return dir;
|
|
76229
|
+
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, { trunk: input.trunk ?? defaultTrunk(cwd), worktreeRoot: worktreeRoot(cwd) }, input.name);
|
|
76230
|
+
if (!parsed.ok)
|
|
76231
|
+
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
|
+
const plan = parsed.plan;
|
|
76233
|
+
const badBranches = plan.slices.filter((slice) => !gitRun4(cwd, ["check-ref-format", "--branch", slice.branch]).ok).map((slice) => `slice ${slice.id}: ${slice.branch} is not a valid branch name`);
|
|
76234
|
+
if (badBranches.length)
|
|
76235
|
+
return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
|
|
76236
|
+
problems: badBranches
|
|
76237
|
+
});
|
|
76238
|
+
const ref = trunkRef(cwd, plan.trunk);
|
|
76239
|
+
const tracked = ref ? nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout) : [];
|
|
76240
|
+
const samples = plan.slices.flatMap((slice) => [...slice.scope, ...slice.owns].flatMap(scopeSamples));
|
|
76241
|
+
const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
|
|
76242
|
+
if (conflicts.length)
|
|
76243
|
+
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 });
|
|
76244
|
+
const existing = readFanout(cwd, plan.name);
|
|
76245
|
+
const previous = existing.ok ? existing.data : null;
|
|
76246
|
+
plan.createdAt = previous?.createdAt || now.toISOString();
|
|
76247
|
+
plan.updatedAt = now.toISOString();
|
|
76248
|
+
const written = writeFanout(cwd, plan);
|
|
76249
|
+
if (!written.ok)
|
|
76250
|
+
return written;
|
|
76251
|
+
appendObserved(cwd, {
|
|
76252
|
+
type: "fanout.planned",
|
|
76253
|
+
actor: input.actor,
|
|
76254
|
+
fanout: plan.name,
|
|
76255
|
+
trunk: plan.trunk,
|
|
76256
|
+
slices: plan.slices.length,
|
|
76257
|
+
ids: plan.slices.slice(0, 20).map((slice) => slice.id)
|
|
76258
|
+
});
|
|
76259
|
+
const grouped = waves(plan.slices);
|
|
76260
|
+
const notes = [];
|
|
76261
|
+
if (!ref)
|
|
76262
|
+
notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
|
|
76263
|
+
grouped.forEach((wave, index) => {
|
|
76264
|
+
if (wave.length > IN_FLIGHT_CAP)
|
|
76265
|
+
notes.push(`wave ${index + 1} has ${wave.length} slices; keep 4-${IN_FLIGHT_CAP} in flight and queue the rest`);
|
|
76266
|
+
});
|
|
76267
|
+
for (const slice of plan.slices)
|
|
76268
|
+
if (slice.dependsOn.length > 1 && slice.base === plan.trunk)
|
|
76269
|
+
notes.push(`slice ${slice.id} depends on ${slice.dependsOn.join(", ")}: start it from ${plan.trunk} after they land`);
|
|
76270
|
+
return {
|
|
76271
|
+
ok: true,
|
|
76272
|
+
data: {
|
|
76273
|
+
name: plan.name,
|
|
76274
|
+
trunk: plan.trunk,
|
|
76275
|
+
file: written.data,
|
|
76276
|
+
created: previous === null,
|
|
76277
|
+
slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
|
|
76278
|
+
id,
|
|
76279
|
+
branch,
|
|
76280
|
+
base,
|
|
76281
|
+
worktree,
|
|
76282
|
+
tier,
|
|
76283
|
+
dependsOn,
|
|
76284
|
+
scope,
|
|
76285
|
+
owns
|
|
76286
|
+
})),
|
|
76287
|
+
overlaps,
|
|
76288
|
+
waves: grouped,
|
|
76289
|
+
landingOrder: landingOrder(plan.slices),
|
|
76290
|
+
notes
|
|
76291
|
+
}
|
|
76292
|
+
};
|
|
76293
|
+
}
|
|
76294
|
+
var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
76295
|
+
ok: false,
|
|
76296
|
+
code,
|
|
76297
|
+
error,
|
|
76298
|
+
...unblock ? { unblock } : {},
|
|
76299
|
+
...data ? { data } : {}
|
|
76300
|
+
}), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER3, filled = (text) => text.trim() !== "" && !PLACEHOLDER3.test(text.trim()), textList = (value) => {
|
|
76301
|
+
if (typeof value === "string")
|
|
76302
|
+
return [value];
|
|
76303
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
76304
|
+
return value;
|
|
76305
|
+
return null;
|
|
76306
|
+
}, ID, quoteList = (items) => items.map((item) => `"${item}"`).join(", "), ownerSuggestion = (slice, paths) => ({
|
|
76307
|
+
kind: "owner",
|
|
76308
|
+
slice,
|
|
76309
|
+
paths,
|
|
76310
|
+
text: `add ${quoteList(paths)} to "owns" of slice ${slice}`
|
|
76311
|
+
}), serializeSuggestion = (ids) => {
|
|
76312
|
+
const edges = ids.slice(1).map((slice, index) => ({ slice, dependsOn: ids[index] }));
|
|
76313
|
+
return {
|
|
76314
|
+
kind: "serialize",
|
|
76315
|
+
edges,
|
|
76316
|
+
text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
|
|
76317
|
+
};
|
|
76318
|
+
}, gitRun4 = (cwd, args) => {
|
|
76319
|
+
const run = spawnSync21("git", args, {
|
|
76320
|
+
cwd,
|
|
76321
|
+
encoding: "utf8",
|
|
76322
|
+
timeout: GIT_TIMEOUTS.local,
|
|
76323
|
+
killSignal: "SIGKILL",
|
|
76324
|
+
windowsHide: true,
|
|
76325
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
76326
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANGUAGE: "C" }
|
|
76327
|
+
});
|
|
76328
|
+
return {
|
|
76329
|
+
ok: run.status === 0,
|
|
76330
|
+
status: run.status,
|
|
76331
|
+
stdout: run.stdout ?? "",
|
|
76332
|
+
stderr: run.error ? String(run.error.message) : run.stderr ?? ""
|
|
76333
|
+
};
|
|
76334
|
+
}, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
|
|
76335
|
+
var init_fanout = __esm(() => {
|
|
76336
|
+
init_rev();
|
|
76337
|
+
init_ledger();
|
|
76338
|
+
init_stack();
|
|
76339
|
+
TIERS = ["mundane", "standard", "hard"];
|
|
76340
|
+
DEFAULT_SHARED = [
|
|
76341
|
+
"**/package.json",
|
|
76342
|
+
"**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
|
|
76343
|
+
"**/{Cargo.lock,go.mod,go.sum,poetry.lock,uv.lock,Gemfile.lock,composer.lock}",
|
|
76344
|
+
"**/index.{ts,tsx,js,jsx,mjs,cjs}",
|
|
76345
|
+
"**/tsconfig.json",
|
|
76346
|
+
".github/**",
|
|
76347
|
+
".gitlab-ci.yml",
|
|
76348
|
+
"CHANGELOG.md"
|
|
76349
|
+
];
|
|
76350
|
+
compiled = new Map;
|
|
76351
|
+
PLACEHOLDER3 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
|
|
76352
|
+
ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
76353
|
+
});
|
|
76354
|
+
|
|
76355
|
+
// packages/workit-core/src/fanout-check.ts
|
|
76356
|
+
function mergeConflicts(cwd, a, b) {
|
|
76357
|
+
const run = gitRun4(cwd, [
|
|
76358
|
+
"merge-tree",
|
|
76359
|
+
"--write-tree",
|
|
76360
|
+
"--name-only",
|
|
76361
|
+
"--no-messages",
|
|
76362
|
+
"-z",
|
|
76363
|
+
a,
|
|
76364
|
+
b
|
|
76365
|
+
]);
|
|
76366
|
+
if (run.status === 0)
|
|
76367
|
+
return [];
|
|
76368
|
+
if (run.status === 1)
|
|
76369
|
+
return [...new Set(nulList2(run.stdout).slice(1))];
|
|
76370
|
+
const first = run.stderr.trim().split(`
|
|
76371
|
+
`)[0] || `git merge-tree exited ${run.status}`;
|
|
76372
|
+
return {
|
|
76373
|
+
error: /write-tree|usage: git merge-tree/u.test(run.stderr) ? `${first} (needs git >= 2.38 for merge-tree --write-tree)` : first
|
|
76374
|
+
};
|
|
76375
|
+
}
|
|
76376
|
+
function checkFanout(cwd, plan, options = {}) {
|
|
76377
|
+
const unknown = (options.only ?? []).filter((id) => !plan.slices.some((s) => s.id === id));
|
|
76378
|
+
if (unknown.length)
|
|
76379
|
+
return fanoutFail("invalid_input", `${unknown.join(", ")}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((slice) => slice.id).join(", ")}`);
|
|
76380
|
+
const trunk = options.trunkRef ?? trunkRef(cwd, plan.trunk);
|
|
76381
|
+
if (!SAFE(trunk) || !resolves(cwd, trunk))
|
|
76382
|
+
return fanoutFail("unavailable", `trunk ${options.trunkRef ?? plan.trunk} does not resolve here`, `git fetch, or pass --base <ref>`);
|
|
76383
|
+
const order = landingOrder(plan.slices);
|
|
76384
|
+
const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
|
|
76385
|
+
const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
|
|
76386
|
+
const ledger = readLedger(cwd);
|
|
76387
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
76388
|
+
const checks = new Map;
|
|
76389
|
+
const changedBy = new Map;
|
|
76390
|
+
for (const slice of plan.slices)
|
|
76391
|
+
checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
|
|
76392
|
+
const conflicts = [];
|
|
76393
|
+
for (let i = 0;i < order.length; i += 1)
|
|
76394
|
+
for (let j = i + 1;j < order.length; j += 1) {
|
|
76395
|
+
const first = order[i];
|
|
76396
|
+
const second = order[j];
|
|
76397
|
+
const a = tips.get(first);
|
|
76398
|
+
const b = tips.get(second);
|
|
76399
|
+
if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
|
|
76400
|
+
continue;
|
|
76401
|
+
const check = checks.get(second);
|
|
76402
|
+
const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
|
|
76403
|
+
const cased = (changedBy.get(second) ?? []).flatMap((file) => {
|
|
76404
|
+
const other = theirs.get(file.toLowerCase());
|
|
76405
|
+
return other !== undefined && other !== file ? [`${other} ~ ${file}`] : [];
|
|
76406
|
+
});
|
|
76407
|
+
if (cased.length) {
|
|
76408
|
+
conflicts.push({
|
|
76409
|
+
first,
|
|
76410
|
+
second,
|
|
76411
|
+
paths: cased,
|
|
76412
|
+
caseOnly: true,
|
|
76413
|
+
resolution: `use ${first}'s spelling on ${second} (git mv), then workit fanout check`
|
|
76414
|
+
});
|
|
76415
|
+
check.siblingConflicts.push({ with: first, paths: cased });
|
|
76416
|
+
check.reasons.push(`differs only in case from ${first}: ${cased.join(", ")}`);
|
|
76417
|
+
}
|
|
76418
|
+
const result = mergeConflicts(cwd, a, b);
|
|
76419
|
+
if (!Array.isArray(result)) {
|
|
76420
|
+
check.reasons.push(`cannot merge with ${first}: ${result.error}`);
|
|
76421
|
+
continue;
|
|
76422
|
+
}
|
|
76423
|
+
if (result.length === 0)
|
|
76424
|
+
continue;
|
|
76425
|
+
const related = byId.get(second)?.dependsOn.includes(first);
|
|
76426
|
+
conflicts.push({
|
|
76427
|
+
first,
|
|
76428
|
+
second,
|
|
76429
|
+
paths: result,
|
|
76430
|
+
resolution: related ? `rebase ${second} onto ${byId.get(first)?.branch} (its dependency), then workit fanout check` : `land ${first} first, then rebase ${second} onto ${plan.trunk} and run workit fanout check; or make ${second} depend on ${first}`
|
|
76431
|
+
});
|
|
76432
|
+
check.siblingConflicts.push({ with: first, paths: result });
|
|
76433
|
+
check.reasons.push(`conflicts with ${first} on ${result.join(", ")}`);
|
|
76434
|
+
}
|
|
76435
|
+
for (const id of order) {
|
|
76436
|
+
const check = checks.get(id);
|
|
76437
|
+
check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
|
|
76438
|
+
if (check.waitsFor.length)
|
|
76439
|
+
check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
|
|
76440
|
+
check.status = check.reasons.length ? "blocked" : "ready";
|
|
76441
|
+
}
|
|
76442
|
+
const selected = options.only?.length ? new Set(options.only) : null;
|
|
76443
|
+
const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
|
|
76444
|
+
const ready = slices.every((check) => check.status === "ready");
|
|
76445
|
+
const blocked = slices.find((check) => check.status === "blocked");
|
|
76446
|
+
const notes = plan.v === FANOUT_VERSION ? [] : [
|
|
76447
|
+
`plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
|
|
76448
|
+
];
|
|
76449
|
+
const next = blocked ? unblockFor(blocked, plan) : `land in this order: ${slices.map((check) => check.id).join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)`;
|
|
76450
|
+
return {
|
|
76451
|
+
ok: true,
|
|
76452
|
+
data: {
|
|
76453
|
+
name: plan.name,
|
|
76454
|
+
trunk: plan.trunk,
|
|
76455
|
+
trunkRef: trunk,
|
|
76456
|
+
ready,
|
|
76457
|
+
slices,
|
|
76458
|
+
conflicts: conflicts.filter((conflict) => !selected || selected.has(conflict.first) || selected.has(conflict.second)),
|
|
76459
|
+
landingOrder: order,
|
|
76460
|
+
notes,
|
|
76461
|
+
next
|
|
76462
|
+
}
|
|
76463
|
+
};
|
|
76464
|
+
}
|
|
76465
|
+
function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
|
|
76466
|
+
const head = tips.get(slice.id) ?? null;
|
|
76467
|
+
const check = {
|
|
76468
|
+
id: slice.id,
|
|
76469
|
+
branch: slice.branch,
|
|
76470
|
+
head,
|
|
76471
|
+
base: slice.base,
|
|
76472
|
+
status: "blocked",
|
|
76473
|
+
changed: 0,
|
|
76474
|
+
outOfScope: [],
|
|
76475
|
+
trunkConflicts: [],
|
|
76476
|
+
siblingConflicts: [],
|
|
76477
|
+
waitsFor: [],
|
|
76478
|
+
verdict: { accepted: false, basis: "missing" },
|
|
76479
|
+
reasons: []
|
|
76480
|
+
};
|
|
76481
|
+
if (!head) {
|
|
76482
|
+
check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
|
|
76483
|
+
return check;
|
|
76484
|
+
}
|
|
76485
|
+
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);
|
|
76487
|
+
if (!SAFE(baseRef)) {
|
|
76488
|
+
check.reasons.push(`base ${slice.base} not found`);
|
|
76489
|
+
return check;
|
|
76490
|
+
}
|
|
76491
|
+
const diff = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", `${baseRef}...${head}`]);
|
|
76492
|
+
if (!diff.ok) {
|
|
76493
|
+
check.reasons.push(`cannot diff ${slice.base}...${slice.branch}: ${diff.stderr.trim()}`);
|
|
76494
|
+
return check;
|
|
76495
|
+
}
|
|
76496
|
+
const changed = nulList2(diff.stdout);
|
|
76497
|
+
changedBy.set(slice.id, changed);
|
|
76498
|
+
check.changed = changed.length;
|
|
76499
|
+
if (changed.length === 0)
|
|
76500
|
+
check.reasons.push(`no changes beyond ${slice.base}`);
|
|
76501
|
+
check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
|
|
76502
|
+
if (check.outOfScope.length)
|
|
76503
|
+
check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
|
|
76504
|
+
const trunkResult = mergeConflicts(cwd, trunk, head);
|
|
76505
|
+
if (Array.isArray(trunkResult)) {
|
|
76506
|
+
check.trunkConflicts = trunkResult;
|
|
76507
|
+
if (trunkResult.length)
|
|
76508
|
+
check.reasons.push(`conflicts with ${plan.trunk} on ${trunkResult.join(", ")}`);
|
|
76509
|
+
} else
|
|
76510
|
+
check.reasons.push(`cannot merge with ${plan.trunk}: ${trunkResult.error}`);
|
|
76511
|
+
const verdict = checkVerdicts(cwd, slice.branch, rows);
|
|
76512
|
+
check.verdict = { accepted: verdict.accepted.accepted, basis: verdict.current.basis };
|
|
76513
|
+
return check;
|
|
76514
|
+
}
|
|
76515
|
+
function unblockFor(check, plan) {
|
|
76516
|
+
if (!check.head)
|
|
76517
|
+
return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
|
|
76518
|
+
if (check.outOfScope.length)
|
|
76519
|
+
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`;
|
|
76520
|
+
if (check.trunkConflicts.length)
|
|
76521
|
+
return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
|
|
76522
|
+
if (check.siblingConflicts.length) {
|
|
76523
|
+
const first = check.siblingConflicts[0].with;
|
|
76524
|
+
return `land ${first} first, then rebase ${check.branch} onto ${plan.trunk} and run workit fanout check`;
|
|
76525
|
+
}
|
|
76526
|
+
if (check.waitsFor.length)
|
|
76527
|
+
return `unblock ${check.waitsFor.join(", ")} first`;
|
|
76528
|
+
return check.reasons[0] ?? "workit fanout check";
|
|
76529
|
+
}
|
|
76530
|
+
var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok;
|
|
76531
|
+
var init_fanout_check = __esm(() => {
|
|
76532
|
+
init_ledger();
|
|
76533
|
+
init_fanout();
|
|
76534
|
+
});
|
|
76535
|
+
|
|
76536
|
+
// packages/workit-cli/src/verbs/fanout.ts
|
|
76537
|
+
var exports_fanout = {};
|
|
76538
|
+
__export(exports_fanout, {
|
|
76539
|
+
run: () => run15
|
|
76540
|
+
});
|
|
76541
|
+
import fs24 from "node:fs";
|
|
76542
|
+
import path44 from "node:path";
|
|
76543
|
+
function renderBlockedPlan(io, result) {
|
|
76544
|
+
if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
|
|
76545
|
+
return;
|
|
76546
|
+
const problems = result.data?.problems;
|
|
76547
|
+
if (Array.isArray(problems))
|
|
76548
|
+
for (const problem of problems)
|
|
76549
|
+
io.stderr(` - ${problem}
|
|
76550
|
+
`);
|
|
76551
|
+
const conflicts = result.data?.conflicts;
|
|
76552
|
+
if (Array.isArray(conflicts))
|
|
76553
|
+
for (const conflict of conflicts)
|
|
76554
|
+
io.stderr(` - ${conflict.slices.join(" + ")}: ${conflict.paths.join(", ")}
|
|
76555
|
+
fix: ${conflict.suggestion.text}${conflict.alternative ? `
|
|
76556
|
+
or: ${conflict.alternative.text}` : ""}
|
|
76557
|
+
`);
|
|
76558
|
+
}
|
|
76559
|
+
function resolveTrunk(io, raw, flag, track) {
|
|
76560
|
+
if (flag !== null)
|
|
76561
|
+
return { trunk: flag };
|
|
76562
|
+
const named = typeof raw === "object" && raw !== null && typeof raw.trunk === "string" ? raw.trunk.trim() || null : null;
|
|
76563
|
+
if (named !== null && track === null)
|
|
76564
|
+
return { trunk: null };
|
|
76565
|
+
const derived = releaseTrunk(io, track, null);
|
|
76566
|
+
if ("error" in derived)
|
|
76567
|
+
return {
|
|
76568
|
+
code: emit(io, fail("blocked", derived.error, {
|
|
76569
|
+
unblock: "workit fanout plan <plan.json> --track <name> # or --trunk <branch>"
|
|
76570
|
+
}))
|
|
76571
|
+
};
|
|
76572
|
+
if (named !== null && derived.trunk !== named)
|
|
76573
|
+
return {
|
|
76574
|
+
code: usage3(io, `the plan names trunk ${named}; --track ${track} targets ${derived.trunk ?? "(none)"}`, PLAN_USAGE2)
|
|
76575
|
+
};
|
|
76576
|
+
return { trunk: derived.tracked || track !== null ? derived.trunk : null };
|
|
76577
|
+
}
|
|
76578
|
+
function plan2(argv, io) {
|
|
76579
|
+
const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
|
|
76580
|
+
if (typeof flags === "string")
|
|
76581
|
+
return usage3(io, flags, PLAN_USAGE2);
|
|
76582
|
+
const track = flags.values.track ?? null;
|
|
76583
|
+
if (track !== null && flags.values.trunk !== undefined)
|
|
76584
|
+
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
|
|
76585
|
+
if (flags.positionals.length !== 1)
|
|
76586
|
+
return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
|
|
76587
|
+
const file = path44.resolve(io.cwd, flags.positionals[0]);
|
|
76588
|
+
let raw;
|
|
76589
|
+
try {
|
|
76590
|
+
raw = JSON.parse(fs24.readFileSync(file, "utf8"));
|
|
76591
|
+
} catch (error) {
|
|
76592
|
+
return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
|
|
76593
|
+
}
|
|
76594
|
+
const trunk = resolveTrunk(io, raw, flags.values.trunk ?? null, track);
|
|
76595
|
+
if ("code" in trunk)
|
|
76596
|
+
return trunk.code;
|
|
76597
|
+
const result = planFanout(io.cwd, {
|
|
76598
|
+
raw,
|
|
76599
|
+
name: flags.values.name ?? null,
|
|
76600
|
+
trunk: trunk.trunk,
|
|
76601
|
+
actor: actorFromEnv(io.env)
|
|
76602
|
+
});
|
|
76603
|
+
if (!result.ok) {
|
|
76604
|
+
const code = failed(io, result);
|
|
76605
|
+
renderBlockedPlan(io, result);
|
|
76606
|
+
return code;
|
|
76607
|
+
}
|
|
76608
|
+
return emit(io, ok(result.data), (data) => [
|
|
76609
|
+
`${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
|
|
76610
|
+
...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
|
|
76611
|
+
worktree ${slice.worktree}`),
|
|
76612
|
+
...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
|
|
76613
|
+
`waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
|
|
76614
|
+
`landing order: ${data.landingOrder.join(", ")}`,
|
|
76615
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
76616
|
+
]);
|
|
76617
|
+
}
|
|
76618
|
+
function check2(argv, io) {
|
|
76619
|
+
const flags = parseFlags(argv, { name: "value", base: "value" });
|
|
76620
|
+
if (typeof flags === "string")
|
|
76621
|
+
return usage3(io, flags, CHECK_USAGE);
|
|
76622
|
+
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
76623
|
+
if (!selected.ok)
|
|
76624
|
+
return failed(io, selected);
|
|
76625
|
+
const result = checkFanout(io.cwd, selected.data, {
|
|
76626
|
+
only: flags.positionals,
|
|
76627
|
+
trunkRef: flags.values.base ?? null
|
|
76628
|
+
});
|
|
76629
|
+
if (!result.ok)
|
|
76630
|
+
return failed(io, result);
|
|
76631
|
+
if (result.data.ready)
|
|
76632
|
+
return emit(io, ok(result.data), renderCheck);
|
|
76633
|
+
if (!io.json)
|
|
76634
|
+
for (const line of renderCheck(result.data))
|
|
76635
|
+
io.stdout(`${line}
|
|
76636
|
+
`);
|
|
76637
|
+
const first = result.data.slices.find((slice) => slice.status === "blocked");
|
|
76638
|
+
return emit(io, fail("blocked", `slice ${first?.id ?? "?"} is not ready: ${first?.reasons[0] ?? "blocked"}`, {
|
|
76639
|
+
data: result.data,
|
|
76640
|
+
unblock: result.data.next
|
|
76641
|
+
}));
|
|
76642
|
+
}
|
|
76643
|
+
async function run15(argv, io) {
|
|
76644
|
+
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
76645
|
+
if (sub === "plan")
|
|
76646
|
+
return plan2(rest, io);
|
|
76647
|
+
if (sub === "check")
|
|
76648
|
+
return check2(rest, io);
|
|
76649
|
+
return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
|
|
76650
|
+
}
|
|
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) => [
|
|
76652
|
+
`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 ? `
|
|
76654
|
+
${slice.reasons.join(`
|
|
76655
|
+
`)}` : ""}`),
|
|
76656
|
+
...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
|
|
76657
|
+
fix: ${conflict.resolution}`),
|
|
76658
|
+
`landing order: ${data.landingOrder.join(", ")}`,
|
|
76659
|
+
`next: ${data.next}`,
|
|
76660
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
76661
|
+
];
|
|
76662
|
+
var init_fanout2 = __esm(() => {
|
|
76663
|
+
init_fanout_check();
|
|
76664
|
+
init_fanout();
|
|
76665
|
+
init_rev();
|
|
76666
|
+
init_ledger();
|
|
76667
|
+
init_output();
|
|
76668
|
+
init_forge_common();
|
|
76669
|
+
});
|
|
76670
|
+
|
|
75406
76671
|
// packages/workit-cli/src/verbs/ledger.ts
|
|
75407
76672
|
var exports_ledger = {};
|
|
75408
76673
|
__export(exports_ledger, {
|
|
75409
|
-
run: () =>
|
|
76674
|
+
run: () => run16
|
|
75410
76675
|
});
|
|
75411
76676
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
75412
76677
|
import { parseArgs } from "node:util";
|
|
@@ -75430,7 +76695,7 @@ function targetBranch(io, rows, branch, pr) {
|
|
|
75430
76695
|
}
|
|
75431
76696
|
return { ok: true, value: branch ?? null };
|
|
75432
76697
|
}
|
|
75433
|
-
async function
|
|
76698
|
+
async function run16(argv, io) {
|
|
75434
76699
|
let parsed;
|
|
75435
76700
|
try {
|
|
75436
76701
|
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
@@ -75458,7 +76723,7 @@ async function run15(argv, io) {
|
|
|
75458
76723
|
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
75459
76724
|
const ledger = readLedger(io.cwd);
|
|
75460
76725
|
if (!ledger.ok)
|
|
75461
|
-
return
|
|
76726
|
+
return failed2(io, ledger);
|
|
75462
76727
|
const { rows } = ledger.value;
|
|
75463
76728
|
if (sub === "list" || sub === "show") {
|
|
75464
76729
|
if (rest.length)
|
|
@@ -75480,7 +76745,7 @@ async function run15(argv, io) {
|
|
|
75480
76745
|
return usage4(io, `unexpected argument: ${rest.at(-1)}`);
|
|
75481
76746
|
const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
|
|
75482
76747
|
if (!target.ok)
|
|
75483
|
-
return
|
|
76748
|
+
return failed2(io, target);
|
|
75484
76749
|
const branch = target.value ?? currentBranch(io.cwd);
|
|
75485
76750
|
if (!branch)
|
|
75486
76751
|
return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
|
|
@@ -75490,7 +76755,7 @@ async function run15(argv, io) {
|
|
|
75490
76755
|
}
|
|
75491
76756
|
const target = targetBranch(io, rows, values.branch, pr);
|
|
75492
76757
|
if (!target.ok)
|
|
75493
|
-
return
|
|
76758
|
+
return failed2(io, target);
|
|
75494
76759
|
const acting = actorFor(io, values);
|
|
75495
76760
|
if (acting instanceof Error)
|
|
75496
76761
|
return usage4(io, acting.message);
|
|
@@ -75529,7 +76794,7 @@ async function run15(argv, io) {
|
|
|
75529
76794
|
..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
|
|
75530
76795
|
})), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
|
|
75531
76796
|
}
|
|
75532
|
-
var
|
|
76797
|
+
var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
|
|
75533
76798
|
const actor = actorFromEnv(io.env);
|
|
75534
76799
|
if (values.session !== undefined && values.as !== undefined)
|
|
75535
76800
|
return new Error("pass --session or --as, not both");
|
|
@@ -75553,7 +76818,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
75553
76818
|
return;
|
|
75554
76819
|
const parsed = Number(value);
|
|
75555
76820
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
|
|
75556
|
-
},
|
|
76821
|
+
}, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), verdictLines = (check) => {
|
|
75557
76822
|
const lines = [
|
|
75558
76823
|
`${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}; review: ${check.review}`
|
|
75559
76824
|
];
|
|
@@ -90550,7 +91815,7 @@ var init_ast = __esm(() => {
|
|
|
90550
91815
|
|
|
90551
91816
|
// packages/workit-cli/src/test-audit/rules.ts
|
|
90552
91817
|
import { builtinModules } from "node:module";
|
|
90553
|
-
import
|
|
91818
|
+
import path45 from "node:path";
|
|
90554
91819
|
function testCallee(call) {
|
|
90555
91820
|
let callee = call.callee;
|
|
90556
91821
|
if (callee.type === "CallExpression")
|
|
@@ -91043,12 +92308,12 @@ function checkConstantsOnly(ctx, emit) {
|
|
|
91043
92308
|
}
|
|
91044
92309
|
function checkOverMocking(ctx, emit) {
|
|
91045
92310
|
const { program } = ctx.parsed;
|
|
91046
|
-
const unit =
|
|
92311
|
+
const unit = path45.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
|
|
91047
92312
|
const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
|
|
91048
92313
|
const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
|
|
91049
92314
|
for (const call of mocked) {
|
|
91050
92315
|
const spec = call.arguments[0].value;
|
|
91051
|
-
const base =
|
|
92316
|
+
const base = path45.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
91052
92317
|
if (base === unit)
|
|
91053
92318
|
emit({
|
|
91054
92319
|
rule: "over-mocking",
|
|
@@ -91436,11 +92701,11 @@ var init_rules = __esm(() => {
|
|
|
91436
92701
|
});
|
|
91437
92702
|
|
|
91438
92703
|
// packages/workit-cli/src/test-audit/audit.ts
|
|
91439
|
-
import { spawnSync as
|
|
92704
|
+
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
91440
92705
|
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
|
|
91441
|
-
import
|
|
92706
|
+
import path46 from "node:path";
|
|
91442
92707
|
function git6(cwd, args) {
|
|
91443
|
-
const run =
|
|
92708
|
+
const run = spawnSync22("git", ["-c", "core.quotepath=off", ...args], {
|
|
91444
92709
|
cwd,
|
|
91445
92710
|
encoding: "utf8",
|
|
91446
92711
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -91451,7 +92716,7 @@ function walkTests(dir, out) {
|
|
|
91451
92716
|
for (const entry of readdirSync8(dir, { withFileTypes: true })) {
|
|
91452
92717
|
if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
|
|
91453
92718
|
continue;
|
|
91454
|
-
const full =
|
|
92719
|
+
const full = path46.join(dir, entry.name);
|
|
91455
92720
|
if (entry.isDirectory()) {
|
|
91456
92721
|
if (!SKIP_DIRS.has(entry.name))
|
|
91457
92722
|
walkTests(full, out);
|
|
@@ -91463,7 +92728,7 @@ function expandPaths(cwd, inputs) {
|
|
|
91463
92728
|
const files = [];
|
|
91464
92729
|
const missing = [];
|
|
91465
92730
|
for (const input of inputs.length > 0 ? inputs : ["."]) {
|
|
91466
|
-
const full =
|
|
92731
|
+
const full = path46.resolve(cwd, input);
|
|
91467
92732
|
if (!existsSync18(full))
|
|
91468
92733
|
missing.push(input);
|
|
91469
92734
|
else if (statSync7(full).isDirectory())
|
|
@@ -91500,11 +92765,11 @@ function changedLines(cwd, base) {
|
|
|
91500
92765
|
for (let offset = 0;offset < count; offset += 1)
|
|
91501
92766
|
lines.add(start + offset);
|
|
91502
92767
|
}
|
|
91503
|
-
changed.set(
|
|
92768
|
+
changed.set(path46.join(root, file), lines);
|
|
91504
92769
|
}
|
|
91505
92770
|
const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
91506
92771
|
for (const file of untracked.stdout.split("\x00").filter(Boolean))
|
|
91507
|
-
changed.set(
|
|
92772
|
+
changed.set(path46.join(root, file), "all");
|
|
91508
92773
|
return changed;
|
|
91509
92774
|
}
|
|
91510
92775
|
function auditFiles(files, display) {
|
|
@@ -91566,7 +92831,7 @@ function auditFiles(files, display) {
|
|
|
91566
92831
|
durationMs: Math.round(performance.now() - started)
|
|
91567
92832
|
};
|
|
91568
92833
|
}
|
|
91569
|
-
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(
|
|
92834
|
+
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path46.sep).join("/"), SEVERITY_ORDER, ruleIds;
|
|
91570
92835
|
var init_audit = __esm(() => {
|
|
91571
92836
|
init_ast();
|
|
91572
92837
|
init_rules();
|
|
@@ -91596,7 +92861,7 @@ import {
|
|
|
91596
92861
|
copyFileSync as copyFileSync6
|
|
91597
92862
|
} from "node:fs";
|
|
91598
92863
|
import os14 from "node:os";
|
|
91599
|
-
import
|
|
92864
|
+
import path47 from "node:path";
|
|
91600
92865
|
function mutantsFor(source, file, lines) {
|
|
91601
92866
|
const parsed = parseSource(source, file);
|
|
91602
92867
|
const out = [];
|
|
@@ -91669,7 +92934,7 @@ function relatedTests(source, testFiles) {
|
|
|
91669
92934
|
for (const match of text.matchAll(SPECIFIER)) {
|
|
91670
92935
|
const spec = match[1];
|
|
91671
92936
|
if (spec.startsWith(".")) {
|
|
91672
|
-
if (stripExt(
|
|
92937
|
+
if (stripExt(path47.posix.join(path47.posix.dirname(file), spec)) === target)
|
|
91673
92938
|
return true;
|
|
91674
92939
|
} else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
|
|
91675
92940
|
return true;
|
|
@@ -91689,12 +92954,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91689
92954
|
for (const name of names) {
|
|
91690
92955
|
if (!name.startsWith(TEMP_PREFIX))
|
|
91691
92956
|
continue;
|
|
91692
|
-
const dir =
|
|
92957
|
+
const dir = path47.join(tmp, name);
|
|
91693
92958
|
try {
|
|
91694
92959
|
const stat = lstatSync7(dir);
|
|
91695
92960
|
if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
|
|
91696
92961
|
continue;
|
|
91697
|
-
const pid = Number(readFileSync20(
|
|
92962
|
+
const pid = Number(readFileSync20(path47.join(dir, PID_FILE), "utf8"));
|
|
91698
92963
|
if (Number.isInteger(pid) && pid > 0 && alive(pid))
|
|
91699
92964
|
continue;
|
|
91700
92965
|
} catch (error) {
|
|
@@ -91711,8 +92976,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91711
92976
|
function linkNodeModules(origin, target, repo, copy) {
|
|
91712
92977
|
mkdirSync9(target, { recursive: true });
|
|
91713
92978
|
for (const entry of readdirSync9(origin, { withFileTypes: true })) {
|
|
91714
|
-
const from =
|
|
91715
|
-
const to =
|
|
92979
|
+
const from = path47.join(origin, entry.name);
|
|
92980
|
+
const to = path47.join(target, entry.name);
|
|
91716
92981
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
91717
92982
|
linkNodeModules(from, to, repo, copy);
|
|
91718
92983
|
continue;
|
|
@@ -91728,9 +92993,9 @@ function linkNodeModules(origin, target, repo, copy) {
|
|
|
91728
92993
|
} catch {
|
|
91729
92994
|
continue;
|
|
91730
92995
|
}
|
|
91731
|
-
const relative =
|
|
91732
|
-
if (!relative.startsWith("..") && !
|
|
91733
|
-
symlinkSync(
|
|
92996
|
+
const relative = path47.relative(repo, real);
|
|
92997
|
+
if (!relative.startsWith("..") && !path47.isAbsolute(relative) && !relative.split(path47.sep).includes("node_modules")) {
|
|
92998
|
+
symlinkSync(path47.join(copy, relative), to, "junction");
|
|
91734
92999
|
continue;
|
|
91735
93000
|
}
|
|
91736
93001
|
}
|
|
@@ -91746,7 +93011,7 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91746
93011
|
}
|
|
91747
93012
|
const realRepo = realpathSync9(repo);
|
|
91748
93013
|
if (real && inside2(realRepo, real)) {
|
|
91749
|
-
symlinkSync(
|
|
93014
|
+
symlinkSync(path47.relative(path47.dirname(to), path47.join(tree, path47.relative(realRepo, real))), to);
|
|
91750
93015
|
return;
|
|
91751
93016
|
}
|
|
91752
93017
|
if (real && statSync8(real).isFile())
|
|
@@ -91755,10 +93020,10 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91755
93020
|
symlinkSync(readlinkSync3(from), to);
|
|
91756
93021
|
}
|
|
91757
93022
|
function writeInside(copy, file, content) {
|
|
91758
|
-
const target =
|
|
93023
|
+
const target = path47.join(copy, file);
|
|
91759
93024
|
try {
|
|
91760
93025
|
const stat = lstatSync7(target);
|
|
91761
|
-
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(
|
|
93026
|
+
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path47.dirname(target))))
|
|
91762
93027
|
return false;
|
|
91763
93028
|
} catch {
|
|
91764
93029
|
return false;
|
|
@@ -91769,9 +93034,9 @@ function writeInside(copy, file, content) {
|
|
|
91769
93034
|
return true;
|
|
91770
93035
|
}
|
|
91771
93036
|
function copyWorkingTree(repo) {
|
|
91772
|
-
const dir = mkdtempSync4(
|
|
91773
|
-
writeFileSync9(
|
|
91774
|
-
const tree =
|
|
93037
|
+
const dir = mkdtempSync4(path47.join(os14.tmpdir(), TEMP_PREFIX));
|
|
93038
|
+
writeFileSync9(path47.join(dir, PID_FILE), String(process.pid));
|
|
93039
|
+
const tree = path47.join(dir, "tree");
|
|
91775
93040
|
const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
|
|
91776
93041
|
if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
|
|
91777
93042
|
const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
|
|
@@ -91788,29 +93053,29 @@ function copyWorkingTree(repo) {
|
|
|
91788
93053
|
for (const relative of listed) {
|
|
91789
93054
|
if (!relative)
|
|
91790
93055
|
continue;
|
|
91791
|
-
const from =
|
|
93056
|
+
const from = path47.join(repo, relative);
|
|
91792
93057
|
let stat;
|
|
91793
93058
|
try {
|
|
91794
93059
|
stat = lstatSync7(from);
|
|
91795
93060
|
} catch {
|
|
91796
93061
|
continue;
|
|
91797
93062
|
}
|
|
91798
|
-
const to =
|
|
91799
|
-
mkdirSync9(
|
|
93063
|
+
const to = path47.join(tree, relative);
|
|
93064
|
+
mkdirSync9(path47.dirname(to), { recursive: true });
|
|
91800
93065
|
if (stat.isSymbolicLink())
|
|
91801
93066
|
copySymlink(repo, tree, from, to);
|
|
91802
93067
|
else if (stat.isFile())
|
|
91803
93068
|
copyFileSync6(from, to);
|
|
91804
|
-
let parent =
|
|
93069
|
+
let parent = path47.dirname(relative);
|
|
91805
93070
|
while (parent !== "." && !dirs.has(parent)) {
|
|
91806
93071
|
dirs.add(parent);
|
|
91807
|
-
parent =
|
|
93072
|
+
parent = path47.dirname(parent);
|
|
91808
93073
|
}
|
|
91809
93074
|
}
|
|
91810
93075
|
for (const parent of dirs) {
|
|
91811
|
-
const modules =
|
|
91812
|
-
if (existsSync19(modules) && !existsSync19(
|
|
91813
|
-
linkNodeModules(modules,
|
|
93076
|
+
const modules = path47.join(repo, parent, "node_modules");
|
|
93077
|
+
if (existsSync19(modules) && !existsSync19(path47.join(tree, parent, "node_modules")))
|
|
93078
|
+
linkNodeModules(modules, path47.join(tree, parent, "node_modules"), repo, tree);
|
|
91814
93079
|
}
|
|
91815
93080
|
return { dir, tree };
|
|
91816
93081
|
}
|
|
@@ -91849,14 +93114,14 @@ async function runMutation(options) {
|
|
|
91849
93114
|
const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
|
|
91850
93115
|
const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
|
|
91851
93116
|
try {
|
|
91852
|
-
return { file, text: readFileSync20(
|
|
93117
|
+
return { file, text: readFileSync20(path47.join(repo, file), "utf8") };
|
|
91853
93118
|
} catch {
|
|
91854
93119
|
return { file, text: "" };
|
|
91855
93120
|
}
|
|
91856
93121
|
});
|
|
91857
93122
|
const candidates = [];
|
|
91858
93123
|
for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
91859
|
-
const relative = toPosix(
|
|
93124
|
+
const relative = toPosix(path47.relative(repo, absolute));
|
|
91860
93125
|
if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
|
|
91861
93126
|
continue;
|
|
91862
93127
|
if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
|
|
@@ -91870,9 +93135,9 @@ async function runMutation(options) {
|
|
|
91870
93135
|
for (const mutant of mutants)
|
|
91871
93136
|
if (!related.has(mutant.file))
|
|
91872
93137
|
related.set(mutant.file, relatedTests(mutant.file, testFiles));
|
|
91873
|
-
const cwdRelative =
|
|
93138
|
+
const cwdRelative = path47.relative(repo, options.cwd);
|
|
91874
93139
|
const asArg = (file) => {
|
|
91875
|
-
const relative = toPosix(
|
|
93140
|
+
const relative = toPosix(path47.relative(cwdRelative, file));
|
|
91876
93141
|
return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
|
|
91877
93142
|
};
|
|
91878
93143
|
const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
|
|
@@ -91907,7 +93172,7 @@ async function runMutation(options) {
|
|
|
91907
93172
|
return finish2(options, 0, results, counts, started);
|
|
91908
93173
|
removeStaleCopies();
|
|
91909
93174
|
const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
|
|
91910
|
-
const copyCwd =
|
|
93175
|
+
const copyCwd = path47.join(copy, cwdRelative);
|
|
91911
93176
|
let current = null;
|
|
91912
93177
|
const restore = () => {
|
|
91913
93178
|
if (current)
|
|
@@ -91942,7 +93207,7 @@ async function runMutation(options) {
|
|
|
91942
93207
|
record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
|
|
91943
93208
|
continue;
|
|
91944
93209
|
}
|
|
91945
|
-
const original = readFileSync20(
|
|
93210
|
+
const original = readFileSync20(path47.join(copy, mutant.file), "utf8");
|
|
91946
93211
|
const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
|
|
91947
93212
|
current = { file: mutant.file, original };
|
|
91948
93213
|
if (!writeInside(copy, mutant.file, mutated)) {
|
|
@@ -91999,8 +93264,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
|
|
|
91999
93264
|
return error.code === "EPERM";
|
|
92000
93265
|
}
|
|
92001
93266
|
}, inside2 = (root, file) => {
|
|
92002
|
-
const relative =
|
|
92003
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
93267
|
+
const relative = path47.relative(root, file);
|
|
93268
|
+
return relative === "" || !relative.startsWith("..") && !path47.isAbsolute(relative);
|
|
92004
93269
|
}, active, killTree = (child) => {
|
|
92005
93270
|
try {
|
|
92006
93271
|
if (child.pid && process.platform !== "win32")
|
|
@@ -92046,10 +93311,10 @@ var init_mutate = __esm(() => {
|
|
|
92046
93311
|
// packages/workit-cli/src/verbs/test-audit.ts
|
|
92047
93312
|
var exports_test_audit = {};
|
|
92048
93313
|
__export(exports_test_audit, {
|
|
92049
|
-
run: () =>
|
|
93314
|
+
run: () => run17
|
|
92050
93315
|
});
|
|
92051
93316
|
import { existsSync as existsSync20 } from "node:fs";
|
|
92052
|
-
import
|
|
93317
|
+
import path48 from "node:path";
|
|
92053
93318
|
function parse7(argv) {
|
|
92054
93319
|
const options = {
|
|
92055
93320
|
paths: [],
|
|
@@ -92143,14 +93408,14 @@ function human(data) {
|
|
|
92143
93408
|
}
|
|
92144
93409
|
return lines;
|
|
92145
93410
|
}
|
|
92146
|
-
async function
|
|
93411
|
+
async function run17(argv, io) {
|
|
92147
93412
|
const options = parse7(argv);
|
|
92148
93413
|
if (typeof options === "string")
|
|
92149
93414
|
return emit(io, fail("invalid_input", `${options}
|
|
92150
|
-
usage: ${
|
|
93415
|
+
usage: ${USAGE10}`));
|
|
92151
93416
|
const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
|
|
92152
|
-
const repo = top.ok ?
|
|
92153
|
-
const display = (file) => toPosix(
|
|
93417
|
+
const repo = top.ok ? path48.resolve(top.stdout.trim()) : null;
|
|
93418
|
+
const display = (file) => toPosix(path48.relative(io.cwd, file) || file);
|
|
92154
93419
|
let base = null;
|
|
92155
93420
|
let changed = null;
|
|
92156
93421
|
if (options.diff || options.mutate) {
|
|
@@ -92233,7 +93498,7 @@ usage: ${USAGE9}`));
|
|
|
92233
93498
|
}
|
|
92234
93499
|
return emit(io, ok(data), human);
|
|
92235
93500
|
}
|
|
92236
|
-
var
|
|
93501
|
+
var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
92237
93502
|
var init_test_audit = __esm(() => {
|
|
92238
93503
|
init_audit();
|
|
92239
93504
|
init_mutate();
|
|
@@ -92251,6 +93516,38 @@ var init_test_audit = __esm(() => {
|
|
|
92251
93516
|
]);
|
|
92252
93517
|
});
|
|
92253
93518
|
|
|
93519
|
+
// packages/workit-cli/src/verbs/knowledge.ts
|
|
93520
|
+
var exports_knowledge = {};
|
|
93521
|
+
__export(exports_knowledge, {
|
|
93522
|
+
run: () => run18
|
|
93523
|
+
});
|
|
93524
|
+
async function run18(argv, io) {
|
|
93525
|
+
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
93526
|
+
if (sub !== "lint" || rest.length > 0)
|
|
93527
|
+
return emit(io, fail("invalid_input", `usage: ${USAGE11}`));
|
|
93528
|
+
const report = lintKnowledge(checkRoot(io.cwd));
|
|
93529
|
+
if (report.unreadable.length > 0)
|
|
93530
|
+
return emit(io, fail("unavailable", `knowledge lint: cannot read ${report.unreadable.join(", ")}`, {
|
|
93531
|
+
data: report,
|
|
93532
|
+
unblock: `make ${report.unreadable.join(", ")} readable, then re-run workit knowledge lint`
|
|
93533
|
+
}));
|
|
93534
|
+
const summary = `knowledge lint: ${report.findings.length} finding${report.findings.length === 1 ? "" : "s"} in ${report.files.length} file${report.files.length === 1 ? "" : "s"} (${report.files.map((file) => `${file.file} ${file.bytes} B`).join(", ") || "no knowledge files"}; AGENTS.md budget ${report.budget} B)`;
|
|
93535
|
+
if (io.json)
|
|
93536
|
+
return emit(io, report.findings.length === 0 ? ok(report) : fail("failed", summary, { data: report }));
|
|
93537
|
+
for (const finding of report.findings)
|
|
93538
|
+
io.stdout(`${finding.file}${finding.line === null ? "" : `:${finding.line}`} [${finding.rule}] ${finding.message}
|
|
93539
|
+
`);
|
|
93540
|
+
io.stdout(`${summary}
|
|
93541
|
+
`);
|
|
93542
|
+
return report.findings.length === 0 ? 0 : 1;
|
|
93543
|
+
}
|
|
93544
|
+
var USAGE11 = "workit knowledge lint [--json]";
|
|
93545
|
+
var init_knowledge2 = __esm(() => {
|
|
93546
|
+
init_check_config();
|
|
93547
|
+
init_knowledge();
|
|
93548
|
+
init_output();
|
|
93549
|
+
});
|
|
93550
|
+
|
|
92254
93551
|
// packages/workit-core/src/core/youtrack-tools.ts
|
|
92255
93552
|
async function logTimeUpdate(input, operation = defaultOperations) {
|
|
92256
93553
|
try {
|
|
@@ -92358,14 +93655,14 @@ var init_youtrack_writes = __esm(() => {
|
|
|
92358
93655
|
// packages/workit-cli/src/verbs/youtrack.ts
|
|
92359
93656
|
var exports_youtrack = {};
|
|
92360
93657
|
__export(exports_youtrack, {
|
|
92361
|
-
run: () =>
|
|
93658
|
+
run: () => run19
|
|
92362
93659
|
});
|
|
92363
93660
|
import { readFileSync as readFileSync21 } from "node:fs";
|
|
92364
|
-
import
|
|
92365
|
-
async function
|
|
93661
|
+
import path49 from "node:path";
|
|
93662
|
+
async function run19(argv, io) {
|
|
92366
93663
|
const [sub, ...rest] = argv;
|
|
92367
93664
|
if (sub !== "note" && sub !== "time" && sub !== "meeting")
|
|
92368
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93665
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
|
|
92369
93666
|
const flags = parseFlags(rest, {
|
|
92370
93667
|
markdown: "value",
|
|
92371
93668
|
file: "value",
|
|
@@ -92374,18 +93671,18 @@ async function run17(argv, io) {
|
|
|
92374
93671
|
date: "value"
|
|
92375
93672
|
});
|
|
92376
93673
|
if (typeof flags === "string")
|
|
92377
|
-
return usage3(io, flags,
|
|
93674
|
+
return usage3(io, flags, USAGE12);
|
|
92378
93675
|
const [issueId, extra] = flags.positionals;
|
|
92379
93676
|
if (!issueId || !ISSUE_RE2.test(issueId))
|
|
92380
|
-
return usage3(io, "an issue id like ABC-123 is required",
|
|
93677
|
+
return usage3(io, "an issue id like ABC-123 is required", USAGE12);
|
|
92381
93678
|
if (extra !== undefined)
|
|
92382
|
-
return usage3(io, `unexpected argument ${extra}`,
|
|
93679
|
+
return usage3(io, `unexpected argument ${extra}`, USAGE12);
|
|
92383
93680
|
const minutes = positiveInt(flags.values.minutes, "--minutes");
|
|
92384
93681
|
if (typeof minutes === "string")
|
|
92385
|
-
return usage3(io, minutes,
|
|
93682
|
+
return usage3(io, minutes, USAGE12);
|
|
92386
93683
|
const date = youTrackWorkDateMs(flags.values.date ?? "auto");
|
|
92387
93684
|
if ("error" in date)
|
|
92388
|
-
return usage3(io, date.error,
|
|
93685
|
+
return usage3(io, date.error, USAGE12);
|
|
92389
93686
|
const dateMs = date.data.dateMs;
|
|
92390
93687
|
const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
|
|
92391
93688
|
const root = io.cwd;
|
|
@@ -92403,24 +93700,24 @@ async function run17(argv, io) {
|
|
|
92403
93700
|
};
|
|
92404
93701
|
if (sub === "note") {
|
|
92405
93702
|
if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
|
|
92406
|
-
return usage3(io, "pass exactly one of --markdown or --file",
|
|
93703
|
+
return usage3(io, "pass exactly one of --markdown or --file", USAGE12);
|
|
92407
93704
|
let markdown = flags.values.markdown ?? "";
|
|
92408
93705
|
if (flags.values.file)
|
|
92409
93706
|
try {
|
|
92410
|
-
markdown = readFileSync21(
|
|
93707
|
+
markdown = readFileSync21(path49.resolve(io.cwd, flags.values.file), "utf8");
|
|
92411
93708
|
} catch (error) {
|
|
92412
|
-
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`,
|
|
93709
|
+
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
|
|
92413
93710
|
}
|
|
92414
93711
|
if (!markdown.trim())
|
|
92415
|
-
return usage3(io, "the note is empty",
|
|
93712
|
+
return usage3(io, "the note is empty", USAGE12);
|
|
92416
93713
|
const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
|
|
92417
93714
|
if (missing)
|
|
92418
93715
|
return emit(io, fail("unavailable", missing));
|
|
92419
93716
|
} else {
|
|
92420
93717
|
if (!minutes)
|
|
92421
|
-
return usage3(io, `${sub} needs --minutes <n>`,
|
|
93718
|
+
return usage3(io, `${sub} needs --minutes <n>`, USAGE12);
|
|
92422
93719
|
if (sub === "meeting" && !flags.values.text)
|
|
92423
|
-
return usage3(io, "meeting needs --text <what the meeting was>",
|
|
93720
|
+
return usage3(io, "meeting needs --text <what the meeting was>", USAGE12);
|
|
92424
93721
|
const text = flags.values.text ?? "";
|
|
92425
93722
|
const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
|
|
92426
93723
|
if (missing)
|
|
@@ -92449,7 +93746,7 @@ async function run17(argv, io) {
|
|
|
92449
93746
|
...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
|
|
92450
93747
|
]);
|
|
92451
93748
|
}
|
|
92452
|
-
var
|
|
93749
|
+
var USAGE12 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
|
|
92453
93750
|
try {
|
|
92454
93751
|
return { ok: true, data: unwrap2(value) };
|
|
92455
93752
|
} catch (error) {
|
|
@@ -92473,14 +93770,14 @@ var init_youtrack2 = __esm(() => {
|
|
|
92473
93770
|
// packages/workit-cli/src/verbs/changelog.ts
|
|
92474
93771
|
var exports_changelog = {};
|
|
92475
93772
|
__export(exports_changelog, {
|
|
92476
|
-
run: () =>
|
|
93773
|
+
run: () => run20
|
|
92477
93774
|
});
|
|
92478
93775
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
92479
|
-
import
|
|
92480
|
-
async function
|
|
93776
|
+
import path50 from "node:path";
|
|
93777
|
+
async function run20(argv, io) {
|
|
92481
93778
|
const [sub, ...rest] = argv;
|
|
92482
93779
|
if (sub !== "apply")
|
|
92483
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93780
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
|
|
92484
93781
|
const flags = parseFlags(rest, {
|
|
92485
93782
|
entries: "value",
|
|
92486
93783
|
path: "value",
|
|
@@ -92488,18 +93785,18 @@ async function run18(argv, io) {
|
|
|
92488
93785
|
preview: "boolean"
|
|
92489
93786
|
});
|
|
92490
93787
|
if (typeof flags === "string")
|
|
92491
|
-
return usage3(io, flags,
|
|
93788
|
+
return usage3(io, flags, USAGE13);
|
|
92492
93789
|
if (flags.positionals.length)
|
|
92493
|
-
return usage3(io, `unexpected argument ${flags.positionals[0]}`,
|
|
93790
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE13);
|
|
92494
93791
|
const normalizeOnly = flags.booleans.has("normalize-only");
|
|
92495
93792
|
if (!normalizeOnly && !flags.values.entries)
|
|
92496
|
-
return usage3(io, "pass --entries <JSON> or --normalize-only",
|
|
93793
|
+
return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE13);
|
|
92497
93794
|
let entries;
|
|
92498
93795
|
if (flags.values.entries)
|
|
92499
93796
|
try {
|
|
92500
93797
|
entries = readEntries(flags.values.entries, io.cwd);
|
|
92501
93798
|
} catch (error) {
|
|
92502
|
-
return usage3(io, `--entries is not valid JSON: ${String(error)}`,
|
|
93799
|
+
return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE13);
|
|
92503
93800
|
}
|
|
92504
93801
|
const input = {
|
|
92505
93802
|
entries,
|
|
@@ -92521,8 +93818,8 @@ async function run18(argv, io) {
|
|
|
92521
93818
|
`changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
|
|
92522
93819
|
]);
|
|
92523
93820
|
}
|
|
92524
|
-
var
|
|
92525
|
-
const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(
|
|
93821
|
+
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(path50.resolve(cwd, raw.slice(1)), "utf8") : raw;
|
|
92526
93823
|
return JSON.parse(text);
|
|
92527
93824
|
};
|
|
92528
93825
|
var init_changelog2 = __esm(() => {
|
|
@@ -92534,7 +93831,7 @@ var init_changelog2 = __esm(() => {
|
|
|
92534
93831
|
// packages/workit-cli/src/verbs/handoff.ts
|
|
92535
93832
|
var exports_handoff = {};
|
|
92536
93833
|
__export(exports_handoff, {
|
|
92537
|
-
run: () =>
|
|
93834
|
+
run: () => run21
|
|
92538
93835
|
});
|
|
92539
93836
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
92540
93837
|
function human2(brief) {
|
|
@@ -92582,11 +93879,11 @@ async function resumeBrief(argv, io) {
|
|
|
92582
93879
|
strict: true
|
|
92583
93880
|
}));
|
|
92584
93881
|
} catch (error) {
|
|
92585
|
-
return emit(io, fail("invalid_input", error.message, { unblock:
|
|
93882
|
+
return emit(io, fail("invalid_input", error.message, { unblock: USAGE14 }));
|
|
92586
93883
|
}
|
|
92587
93884
|
const last = values.last === undefined ? 10 : Number(values.last);
|
|
92588
93885
|
if (!Number.isInteger(last) || last < 1)
|
|
92589
|
-
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock:
|
|
93886
|
+
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE14 }));
|
|
92590
93887
|
const built = buildHandoff(io.cwd, {
|
|
92591
93888
|
last,
|
|
92592
93889
|
note: values.note ?? null,
|
|
@@ -92604,13 +93901,13 @@ async function resumeBrief(argv, io) {
|
|
|
92604
93901
|
}
|
|
92605
93902
|
return emit(io, ok({ ...data, recorded }), human2);
|
|
92606
93903
|
}
|
|
92607
|
-
async function
|
|
93904
|
+
async function run21(argv, io) {
|
|
92608
93905
|
if (!argv.includes("--task"))
|
|
92609
93906
|
return resumeBrief(argv, io);
|
|
92610
93907
|
await Promise.resolve().then(() => init_task());
|
|
92611
93908
|
return runTaskCommand(["handoff", ...argv]);
|
|
92612
93909
|
}
|
|
92613
|
-
var
|
|
93910
|
+
var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
92614
93911
|
var init_handoff = __esm(() => {
|
|
92615
93912
|
init_ledger();
|
|
92616
93913
|
init_output();
|
|
@@ -92619,7 +93916,7 @@ var init_handoff = __esm(() => {
|
|
|
92619
93916
|
// packages/workit-cli/src/main.ts
|
|
92620
93917
|
init_package();
|
|
92621
93918
|
init_output();
|
|
92622
|
-
import
|
|
93919
|
+
import path51 from "node:path";
|
|
92623
93920
|
|
|
92624
93921
|
// packages/workit-cli/src/verbs/registry.ts
|
|
92625
93922
|
var TASK_FAMILY_NAMES = [
|
|
@@ -92731,6 +94028,13 @@ var VERBS = [
|
|
|
92731
94028
|
summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
|
|
92732
94029
|
load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
|
|
92733
94030
|
},
|
|
94031
|
+
{
|
|
94032
|
+
name: "fanout",
|
|
94033
|
+
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), then gate fan-in: out-of-scope edits, trunk and sibling merge conflicts, landing order",
|
|
94036
|
+
load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
|
|
94037
|
+
},
|
|
92734
94038
|
{
|
|
92735
94039
|
name: "ledger",
|
|
92736
94040
|
group: "delivery",
|
|
@@ -92745,6 +94049,13 @@ var VERBS = [
|
|
|
92745
94049
|
summary: "Flag tautological and low-value tests with a suggested independent oracle; --mutate checks changed lines with diff-scoped mutation",
|
|
92746
94050
|
load: () => Promise.resolve().then(() => (init_test_audit(), exports_test_audit))
|
|
92747
94051
|
},
|
|
94052
|
+
{
|
|
94053
|
+
name: "knowledge",
|
|
94054
|
+
group: "delivery",
|
|
94055
|
+
usage: "workit knowledge lint [--json]",
|
|
94056
|
+
summary: "Lint agent knowledge files: AGENTS.md byte budget, broken local links, scaffold-only files, duplicated rules (exit 1 on findings)",
|
|
94057
|
+
load: () => Promise.resolve().then(() => (init_knowledge2(), exports_knowledge))
|
|
94058
|
+
},
|
|
92748
94059
|
{
|
|
92749
94060
|
name: "youtrack",
|
|
92750
94061
|
group: "delivery",
|
|
@@ -92849,7 +94160,7 @@ var defaultIo = (json) => ({
|
|
|
92849
94160
|
});
|
|
92850
94161
|
async function main(argv, overrides = {}, options = {}) {
|
|
92851
94162
|
const parsed = parseGlobals(argv);
|
|
92852
|
-
const cwd =
|
|
94163
|
+
const cwd = path51.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
92853
94164
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
92854
94165
|
if (parsed.error)
|
|
92855
94166
|
return emit(io, fail("invalid_input", parsed.error));
|