@brainervirus/workit-claude-code 8.38.5 → 8.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/workit.js +427 -240
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workit",
|
|
3
3
|
"displayName": "Workit",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.39.0",
|
|
5
5
|
"description": "Workflow rails for agentic coding: task context on every session, branch policy on shell commands, and the workit method skills",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "BrainerVirus"
|
package/dist/workit.js
CHANGED
|
@@ -9317,7 +9317,7 @@ var package_default;
|
|
|
9317
9317
|
var init_package = __esm(() => {
|
|
9318
9318
|
package_default = {
|
|
9319
9319
|
name: "@brainervirus/workit-cli",
|
|
9320
|
-
version: "8.
|
|
9320
|
+
version: "8.39.0",
|
|
9321
9321
|
private: false,
|
|
9322
9322
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
9323
9323
|
keywords: [
|
|
@@ -9355,13 +9355,13 @@ var init_package = __esm(() => {
|
|
|
9355
9355
|
},
|
|
9356
9356
|
dependencies: {
|
|
9357
9357
|
"@babel/parser": "7.29.9",
|
|
9358
|
-
"@brainervirus/workit-cursor": "^8.
|
|
9358
|
+
"@brainervirus/workit-cursor": "^8.39.0",
|
|
9359
9359
|
"@inkjs/ui": "2.0.0",
|
|
9360
9360
|
ink: "7.1.1",
|
|
9361
9361
|
react: "19.2.8"
|
|
9362
9362
|
},
|
|
9363
9363
|
devDependencies: {
|
|
9364
|
-
"@brainervirus/workit-core": "^8.
|
|
9364
|
+
"@brainervirus/workit-core": "^8.39.0"
|
|
9365
9365
|
},
|
|
9366
9366
|
engines: {
|
|
9367
9367
|
node: ">=24"
|
|
@@ -82478,6 +82478,74 @@ function syncStack(cwd, ctx, stack, options, actor) {
|
|
|
82478
82478
|
}
|
|
82479
82479
|
return { ok: true, data: outcome };
|
|
82480
82480
|
}
|
|
82481
|
+
function pruneMergedBranches(cwd, ctx, stack, options) {
|
|
82482
|
+
const outcome = { deleted: [], kept: [] };
|
|
82483
|
+
const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
|
|
82484
|
+
const trunkSha = trunkTip(cwd, remote, stack.trunk);
|
|
82485
|
+
const keep = (branch, reason) => outcome.kept.push({ branch, reason });
|
|
82486
|
+
for (const entry of stack.branches) {
|
|
82487
|
+
if (!entry.merged || !safeRef3(entry.branch) || entry.branch === stack.trunk)
|
|
82488
|
+
continue;
|
|
82489
|
+
const tip = branchTip(cwd, entry.branch);
|
|
82490
|
+
if (!tip)
|
|
82491
|
+
continue;
|
|
82492
|
+
if (isProtectedTarget(cwd, entry.branch)) {
|
|
82493
|
+
keep(entry.branch, "a protected branch under the workspace branch policy; never deleted");
|
|
82494
|
+
continue;
|
|
82495
|
+
}
|
|
82496
|
+
if (entry.lastHead === null || tip !== entry.lastHead) {
|
|
82497
|
+
keep(entry.branch, `its tip ${tip.slice(0, 12)} is not recorded by the stack (last ${entry.lastHead?.slice(0, 12) ?? "none"}); commits made after it merged stay`);
|
|
82498
|
+
continue;
|
|
82499
|
+
}
|
|
82500
|
+
const owner = worktreeOf(cwd, entry.branch);
|
|
82501
|
+
if (owner) {
|
|
82502
|
+
keep(entry.branch, `checked out in ${owner}; switch it away, then git branch -D ${entry.branch}`);
|
|
82503
|
+
continue;
|
|
82504
|
+
}
|
|
82505
|
+
const safe = safeToDelete(cwd, ctx, remote, trunkSha, entry, tip);
|
|
82506
|
+
if (safe === null) {
|
|
82507
|
+
keep(entry.branch, `${tip.slice(0, 12)} is not on ${stack.trunk}, not the head its PR merged at and not on ${remote}/${entry.branch}: deleting it could lose commits (push them, or delete it yourself)`);
|
|
82508
|
+
continue;
|
|
82509
|
+
}
|
|
82510
|
+
if (!options.dryRun) {
|
|
82511
|
+
const removed = gitRun3(cwd, ["branch", "-D", "--", entry.branch]);
|
|
82512
|
+
if (!removed.ok) {
|
|
82513
|
+
keep(entry.branch, `git branch -D failed: ${removed.stderr.trim().split(`
|
|
82514
|
+
`)[0] ?? ""}`);
|
|
82515
|
+
continue;
|
|
82516
|
+
}
|
|
82517
|
+
appendObserved(cwd, {
|
|
82518
|
+
type: "stack.branch.deleted",
|
|
82519
|
+
actor: options.actor,
|
|
82520
|
+
stack: stack.name,
|
|
82521
|
+
branch: entry.branch,
|
|
82522
|
+
head: tip,
|
|
82523
|
+
pr: entry.pr,
|
|
82524
|
+
because: safe
|
|
82525
|
+
});
|
|
82526
|
+
}
|
|
82527
|
+
outcome.deleted.push({ branch: entry.branch, sha: tip });
|
|
82528
|
+
}
|
|
82529
|
+
return outcome;
|
|
82530
|
+
}
|
|
82531
|
+
function safeToDelete(cwd, ctx, remote, trunkSha, entry, tip) {
|
|
82532
|
+
if (trunkSha && isAncestor2(cwd, tip, trunkSha))
|
|
82533
|
+
return "on_trunk";
|
|
82534
|
+
if (ctx && entry.pr) {
|
|
82535
|
+
const read = statusDoc(ctx, entry.pr);
|
|
82536
|
+
if (read.ok && read.data.state === "merged" && read.data.head.sha === tip)
|
|
82537
|
+
return "merged_head";
|
|
82538
|
+
}
|
|
82539
|
+
const own = pushRemoteName(cwd, entry.branch) ?? remote;
|
|
82540
|
+
const remoteTip = remoteRefTip(cwd, own, `refs/heads/${entry.branch}`);
|
|
82541
|
+
if (remoteTip.ok && remoteTip.sha) {
|
|
82542
|
+
if (!hasCommit(cwd, remoteTip.sha))
|
|
82543
|
+
fetchRefs(cwd, own, [remoteTip.sha]);
|
|
82544
|
+
if (hasCommit(cwd, remoteTip.sha) && isAncestor2(cwd, tip, remoteTip.sha))
|
|
82545
|
+
return "on_remote";
|
|
82546
|
+
}
|
|
82547
|
+
return null;
|
|
82548
|
+
}
|
|
82481
82549
|
async function awaitChecks(ctx, pr, head, options) {
|
|
82482
82550
|
let waited = 0;
|
|
82483
82551
|
for (let poll = 0;; poll += 1) {
|
|
@@ -82718,6 +82786,7 @@ ${run.stdout}`.split(`
|
|
|
82718
82786
|
}, commitCount = (cwd, base, head) => Number(gitRun3(cwd, ["rev-list", "--count", "--no-merges", `${base}..${head}`]).stdout.trim()) || 0, shortStat = (cwd, base, head) => gitRun3(cwd, ["diff", "--shortstat", `${base}...${head}`]).stdout.trim();
|
|
82719
82787
|
var init_stack = __esm(() => {
|
|
82720
82788
|
init_autonomy();
|
|
82789
|
+
init_branch();
|
|
82721
82790
|
init_store_lock();
|
|
82722
82791
|
init_pr_ops();
|
|
82723
82792
|
init_report2();
|
|
@@ -85168,8 +85237,213 @@ var init_pr_edit = __esm(() => {
|
|
|
85168
85237
|
init_report2();
|
|
85169
85238
|
});
|
|
85170
85239
|
|
|
85171
|
-
// packages/workit-core/src/
|
|
85240
|
+
// packages/workit-core/src/fanout-landed.ts
|
|
85172
85241
|
import { spawnSync as spawnSync26 } from "node:child_process";
|
|
85242
|
+
function forgeBreaker(resolved) {
|
|
85243
|
+
if (!resolved)
|
|
85244
|
+
return { forge: null, down: () => null };
|
|
85245
|
+
let tripped = null;
|
|
85246
|
+
const guard = (call) => (...args) => {
|
|
85247
|
+
if (tripped)
|
|
85248
|
+
return { ok: false, code: "unavailable", error: tripped };
|
|
85249
|
+
const result = call(...args);
|
|
85250
|
+
if (!result.ok && result.code === "unavailable")
|
|
85251
|
+
tripped = result.error;
|
|
85252
|
+
return result;
|
|
85253
|
+
};
|
|
85254
|
+
const inner = resolved.forge;
|
|
85255
|
+
const forge = Object.create(inner);
|
|
85256
|
+
forge.findPr = guard(inner.findPr.bind(inner));
|
|
85257
|
+
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
85258
|
+
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
85259
|
+
}
|
|
85260
|
+
function branchCreatedAt(cwd, branch) {
|
|
85261
|
+
if (!branch || branch.startsWith("-"))
|
|
85262
|
+
return null;
|
|
85263
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
85264
|
+
if (!log.ok)
|
|
85265
|
+
return null;
|
|
85266
|
+
return log.stdout.trim().split(`
|
|
85267
|
+
`).findLast(Boolean) ?? null;
|
|
85268
|
+
}
|
|
85269
|
+
function hasOwnCommits2(cwd, branch, start, head) {
|
|
85270
|
+
if (!branch || branch.startsWith("-"))
|
|
85271
|
+
return false;
|
|
85272
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
85273
|
+
if (!log.ok)
|
|
85274
|
+
return false;
|
|
85275
|
+
for (const line of log.stdout.split(`
|
|
85276
|
+
`)) {
|
|
85277
|
+
const [sha, subject = ""] = line.split("\x00");
|
|
85278
|
+
if (!sha)
|
|
85279
|
+
continue;
|
|
85280
|
+
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
85281
|
+
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
85282
|
+
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
85283
|
+
continue;
|
|
85284
|
+
if (sha !== head && !isAncestor3(cwd, sha, head))
|
|
85285
|
+
continue;
|
|
85286
|
+
if (start && (sha === start || isAncestor3(cwd, sha, start)))
|
|
85287
|
+
continue;
|
|
85288
|
+
return true;
|
|
85289
|
+
}
|
|
85290
|
+
return false;
|
|
85291
|
+
}
|
|
85292
|
+
function reverted(cwd, trunk, landedHead, start) {
|
|
85293
|
+
const fork = mergeBase(cwd, landedHead, trunk);
|
|
85294
|
+
if (fork && fork !== landedHead)
|
|
85295
|
+
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
85296
|
+
const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
85297
|
+
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
85298
|
+
}
|
|
85299
|
+
function unchangedSince(cwd, before, after, trunk) {
|
|
85300
|
+
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
85301
|
+
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
85302
|
+
if (files.length === 0)
|
|
85303
|
+
return false;
|
|
85304
|
+
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
85305
|
+
}
|
|
85306
|
+
function trunkPatchIds(ctx, fork) {
|
|
85307
|
+
const cache = ctx.cache ??= new Map;
|
|
85308
|
+
const known = cache.get(fork);
|
|
85309
|
+
if (known)
|
|
85310
|
+
return known;
|
|
85311
|
+
const ids = new Set;
|
|
85312
|
+
const log = gitRun4(ctx.cwd, [
|
|
85313
|
+
"log",
|
|
85314
|
+
"--no-merges",
|
|
85315
|
+
`--max-count=${TRUNK_SCAN}`,
|
|
85316
|
+
"-p",
|
|
85317
|
+
"--format=commit %H",
|
|
85318
|
+
"-U3",
|
|
85319
|
+
"--diff-algorithm=myers",
|
|
85320
|
+
"--indent-heuristic",
|
|
85321
|
+
"--no-color",
|
|
85322
|
+
"--no-ext-diff",
|
|
85323
|
+
"--no-textconv",
|
|
85324
|
+
"--no-renames",
|
|
85325
|
+
"--full-index",
|
|
85326
|
+
"--src-prefix=a/",
|
|
85327
|
+
"--dst-prefix=b/",
|
|
85328
|
+
`${fork}..${ctx.trunk}`,
|
|
85329
|
+
"--"
|
|
85330
|
+
]);
|
|
85331
|
+
if (log.ok && log.stdout.trim()) {
|
|
85332
|
+
const run = spawnSync26("git", ["patch-id", "--stable"], {
|
|
85333
|
+
cwd: ctx.cwd,
|
|
85334
|
+
input: log.stdout,
|
|
85335
|
+
encoding: "utf8",
|
|
85336
|
+
timeout: GIT_TIMEOUTS.local,
|
|
85337
|
+
windowsHide: true,
|
|
85338
|
+
maxBuffer: 64 * 1024 * 1024
|
|
85339
|
+
});
|
|
85340
|
+
for (const line of (run.stdout ?? "").split(`
|
|
85341
|
+
`)) {
|
|
85342
|
+
const id = line.trim().split(/\s+/u)[0];
|
|
85343
|
+
if (id)
|
|
85344
|
+
ids.add(id);
|
|
85345
|
+
}
|
|
85346
|
+
}
|
|
85347
|
+
cache.set(fork, ids);
|
|
85348
|
+
return ids;
|
|
85349
|
+
}
|
|
85350
|
+
function inSliceRun(row, slice) {
|
|
85351
|
+
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
85352
|
+
return row.sliceHash === slice.hash;
|
|
85353
|
+
const since = Date.parse(slice.hashSince);
|
|
85354
|
+
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85355
|
+
}
|
|
85356
|
+
function linked(rows, input, slice, head) {
|
|
85357
|
+
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
85358
|
+
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85359
|
+
return rows.some((row) => {
|
|
85360
|
+
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
85361
|
+
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
85362
|
+
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
85363
|
+
});
|
|
85364
|
+
}
|
|
85365
|
+
function detectLanding(ctx, slice, input) {
|
|
85366
|
+
const out = {
|
|
85367
|
+
landed: false,
|
|
85368
|
+
how: null,
|
|
85369
|
+
pr: null,
|
|
85370
|
+
landedHead: null,
|
|
85371
|
+
forgeError: null,
|
|
85372
|
+
note: null
|
|
85373
|
+
};
|
|
85374
|
+
const landed = (how, landedHead) => {
|
|
85375
|
+
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
85376
|
+
return { ...out, landed: true, how, landedHead };
|
|
85377
|
+
return {
|
|
85378
|
+
...out,
|
|
85379
|
+
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
85380
|
+
};
|
|
85381
|
+
};
|
|
85382
|
+
const { head } = input;
|
|
85383
|
+
if (ctx.forge) {
|
|
85384
|
+
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
85385
|
+
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
85386
|
+
projectId: ctx.forge.headProjectId,
|
|
85387
|
+
sha: head
|
|
85388
|
+
});
|
|
85389
|
+
if (!found.ok)
|
|
85390
|
+
out.forgeError = found.error;
|
|
85391
|
+
else if (found.data) {
|
|
85392
|
+
out.pr = found.data;
|
|
85393
|
+
if (found.data.state === "merged") {
|
|
85394
|
+
const merged = found.data.headSha;
|
|
85395
|
+
const label = `#${found.data.number}`;
|
|
85396
|
+
if (!head) {
|
|
85397
|
+
if (!linked(input.rows, input, slice, merged)) {
|
|
85398
|
+
out.pr = null;
|
|
85399
|
+
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
85400
|
+
return out;
|
|
85401
|
+
}
|
|
85402
|
+
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
85403
|
+
}
|
|
85404
|
+
if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
|
|
85405
|
+
return landed("pr_merged", merged ?? head);
|
|
85406
|
+
out.note = isAncestor3(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
|
|
85407
|
+
return out;
|
|
85408
|
+
}
|
|
85409
|
+
}
|
|
85410
|
+
}
|
|
85411
|
+
if (!head)
|
|
85412
|
+
return out;
|
|
85413
|
+
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
85414
|
+
if (fork === head) {
|
|
85415
|
+
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
85416
|
+
}
|
|
85417
|
+
if (!fork)
|
|
85418
|
+
return out;
|
|
85419
|
+
const trunkIds = trunkPatchIds(ctx, fork);
|
|
85420
|
+
if (trunkIds.size === 0)
|
|
85421
|
+
return out;
|
|
85422
|
+
const froms = [fork];
|
|
85423
|
+
if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
|
|
85424
|
+
froms.push(input.parentHead);
|
|
85425
|
+
for (const from of froms) {
|
|
85426
|
+
const id = patchId(ctx.cwd, from, head);
|
|
85427
|
+
if (id && trunkIds.has(id))
|
|
85428
|
+
return landed("squash_patch", head);
|
|
85429
|
+
}
|
|
85430
|
+
return out;
|
|
85431
|
+
}
|
|
85432
|
+
function sliceStart(cwd, rows, fanout, slice) {
|
|
85433
|
+
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
85434
|
+
if (reflog)
|
|
85435
|
+
return reflog;
|
|
85436
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
85437
|
+
return row?.baseSha ?? null;
|
|
85438
|
+
}
|
|
85439
|
+
var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
85440
|
+
var init_fanout_landed = __esm(() => {
|
|
85441
|
+
init_rev();
|
|
85442
|
+
init_fanout();
|
|
85443
|
+
});
|
|
85444
|
+
|
|
85445
|
+
// packages/workit-core/src/core/live-session.ts
|
|
85446
|
+
import { spawnSync as spawnSync27 } from "node:child_process";
|
|
85173
85447
|
var LIVE_SESSION_TTL_MS, liveProbe, PIDS;
|
|
85174
85448
|
var init_live_session = __esm(() => {
|
|
85175
85449
|
init_paths();
|
|
@@ -85178,7 +85452,7 @@ var init_live_session = __esm(() => {
|
|
|
85178
85452
|
LIVE_SESSION_TTL_MS = 15 * 60000;
|
|
85179
85453
|
liveProbe = {
|
|
85180
85454
|
platform: process.platform,
|
|
85181
|
-
run: (args) =>
|
|
85455
|
+
run: (args) => spawnSync27("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
|
|
85182
85456
|
start: (pid) => processStartOf(pid)
|
|
85183
85457
|
};
|
|
85184
85458
|
PIDS = new Map;
|
|
@@ -85471,8 +85745,65 @@ function worktreeDependencies(main, worktree) {
|
|
|
85471
85745
|
]
|
|
85472
85746
|
};
|
|
85473
85747
|
}
|
|
85748
|
+
const stale = staleBunInstall(main);
|
|
85749
|
+
if (stale && !fs32.existsSync(path64.join(worktree, "node_modules"))) {
|
|
85750
|
+
const command = `bun install --frozen-lockfile --cwd ${worktree}`;
|
|
85751
|
+
return {
|
|
85752
|
+
linked: null,
|
|
85753
|
+
install: command,
|
|
85754
|
+
notes: [
|
|
85755
|
+
`the main checkout's node_modules (${main}) do not match its bun.lock (${stale}), so they were not linked; install dependencies first: ${command}`
|
|
85756
|
+
]
|
|
85757
|
+
};
|
|
85758
|
+
}
|
|
85474
85759
|
return { ...linkWorktreeDependencies(main, worktree), install: null };
|
|
85475
85760
|
}
|
|
85761
|
+
function staleBunInstall(main) {
|
|
85762
|
+
if (!fs32.existsSync(path64.join(main, "node_modules")))
|
|
85763
|
+
return null;
|
|
85764
|
+
const text = readBytes(path64.join(main, "bun.lock"))?.toString("utf8");
|
|
85765
|
+
if (!text)
|
|
85766
|
+
return null;
|
|
85767
|
+
let packages;
|
|
85768
|
+
try {
|
|
85769
|
+
const parsed = JSON.parse(text.replace(/,(\s*[}\]])/gu, "$1"));
|
|
85770
|
+
if (!parsed.packages || typeof parsed.packages !== "object")
|
|
85771
|
+
return null;
|
|
85772
|
+
packages = parsed.packages;
|
|
85773
|
+
} catch {
|
|
85774
|
+
return null;
|
|
85775
|
+
}
|
|
85776
|
+
const problems = [];
|
|
85777
|
+
let scanned = 0;
|
|
85778
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
85779
|
+
if (scanned++ >= STALE_SCAN || problems.length >= 3)
|
|
85780
|
+
break;
|
|
85781
|
+
const spec = Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : null;
|
|
85782
|
+
if (!spec)
|
|
85783
|
+
continue;
|
|
85784
|
+
const at = spec.lastIndexOf("@");
|
|
85785
|
+
if (at <= 0)
|
|
85786
|
+
continue;
|
|
85787
|
+
const name = spec.slice(0, at);
|
|
85788
|
+
const version = spec.slice(at + 1);
|
|
85789
|
+
if (name !== key || version.includes(":"))
|
|
85790
|
+
continue;
|
|
85791
|
+
const manifest = readBytes(path64.join(main, "node_modules", key, "package.json"));
|
|
85792
|
+
if (!manifest) {
|
|
85793
|
+
problems.push(`${key} missing`);
|
|
85794
|
+
continue;
|
|
85795
|
+
}
|
|
85796
|
+
let installed;
|
|
85797
|
+
try {
|
|
85798
|
+
installed = JSON.parse(manifest.toString("utf8")).version;
|
|
85799
|
+
} catch {
|
|
85800
|
+
installed = null;
|
|
85801
|
+
}
|
|
85802
|
+
if (installed !== version)
|
|
85803
|
+
problems.push(`${key} ${String(installed ?? "?")} installed, bun.lock has ${version}`);
|
|
85804
|
+
}
|
|
85805
|
+
return problems.length ? problems.join("; ") : null;
|
|
85806
|
+
}
|
|
85476
85807
|
function ensureScratch(worktree) {
|
|
85477
85808
|
const scratch = path64.join(worktree, SCRATCH_DIR);
|
|
85478
85809
|
fs32.mkdirSync(scratch, { recursive: true });
|
|
@@ -85549,14 +85880,19 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85549
85880
|
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
85550
85881
|
} else {
|
|
85551
85882
|
mode = "new";
|
|
85552
|
-
const
|
|
85553
|
-
|
|
85883
|
+
const landedOn = dependenciesLanded(cwd, plan, slice, input.forge);
|
|
85884
|
+
if (landedOn) {
|
|
85885
|
+
base.base = plan.trunk;
|
|
85886
|
+
notes.push(landedOn);
|
|
85887
|
+
}
|
|
85888
|
+
const parent = landedOn ? undefined : plan.slices.find((other) => other.branch === slice.base);
|
|
85889
|
+
const from = base.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
85554
85890
|
if (!from)
|
|
85555
85891
|
return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
|
|
85556
85892
|
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
85557
85893
|
if (!added.ok)
|
|
85558
85894
|
return worktreeFailed(added.stderr);
|
|
85559
|
-
const branched = gitBranch(target, { name: slice.branch, base:
|
|
85895
|
+
const branched = gitBranch(target, { name: slice.branch, base: base.base });
|
|
85560
85896
|
if (!branched.ok) {
|
|
85561
85897
|
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
85562
85898
|
return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
|
|
@@ -85586,6 +85922,7 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85586
85922
|
sliceHash: slice.hash,
|
|
85587
85923
|
...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
|
|
85588
85924
|
...baseSha ? { baseSha } : {},
|
|
85925
|
+
...base.base === slice.base ? {} : { startBase: base.base },
|
|
85589
85926
|
...deps.install ? { install: deps.install } : {}
|
|
85590
85927
|
});
|
|
85591
85928
|
return {
|
|
@@ -85602,6 +85939,35 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85602
85939
|
}
|
|
85603
85940
|
};
|
|
85604
85941
|
}
|
|
85942
|
+
function dependenciesLanded(cwd, plan, slice, forge) {
|
|
85943
|
+
if (slice.base === plan.trunk || slice.dependsOn.length === 0)
|
|
85944
|
+
return null;
|
|
85945
|
+
const deps = slice.dependsOn.map((id) => plan.slices.find((other) => other.id === id));
|
|
85946
|
+
if (deps.some((dep) => !dep))
|
|
85947
|
+
return null;
|
|
85948
|
+
const trunk = trunkRef(cwd, plan.trunk);
|
|
85949
|
+
if (!trunk)
|
|
85950
|
+
return null;
|
|
85951
|
+
const ledger = readLedger(cwd);
|
|
85952
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
85953
|
+
const ctx = { cwd, trunk, forge: forgeBreaker(forge?.() ?? null).forge };
|
|
85954
|
+
const landed = [];
|
|
85955
|
+
for (const dep of deps) {
|
|
85956
|
+
const landing = detectLanding(ctx, dep, {
|
|
85957
|
+
head: branchRef(cwd, dep.branch),
|
|
85958
|
+
createdFrom: sliceStart(cwd, rows, plan.name, dep),
|
|
85959
|
+
parentHead: null,
|
|
85960
|
+
fanout: plan.name,
|
|
85961
|
+
since: dep.hashSince || plan.createdAt || null,
|
|
85962
|
+
sliceHash: dep.hash || null,
|
|
85963
|
+
rows
|
|
85964
|
+
});
|
|
85965
|
+
if (!landing.landed)
|
|
85966
|
+
return null;
|
|
85967
|
+
landed.push(`${dep.branch} landed (${landing.how ?? "pr_merged"}${landing.pr ? ` #${landing.pr.number}` : ""})`);
|
|
85968
|
+
}
|
|
85969
|
+
return `${landed.join(", ")}: started from ${plan.trunk}, not from ${slice.base}'s pre-merge tip`;
|
|
85970
|
+
}
|
|
85605
85971
|
function releaseSliceWorktree(cwd, plan, input) {
|
|
85606
85972
|
const found = findSlice(plan, input.slice);
|
|
85607
85973
|
if (!found.ok)
|
|
@@ -85744,12 +86110,13 @@ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000,
|
|
|
85744
86110
|
} catch {
|
|
85745
86111
|
return null;
|
|
85746
86112
|
}
|
|
85747
|
-
}, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
86113
|
+
}, STALE_SCAN = 5000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
85748
86114
|
`)[0] || "unknown error"}`);
|
|
85749
86115
|
var init_fanout_worktree = __esm(() => {
|
|
85750
86116
|
init_ops();
|
|
85751
86117
|
init_rev();
|
|
85752
86118
|
init_ledger();
|
|
86119
|
+
init_fanout_landed();
|
|
85753
86120
|
init_worktree_confine();
|
|
85754
86121
|
init_worktree_deps();
|
|
85755
86122
|
init_fanout();
|
|
@@ -87382,7 +87749,7 @@ var init_git2 = __esm(() => {
|
|
|
87382
87749
|
});
|
|
87383
87750
|
|
|
87384
87751
|
// packages/workit-core/src/forge/verify.ts
|
|
87385
|
-
import { spawnSync as
|
|
87752
|
+
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
87386
87753
|
function finish(cwd, report, actor) {
|
|
87387
87754
|
const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
|
|
87388
87755
|
const out = { ...report, delivered };
|
|
@@ -87595,7 +87962,7 @@ var NPM_NAME, SEMVER2, SAFE_ARG, systemNpm = (args) => {
|
|
|
87595
87962
|
};
|
|
87596
87963
|
const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
|
|
87597
87964
|
const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
|
|
87598
|
-
const result = process.platform === "win32" ?
|
|
87965
|
+
const result = process.platform === "win32" ? spawnSync28(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync28("npm", [...args], options);
|
|
87599
87966
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
87600
87967
|
}, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
|
|
87601
87968
|
try {
|
|
@@ -87820,7 +88187,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87820
88187
|
return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
|
|
87821
88188
|
if (!tracked || name === openRelease)
|
|
87822
88189
|
continue;
|
|
87823
|
-
if (!
|
|
88190
|
+
if (!isAncestor4(cwd, tracked, local))
|
|
87824
88191
|
return failure3("blocked", `branch_behind: local ${name} is behind or diverged from ${remote}/${name}; git-flow finishes only onto up-to-date branches`, `git switch ${name} && git merge --ff-only ${remote}/${name} # then: workit release finish ${version}`);
|
|
87825
88192
|
const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
|
|
87826
88193
|
const foreign = git5(cwd, [
|
|
@@ -87830,16 +88197,16 @@ function planReleaseFinish(cwd, input) {
|
|
|
87830
88197
|
`^${releaseTip}`,
|
|
87831
88198
|
...productionTracked ? [`^${productionTracked}`] : []
|
|
87832
88199
|
]).stdout.split(`
|
|
87833
|
-
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok &&
|
|
88200
|
+
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor4(cwd, releaseTip, sha)));
|
|
87834
88201
|
if (foreign.length)
|
|
87835
88202
|
return failure3("blocked", `branch_ahead: local ${name} has ${foreign.length} commit(s) ${remote}/${name} lacks that are not part of ${branch}; the finish push would publish them`, `land them through review first, or: git switch ${name} && git reset --keep ${remote}/${name} # then: workit release finish ${version}`);
|
|
87836
88203
|
}
|
|
87837
|
-
if (branchTip && remoteBranch && !
|
|
88204
|
+
if (branchTip && remoteBranch && !isAncestor4(cwd, remoteBranch, branchTip))
|
|
87838
88205
|
return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
|
|
87839
88206
|
const production = resolveRef(cwd, `refs/heads/${line.production}`);
|
|
87840
|
-
const merged =
|
|
88207
|
+
const merged = isAncestor4(cwd, releaseTip, production);
|
|
87841
88208
|
const localTag = resolveRef(cwd, `refs/tags/${tag}`);
|
|
87842
|
-
if (localTag && !(
|
|
88209
|
+
if (localTag && !(isAncestor4(cwd, localTag, production) && isAncestor4(cwd, releaseTip, localTag)))
|
|
87843
88210
|
return failure3("blocked", `tag_exists: ${tag} (${localTag.slice(0, 12)}) is not this release's merge on ${line.production}`, "a release tag never moves; finish under the next version");
|
|
87844
88211
|
if (remote) {
|
|
87845
88212
|
const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
|
|
@@ -87853,7 +88220,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87853
88220
|
tagged: localTag !== null,
|
|
87854
88221
|
mergedBack: Object.fromEntries(mergeBack.map((name) => [
|
|
87855
88222
|
name,
|
|
87856
|
-
localTag !== null &&
|
|
88223
|
+
localTag !== null && isAncestor4(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
|
|
87857
88224
|
])),
|
|
87858
88225
|
branchDeleted: branchTip === null
|
|
87859
88226
|
};
|
|
@@ -88015,7 +88382,7 @@ var VERSION, PLACEHOLDER4, validVersion = (version) => VERSION.test(version) &&
|
|
|
88015
88382
|
return ledger.value.rows.filter((row) => row.type.startsWith("release."));
|
|
88016
88383
|
}, record2 = (cwd, actor, row) => {
|
|
88017
88384
|
appendObserved(cwd, { ...row, actor });
|
|
88018
|
-
}, KINDS,
|
|
88385
|
+
}, KINDS, isAncestor4 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
|
|
88019
88386
|
if (currentBranch(cwd) === branch)
|
|
88020
88387
|
return success2(undefined);
|
|
88021
88388
|
const switched = git5(cwd, ["switch", "-q", branch]);
|
|
@@ -88481,24 +88848,25 @@ async function status3(argv, io) {
|
|
|
88481
88848
|
...data.next ? [`next: ${data.next}`] : []
|
|
88482
88849
|
]);
|
|
88483
88850
|
}
|
|
88484
|
-
async function sync(argv, io) {
|
|
88851
|
+
async function sync(argv, io, restack = false) {
|
|
88852
|
+
const usageText = restack ? RESTACK_USAGE : SYNC_USAGE;
|
|
88485
88853
|
const flags = parseFlags(argv, {
|
|
88486
88854
|
name: "value",
|
|
88487
|
-
local: "boolean",
|
|
88855
|
+
...restack ? {} : { local: "boolean" },
|
|
88488
88856
|
"dry-run": "boolean",
|
|
88489
88857
|
force: "list"
|
|
88490
88858
|
});
|
|
88491
88859
|
if (typeof flags === "string")
|
|
88492
|
-
return usage4(io, flags,
|
|
88860
|
+
return usage4(io, flags, usageText);
|
|
88493
88861
|
if (flags.positionals.length)
|
|
88494
|
-
return usage4(io, `unexpected argument ${flags.positionals[0]}`,
|
|
88862
|
+
return usage4(io, `unexpected argument ${flags.positionals[0]}`, usageText);
|
|
88495
88863
|
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
88496
88864
|
if (!selected.ok)
|
|
88497
88865
|
return stackFailed(io, selected);
|
|
88498
88866
|
const force = new Set(flags.lists.force ?? []);
|
|
88499
88867
|
const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
|
|
88500
88868
|
if (unknown.length)
|
|
88501
|
-
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`,
|
|
88869
|
+
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, usageText);
|
|
88502
88870
|
const local = flags.booleans.has("local");
|
|
88503
88871
|
let ctx = null;
|
|
88504
88872
|
if (!local) {
|
|
@@ -88517,14 +88885,23 @@ async function sync(argv, io) {
|
|
|
88517
88885
|
const fresh = readStack(io.cwd, name);
|
|
88518
88886
|
if (!fresh.ok)
|
|
88519
88887
|
return fresh;
|
|
88520
|
-
|
|
88521
|
-
|
|
88522
|
-
|
|
88523
|
-
|
|
88524
|
-
|
|
88888
|
+
const stack = fresh.data ?? selected.data;
|
|
88889
|
+
const dryRun = flags.booleans.has("dry-run");
|
|
88890
|
+
const synced = syncStack(io.cwd, ctx, stack, { publish: !local, dryRun, force }, actorFromEnv(io.env));
|
|
88891
|
+
if (!synced.ok || !restack)
|
|
88892
|
+
return synced;
|
|
88893
|
+
return {
|
|
88894
|
+
ok: true,
|
|
88895
|
+
data: {
|
|
88896
|
+
...synced.data,
|
|
88897
|
+
pruned: pruneMergedBranches(io.cwd, ctx, stack, { dryRun, actor: actorFromEnv(io.env) })
|
|
88898
|
+
}
|
|
88899
|
+
};
|
|
88525
88900
|
});
|
|
88526
88901
|
if (!result.ok)
|
|
88527
88902
|
return stackFailed(io, result);
|
|
88903
|
+
if (restack)
|
|
88904
|
+
return emit(io, ok2(result.data), renderRestack);
|
|
88528
88905
|
return emit(io, ok2(result.data), renderSync);
|
|
88529
88906
|
}
|
|
88530
88907
|
async function land(argv, io) {
|
|
@@ -88611,11 +88988,13 @@ async function run20(argv, io) {
|
|
|
88611
88988
|
return status3(rest, io);
|
|
88612
88989
|
if (sub === "sync")
|
|
88613
88990
|
return sync(rest, io);
|
|
88991
|
+
if (sub === "restack")
|
|
88992
|
+
return sync(rest, io, true);
|
|
88614
88993
|
if (sub === "land")
|
|
88615
88994
|
return land(rest, io);
|
|
88616
88995
|
return usage4(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE13);
|
|
88617
88996
|
}
|
|
88618
|
-
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] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE13 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
|
|
88997
|
+
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]", RESTACK_USAGE = "workit stack restack [--name <n>] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE13 = "workit stack plan|status|sync|restack|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
|
|
88619
88998
|
unblock: result.unblock,
|
|
88620
88999
|
data: result.data ?? {}
|
|
88621
89000
|
})), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
@@ -88626,6 +89005,10 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
88626
89005
|
step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
|
|
88627
89006
|
step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
|
|
88628
89007
|
].filter(Boolean).join(" "))
|
|
89008
|
+
], renderRestack = (data) => [
|
|
89009
|
+
...renderSync(data),
|
|
89010
|
+
...data.pruned.deleted.map((item) => `${data.dryRun ? "would delete" : "deleted"} merged branch ${item.branch} at ${item.sha} (local; restore: git branch ${item.branch} ${item.sha})`),
|
|
89011
|
+
...data.pruned.kept.map((item) => `kept merged branch ${item.branch}: ${item.reason}`)
|
|
88629
89012
|
], METHODS3;
|
|
88630
89013
|
var init_stack2 = __esm(() => {
|
|
88631
89014
|
init_ledger();
|
|
@@ -88665,6 +89048,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88665
89048
|
const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
|
|
88666
89049
|
const made = ledger.value.rows.findLast((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, planned));
|
|
88667
89050
|
const fresh = Boolean(head) && made?.baseSha === head;
|
|
89051
|
+
const sliceBase = typeof made?.startBase === "string" ? made.startBase : slice.base;
|
|
88668
89052
|
const mode = input.mode ?? (head && !fresh ? "resume" : "new");
|
|
88669
89053
|
if (mode === "resume" && !head)
|
|
88670
89054
|
return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
|
|
@@ -88693,13 +89077,13 @@ function renderBrief(cwd, plan, input) {
|
|
|
88693
89077
|
`GOAL: ${slice.goal}`,
|
|
88694
89078
|
`SCOPE: ${slice.scope.join(", ")}`,
|
|
88695
89079
|
...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
|
|
88696
|
-
` branch: ${branch} base: ${
|
|
89080
|
+
` branch: ${branch} base: ${sliceBase}`,
|
|
88697
89081
|
`CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
|
|
88698
89082
|
...slice.issues.length ? [
|
|
88699
89083
|
`ISSUE: ${slice.issues.join(", ")} (\`workit pr create\` on ${branch} links ${slice.issues.length === 1 ? "it" : "them"} by itself; no --issue needed)`
|
|
88700
89084
|
] : [],
|
|
88701
89085
|
...mode === "resume" && head ? [
|
|
88702
|
-
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${
|
|
89086
|
+
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${sliceBase}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
|
|
88703
89087
|
] : [],
|
|
88704
89088
|
"ACCEPTANCE:",
|
|
88705
89089
|
...slice.acceptance.map((line) => ` - ${line}`),
|
|
@@ -88722,7 +89106,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88722
89106
|
exists ? `WORKTREE: ${planned} (made by \`workit fanout worktree create ${slice.id}\`; yours alone)` : `WORKTREE: ${planned} (not made yet: the lead runs \`workit fanout worktree create ${slice.id}\` before spawning, then renders this brief again)`,
|
|
88723
89107
|
...exists && typeof made?.install === "string" && !fs35.existsSync(path68.join(planned, "node_modules")) ? [`INSTALL: \`${made.install}\` first: the main checkout's node_modules was not linked here`] : [],
|
|
88724
89108
|
...identityLines(identity, minted),
|
|
88725
|
-
...rules(mode, branch,
|
|
89109
|
+
...rules(mode, branch, sliceBase, exists ? planned : null)
|
|
88726
89110
|
];
|
|
88727
89111
|
return {
|
|
88728
89112
|
ok: true,
|
|
@@ -88732,7 +89116,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88732
89116
|
mode,
|
|
88733
89117
|
attempt: input.attempt,
|
|
88734
89118
|
branch,
|
|
88735
|
-
base:
|
|
89119
|
+
base: sliceBase,
|
|
88736
89120
|
tier: slice.tier,
|
|
88737
89121
|
issues: slice.issues,
|
|
88738
89122
|
model: model?.model ?? null,
|
|
@@ -88797,211 +89181,6 @@ var init_fanout_brief = __esm(() => {
|
|
|
88797
89181
|
];
|
|
88798
89182
|
});
|
|
88799
89183
|
|
|
88800
|
-
// packages/workit-core/src/fanout-landed.ts
|
|
88801
|
-
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
88802
|
-
function forgeBreaker(resolved) {
|
|
88803
|
-
if (!resolved)
|
|
88804
|
-
return { forge: null, down: () => null };
|
|
88805
|
-
let tripped = null;
|
|
88806
|
-
const guard = (call) => (...args) => {
|
|
88807
|
-
if (tripped)
|
|
88808
|
-
return { ok: false, code: "unavailable", error: tripped };
|
|
88809
|
-
const result = call(...args);
|
|
88810
|
-
if (!result.ok && result.code === "unavailable")
|
|
88811
|
-
tripped = result.error;
|
|
88812
|
-
return result;
|
|
88813
|
-
};
|
|
88814
|
-
const inner = resolved.forge;
|
|
88815
|
-
const forge = Object.create(inner);
|
|
88816
|
-
forge.findPr = guard(inner.findPr.bind(inner));
|
|
88817
|
-
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
88818
|
-
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
88819
|
-
}
|
|
88820
|
-
function branchCreatedAt(cwd, branch) {
|
|
88821
|
-
if (!branch || branch.startsWith("-"))
|
|
88822
|
-
return null;
|
|
88823
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
88824
|
-
if (!log.ok)
|
|
88825
|
-
return null;
|
|
88826
|
-
return log.stdout.trim().split(`
|
|
88827
|
-
`).findLast(Boolean) ?? null;
|
|
88828
|
-
}
|
|
88829
|
-
function hasOwnCommits2(cwd, branch, start, head) {
|
|
88830
|
-
if (!branch || branch.startsWith("-"))
|
|
88831
|
-
return false;
|
|
88832
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
88833
|
-
if (!log.ok)
|
|
88834
|
-
return false;
|
|
88835
|
-
for (const line of log.stdout.split(`
|
|
88836
|
-
`)) {
|
|
88837
|
-
const [sha, subject = ""] = line.split("\x00");
|
|
88838
|
-
if (!sha)
|
|
88839
|
-
continue;
|
|
88840
|
-
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
88841
|
-
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
88842
|
-
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
88843
|
-
continue;
|
|
88844
|
-
if (sha !== head && !isAncestor4(cwd, sha, head))
|
|
88845
|
-
continue;
|
|
88846
|
-
if (start && (sha === start || isAncestor4(cwd, sha, start)))
|
|
88847
|
-
continue;
|
|
88848
|
-
return true;
|
|
88849
|
-
}
|
|
88850
|
-
return false;
|
|
88851
|
-
}
|
|
88852
|
-
function reverted(cwd, trunk, landedHead, start) {
|
|
88853
|
-
const fork = mergeBase(cwd, landedHead, trunk);
|
|
88854
|
-
if (fork && fork !== landedHead)
|
|
88855
|
-
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
88856
|
-
const before = start && start !== landedHead && isAncestor4(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
88857
|
-
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
88858
|
-
}
|
|
88859
|
-
function unchangedSince(cwd, before, after, trunk) {
|
|
88860
|
-
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
88861
|
-
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
88862
|
-
if (files.length === 0)
|
|
88863
|
-
return false;
|
|
88864
|
-
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
88865
|
-
}
|
|
88866
|
-
function trunkPatchIds(ctx, fork) {
|
|
88867
|
-
const cache = ctx.cache ??= new Map;
|
|
88868
|
-
const known = cache.get(fork);
|
|
88869
|
-
if (known)
|
|
88870
|
-
return known;
|
|
88871
|
-
const ids = new Set;
|
|
88872
|
-
const log = gitRun4(ctx.cwd, [
|
|
88873
|
-
"log",
|
|
88874
|
-
"--no-merges",
|
|
88875
|
-
`--max-count=${TRUNK_SCAN}`,
|
|
88876
|
-
"-p",
|
|
88877
|
-
"--format=commit %H",
|
|
88878
|
-
"-U3",
|
|
88879
|
-
"--diff-algorithm=myers",
|
|
88880
|
-
"--indent-heuristic",
|
|
88881
|
-
"--no-color",
|
|
88882
|
-
"--no-ext-diff",
|
|
88883
|
-
"--no-textconv",
|
|
88884
|
-
"--no-renames",
|
|
88885
|
-
"--full-index",
|
|
88886
|
-
"--src-prefix=a/",
|
|
88887
|
-
"--dst-prefix=b/",
|
|
88888
|
-
`${fork}..${ctx.trunk}`,
|
|
88889
|
-
"--"
|
|
88890
|
-
]);
|
|
88891
|
-
if (log.ok && log.stdout.trim()) {
|
|
88892
|
-
const run = spawnSync28("git", ["patch-id", "--stable"], {
|
|
88893
|
-
cwd: ctx.cwd,
|
|
88894
|
-
input: log.stdout,
|
|
88895
|
-
encoding: "utf8",
|
|
88896
|
-
timeout: GIT_TIMEOUTS.local,
|
|
88897
|
-
windowsHide: true,
|
|
88898
|
-
maxBuffer: 64 * 1024 * 1024
|
|
88899
|
-
});
|
|
88900
|
-
for (const line of (run.stdout ?? "").split(`
|
|
88901
|
-
`)) {
|
|
88902
|
-
const id = line.trim().split(/\s+/u)[0];
|
|
88903
|
-
if (id)
|
|
88904
|
-
ids.add(id);
|
|
88905
|
-
}
|
|
88906
|
-
}
|
|
88907
|
-
cache.set(fork, ids);
|
|
88908
|
-
return ids;
|
|
88909
|
-
}
|
|
88910
|
-
function inSliceRun(row, slice) {
|
|
88911
|
-
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
88912
|
-
return row.sliceHash === slice.hash;
|
|
88913
|
-
const since = Date.parse(slice.hashSince);
|
|
88914
|
-
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88915
|
-
}
|
|
88916
|
-
function linked(rows, input, slice, head) {
|
|
88917
|
-
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
88918
|
-
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88919
|
-
return rows.some((row) => {
|
|
88920
|
-
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
88921
|
-
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
88922
|
-
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
88923
|
-
});
|
|
88924
|
-
}
|
|
88925
|
-
function detectLanding(ctx, slice, input) {
|
|
88926
|
-
const out = {
|
|
88927
|
-
landed: false,
|
|
88928
|
-
how: null,
|
|
88929
|
-
pr: null,
|
|
88930
|
-
landedHead: null,
|
|
88931
|
-
forgeError: null,
|
|
88932
|
-
note: null
|
|
88933
|
-
};
|
|
88934
|
-
const landed = (how, landedHead) => {
|
|
88935
|
-
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
88936
|
-
return { ...out, landed: true, how, landedHead };
|
|
88937
|
-
return {
|
|
88938
|
-
...out,
|
|
88939
|
-
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
88940
|
-
};
|
|
88941
|
-
};
|
|
88942
|
-
const { head } = input;
|
|
88943
|
-
if (ctx.forge) {
|
|
88944
|
-
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
88945
|
-
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
88946
|
-
projectId: ctx.forge.headProjectId,
|
|
88947
|
-
sha: head
|
|
88948
|
-
});
|
|
88949
|
-
if (!found.ok)
|
|
88950
|
-
out.forgeError = found.error;
|
|
88951
|
-
else if (found.data) {
|
|
88952
|
-
out.pr = found.data;
|
|
88953
|
-
if (found.data.state === "merged") {
|
|
88954
|
-
const merged = found.data.headSha;
|
|
88955
|
-
const label = `#${found.data.number}`;
|
|
88956
|
-
if (!head) {
|
|
88957
|
-
if (!linked(input.rows, input, slice, merged)) {
|
|
88958
|
-
out.pr = null;
|
|
88959
|
-
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
88960
|
-
return out;
|
|
88961
|
-
}
|
|
88962
|
-
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
88963
|
-
}
|
|
88964
|
-
if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
|
|
88965
|
-
return landed("pr_merged", merged ?? head);
|
|
88966
|
-
out.note = isAncestor4(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
|
|
88967
|
-
return out;
|
|
88968
|
-
}
|
|
88969
|
-
}
|
|
88970
|
-
}
|
|
88971
|
-
if (!head)
|
|
88972
|
-
return out;
|
|
88973
|
-
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
88974
|
-
if (fork === head) {
|
|
88975
|
-
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
88976
|
-
}
|
|
88977
|
-
if (!fork)
|
|
88978
|
-
return out;
|
|
88979
|
-
const trunkIds = trunkPatchIds(ctx, fork);
|
|
88980
|
-
if (trunkIds.size === 0)
|
|
88981
|
-
return out;
|
|
88982
|
-
const froms = [fork];
|
|
88983
|
-
if (input.parentHead && input.parentHead !== head && isAncestor4(ctx.cwd, input.parentHead, head))
|
|
88984
|
-
froms.push(input.parentHead);
|
|
88985
|
-
for (const from of froms) {
|
|
88986
|
-
const id = patchId(ctx.cwd, from, head);
|
|
88987
|
-
if (id && trunkIds.has(id))
|
|
88988
|
-
return landed("squash_patch", head);
|
|
88989
|
-
}
|
|
88990
|
-
return out;
|
|
88991
|
-
}
|
|
88992
|
-
function sliceStart(cwd, rows, fanout, slice) {
|
|
88993
|
-
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
88994
|
-
if (reflog)
|
|
88995
|
-
return reflog;
|
|
88996
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
88997
|
-
return row?.baseSha ?? null;
|
|
88998
|
-
}
|
|
88999
|
-
var isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
89000
|
-
var init_fanout_landed = __esm(() => {
|
|
89001
|
-
init_rev();
|
|
89002
|
-
init_fanout();
|
|
89003
|
-
});
|
|
89004
|
-
|
|
89005
89184
|
// packages/workit-core/src/fanout-check.ts
|
|
89006
89185
|
function mergeConflicts(cwd, a, b) {
|
|
89007
89186
|
const run = gitRun4(cwd, [
|
|
@@ -89184,7 +89363,9 @@ function inspect(cwd, plan, slice, ctx, changedBy) {
|
|
|
89184
89363
|
return check;
|
|
89185
89364
|
}
|
|
89186
89365
|
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
89187
|
-
const
|
|
89366
|
+
const parentLanded = parent ? ctx.landings.get(parent.id)?.landed === true : false;
|
|
89367
|
+
const parentRef = parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : null;
|
|
89368
|
+
const baseRef = slice.base === plan.trunk ? trunk : parent ? parentLanded && !(parentRef && isAncestor5(cwd, parentRef, head)) ? trunk : parentRef : branchRef(cwd, slice.base);
|
|
89188
89369
|
if (!SAFE(baseRef)) {
|
|
89189
89370
|
check.reasons.push(`base ${slice.base} not found`);
|
|
89190
89371
|
return check;
|
|
@@ -89652,7 +89833,8 @@ function worktree(argv, io) {
|
|
|
89652
89833
|
if (action === "create") {
|
|
89653
89834
|
const result = createSliceWorktree(io.cwd, selected.data, {
|
|
89654
89835
|
slice: flags.positionals[0],
|
|
89655
|
-
actor
|
|
89836
|
+
actor,
|
|
89837
|
+
forge: () => forgeFor(io, false).forge
|
|
89656
89838
|
});
|
|
89657
89839
|
if (!result.ok)
|
|
89658
89840
|
return failed(io, result);
|
|
@@ -107587,6 +107769,11 @@ var VERBS = [
|
|
|
107587
107769
|
usage: "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]…",
|
|
107588
107770
|
summary: "Restack onto the moved bases, lease-push and retarget the PRs"
|
|
107589
107771
|
},
|
|
107772
|
+
{
|
|
107773
|
+
name: "restack",
|
|
107774
|
+
usage: "workit stack restack [--name <n>] [--dry-run] [--force <branch>]…",
|
|
107775
|
+
summary: "After a parent merged (squash too): sync, then delete the merged stack branches locally"
|
|
107776
|
+
},
|
|
107590
107777
|
{
|
|
107591
107778
|
name: "land",
|
|
107592
107779
|
usage: "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s]",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-claude-code",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.39.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit Claude Code plugin — session and per-turn task context, branch policy on git shell commands, workit method skills, and verifier/reviewer/implementer agents",
|
|
6
6
|
"keywords": [
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"build": "bun scripts/build.ts"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@brainervirus/workit-cli": "^8.
|
|
43
|
-
"@brainervirus/workit-core": "^8.
|
|
42
|
+
"@brainervirus/workit-cli": "^8.39.0",
|
|
43
|
+
"@brainervirus/workit-core": "^8.39.0"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=24"
|