@brainervirus/workit-claude-code 7.6.0 → 7.7.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 +28 -28
- package/dist/workit.js +758 -363
- package/package.json +3 -3
- package/skills/fanout/SKILL.md +46 -46
- package/skills/fanout/references/brief.md +15 -8
- package/skills/fanout/references/orchestration.md +117 -0
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.7.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.7.0",
|
|
107
|
+
"@brainervirus/workit-cursor": "^7.7.0",
|
|
108
|
+
"@brainervirus/workit-opencode": "^7.7.0",
|
|
109
109
|
"@inkjs/ui": "2.0.0",
|
|
110
110
|
ink: "7.1.1",
|
|
111
111
|
react: "19.2.8"
|
|
@@ -13426,6 +13426,70 @@ function recordRuling(context, input) {
|
|
|
13426
13426
|
refs: input.refs ?? []
|
|
13427
13427
|
}, { now: context.now });
|
|
13428
13428
|
}
|
|
13429
|
+
function activeStanding(rows, fanout, leads = null) {
|
|
13430
|
+
const active = [];
|
|
13431
|
+
for (const row of rows) {
|
|
13432
|
+
if (row.fanout !== fanout || !fromLead(row, leads))
|
|
13433
|
+
continue;
|
|
13434
|
+
if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
|
|
13435
|
+
if (!row.superseded)
|
|
13436
|
+
active.push(row);
|
|
13437
|
+
} else if (row.type === "standing.cleared") {
|
|
13438
|
+
const target = str2(row.target);
|
|
13439
|
+
if (target === null)
|
|
13440
|
+
active.length = 0;
|
|
13441
|
+
else {
|
|
13442
|
+
const index = active.findIndex((order) => order.id === target);
|
|
13443
|
+
if (index >= 0)
|
|
13444
|
+
active.splice(index, 1);
|
|
13445
|
+
}
|
|
13446
|
+
}
|
|
13447
|
+
}
|
|
13448
|
+
return active;
|
|
13449
|
+
}
|
|
13450
|
+
function leadOnly(context, lead) {
|
|
13451
|
+
const session = context.actor.session;
|
|
13452
|
+
if (lead !== null ? session === lead : !isDelegateSession(session))
|
|
13453
|
+
return { ok: true, value: undefined };
|
|
13454
|
+
return err("blocked", lead !== null ? `session ${session ?? "(none)"} is not the fanout's lead (${lead}): standing orders come from the lead` : `session ${session} is a worker or verifier: standing orders come from the lead`, lead !== null ? `the lead in a new session? workit fanout plan <plan.json> --take-lead from this session; a worker or verifier reports the order to the lead instead` : "a worker or verifier reports the order to the lead instead");
|
|
13455
|
+
}
|
|
13456
|
+
function recordStanding(context, input) {
|
|
13457
|
+
const what = required2(input.what, "<order>");
|
|
13458
|
+
if (!what.ok)
|
|
13459
|
+
return what;
|
|
13460
|
+
if (/\p{Cc}/u.test(what.value))
|
|
13461
|
+
return err("invalid_input", "a standing order is one line: no newlines or control characters", 'add each order separately: workit ledger standing add "<order>"');
|
|
13462
|
+
const lead = leadOnly(context, input.lead);
|
|
13463
|
+
if (!lead.ok)
|
|
13464
|
+
return lead;
|
|
13465
|
+
const link = checkSupersede(context, "standing");
|
|
13466
|
+
if (!link.ok)
|
|
13467
|
+
return link;
|
|
13468
|
+
return appendRow(context.cwd, {
|
|
13469
|
+
...common(context, noteKey(context)),
|
|
13470
|
+
type: "standing",
|
|
13471
|
+
fanout: input.fanout,
|
|
13472
|
+
what: what.value
|
|
13473
|
+
}, { now: context.now });
|
|
13474
|
+
}
|
|
13475
|
+
function clearStanding(context, input) {
|
|
13476
|
+
const lead = leadOnly(context, input.lead);
|
|
13477
|
+
if (!lead.ok)
|
|
13478
|
+
return lead;
|
|
13479
|
+
if (input.target !== null) {
|
|
13480
|
+
const ledger = readLedger(context.cwd);
|
|
13481
|
+
if (!ledger.ok)
|
|
13482
|
+
return ledger;
|
|
13483
|
+
if (!activeStanding(ledger.value.rows, input.fanout, input.leads ?? null).some((row) => row.id === input.target))
|
|
13484
|
+
return err("not_found", `no standing order ${input.target} in force for fanout ${input.fanout}`, `workit ledger standing list --fanout ${input.fanout}`);
|
|
13485
|
+
}
|
|
13486
|
+
return appendRow(context.cwd, {
|
|
13487
|
+
...common(context, noteKey(context)),
|
|
13488
|
+
type: "standing.cleared",
|
|
13489
|
+
fanout: input.fanout,
|
|
13490
|
+
target: input.target
|
|
13491
|
+
}, { now: context.now });
|
|
13492
|
+
}
|
|
13429
13493
|
function recordVerdict(context, input) {
|
|
13430
13494
|
if (!VERDICT_RESULTS.includes(input.result))
|
|
13431
13495
|
return err("invalid_input", `unknown verdict "${input.result}"; expected ${VERDICT_RESULTS.join("|")}`);
|
|
@@ -13504,6 +13568,12 @@ function summarizeRow(row) {
|
|
|
13504
13568
|
case "handoff":
|
|
13505
13569
|
summary = `next: ${text("next")}${text("note") ? ` (note: ${text("note")})` : ""}`;
|
|
13506
13570
|
break;
|
|
13571
|
+
case "standing":
|
|
13572
|
+
summary = `${text("what")} (fanout ${text("fanout")})`;
|
|
13573
|
+
break;
|
|
13574
|
+
case "standing.cleared":
|
|
13575
|
+
summary = `cleared ${text("target") || "every standing order"} (fanout ${text("fanout")})`;
|
|
13576
|
+
break;
|
|
13507
13577
|
case "commit.recorded":
|
|
13508
13578
|
summary = `${(row.head ?? "?").slice(0, 12)} ${text("subject")}${row.actor.session ? "" : " (no session)"}`;
|
|
13509
13579
|
break;
|
|
@@ -13730,7 +13800,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING
|
|
|
13730
13800
|
}, required2 = (value, flag) => {
|
|
13731
13801
|
const text = value?.trim();
|
|
13732
13802
|
return text ? { ok: true, value: text } : err("invalid_input", `${flag} is required`);
|
|
13733
|
-
}, OBSERVED_ONLY, MAX_DIRTY_PATHS = 10, dirtyState = (cwd) => {
|
|
13803
|
+
}, fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session), isDelegateSession = (session) => session !== null && (/-w-[a-z0-9._-]+(?:\+try\d)?$/u.test(session) || /-v\d+$/u.test(session) || session.includes(":")), OBSERVED_ONLY, MAX_DIRTY_PATHS = 10, dirtyState = (cwd) => {
|
|
13734
13804
|
const run = gitRun2(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=normal"], GIT_TIMEOUTS.worktree);
|
|
13735
13805
|
if (!run.ok)
|
|
13736
13806
|
return { dirty: false, files: 0, paths: [] };
|
|
@@ -75713,6 +75783,7 @@ var init_stack2 = __esm(() => {
|
|
|
75713
75783
|
|
|
75714
75784
|
// packages/workit-core/src/fanout.ts
|
|
75715
75785
|
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
75786
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
75716
75787
|
import fs23 from "node:fs";
|
|
75717
75788
|
import path43 from "node:path";
|
|
75718
75789
|
function expandBraces(pattern, limit = 64) {
|
|
@@ -75874,6 +75945,9 @@ function parsePlan(raw, defaults, name) {
|
|
|
75874
75945
|
else
|
|
75875
75946
|
shared.push(normalized.pattern);
|
|
75876
75947
|
}
|
|
75948
|
+
const fanIn = raw.fanIn ?? "prs";
|
|
75949
|
+
if (!FAN_IN_MODES.includes(fanIn))
|
|
75950
|
+
problems.push(`fanIn must be one of ${FAN_IN_MODES.join(", ")}`);
|
|
75877
75951
|
if (!Array.isArray(raw.slices) || raw.slices.length === 0)
|
|
75878
75952
|
return { ok: false, problems: [...problems, "slices: list at least one slice"] };
|
|
75879
75953
|
const slices = [];
|
|
@@ -75955,7 +76029,9 @@ function parsePlan(raw, defaults, name) {
|
|
|
75955
76029
|
verify,
|
|
75956
76030
|
forbidden,
|
|
75957
76031
|
context: optional("context"),
|
|
75958
|
-
timebox: optional("timebox")
|
|
76032
|
+
timebox: optional("timebox"),
|
|
76033
|
+
hash: "",
|
|
76034
|
+
hashSince: ""
|
|
75959
76035
|
});
|
|
75960
76036
|
});
|
|
75961
76037
|
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
@@ -75988,8 +76064,11 @@ function parsePlan(raw, defaults, name) {
|
|
|
75988
76064
|
v: FANOUT_VERSION,
|
|
75989
76065
|
name: planName,
|
|
75990
76066
|
trunk,
|
|
76067
|
+
fanIn,
|
|
75991
76068
|
shared,
|
|
75992
76069
|
slices,
|
|
76070
|
+
leadSession: null,
|
|
76071
|
+
formerLeads: [],
|
|
75993
76072
|
createdAt: "",
|
|
75994
76073
|
updatedAt: ""
|
|
75995
76074
|
}
|
|
@@ -76175,6 +76254,18 @@ function worktreeRoot(cwd) {
|
|
|
76175
76254
|
const main = mainCheckout(cwd);
|
|
76176
76255
|
return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
|
|
76177
76256
|
}
|
|
76257
|
+
function sliceHash(slice) {
|
|
76258
|
+
const {
|
|
76259
|
+
worktree: _worktree,
|
|
76260
|
+
hash: _hash,
|
|
76261
|
+
hashSince: _since,
|
|
76262
|
+
tier: _tier,
|
|
76263
|
+
timebox: _timebox,
|
|
76264
|
+
context: _context,
|
|
76265
|
+
...definition
|
|
76266
|
+
} = slice;
|
|
76267
|
+
return createHash16("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
|
|
76268
|
+
}
|
|
76178
76269
|
function fanoutsDir(cwd) {
|
|
76179
76270
|
const root = storeRoot(cwd);
|
|
76180
76271
|
if (!root.ok)
|
|
@@ -76215,20 +76306,29 @@ function parseStored(raw) {
|
|
|
76215
76306
|
verify: list(item.verify),
|
|
76216
76307
|
forbidden: list(item.forbidden),
|
|
76217
76308
|
context: typeof item.context === "string" ? item.context : null,
|
|
76218
|
-
timebox: typeof item.timebox === "string" ? item.timebox : null
|
|
76309
|
+
timebox: typeof item.timebox === "string" ? item.timebox : null,
|
|
76310
|
+
hash: text(item.hash),
|
|
76311
|
+
hashSince: text(item.hashSince) || text(raw.createdAt)
|
|
76219
76312
|
});
|
|
76220
76313
|
}
|
|
76314
|
+
for (const slice of slices)
|
|
76315
|
+
slice.hash ||= sliceHash(slice);
|
|
76221
76316
|
const shared = globs(raw.shared);
|
|
76222
76317
|
const ids = new Set(slices.map((slice) => slice.id));
|
|
76223
76318
|
if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
|
|
76224
76319
|
return null;
|
|
76320
|
+
const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
|
|
76321
|
+
const createdAt = text(raw.createdAt);
|
|
76225
76322
|
return {
|
|
76226
76323
|
v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
|
|
76227
76324
|
name: raw.name,
|
|
76228
76325
|
trunk: raw.trunk,
|
|
76326
|
+
fanIn,
|
|
76229
76327
|
shared,
|
|
76230
76328
|
slices,
|
|
76231
|
-
|
|
76329
|
+
leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
|
|
76330
|
+
formerLeads: list(raw.formerLeads),
|
|
76331
|
+
createdAt,
|
|
76232
76332
|
updatedAt: text(raw.updatedAt)
|
|
76233
76333
|
};
|
|
76234
76334
|
}
|
|
@@ -76308,6 +76408,11 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76308
76408
|
const dir = fanoutsDir(cwd);
|
|
76309
76409
|
if (!dir.ok)
|
|
76310
76410
|
return dir;
|
|
76411
|
+
const session = input.actor.session;
|
|
76412
|
+
if (input.takeLead && !session)
|
|
76413
|
+
return fanoutFail("blocked", "--take-lead needs a session: WORKIT_SESSION_ID is not set", "export WORKIT_SESSION_ID=<your session id>, then plan again with --take-lead");
|
|
76414
|
+
if (input.takeLead && isDelegateSession(session))
|
|
76415
|
+
return fanoutFail("blocked", `session ${session} is a worker or verifier id; only a lead takes the lead`, "run --take-lead from the lead's own session");
|
|
76311
76416
|
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
|
|
76312
76417
|
trunk: input.trunk ?? defaultTrunk(cwd),
|
|
76313
76418
|
root: mainCheckout(cwd),
|
|
@@ -76316,6 +76421,8 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76316
76421
|
if (!parsed.ok)
|
|
76317
76422
|
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 });
|
|
76318
76423
|
const plan = parsed.plan;
|
|
76424
|
+
if (plan.fanIn === "integration" && plan.trunk === defaultTrunk(cwd))
|
|
76425
|
+
return fanoutFail("invalid_input", `fanIn "integration" needs an integration branch as the trunk, not ${plan.trunk}`, `workit git branch <integration-branch> --base ${plan.trunk}, then plan with --trunk <integration-branch>`);
|
|
76319
76426
|
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`);
|
|
76320
76427
|
if (badBranches.length)
|
|
76321
76428
|
return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
|
|
@@ -76344,20 +76451,50 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76344
76451
|
const existing = readFanout(cwd, plan.name);
|
|
76345
76452
|
const previous = existing.ok ? existing.data : null;
|
|
76346
76453
|
plan.createdAt = previous?.createdAt || now.toISOString();
|
|
76454
|
+
plan.leadSession = input.takeLead ? session : previous ? previous.leadSession : session;
|
|
76455
|
+
plan.formerLeads = [
|
|
76456
|
+
...new Set([
|
|
76457
|
+
...previous?.formerLeads ?? [],
|
|
76458
|
+
...previous?.leadSession && previous.leadSession !== plan.leadSession ? [previous.leadSession] : []
|
|
76459
|
+
])
|
|
76460
|
+
].filter((former) => former !== plan.leadSession);
|
|
76347
76461
|
plan.updatedAt = now.toISOString();
|
|
76462
|
+
for (const slice of plan.slices) {
|
|
76463
|
+
slice.hash = sliceHash(slice);
|
|
76464
|
+
const before = previous?.slices.find((old) => old.id === slice.id);
|
|
76465
|
+
slice.hashSince = before && before.hash === slice.hash ? before.hashSince || plan.updatedAt : plan.updatedAt;
|
|
76466
|
+
}
|
|
76348
76467
|
const written = writeFanout(cwd, plan);
|
|
76349
76468
|
if (!written.ok)
|
|
76350
76469
|
return written;
|
|
76470
|
+
if (previous && plan.leadSession !== previous.leadSession)
|
|
76471
|
+
appendObserved(cwd, {
|
|
76472
|
+
type: "fanout.lead.changed",
|
|
76473
|
+
actor: input.actor,
|
|
76474
|
+
fanout: plan.name,
|
|
76475
|
+
from: previous.leadSession,
|
|
76476
|
+
to: plan.leadSession
|
|
76477
|
+
});
|
|
76351
76478
|
appendObserved(cwd, {
|
|
76352
76479
|
type: "fanout.planned",
|
|
76353
76480
|
actor: input.actor,
|
|
76354
76481
|
fanout: plan.name,
|
|
76355
76482
|
trunk: plan.trunk,
|
|
76483
|
+
fanIn: plan.fanIn,
|
|
76356
76484
|
slices: plan.slices.length,
|
|
76357
76485
|
ids: plan.slices.slice(0, 20).map((slice) => slice.id)
|
|
76358
76486
|
});
|
|
76359
76487
|
const grouped = waves(plan.slices);
|
|
76360
76488
|
const notes = [];
|
|
76489
|
+
const warnings = [];
|
|
76490
|
+
if (!input.takeLead && previous?.leadSession && session && session !== previous.leadSession)
|
|
76491
|
+
notes.push(`the lead stays ${previous.leadSession}; this session is ${session}. Leading from this session now? workit fanout plan <plan.json> --take-lead`);
|
|
76492
|
+
if (previous === null) {
|
|
76493
|
+
const ledger = readLedger(cwd);
|
|
76494
|
+
const inForce = ledger.ok ? activeStanding(ledger.value.rows, plan.name, planLeads(plan)) : [];
|
|
76495
|
+
if (inForce.length)
|
|
76496
|
+
warnings.push(`${inForce.length} standing order${inForce.length === 1 ? " is" : "s are"} already in force for fanout ${plan.name}, and every brief will carry ${inForce.length === 1 ? "it" : "them"}: ${inForce.map((row) => `"${String(row.what)}"`).join("; ")}. From an earlier run? workit ledger standing clear --fanout ${plan.name}`);
|
|
76497
|
+
}
|
|
76361
76498
|
if (!ref)
|
|
76362
76499
|
notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
|
|
76363
76500
|
grouped.forEach((wave, index) => {
|
|
@@ -76372,6 +76509,7 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76372
76509
|
data: {
|
|
76373
76510
|
name: plan.name,
|
|
76374
76511
|
trunk: plan.trunk,
|
|
76512
|
+
fanIn: plan.fanIn,
|
|
76375
76513
|
file: written.data,
|
|
76376
76514
|
created: previous === null,
|
|
76377
76515
|
slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
|
|
@@ -76387,11 +76525,12 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76387
76525
|
overlaps,
|
|
76388
76526
|
waves: grouped,
|
|
76389
76527
|
landingOrder: landingOrder(plan.slices),
|
|
76390
|
-
notes
|
|
76528
|
+
notes,
|
|
76529
|
+
warnings
|
|
76391
76530
|
}
|
|
76392
76531
|
};
|
|
76393
76532
|
}
|
|
76394
|
-
var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
76533
|
+
var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock, data) => ({
|
|
76395
76534
|
ok: false,
|
|
76396
76535
|
code,
|
|
76397
76536
|
error,
|
|
@@ -76431,12 +76570,13 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
|
76431
76570
|
stdout: run.stdout ?? "",
|
|
76432
76571
|
stderr: run.error ? String(run.error.message) : run.stderr ?? ""
|
|
76433
76572
|
};
|
|
76434
|
-
}, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
|
|
76573
|
+
}, nulList2 = (text) => text.split("\x00").filter(Boolean), planLeads = (plan) => plan.leadSession ? [plan.leadSession, ...plan.formerLeads] : null, IN_FLIGHT_CAP = 6;
|
|
76435
76574
|
var init_fanout = __esm(() => {
|
|
76436
76575
|
init_rev();
|
|
76437
76576
|
init_ledger();
|
|
76438
76577
|
init_stack();
|
|
76439
76578
|
TIERS = ["mundane", "standard", "hard"];
|
|
76579
|
+
FAN_IN_MODES = ["prs", "integration"];
|
|
76440
76580
|
DEFAULT_SHARED = [
|
|
76441
76581
|
"**/package.json",
|
|
76442
76582
|
"**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
|
|
@@ -76452,6 +76592,488 @@ var init_fanout = __esm(() => {
|
|
|
76452
76592
|
ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
76453
76593
|
});
|
|
76454
76594
|
|
|
76595
|
+
// packages/workit-core/src/fanout-worktree.ts
|
|
76596
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
76597
|
+
import fs24 from "node:fs";
|
|
76598
|
+
import path44 from "node:path";
|
|
76599
|
+
function rowSample(lines) {
|
|
76600
|
+
const out = [];
|
|
76601
|
+
let bytes = 0;
|
|
76602
|
+
for (const line of lines) {
|
|
76603
|
+
const entry = line.slice(0, 200);
|
|
76604
|
+
bytes += Buffer.byteLength(entry) + 3;
|
|
76605
|
+
if (bytes > DIRTY_ROW_BYTES)
|
|
76606
|
+
break;
|
|
76607
|
+
out.push(entry);
|
|
76608
|
+
}
|
|
76609
|
+
return out;
|
|
76610
|
+
}
|
|
76611
|
+
function listWorktrees(cwd) {
|
|
76612
|
+
const out = [];
|
|
76613
|
+
let current = null;
|
|
76614
|
+
for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
|
|
76615
|
+
if (field.startsWith("worktree ")) {
|
|
76616
|
+
current = { path: field.slice(9), head: null, branch: null, locked: false };
|
|
76617
|
+
out.push(current);
|
|
76618
|
+
} else if (!current)
|
|
76619
|
+
continue;
|
|
76620
|
+
else if (field.startsWith("HEAD "))
|
|
76621
|
+
current.head = field.slice(5);
|
|
76622
|
+
else if (field.startsWith("branch "))
|
|
76623
|
+
current.branch = field.slice(7);
|
|
76624
|
+
else if (field === "locked" || field.startsWith("locked "))
|
|
76625
|
+
current.locked = true;
|
|
76626
|
+
}
|
|
76627
|
+
return out;
|
|
76628
|
+
}
|
|
76629
|
+
function createdRow(cwd, plan, slice, target) {
|
|
76630
|
+
const ledger = readLedger(cwd);
|
|
76631
|
+
if (!ledger.ok)
|
|
76632
|
+
return null;
|
|
76633
|
+
const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
|
|
76634
|
+
const rows = ledger.value.rows;
|
|
76635
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
|
|
76636
|
+
if (!row)
|
|
76637
|
+
return null;
|
|
76638
|
+
const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
|
|
76639
|
+
if (removedSince)
|
|
76640
|
+
return null;
|
|
76641
|
+
const admin = adminDir(target);
|
|
76642
|
+
if (!admin)
|
|
76643
|
+
return null;
|
|
76644
|
+
if (typeof row.adminToken === "string")
|
|
76645
|
+
return row.adminToken === admin.token && row.adminId === admin.id && row.adminIno === admin.ino ? { row, exact: true } : null;
|
|
76646
|
+
const made = admin.birthMs ?? admin.mtimeMs;
|
|
76647
|
+
if (made + CLOCK_SLACK_MS < Date.parse(row.at))
|
|
76648
|
+
return null;
|
|
76649
|
+
return { row, exact: false };
|
|
76650
|
+
}
|
|
76651
|
+
function adminDir(worktree) {
|
|
76652
|
+
const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
|
|
76653
|
+
if (!dir.ok)
|
|
76654
|
+
return null;
|
|
76655
|
+
const file = dir.stdout.trim();
|
|
76656
|
+
let token = null;
|
|
76657
|
+
try {
|
|
76658
|
+
token = fs24.readFileSync(path44.join(file, TOKEN_FILE), "utf8").trim() || null;
|
|
76659
|
+
} catch {
|
|
76660
|
+
token = null;
|
|
76661
|
+
}
|
|
76662
|
+
try {
|
|
76663
|
+
const stat = worktreeDeps.stat(file);
|
|
76664
|
+
return {
|
|
76665
|
+
path: file,
|
|
76666
|
+
id: path44.basename(file),
|
|
76667
|
+
ino: stat.ino.toString(),
|
|
76668
|
+
token,
|
|
76669
|
+
birthMs: stat.birthtimeNs > 0n ? Number(stat.birthtimeMs) : null,
|
|
76670
|
+
mtimeMs: Number(stat.mtimeMs)
|
|
76671
|
+
};
|
|
76672
|
+
} catch {
|
|
76673
|
+
return null;
|
|
76674
|
+
}
|
|
76675
|
+
}
|
|
76676
|
+
function markAdminDir(worktree) {
|
|
76677
|
+
const admin = adminDir(worktree);
|
|
76678
|
+
if (!admin)
|
|
76679
|
+
return null;
|
|
76680
|
+
const token = randomBytes4(16).toString("hex");
|
|
76681
|
+
try {
|
|
76682
|
+
fs24.writeFileSync(path44.join(admin.path, TOKEN_FILE), `${token}
|
|
76683
|
+
`);
|
|
76684
|
+
} catch {
|
|
76685
|
+
return null;
|
|
76686
|
+
}
|
|
76687
|
+
return { ...admin, token };
|
|
76688
|
+
}
|
|
76689
|
+
function findSlice(plan, id) {
|
|
76690
|
+
const slice = plan.slices.find((candidate) => candidate.id === id);
|
|
76691
|
+
return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
|
|
76692
|
+
}
|
|
76693
|
+
function excludeScratch(cwd) {
|
|
76694
|
+
const common = gitCommonDir(cwd);
|
|
76695
|
+
if (!common)
|
|
76696
|
+
return;
|
|
76697
|
+
const file = path44.join(common, "info", "exclude");
|
|
76698
|
+
let text = "";
|
|
76699
|
+
try {
|
|
76700
|
+
text = fs24.readFileSync(file, "utf8");
|
|
76701
|
+
} catch {
|
|
76702
|
+
text = "";
|
|
76703
|
+
}
|
|
76704
|
+
if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
|
|
76705
|
+
return;
|
|
76706
|
+
fs24.mkdirSync(path44.dirname(file), { recursive: true });
|
|
76707
|
+
fs24.appendFileSync(file, `${text && !text.endsWith(`
|
|
76708
|
+
`) ? `
|
|
76709
|
+
` : ""}# workit fanout: per-worker scratch dirs
|
|
76710
|
+
${EXCLUDE_LINE}
|
|
76711
|
+
`);
|
|
76712
|
+
}
|
|
76713
|
+
function ensureScratch(worktree) {
|
|
76714
|
+
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
76715
|
+
fs24.mkdirSync(scratch, { recursive: true });
|
|
76716
|
+
return scratch;
|
|
76717
|
+
}
|
|
76718
|
+
function createSliceWorktree(cwd, plan, input) {
|
|
76719
|
+
const found = findSlice(plan, input.slice);
|
|
76720
|
+
if (!found.ok)
|
|
76721
|
+
return found;
|
|
76722
|
+
const slice = found.data;
|
|
76723
|
+
if (!slice.worktree)
|
|
76724
|
+
return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
|
|
76725
|
+
const target = slicePath(cwd, slice);
|
|
76726
|
+
const worktrees = listWorktrees(cwd);
|
|
76727
|
+
const localRef = `refs/heads/${slice.branch}`;
|
|
76728
|
+
const existing = worktrees.find((entry) => samePath(entry.path, target));
|
|
76729
|
+
const base = {
|
|
76730
|
+
fanout: plan.name,
|
|
76731
|
+
slice: slice.id,
|
|
76732
|
+
branch: slice.branch,
|
|
76733
|
+
base: slice.base,
|
|
76734
|
+
path: target
|
|
76735
|
+
};
|
|
76736
|
+
if (existing) {
|
|
76737
|
+
if (existing.branch !== null && existing.branch !== localRef)
|
|
76738
|
+
return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
|
|
76739
|
+
if (!createdRow(cwd, plan, slice, target))
|
|
76740
|
+
return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
|
|
76741
|
+
excludeScratch(cwd);
|
|
76742
|
+
return {
|
|
76743
|
+
ok: true,
|
|
76744
|
+
data: {
|
|
76745
|
+
...base,
|
|
76746
|
+
scratch: ensureScratch(target),
|
|
76747
|
+
head: existing.head ?? "",
|
|
76748
|
+
mode: resolveRef(cwd, localRef) ? "resume" : "new",
|
|
76749
|
+
created: false,
|
|
76750
|
+
notes: ["the worktree already exists; nothing was changed"]
|
|
76751
|
+
}
|
|
76752
|
+
};
|
|
76753
|
+
}
|
|
76754
|
+
const elsewhere = worktrees.find((entry) => entry.branch === localRef);
|
|
76755
|
+
if (elsewhere)
|
|
76756
|
+
return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
|
|
76757
|
+
let dirExisted = false;
|
|
76758
|
+
try {
|
|
76759
|
+
const stat = fs24.statSync(target);
|
|
76760
|
+
if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
|
|
76761
|
+
return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
|
|
76762
|
+
dirExisted = true;
|
|
76763
|
+
} catch (error) {
|
|
76764
|
+
if (error.code !== "ENOENT")
|
|
76765
|
+
return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
|
|
76766
|
+
}
|
|
76767
|
+
const notes = [];
|
|
76768
|
+
let mode = "resume";
|
|
76769
|
+
if (resolveRef(cwd, localRef)) {
|
|
76770
|
+
const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
|
|
76771
|
+
if (!added.ok)
|
|
76772
|
+
return worktreeFailed(added.stderr);
|
|
76773
|
+
} else {
|
|
76774
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
76775
|
+
const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
|
|
76776
|
+
if (resolveRef(cwd, remoteRef)) {
|
|
76777
|
+
const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
|
|
76778
|
+
if (!added.ok)
|
|
76779
|
+
return worktreeFailed(added.stderr);
|
|
76780
|
+
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
76781
|
+
} else {
|
|
76782
|
+
mode = "new";
|
|
76783
|
+
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
76784
|
+
const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
76785
|
+
if (!from)
|
|
76786
|
+
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}`);
|
|
76787
|
+
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
76788
|
+
if (!added.ok)
|
|
76789
|
+
return worktreeFailed(added.stderr);
|
|
76790
|
+
const branched = gitBranch(target, { name: slice.branch, base: slice.base });
|
|
76791
|
+
if (!branched.ok) {
|
|
76792
|
+
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
76793
|
+
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}`);
|
|
76794
|
+
}
|
|
76795
|
+
notes.push(...branched.data.notes);
|
|
76796
|
+
}
|
|
76797
|
+
}
|
|
76798
|
+
excludeScratch(cwd);
|
|
76799
|
+
const scratch = ensureScratch(target);
|
|
76800
|
+
const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
|
|
76801
|
+
const baseSha = mode === "new" ? head : null;
|
|
76802
|
+
const admin = markAdminDir(target);
|
|
76803
|
+
appendObserved(cwd, {
|
|
76804
|
+
type: "fanout.worktree.created",
|
|
76805
|
+
actor: input.actor,
|
|
76806
|
+
fanout: plan.name,
|
|
76807
|
+
slice: slice.id,
|
|
76808
|
+
branch: slice.branch,
|
|
76809
|
+
path: target,
|
|
76810
|
+
scratch,
|
|
76811
|
+
mode,
|
|
76812
|
+
head,
|
|
76813
|
+
dirExisted,
|
|
76814
|
+
planCreatedAt: plan.createdAt,
|
|
76815
|
+
sliceHash: slice.hash,
|
|
76816
|
+
...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
|
|
76817
|
+
...baseSha ? { baseSha } : {}
|
|
76818
|
+
});
|
|
76819
|
+
return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
|
|
76820
|
+
}
|
|
76821
|
+
function releaseSliceWorktree(cwd, plan, input) {
|
|
76822
|
+
const found = findSlice(plan, input.slice);
|
|
76823
|
+
if (!found.ok)
|
|
76824
|
+
return found;
|
|
76825
|
+
const slice = found.data;
|
|
76826
|
+
const target = slicePath(cwd, slice);
|
|
76827
|
+
const worktrees = listWorktrees(cwd);
|
|
76828
|
+
const index = worktrees.findIndex((entry) => samePath(entry.path, target));
|
|
76829
|
+
const out = {
|
|
76830
|
+
fanout: plan.name,
|
|
76831
|
+
slice: slice.id,
|
|
76832
|
+
branch: slice.branch,
|
|
76833
|
+
path: target,
|
|
76834
|
+
head: null,
|
|
76835
|
+
dirty: [],
|
|
76836
|
+
forced: input.force,
|
|
76837
|
+
removed: false,
|
|
76838
|
+
unpushed: null,
|
|
76839
|
+
notes: []
|
|
76840
|
+
};
|
|
76841
|
+
if (index < 0) {
|
|
76842
|
+
if (fs24.existsSync(target))
|
|
76843
|
+
return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
|
|
76844
|
+
out.notes.push(`no worktree at ${target}; nothing to release`);
|
|
76845
|
+
return { ok: true, data: out };
|
|
76846
|
+
}
|
|
76847
|
+
const entry = worktrees[index];
|
|
76848
|
+
if (index === 0)
|
|
76849
|
+
return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
|
|
76850
|
+
if (inside2(cwd, entry.path))
|
|
76851
|
+
return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
|
|
76852
|
+
if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
|
|
76853
|
+
return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
|
|
76854
|
+
const owned = createdRow(cwd, plan, slice, entry.path);
|
|
76855
|
+
if (!owned)
|
|
76856
|
+
return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
|
|
76857
|
+
if (input.force && !owned.exact)
|
|
76858
|
+
return fanoutFail("blocked", `${entry.path} was created before workit recorded worktree identity, so it cannot be told from a worktree someone re-added there; --force is refused and nothing was removed`, `release it without --force, or remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
|
|
76859
|
+
const made = owned.row;
|
|
76860
|
+
out.head = entry.head;
|
|
76861
|
+
const status = gitRun4(entry.path, [
|
|
76862
|
+
"status",
|
|
76863
|
+
"--porcelain=v1",
|
|
76864
|
+
"-z",
|
|
76865
|
+
"--untracked-files=all",
|
|
76866
|
+
"--ignore-submodules=none"
|
|
76867
|
+
]);
|
|
76868
|
+
if (!status.ok)
|
|
76869
|
+
return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
|
|
76870
|
+
`)[0]}`, "nothing was removed; inspect the worktree by hand");
|
|
76871
|
+
const entries = porcelainEntries(status.stdout);
|
|
76872
|
+
const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
|
|
76873
|
+
out.dirty = entries.filter((line) => !inScratch(line));
|
|
76874
|
+
out.unpushed = unpushedCount(cwd, slice.branch);
|
|
76875
|
+
const refused = out.dirty.length > 0 && !input.force;
|
|
76876
|
+
const recorded = appendObserved(cwd, {
|
|
76877
|
+
type: "fanout.worktree.released",
|
|
76878
|
+
actor: input.actor,
|
|
76879
|
+
fanout: plan.name,
|
|
76880
|
+
slice: slice.id,
|
|
76881
|
+
branch: slice.branch,
|
|
76882
|
+
path: entry.path,
|
|
76883
|
+
head: entry.head,
|
|
76884
|
+
uncommittedCount: out.dirty.length,
|
|
76885
|
+
uncommitted: rowSample(out.dirty),
|
|
76886
|
+
forced: input.force,
|
|
76887
|
+
outcome: refused ? "refused" : "removed"
|
|
76888
|
+
});
|
|
76889
|
+
if (!recorded.ok)
|
|
76890
|
+
return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
|
|
76891
|
+
if (refused)
|
|
76892
|
+
return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
|
|
76893
|
+
const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
|
|
76894
|
+
let removed = gitRun4(cwd, args, SLOW);
|
|
76895
|
+
if (!removed.ok && entries.some(inScratch)) {
|
|
76896
|
+
removeScratch(entry.path);
|
|
76897
|
+
removed = gitRun4(cwd, args, SLOW);
|
|
76898
|
+
}
|
|
76899
|
+
if (!removed.ok)
|
|
76900
|
+
return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
|
|
76901
|
+
`)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
|
|
76902
|
+
out.removed = true;
|
|
76903
|
+
if (made.dirExisted === true)
|
|
76904
|
+
fs24.mkdirSync(entry.path, { recursive: true });
|
|
76905
|
+
if (out.unpushed)
|
|
76906
|
+
out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
|
|
76907
|
+
return { ok: true, data: out };
|
|
76908
|
+
}
|
|
76909
|
+
function porcelainEntries(stdout) {
|
|
76910
|
+
const fields = nulList2(stdout);
|
|
76911
|
+
const out = [];
|
|
76912
|
+
for (let index = 0;index < fields.length; index += 1) {
|
|
76913
|
+
const entry = fields[index];
|
|
76914
|
+
out.push(entry);
|
|
76915
|
+
if (entry[0] === "R" || entry[0] === "C")
|
|
76916
|
+
index += 1;
|
|
76917
|
+
}
|
|
76918
|
+
return out;
|
|
76919
|
+
}
|
|
76920
|
+
function unpushedCount(cwd, branch) {
|
|
76921
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
76922
|
+
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
76923
|
+
if (!local)
|
|
76924
|
+
return null;
|
|
76925
|
+
const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
76926
|
+
if (!tracking)
|
|
76927
|
+
return null;
|
|
76928
|
+
const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
|
|
76929
|
+
return count.ok ? Number(count.stdout.trim()) : null;
|
|
76930
|
+
}
|
|
76931
|
+
function removeScratch(worktree) {
|
|
76932
|
+
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
76933
|
+
let stat;
|
|
76934
|
+
try {
|
|
76935
|
+
stat = fs24.lstatSync(scratch);
|
|
76936
|
+
} catch {
|
|
76937
|
+
return;
|
|
76938
|
+
}
|
|
76939
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
76940
|
+
fs24.unlinkSync(scratch);
|
|
76941
|
+
else
|
|
76942
|
+
fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
|
|
76943
|
+
}
|
|
76944
|
+
var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
|
|
76945
|
+
try {
|
|
76946
|
+
return fs24.realpathSync.native(target);
|
|
76947
|
+
} catch {
|
|
76948
|
+
return path44.resolve(target);
|
|
76949
|
+
}
|
|
76950
|
+
}, samePath = (a, b) => {
|
|
76951
|
+
const left = canonical2(a);
|
|
76952
|
+
const right = canonical2(b);
|
|
76953
|
+
return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
76954
|
+
}, inside2 = (child, parent) => {
|
|
76955
|
+
const relative = path44.relative(canonical2(parent), canonical2(child));
|
|
76956
|
+
return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
|
|
76957
|
+
}, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
76958
|
+
`)[0] || "unknown error"}`);
|
|
76959
|
+
var init_fanout_worktree = __esm(() => {
|
|
76960
|
+
init_ops();
|
|
76961
|
+
init_rev();
|
|
76962
|
+
init_ledger();
|
|
76963
|
+
init_fanout();
|
|
76964
|
+
SLOW = GIT_TIMEOUTS.worktree;
|
|
76965
|
+
EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
|
|
76966
|
+
worktreeDeps = {
|
|
76967
|
+
stat: (file) => fs24.statSync(file, { bigint: true })
|
|
76968
|
+
};
|
|
76969
|
+
});
|
|
76970
|
+
|
|
76971
|
+
// packages/workit-core/src/fanout-brief.ts
|
|
76972
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
76973
|
+
function workerSession(lead, slice, attempt) {
|
|
76974
|
+
const tail = attempt === null ? "" : `+try${attempt}`;
|
|
76975
|
+
const full = `${lead}-w-${slice}${tail}`;
|
|
76976
|
+
if (full.length <= SESSION_MAX)
|
|
76977
|
+
return full;
|
|
76978
|
+
const digest = createHash17("sha256").update(slice).digest("hex").slice(0, 12);
|
|
76979
|
+
return `${lead.slice(0, SESSION_MAX - 3 - digest.length - tail.length)}-w-${digest}${tail}`;
|
|
76980
|
+
}
|
|
76981
|
+
function renderBrief(cwd, plan, input) {
|
|
76982
|
+
const found = findSlice(plan, input.slice);
|
|
76983
|
+
if (!found.ok)
|
|
76984
|
+
return found;
|
|
76985
|
+
const slice = found.data;
|
|
76986
|
+
const ledger = readLedger(cwd);
|
|
76987
|
+
if (!ledger.ok)
|
|
76988
|
+
return fanoutFail(ledger.code, ledger.error, ledger.unblock);
|
|
76989
|
+
if (!input.actor.session)
|
|
76990
|
+
return fanoutFail("blocked", "WORKIT_SESSION_ID is not set: worker session ids derive from the lead's", "export WORKIT_SESSION_ID=<your session id>, then render the brief again");
|
|
76991
|
+
const branch = input.attempt === null ? slice.branch : `${slice.branch}-try${input.attempt}`;
|
|
76992
|
+
const taken = plan.slices.find((other) => other.id !== slice.id && other.branch === branch);
|
|
76993
|
+
if (taken)
|
|
76994
|
+
return fanoutFail("invalid_input", `attempt branch ${branch} is slice ${taken.id}'s branch`, "pick another --attempt number, or rename one of the branches in the plan");
|
|
76995
|
+
const head = branchRef(cwd, branch);
|
|
76996
|
+
const mode = input.mode ?? (head ? "resume" : "new");
|
|
76997
|
+
if (mode === "resume" && !head)
|
|
76998
|
+
return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
|
|
76999
|
+
const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
|
|
77000
|
+
const exists = listWorktrees(cwd).some((entry) => samePath(entry.path, planned));
|
|
77001
|
+
excludeScratch(cwd);
|
|
77002
|
+
const scratch = exists ? `${planned}/${SCRATCH_DIR}` : SCRATCH_DIR;
|
|
77003
|
+
const scratchLine = exists ? `SCRATCH: ${scratch} (yours alone; temp files go here, never a shared path)` : `SCRATCH: ${SCRATCH_DIR}/ at your worktree root (mkdir -p it; git ignores it; never a shared path)`;
|
|
77004
|
+
const standing = activeStanding(ledger.value.rows, plan.name, planLeads(plan)).map((row) => ({
|
|
77005
|
+
id: row.id,
|
|
77006
|
+
what: String(row.what)
|
|
77007
|
+
}));
|
|
77008
|
+
const session = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
|
|
77009
|
+
const fanIn = plan.fanIn === "integration" ? [
|
|
77010
|
+
`FAN-IN: integration branch ${plan.trunk}. Before you report, merge its tip into your branch`,
|
|
77011
|
+
` (\`git merge --no-edit ${plan.trunk}\`), resolve conflicts inside SCOPE and re-run VERIFY, so the`,
|
|
77012
|
+
" lead's merge is a fast-forward. No other merge, and no rebase, retarget or force-push."
|
|
77013
|
+
] : [
|
|
77014
|
+
"FAN-IN: one PR per slice. Never rebase, retarget, merge or force-push: the lead owns topology."
|
|
77015
|
+
];
|
|
77016
|
+
const lines = [
|
|
77017
|
+
`MODE: ${mode}${input.attempt === null ? "" : ` (race attempt ${input.attempt}: the lead keeps the attempt with the best verdict)`}`,
|
|
77018
|
+
`GOAL: ${slice.goal}`,
|
|
77019
|
+
`SCOPE: ${slice.scope.join(", ")}`,
|
|
77020
|
+
...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
|
|
77021
|
+
` branch: ${branch} base: ${slice.base}`,
|
|
77022
|
+
`CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
|
|
77023
|
+
...mode === "resume" && head ? [
|
|
77024
|
+
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${slice.base}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
|
|
77025
|
+
] : [],
|
|
77026
|
+
"ACCEPTANCE:",
|
|
77027
|
+
...slice.acceptance.map((line) => ` - ${line}`),
|
|
77028
|
+
`VERIFY: ${slice.verify.join("; ")}`,
|
|
77029
|
+
`TIER: ${slice.tier}`,
|
|
77030
|
+
`TIMEBOX: ${slice.timebox ?? DEFAULT_TIMEBOX}; past it without a new commit you will be replaced`,
|
|
77031
|
+
scratchLine,
|
|
77032
|
+
`FORBIDDEN: ${slice.forbidden.join("; ")}`,
|
|
77033
|
+
...fanIn,
|
|
77034
|
+
...REPORT,
|
|
77035
|
+
"STANDING:",
|
|
77036
|
+
...standing.length ? standing.map((order) => ` - ${order.what}`) : [" - none recorded beyond this brief"],
|
|
77037
|
+
` export WORKIT_SESSION_ID=${session}`,
|
|
77038
|
+
...rules(mode, branch, slice.base)
|
|
77039
|
+
];
|
|
77040
|
+
return {
|
|
77041
|
+
ok: true,
|
|
77042
|
+
data: {
|
|
77043
|
+
fanout: plan.name,
|
|
77044
|
+
slice: slice.id,
|
|
77045
|
+
mode,
|
|
77046
|
+
attempt: input.attempt,
|
|
77047
|
+
branch,
|
|
77048
|
+
base: slice.base,
|
|
77049
|
+
tier: slice.tier,
|
|
77050
|
+
scratch,
|
|
77051
|
+
session,
|
|
77052
|
+
standing,
|
|
77053
|
+
text: `${lines.join(`
|
|
77054
|
+
`)}
|
|
77055
|
+
`
|
|
77056
|
+
}
|
|
77057
|
+
};
|
|
77058
|
+
}
|
|
77059
|
+
var DEFAULT_TIMEBOX = "30 minutes", REPORT, rules = (mode, branch, base) => [
|
|
77060
|
+
"RULES:",
|
|
77061
|
+
mode === "new" ? ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).` : ` 1. First command: \`git switch ${branch}\`, then continue from its head.`,
|
|
77062
|
+
' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
|
|
77063
|
+
" 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
|
|
77064
|
+
" 4. Never record a verdict on your own work."
|
|
77065
|
+
], sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
|
|
77066
|
+
var init_fanout_brief = __esm(() => {
|
|
77067
|
+
init_ledger();
|
|
77068
|
+
init_fanout();
|
|
77069
|
+
init_fanout_worktree();
|
|
77070
|
+
REPORT = [
|
|
77071
|
+
"REPORT: branch, head SHA, files changed, each VERIFY command with its exit code,",
|
|
77072
|
+
" each ACCEPTANCE line met / not met, rulings you made (`workit ledger ruling`),",
|
|
77073
|
+
" anything out of scope as a follow-up, not a diff."
|
|
77074
|
+
];
|
|
77075
|
+
});
|
|
77076
|
+
|
|
76455
77077
|
// packages/workit-core/src/fanout-landed.ts
|
|
76456
77078
|
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
76457
77079
|
function forgeBreaker(resolved) {
|
|
@@ -76562,9 +77184,20 @@ function trunkPatchIds(ctx, fork) {
|
|
|
76562
77184
|
cache.set(fork, ids);
|
|
76563
77185
|
return ids;
|
|
76564
77186
|
}
|
|
77187
|
+
function inSliceRun(row, slice) {
|
|
77188
|
+
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
77189
|
+
return row.sliceHash === slice.hash;
|
|
77190
|
+
const since = Date.parse(slice.hashSince);
|
|
77191
|
+
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
77192
|
+
}
|
|
76565
77193
|
function linked(rows, input, slice, head) {
|
|
76566
77194
|
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
76567
|
-
|
|
77195
|
+
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
77196
|
+
return rows.some((row) => {
|
|
77197
|
+
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
77198
|
+
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
77199
|
+
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
77200
|
+
});
|
|
76568
77201
|
}
|
|
76569
77202
|
function detectLanding(ctx, slice, input) {
|
|
76570
77203
|
const out = {
|
|
@@ -76637,7 +77270,7 @@ function sliceStart(cwd, rows, fanout, slice) {
|
|
|
76637
77270
|
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
76638
77271
|
if (reflog)
|
|
76639
77272
|
return reflog;
|
|
76640
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string");
|
|
77273
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
76641
77274
|
return row?.baseSha ?? null;
|
|
76642
77275
|
}
|
|
76643
77276
|
var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
@@ -76786,7 +77419,8 @@ function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
|
|
|
76786
77419
|
createdFrom: sliceStart(cwd, rows, plan.name, slice),
|
|
76787
77420
|
parentHead,
|
|
76788
77421
|
fanout: plan.name,
|
|
76789
|
-
since: plan.createdAt || null,
|
|
77422
|
+
since: slice.hashSince || plan.createdAt || null,
|
|
77423
|
+
sliceHash: slice.hash || null,
|
|
76790
77424
|
rows
|
|
76791
77425
|
}));
|
|
76792
77426
|
}
|
|
@@ -76928,7 +77562,7 @@ function activity(cwd, plan, slice, rows) {
|
|
|
76928
77562
|
head,
|
|
76929
77563
|
commit,
|
|
76930
77564
|
last: newest(commit, reflog, ...ledger),
|
|
76931
|
-
worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id)
|
|
77565
|
+
worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id && inSliceRun(row, slice))
|
|
76932
77566
|
};
|
|
76933
77567
|
}
|
|
76934
77568
|
function fanoutStatus(cwd, plan, options) {
|
|
@@ -77038,7 +77672,7 @@ function fanoutStatus(cwd, plan, options) {
|
|
|
77038
77672
|
else if (row.branchExists)
|
|
77039
77673
|
row.state = "active";
|
|
77040
77674
|
else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
|
|
77041
|
-
const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
|
|
77675
|
+
const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id && inSliceRun(candidate, byId.get(id)));
|
|
77042
77676
|
row.state = created ? "active" : "not_started";
|
|
77043
77677
|
if (!created)
|
|
77044
77678
|
spawnable.push(id);
|
|
@@ -77090,340 +77724,6 @@ var init_fanout_status = __esm(() => {
|
|
|
77090
77724
|
DEFAULT_STUCK_MS = 30 * 60000;
|
|
77091
77725
|
});
|
|
77092
77726
|
|
|
77093
|
-
// packages/workit-core/src/fanout-worktree.ts
|
|
77094
|
-
import fs24 from "node:fs";
|
|
77095
|
-
import path44 from "node:path";
|
|
77096
|
-
function rowSample(lines) {
|
|
77097
|
-
const out = [];
|
|
77098
|
-
let bytes = 0;
|
|
77099
|
-
for (const line of lines) {
|
|
77100
|
-
const entry = line.slice(0, 200);
|
|
77101
|
-
bytes += Buffer.byteLength(entry) + 3;
|
|
77102
|
-
if (bytes > DIRTY_ROW_BYTES)
|
|
77103
|
-
break;
|
|
77104
|
-
out.push(entry);
|
|
77105
|
-
}
|
|
77106
|
-
return out;
|
|
77107
|
-
}
|
|
77108
|
-
function listWorktrees(cwd) {
|
|
77109
|
-
const out = [];
|
|
77110
|
-
let current = null;
|
|
77111
|
-
for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
|
|
77112
|
-
if (field.startsWith("worktree ")) {
|
|
77113
|
-
current = { path: field.slice(9), head: null, branch: null, locked: false };
|
|
77114
|
-
out.push(current);
|
|
77115
|
-
} else if (!current)
|
|
77116
|
-
continue;
|
|
77117
|
-
else if (field.startsWith("HEAD "))
|
|
77118
|
-
current.head = field.slice(5);
|
|
77119
|
-
else if (field.startsWith("branch "))
|
|
77120
|
-
current.branch = field.slice(7);
|
|
77121
|
-
else if (field === "locked" || field.startsWith("locked "))
|
|
77122
|
-
current.locked = true;
|
|
77123
|
-
}
|
|
77124
|
-
return out;
|
|
77125
|
-
}
|
|
77126
|
-
function createdRow(cwd, plan, slice, target) {
|
|
77127
|
-
const ledger = readLedger(cwd);
|
|
77128
|
-
if (!ledger.ok)
|
|
77129
|
-
return null;
|
|
77130
|
-
const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
|
|
77131
|
-
const rows = ledger.value.rows;
|
|
77132
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
|
|
77133
|
-
if (!row)
|
|
77134
|
-
return null;
|
|
77135
|
-
const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
|
|
77136
|
-
if (removedSince)
|
|
77137
|
-
return null;
|
|
77138
|
-
const made = adminDirTime(target);
|
|
77139
|
-
if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
|
|
77140
|
-
return null;
|
|
77141
|
-
return row;
|
|
77142
|
-
}
|
|
77143
|
-
function adminDirTime(worktree) {
|
|
77144
|
-
const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
|
|
77145
|
-
if (!dir.ok)
|
|
77146
|
-
return null;
|
|
77147
|
-
try {
|
|
77148
|
-
const stat = fs24.statSync(dir.stdout.trim());
|
|
77149
|
-
return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
|
|
77150
|
-
} catch {
|
|
77151
|
-
return null;
|
|
77152
|
-
}
|
|
77153
|
-
}
|
|
77154
|
-
function findSlice(plan, id) {
|
|
77155
|
-
const slice = plan.slices.find((candidate) => candidate.id === id);
|
|
77156
|
-
return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
|
|
77157
|
-
}
|
|
77158
|
-
function excludeScratch(cwd) {
|
|
77159
|
-
const common = gitCommonDir(cwd);
|
|
77160
|
-
if (!common)
|
|
77161
|
-
return;
|
|
77162
|
-
const file = path44.join(common, "info", "exclude");
|
|
77163
|
-
let text = "";
|
|
77164
|
-
try {
|
|
77165
|
-
text = fs24.readFileSync(file, "utf8");
|
|
77166
|
-
} catch {
|
|
77167
|
-
text = "";
|
|
77168
|
-
}
|
|
77169
|
-
if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
|
|
77170
|
-
return;
|
|
77171
|
-
fs24.mkdirSync(path44.dirname(file), { recursive: true });
|
|
77172
|
-
fs24.appendFileSync(file, `${text && !text.endsWith(`
|
|
77173
|
-
`) ? `
|
|
77174
|
-
` : ""}# workit fanout: per-worker scratch dirs
|
|
77175
|
-
${EXCLUDE_LINE}
|
|
77176
|
-
`);
|
|
77177
|
-
}
|
|
77178
|
-
function ensureScratch(worktree) {
|
|
77179
|
-
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77180
|
-
fs24.mkdirSync(scratch, { recursive: true });
|
|
77181
|
-
return scratch;
|
|
77182
|
-
}
|
|
77183
|
-
function createSliceWorktree(cwd, plan, input) {
|
|
77184
|
-
const found = findSlice(plan, input.slice);
|
|
77185
|
-
if (!found.ok)
|
|
77186
|
-
return found;
|
|
77187
|
-
const slice = found.data;
|
|
77188
|
-
if (!slice.worktree)
|
|
77189
|
-
return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
|
|
77190
|
-
const target = slicePath(cwd, slice);
|
|
77191
|
-
const worktrees = listWorktrees(cwd);
|
|
77192
|
-
const localRef = `refs/heads/${slice.branch}`;
|
|
77193
|
-
const existing = worktrees.find((entry) => samePath(entry.path, target));
|
|
77194
|
-
const base = {
|
|
77195
|
-
fanout: plan.name,
|
|
77196
|
-
slice: slice.id,
|
|
77197
|
-
branch: slice.branch,
|
|
77198
|
-
base: slice.base,
|
|
77199
|
-
path: target
|
|
77200
|
-
};
|
|
77201
|
-
if (existing) {
|
|
77202
|
-
if (existing.branch !== null && existing.branch !== localRef)
|
|
77203
|
-
return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
|
|
77204
|
-
if (!createdRow(cwd, plan, slice, target))
|
|
77205
|
-
return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
|
|
77206
|
-
excludeScratch(cwd);
|
|
77207
|
-
return {
|
|
77208
|
-
ok: true,
|
|
77209
|
-
data: {
|
|
77210
|
-
...base,
|
|
77211
|
-
scratch: ensureScratch(target),
|
|
77212
|
-
head: existing.head ?? "",
|
|
77213
|
-
mode: resolveRef(cwd, localRef) ? "resume" : "new",
|
|
77214
|
-
created: false,
|
|
77215
|
-
notes: ["the worktree already exists; nothing was changed"]
|
|
77216
|
-
}
|
|
77217
|
-
};
|
|
77218
|
-
}
|
|
77219
|
-
const elsewhere = worktrees.find((entry) => entry.branch === localRef);
|
|
77220
|
-
if (elsewhere)
|
|
77221
|
-
return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
|
|
77222
|
-
let dirExisted = false;
|
|
77223
|
-
try {
|
|
77224
|
-
const stat = fs24.statSync(target);
|
|
77225
|
-
if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
|
|
77226
|
-
return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
|
|
77227
|
-
dirExisted = true;
|
|
77228
|
-
} catch (error) {
|
|
77229
|
-
if (error.code !== "ENOENT")
|
|
77230
|
-
return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
|
|
77231
|
-
}
|
|
77232
|
-
const notes = [];
|
|
77233
|
-
let mode = "resume";
|
|
77234
|
-
if (resolveRef(cwd, localRef)) {
|
|
77235
|
-
const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
|
|
77236
|
-
if (!added.ok)
|
|
77237
|
-
return worktreeFailed(added.stderr);
|
|
77238
|
-
} else {
|
|
77239
|
-
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77240
|
-
const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
|
|
77241
|
-
if (resolveRef(cwd, remoteRef)) {
|
|
77242
|
-
const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
|
|
77243
|
-
if (!added.ok)
|
|
77244
|
-
return worktreeFailed(added.stderr);
|
|
77245
|
-
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
77246
|
-
} else {
|
|
77247
|
-
mode = "new";
|
|
77248
|
-
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
77249
|
-
const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
77250
|
-
if (!from)
|
|
77251
|
-
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}`);
|
|
77252
|
-
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
77253
|
-
if (!added.ok)
|
|
77254
|
-
return worktreeFailed(added.stderr);
|
|
77255
|
-
const branched = gitBranch(target, { name: slice.branch, base: slice.base });
|
|
77256
|
-
if (!branched.ok) {
|
|
77257
|
-
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
77258
|
-
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}`);
|
|
77259
|
-
}
|
|
77260
|
-
notes.push(...branched.data.notes);
|
|
77261
|
-
}
|
|
77262
|
-
}
|
|
77263
|
-
excludeScratch(cwd);
|
|
77264
|
-
const scratch = ensureScratch(target);
|
|
77265
|
-
const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
|
|
77266
|
-
const baseSha = mode === "new" ? head : null;
|
|
77267
|
-
appendObserved(cwd, {
|
|
77268
|
-
type: "fanout.worktree.created",
|
|
77269
|
-
actor: input.actor,
|
|
77270
|
-
fanout: plan.name,
|
|
77271
|
-
slice: slice.id,
|
|
77272
|
-
branch: slice.branch,
|
|
77273
|
-
path: target,
|
|
77274
|
-
scratch,
|
|
77275
|
-
mode,
|
|
77276
|
-
head,
|
|
77277
|
-
dirExisted,
|
|
77278
|
-
planCreatedAt: plan.createdAt,
|
|
77279
|
-
...baseSha ? { baseSha } : {}
|
|
77280
|
-
});
|
|
77281
|
-
return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
|
|
77282
|
-
}
|
|
77283
|
-
function releaseSliceWorktree(cwd, plan, input) {
|
|
77284
|
-
const found = findSlice(plan, input.slice);
|
|
77285
|
-
if (!found.ok)
|
|
77286
|
-
return found;
|
|
77287
|
-
const slice = found.data;
|
|
77288
|
-
const target = slicePath(cwd, slice);
|
|
77289
|
-
const worktrees = listWorktrees(cwd);
|
|
77290
|
-
const index = worktrees.findIndex((entry) => samePath(entry.path, target));
|
|
77291
|
-
const out = {
|
|
77292
|
-
fanout: plan.name,
|
|
77293
|
-
slice: slice.id,
|
|
77294
|
-
branch: slice.branch,
|
|
77295
|
-
path: target,
|
|
77296
|
-
head: null,
|
|
77297
|
-
dirty: [],
|
|
77298
|
-
forced: input.force,
|
|
77299
|
-
removed: false,
|
|
77300
|
-
unpushed: null,
|
|
77301
|
-
notes: []
|
|
77302
|
-
};
|
|
77303
|
-
if (index < 0) {
|
|
77304
|
-
if (fs24.existsSync(target))
|
|
77305
|
-
return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
|
|
77306
|
-
out.notes.push(`no worktree at ${target}; nothing to release`);
|
|
77307
|
-
return { ok: true, data: out };
|
|
77308
|
-
}
|
|
77309
|
-
const entry = worktrees[index];
|
|
77310
|
-
if (index === 0)
|
|
77311
|
-
return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
|
|
77312
|
-
if (inside2(cwd, entry.path))
|
|
77313
|
-
return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
|
|
77314
|
-
if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
|
|
77315
|
-
return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
|
|
77316
|
-
const made = createdRow(cwd, plan, slice, entry.path);
|
|
77317
|
-
if (!made)
|
|
77318
|
-
return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
|
|
77319
|
-
out.head = entry.head;
|
|
77320
|
-
const status = gitRun4(entry.path, [
|
|
77321
|
-
"status",
|
|
77322
|
-
"--porcelain=v1",
|
|
77323
|
-
"-z",
|
|
77324
|
-
"--untracked-files=all",
|
|
77325
|
-
"--ignore-submodules=none"
|
|
77326
|
-
]);
|
|
77327
|
-
if (!status.ok)
|
|
77328
|
-
return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
|
|
77329
|
-
`)[0]}`, "nothing was removed; inspect the worktree by hand");
|
|
77330
|
-
const entries = porcelainEntries(status.stdout);
|
|
77331
|
-
const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
|
|
77332
|
-
out.dirty = entries.filter((line) => !inScratch(line));
|
|
77333
|
-
out.unpushed = unpushedCount(cwd, slice.branch);
|
|
77334
|
-
const refused = out.dirty.length > 0 && !input.force;
|
|
77335
|
-
const recorded = appendObserved(cwd, {
|
|
77336
|
-
type: "fanout.worktree.released",
|
|
77337
|
-
actor: input.actor,
|
|
77338
|
-
fanout: plan.name,
|
|
77339
|
-
slice: slice.id,
|
|
77340
|
-
branch: slice.branch,
|
|
77341
|
-
path: entry.path,
|
|
77342
|
-
head: entry.head,
|
|
77343
|
-
uncommittedCount: out.dirty.length,
|
|
77344
|
-
uncommitted: rowSample(out.dirty),
|
|
77345
|
-
forced: input.force,
|
|
77346
|
-
outcome: refused ? "refused" : "removed"
|
|
77347
|
-
});
|
|
77348
|
-
if (!recorded.ok)
|
|
77349
|
-
return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
|
|
77350
|
-
if (refused)
|
|
77351
|
-
return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
|
|
77352
|
-
const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
|
|
77353
|
-
let removed = gitRun4(cwd, args, SLOW);
|
|
77354
|
-
if (!removed.ok && entries.some(inScratch)) {
|
|
77355
|
-
removeScratch(entry.path);
|
|
77356
|
-
removed = gitRun4(cwd, args, SLOW);
|
|
77357
|
-
}
|
|
77358
|
-
if (!removed.ok)
|
|
77359
|
-
return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
|
|
77360
|
-
`)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
|
|
77361
|
-
out.removed = true;
|
|
77362
|
-
if (made.dirExisted === true)
|
|
77363
|
-
fs24.mkdirSync(entry.path, { recursive: true });
|
|
77364
|
-
if (out.unpushed)
|
|
77365
|
-
out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
|
|
77366
|
-
return { ok: true, data: out };
|
|
77367
|
-
}
|
|
77368
|
-
function porcelainEntries(stdout) {
|
|
77369
|
-
const fields = nulList2(stdout);
|
|
77370
|
-
const out = [];
|
|
77371
|
-
for (let index = 0;index < fields.length; index += 1) {
|
|
77372
|
-
const entry = fields[index];
|
|
77373
|
-
out.push(entry);
|
|
77374
|
-
if (entry[0] === "R" || entry[0] === "C")
|
|
77375
|
-
index += 1;
|
|
77376
|
-
}
|
|
77377
|
-
return out;
|
|
77378
|
-
}
|
|
77379
|
-
function unpushedCount(cwd, branch) {
|
|
77380
|
-
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77381
|
-
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
77382
|
-
if (!local)
|
|
77383
|
-
return null;
|
|
77384
|
-
const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
77385
|
-
if (!tracking)
|
|
77386
|
-
return null;
|
|
77387
|
-
const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
|
|
77388
|
-
return count.ok ? Number(count.stdout.trim()) : null;
|
|
77389
|
-
}
|
|
77390
|
-
function removeScratch(worktree) {
|
|
77391
|
-
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77392
|
-
let stat;
|
|
77393
|
-
try {
|
|
77394
|
-
stat = fs24.lstatSync(scratch);
|
|
77395
|
-
} catch {
|
|
77396
|
-
return;
|
|
77397
|
-
}
|
|
77398
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
77399
|
-
fs24.unlinkSync(scratch);
|
|
77400
|
-
else
|
|
77401
|
-
fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
|
|
77402
|
-
}
|
|
77403
|
-
var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
|
|
77404
|
-
try {
|
|
77405
|
-
return fs24.realpathSync.native(target);
|
|
77406
|
-
} catch {
|
|
77407
|
-
return path44.resolve(target);
|
|
77408
|
-
}
|
|
77409
|
-
}, samePath = (a, b) => {
|
|
77410
|
-
const left = canonical2(a);
|
|
77411
|
-
const right = canonical2(b);
|
|
77412
|
-
return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
77413
|
-
}, inside2 = (child, parent) => {
|
|
77414
|
-
const relative = path44.relative(canonical2(parent), canonical2(child));
|
|
77415
|
-
return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
|
|
77416
|
-
}, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
77417
|
-
`)[0] || "unknown error"}`);
|
|
77418
|
-
var init_fanout_worktree = __esm(() => {
|
|
77419
|
-
init_ops();
|
|
77420
|
-
init_rev();
|
|
77421
|
-
init_ledger();
|
|
77422
|
-
init_fanout();
|
|
77423
|
-
SLOW = GIT_TIMEOUTS.worktree;
|
|
77424
|
-
EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
|
|
77425
|
-
});
|
|
77426
|
-
|
|
77427
77727
|
// packages/workit-cli/src/verbs/fanout.ts
|
|
77428
77728
|
var exports_fanout = {};
|
|
77429
77729
|
__export(exports_fanout, {
|
|
@@ -77478,7 +77778,12 @@ function resolveTrunk(io, raw, flag, track) {
|
|
|
77478
77778
|
return { trunk: derived.tracked || track !== null ? derived.trunk : null };
|
|
77479
77779
|
}
|
|
77480
77780
|
function plan2(argv, io) {
|
|
77481
|
-
const flags = parseFlags(argv, {
|
|
77781
|
+
const flags = parseFlags(argv, {
|
|
77782
|
+
name: "value",
|
|
77783
|
+
trunk: "value",
|
|
77784
|
+
track: "value",
|
|
77785
|
+
"take-lead": "boolean"
|
|
77786
|
+
});
|
|
77482
77787
|
if (typeof flags === "string")
|
|
77483
77788
|
return usage3(io, flags, PLAN_USAGE2);
|
|
77484
77789
|
const track = flags.values.track ?? null;
|
|
@@ -77500,7 +77805,8 @@ function plan2(argv, io) {
|
|
|
77500
77805
|
raw,
|
|
77501
77806
|
name: flags.values.name ?? null,
|
|
77502
77807
|
trunk: trunk.trunk,
|
|
77503
|
-
actor: actorFromEnv(io.env)
|
|
77808
|
+
actor: actorFromEnv(io.env),
|
|
77809
|
+
takeLead: flags.booleans.has("take-lead")
|
|
77504
77810
|
});
|
|
77505
77811
|
if (!result.ok) {
|
|
77506
77812
|
const code = failed(io, result);
|
|
@@ -77508,15 +77814,42 @@ function plan2(argv, io) {
|
|
|
77508
77814
|
return code;
|
|
77509
77815
|
}
|
|
77510
77816
|
return emit(io, ok(result.data), (data) => [
|
|
77511
|
-
`${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
|
|
77817
|
+
`${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}${data.fanIn === "integration" ? " (integration branch: one PR)" : ""}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
|
|
77512
77818
|
...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
|
|
77513
77819
|
worktree ${slice.worktree}`),
|
|
77514
77820
|
...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
|
|
77515
77821
|
`waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
|
|
77516
77822
|
`landing order: ${data.landingOrder.join(", ")}`,
|
|
77517
|
-
...data.notes.map((note) => `note: ${note}`)
|
|
77823
|
+
...data.notes.map((note) => `note: ${note}`),
|
|
77824
|
+
...data.warnings.map((warning) => `warning: ${warning}`)
|
|
77518
77825
|
]);
|
|
77519
77826
|
}
|
|
77827
|
+
function brief(argv, io) {
|
|
77828
|
+
const flags = parseFlags(argv, { name: "value", mode: "value", attempt: "value" });
|
|
77829
|
+
if (typeof flags === "string")
|
|
77830
|
+
return usage3(io, flags, BRIEF_USAGE);
|
|
77831
|
+
if (flags.positionals.length !== 1)
|
|
77832
|
+
return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <slice>", BRIEF_USAGE);
|
|
77833
|
+
const mode = flags.values.mode ?? null;
|
|
77834
|
+
if (mode !== null && mode !== "new" && mode !== "resume")
|
|
77835
|
+
return usage3(io, "--mode takes new or resume", BRIEF_USAGE);
|
|
77836
|
+
const rawAttempt = flags.values.attempt;
|
|
77837
|
+
const attempt = rawAttempt === undefined ? null : Number(rawAttempt);
|
|
77838
|
+
if (attempt !== null && !(Number.isInteger(attempt) && attempt >= 1 && attempt <= 9))
|
|
77839
|
+
return usage3(io, "--attempt takes a number from 1 to 9", BRIEF_USAGE);
|
|
77840
|
+
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
77841
|
+
if (!selected.ok)
|
|
77842
|
+
return failed(io, selected);
|
|
77843
|
+
const result = renderBrief(io.cwd, selected.data, {
|
|
77844
|
+
slice: flags.positionals[0],
|
|
77845
|
+
mode,
|
|
77846
|
+
attempt,
|
|
77847
|
+
actor: actorFromEnv(io.env)
|
|
77848
|
+
});
|
|
77849
|
+
if (!result.ok)
|
|
77850
|
+
return failed(io, result);
|
|
77851
|
+
return emit(io, ok(result.data), (data) => data.text.trimEnd());
|
|
77852
|
+
}
|
|
77520
77853
|
function check2(argv, io) {
|
|
77521
77854
|
const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
|
|
77522
77855
|
if (typeof flags === "string")
|
|
@@ -77618,6 +77951,8 @@ async function run15(argv, io) {
|
|
|
77618
77951
|
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
77619
77952
|
if (sub === "plan")
|
|
77620
77953
|
return plan2(rest, io);
|
|
77954
|
+
if (sub === "brief")
|
|
77955
|
+
return brief(rest, io);
|
|
77621
77956
|
if (sub === "check")
|
|
77622
77957
|
return check2(rest, io);
|
|
77623
77958
|
if (sub === "status")
|
|
@@ -77626,7 +77961,7 @@ async function run15(argv, io) {
|
|
|
77626
77961
|
return worktree(rest, io);
|
|
77627
77962
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
|
|
77628
77963
|
}
|
|
77629
|
-
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
77964
|
+
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] [--json]", BRIEF_USAGE = "workit fanout brief <slice> [--name <n>] [--mode new|resume] [--attempt <n>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|brief|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
77630
77965
|
`fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
|
|
77631
77966
|
...data.slices.map((slice) => slice.status === "landed" ? ` -- ${slice.id} (${slice.branch}) landed (${slice.landed?.how}${slice.landed?.pr ? ` #${slice.landed.pr}` : ""})` : ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
|
|
77632
77967
|
${slice.reasons.join(`
|
|
@@ -77664,6 +77999,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
|
|
|
77664
77999
|
];
|
|
77665
78000
|
};
|
|
77666
78001
|
var init_fanout2 = __esm(() => {
|
|
78002
|
+
init_fanout_brief();
|
|
77667
78003
|
init_fanout_check();
|
|
77668
78004
|
init_fanout_status();
|
|
77669
78005
|
init_fanout_worktree();
|
|
@@ -77679,7 +78015,7 @@ var exports_ledger = {};
|
|
|
77679
78015
|
__export(exports_ledger, {
|
|
77680
78016
|
run: () => run16
|
|
77681
78017
|
});
|
|
77682
|
-
import { randomBytes as
|
|
78018
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
77683
78019
|
import { parseArgs } from "node:util";
|
|
77684
78020
|
function targetBranch(io, rows, branch, pr) {
|
|
77685
78021
|
if (pr !== undefined) {
|
|
@@ -77725,8 +78061,10 @@ async function run16(argv, io) {
|
|
|
77725
78061
|
const text = rest.join(" ");
|
|
77726
78062
|
if (sub === undefined)
|
|
77727
78063
|
return usage4(io, "missing subcommand");
|
|
77728
|
-
if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
|
|
78064
|
+
if (!["decision", "ruling", "verdict", "standing", "list", "show", "check"].includes(sub))
|
|
77729
78065
|
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
78066
|
+
if (sub === "standing")
|
|
78067
|
+
return standing(io, values, rest);
|
|
77730
78068
|
const ledger = readLedger(io.cwd);
|
|
77731
78069
|
if (!ledger.ok)
|
|
77732
78070
|
return failed2(io, ledger);
|
|
@@ -77804,7 +78142,62 @@ async function run16(argv, io) {
|
|
|
77804
78142
|
`);
|
|
77805
78143
|
return fromResult(io, verdict, (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)` : ""}`);
|
|
77806
78144
|
}
|
|
77807
|
-
|
|
78145
|
+
function standingFanout(io, values) {
|
|
78146
|
+
if (values.fanout !== undefined) {
|
|
78147
|
+
const name = values.fanout.trim();
|
|
78148
|
+
if (!name)
|
|
78149
|
+
return new Error("--fanout needs a plan name");
|
|
78150
|
+
const read = readFanout(io.cwd, name);
|
|
78151
|
+
return { name, plan: read.ok ? read.data : null };
|
|
78152
|
+
}
|
|
78153
|
+
const selected = selectFanout(io.cwd, null, currentBranch(io.cwd));
|
|
78154
|
+
return selected.ok ? { name: selected.data.name, plan: selected.data } : new Error(`${selected.error}; pass --fanout <name>`);
|
|
78155
|
+
}
|
|
78156
|
+
async function standing(io, values, args) {
|
|
78157
|
+
const [action, ...rest] = args;
|
|
78158
|
+
if (action !== "add" && action !== "list" && action !== "clear")
|
|
78159
|
+
return emit(io, fail("invalid_input", action ? `unknown standing action "${action}"` : "missing add, list or clear", { unblock: STANDING_USAGE }));
|
|
78160
|
+
const target = standingFanout(io, values);
|
|
78161
|
+
if (target instanceof Error)
|
|
78162
|
+
return emit(io, fail("invalid_input", target.message, { unblock: STANDING_USAGE }));
|
|
78163
|
+
const fanout = target.name;
|
|
78164
|
+
const lead = target.plan?.leadSession ?? null;
|
|
78165
|
+
const leads = target.plan ? planLeads(target.plan) : null;
|
|
78166
|
+
if (action === "list") {
|
|
78167
|
+
if (rest.length)
|
|
78168
|
+
return usage4(io, `unexpected argument: ${rest[0]}`);
|
|
78169
|
+
const ledger = readLedger(io.cwd);
|
|
78170
|
+
if (!ledger.ok)
|
|
78171
|
+
return failed2(io, ledger);
|
|
78172
|
+
const orders = activeStanding(ledger.value.rows, fanout, leads).map((row) => ({
|
|
78173
|
+
id: row.id,
|
|
78174
|
+
at: row.at,
|
|
78175
|
+
what: row.what
|
|
78176
|
+
}));
|
|
78177
|
+
return emit(io, ok({ fanout, orders }), (data) => data.orders.length ? [
|
|
78178
|
+
`standing orders for fanout ${fanout}:`,
|
|
78179
|
+
...data.orders.map((o) => ` ${o.id} ${o.what}`)
|
|
78180
|
+
] : `no standing orders for fanout ${fanout}`);
|
|
78181
|
+
}
|
|
78182
|
+
const acting = actorFor(io, values);
|
|
78183
|
+
if (acting instanceof Error)
|
|
78184
|
+
return usage4(io, acting.message);
|
|
78185
|
+
const context = {
|
|
78186
|
+
cwd: io.cwd,
|
|
78187
|
+
actor: acting.actor,
|
|
78188
|
+
...values.supersedes ? { supersedes: values.supersedes } : {}
|
|
78189
|
+
};
|
|
78190
|
+
if (action === "add" && target.plan === null)
|
|
78191
|
+
return emit(io, fail("not_found", `no fanout plan named ${fanout}`, {
|
|
78192
|
+
unblock: "workit fanout plan <plan.json> first, or name an existing plan with --fanout"
|
|
78193
|
+
}));
|
|
78194
|
+
if (action === "add")
|
|
78195
|
+
return fromResult(io, recordStanding(context, { fanout, lead, what: rest.join(" ") }), (row) => `recorded standing order ${row.id} for fanout ${fanout}`);
|
|
78196
|
+
if (rest.length > 1)
|
|
78197
|
+
return usage4(io, `unexpected argument: ${rest[1]}`);
|
|
78198
|
+
return fromResult(io, clearStanding(context, { fanout, lead, leads, target: rest[0] ?? null }), (row) => row.target ? `cleared standing order ${row.target} for fanout ${fanout}` : `cleared every standing order for fanout ${fanout}`);
|
|
78199
|
+
}
|
|
78200
|
+
var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (workit help ledger for the grammar)", STANDING_USAGE = 'workit ledger standing add "<order>" | list | clear [<id>] [--fanout <name>] [--json]', OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
|
|
77808
78201
|
const actor = actorFromEnv(io.env);
|
|
77809
78202
|
if (values.session !== undefined && values.as !== undefined)
|
|
77810
78203
|
return new Error("pass --session or --as, not both");
|
|
@@ -77818,7 +78211,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
77818
78211
|
return new Error("--as takes a lowercase role, e.g. verifier");
|
|
77819
78212
|
const prefix = (actor.session ?? actor.host).slice(0, 96);
|
|
77820
78213
|
return {
|
|
77821
|
-
actor: { ...actor, session: `${prefix}:${values.as}:${
|
|
78214
|
+
actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes5(4).toString("hex")}` },
|
|
77822
78215
|
derivedFrom: actor.session
|
|
77823
78216
|
};
|
|
77824
78217
|
}
|
|
@@ -77841,6 +78234,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
77841
78234
|
};
|
|
77842
78235
|
var init_ledger2 = __esm(() => {
|
|
77843
78236
|
init_ledger();
|
|
78237
|
+
init_fanout();
|
|
77844
78238
|
init_rev();
|
|
77845
78239
|
init_implicit_task();
|
|
77846
78240
|
init_output();
|
|
@@ -77861,6 +78255,7 @@ var init_ledger2 = __esm(() => {
|
|
|
77861
78255
|
supersedes: { type: "string" },
|
|
77862
78256
|
session: { type: "string" },
|
|
77863
78257
|
as: { type: "string" },
|
|
78258
|
+
fanout: { type: "string" },
|
|
77864
78259
|
json: { type: "boolean" }
|
|
77865
78260
|
};
|
|
77866
78261
|
SESSION_SAFE2 = /^[A-Za-z0-9_.:@/+-]{1,128}$/;
|
|
@@ -95044,15 +95439,15 @@ var VERBS = [
|
|
|
95044
95439
|
{
|
|
95045
95440
|
name: "fanout",
|
|
95046
95441
|
group: "delivery",
|
|
95047
|
-
usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
|
|
95048
|
-
summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
|
|
95442
|
+
usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] | fanout brief <slice> [--mode new|resume] [--attempt <n>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
|
|
95443
|
+
summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), render each worker's brief with the standing orders, watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
|
|
95049
95444
|
load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
|
|
95050
95445
|
},
|
|
95051
95446
|
{
|
|
95052
95447
|
name: "ledger",
|
|
95053
95448
|
group: "delivery",
|
|
95054
|
-
usage: "workit ledger decision|ruling|verdict|list|check ...",
|
|
95055
|
-
summary: "Record and query decisions, rulings
|
|
95449
|
+
usage: "workit ledger decision|ruling|verdict|standing|list|check ...",
|
|
95450
|
+
summary: "Record and query decisions, rulings, SHA-keyed verdicts (patch-id carry-over across rebases) and a fanout's standing orders",
|
|
95056
95451
|
load: () => Promise.resolve().then(() => (init_ledger2(), exports_ledger))
|
|
95057
95452
|
},
|
|
95058
95453
|
{
|