@brainervirus/workit-claude-code 7.5.2 → 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 +29 -28
- package/dist/workit.js +760 -364
- package/package.json +3 -3
- package/skills/architecture/SKILL.md +72 -0
- package/skills/architecture/agents/openai.yaml +3 -0
- package/skills/architecture/references/instruction-files.md +52 -0
- package/skills/architecture/references/vocabulary.md +53 -0
- 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: [] };
|
|
@@ -63145,7 +63215,8 @@ var init_skill_manifests = __esm(() => {
|
|
|
63145
63215
|
"workit-deslop",
|
|
63146
63216
|
"workit-fanout",
|
|
63147
63217
|
"workit-verify-app",
|
|
63148
|
-
"workit-retro"
|
|
63218
|
+
"workit-retro",
|
|
63219
|
+
"workit-architecture"
|
|
63149
63220
|
];
|
|
63150
63221
|
if (false) {}
|
|
63151
63222
|
});
|
|
@@ -75712,6 +75783,7 @@ var init_stack2 = __esm(() => {
|
|
|
75712
75783
|
|
|
75713
75784
|
// packages/workit-core/src/fanout.ts
|
|
75714
75785
|
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
75786
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
75715
75787
|
import fs23 from "node:fs";
|
|
75716
75788
|
import path43 from "node:path";
|
|
75717
75789
|
function expandBraces(pattern, limit = 64) {
|
|
@@ -75873,6 +75945,9 @@ function parsePlan(raw, defaults, name) {
|
|
|
75873
75945
|
else
|
|
75874
75946
|
shared.push(normalized.pattern);
|
|
75875
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(", ")}`);
|
|
75876
75951
|
if (!Array.isArray(raw.slices) || raw.slices.length === 0)
|
|
75877
75952
|
return { ok: false, problems: [...problems, "slices: list at least one slice"] };
|
|
75878
75953
|
const slices = [];
|
|
@@ -75954,7 +76029,9 @@ function parsePlan(raw, defaults, name) {
|
|
|
75954
76029
|
verify,
|
|
75955
76030
|
forbidden,
|
|
75956
76031
|
context: optional("context"),
|
|
75957
|
-
timebox: optional("timebox")
|
|
76032
|
+
timebox: optional("timebox"),
|
|
76033
|
+
hash: "",
|
|
76034
|
+
hashSince: ""
|
|
75958
76035
|
});
|
|
75959
76036
|
});
|
|
75960
76037
|
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
@@ -75987,8 +76064,11 @@ function parsePlan(raw, defaults, name) {
|
|
|
75987
76064
|
v: FANOUT_VERSION,
|
|
75988
76065
|
name: planName,
|
|
75989
76066
|
trunk,
|
|
76067
|
+
fanIn,
|
|
75990
76068
|
shared,
|
|
75991
76069
|
slices,
|
|
76070
|
+
leadSession: null,
|
|
76071
|
+
formerLeads: [],
|
|
75992
76072
|
createdAt: "",
|
|
75993
76073
|
updatedAt: ""
|
|
75994
76074
|
}
|
|
@@ -76174,6 +76254,18 @@ function worktreeRoot(cwd) {
|
|
|
76174
76254
|
const main = mainCheckout(cwd);
|
|
76175
76255
|
return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
|
|
76176
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
|
+
}
|
|
76177
76269
|
function fanoutsDir(cwd) {
|
|
76178
76270
|
const root = storeRoot(cwd);
|
|
76179
76271
|
if (!root.ok)
|
|
@@ -76214,20 +76306,29 @@ function parseStored(raw) {
|
|
|
76214
76306
|
verify: list(item.verify),
|
|
76215
76307
|
forbidden: list(item.forbidden),
|
|
76216
76308
|
context: typeof item.context === "string" ? item.context : null,
|
|
76217
|
-
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)
|
|
76218
76312
|
});
|
|
76219
76313
|
}
|
|
76314
|
+
for (const slice of slices)
|
|
76315
|
+
slice.hash ||= sliceHash(slice);
|
|
76220
76316
|
const shared = globs(raw.shared);
|
|
76221
76317
|
const ids = new Set(slices.map((slice) => slice.id));
|
|
76222
76318
|
if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
|
|
76223
76319
|
return null;
|
|
76320
|
+
const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
|
|
76321
|
+
const createdAt = text(raw.createdAt);
|
|
76224
76322
|
return {
|
|
76225
76323
|
v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
|
|
76226
76324
|
name: raw.name,
|
|
76227
76325
|
trunk: raw.trunk,
|
|
76326
|
+
fanIn,
|
|
76228
76327
|
shared,
|
|
76229
76328
|
slices,
|
|
76230
|
-
|
|
76329
|
+
leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
|
|
76330
|
+
formerLeads: list(raw.formerLeads),
|
|
76331
|
+
createdAt,
|
|
76231
76332
|
updatedAt: text(raw.updatedAt)
|
|
76232
76333
|
};
|
|
76233
76334
|
}
|
|
@@ -76307,6 +76408,11 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76307
76408
|
const dir = fanoutsDir(cwd);
|
|
76308
76409
|
if (!dir.ok)
|
|
76309
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");
|
|
76310
76416
|
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
|
|
76311
76417
|
trunk: input.trunk ?? defaultTrunk(cwd),
|
|
76312
76418
|
root: mainCheckout(cwd),
|
|
@@ -76315,6 +76421,8 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76315
76421
|
if (!parsed.ok)
|
|
76316
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 });
|
|
76317
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>`);
|
|
76318
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`);
|
|
76319
76427
|
if (badBranches.length)
|
|
76320
76428
|
return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
|
|
@@ -76343,20 +76451,50 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76343
76451
|
const existing = readFanout(cwd, plan.name);
|
|
76344
76452
|
const previous = existing.ok ? existing.data : null;
|
|
76345
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);
|
|
76346
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
|
+
}
|
|
76347
76467
|
const written = writeFanout(cwd, plan);
|
|
76348
76468
|
if (!written.ok)
|
|
76349
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
|
+
});
|
|
76350
76478
|
appendObserved(cwd, {
|
|
76351
76479
|
type: "fanout.planned",
|
|
76352
76480
|
actor: input.actor,
|
|
76353
76481
|
fanout: plan.name,
|
|
76354
76482
|
trunk: plan.trunk,
|
|
76483
|
+
fanIn: plan.fanIn,
|
|
76355
76484
|
slices: plan.slices.length,
|
|
76356
76485
|
ids: plan.slices.slice(0, 20).map((slice) => slice.id)
|
|
76357
76486
|
});
|
|
76358
76487
|
const grouped = waves(plan.slices);
|
|
76359
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
|
+
}
|
|
76360
76498
|
if (!ref)
|
|
76361
76499
|
notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
|
|
76362
76500
|
grouped.forEach((wave, index) => {
|
|
@@ -76371,6 +76509,7 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76371
76509
|
data: {
|
|
76372
76510
|
name: plan.name,
|
|
76373
76511
|
trunk: plan.trunk,
|
|
76512
|
+
fanIn: plan.fanIn,
|
|
76374
76513
|
file: written.data,
|
|
76375
76514
|
created: previous === null,
|
|
76376
76515
|
slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
|
|
@@ -76386,11 +76525,12 @@ function planFanout(cwd, input, now = new Date) {
|
|
|
76386
76525
|
overlaps,
|
|
76387
76526
|
waves: grouped,
|
|
76388
76527
|
landingOrder: landingOrder(plan.slices),
|
|
76389
|
-
notes
|
|
76528
|
+
notes,
|
|
76529
|
+
warnings
|
|
76390
76530
|
}
|
|
76391
76531
|
};
|
|
76392
76532
|
}
|
|
76393
|
-
var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
76533
|
+
var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock, data) => ({
|
|
76394
76534
|
ok: false,
|
|
76395
76535
|
code,
|
|
76396
76536
|
error,
|
|
@@ -76430,12 +76570,13 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
|
76430
76570
|
stdout: run.stdout ?? "",
|
|
76431
76571
|
stderr: run.error ? String(run.error.message) : run.stderr ?? ""
|
|
76432
76572
|
};
|
|
76433
|
-
}, 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;
|
|
76434
76574
|
var init_fanout = __esm(() => {
|
|
76435
76575
|
init_rev();
|
|
76436
76576
|
init_ledger();
|
|
76437
76577
|
init_stack();
|
|
76438
76578
|
TIERS = ["mundane", "standard", "hard"];
|
|
76579
|
+
FAN_IN_MODES = ["prs", "integration"];
|
|
76439
76580
|
DEFAULT_SHARED = [
|
|
76440
76581
|
"**/package.json",
|
|
76441
76582
|
"**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
|
|
@@ -76451,6 +76592,488 @@ var init_fanout = __esm(() => {
|
|
|
76451
76592
|
ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
76452
76593
|
});
|
|
76453
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
|
+
|
|
76454
77077
|
// packages/workit-core/src/fanout-landed.ts
|
|
76455
77078
|
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
76456
77079
|
function forgeBreaker(resolved) {
|
|
@@ -76561,9 +77184,20 @@ function trunkPatchIds(ctx, fork) {
|
|
|
76561
77184
|
cache.set(fork, ids);
|
|
76562
77185
|
return ids;
|
|
76563
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
|
+
}
|
|
76564
77193
|
function linked(rows, input, slice, head) {
|
|
76565
77194
|
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
76566
|
-
|
|
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
|
+
});
|
|
76567
77201
|
}
|
|
76568
77202
|
function detectLanding(ctx, slice, input) {
|
|
76569
77203
|
const out = {
|
|
@@ -76636,7 +77270,7 @@ function sliceStart(cwd, rows, fanout, slice) {
|
|
|
76636
77270
|
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
76637
77271
|
if (reflog)
|
|
76638
77272
|
return reflog;
|
|
76639
|
-
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));
|
|
76640
77274
|
return row?.baseSha ?? null;
|
|
76641
77275
|
}
|
|
76642
77276
|
var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
@@ -76785,7 +77419,8 @@ function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
|
|
|
76785
77419
|
createdFrom: sliceStart(cwd, rows, plan.name, slice),
|
|
76786
77420
|
parentHead,
|
|
76787
77421
|
fanout: plan.name,
|
|
76788
|
-
since: plan.createdAt || null,
|
|
77422
|
+
since: slice.hashSince || plan.createdAt || null,
|
|
77423
|
+
sliceHash: slice.hash || null,
|
|
76789
77424
|
rows
|
|
76790
77425
|
}));
|
|
76791
77426
|
}
|
|
@@ -76927,7 +77562,7 @@ function activity(cwd, plan, slice, rows) {
|
|
|
76927
77562
|
head,
|
|
76928
77563
|
commit,
|
|
76929
77564
|
last: newest(commit, reflog, ...ledger),
|
|
76930
|
-
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))
|
|
76931
77566
|
};
|
|
76932
77567
|
}
|
|
76933
77568
|
function fanoutStatus(cwd, plan, options) {
|
|
@@ -77037,7 +77672,7 @@ function fanoutStatus(cwd, plan, options) {
|
|
|
77037
77672
|
else if (row.branchExists)
|
|
77038
77673
|
row.state = "active";
|
|
77039
77674
|
else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
|
|
77040
|
-
const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
|
|
77675
|
+
const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id && inSliceRun(candidate, byId.get(id)));
|
|
77041
77676
|
row.state = created ? "active" : "not_started";
|
|
77042
77677
|
if (!created)
|
|
77043
77678
|
spawnable.push(id);
|
|
@@ -77089,340 +77724,6 @@ var init_fanout_status = __esm(() => {
|
|
|
77089
77724
|
DEFAULT_STUCK_MS = 30 * 60000;
|
|
77090
77725
|
});
|
|
77091
77726
|
|
|
77092
|
-
// packages/workit-core/src/fanout-worktree.ts
|
|
77093
|
-
import fs24 from "node:fs";
|
|
77094
|
-
import path44 from "node:path";
|
|
77095
|
-
function rowSample(lines) {
|
|
77096
|
-
const out = [];
|
|
77097
|
-
let bytes = 0;
|
|
77098
|
-
for (const line of lines) {
|
|
77099
|
-
const entry = line.slice(0, 200);
|
|
77100
|
-
bytes += Buffer.byteLength(entry) + 3;
|
|
77101
|
-
if (bytes > DIRTY_ROW_BYTES)
|
|
77102
|
-
break;
|
|
77103
|
-
out.push(entry);
|
|
77104
|
-
}
|
|
77105
|
-
return out;
|
|
77106
|
-
}
|
|
77107
|
-
function listWorktrees(cwd) {
|
|
77108
|
-
const out = [];
|
|
77109
|
-
let current = null;
|
|
77110
|
-
for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
|
|
77111
|
-
if (field.startsWith("worktree ")) {
|
|
77112
|
-
current = { path: field.slice(9), head: null, branch: null, locked: false };
|
|
77113
|
-
out.push(current);
|
|
77114
|
-
} else if (!current)
|
|
77115
|
-
continue;
|
|
77116
|
-
else if (field.startsWith("HEAD "))
|
|
77117
|
-
current.head = field.slice(5);
|
|
77118
|
-
else if (field.startsWith("branch "))
|
|
77119
|
-
current.branch = field.slice(7);
|
|
77120
|
-
else if (field === "locked" || field.startsWith("locked "))
|
|
77121
|
-
current.locked = true;
|
|
77122
|
-
}
|
|
77123
|
-
return out;
|
|
77124
|
-
}
|
|
77125
|
-
function createdRow(cwd, plan, slice, target) {
|
|
77126
|
-
const ledger = readLedger(cwd);
|
|
77127
|
-
if (!ledger.ok)
|
|
77128
|
-
return null;
|
|
77129
|
-
const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
|
|
77130
|
-
const rows = ledger.value.rows;
|
|
77131
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
|
|
77132
|
-
if (!row)
|
|
77133
|
-
return null;
|
|
77134
|
-
const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
|
|
77135
|
-
if (removedSince)
|
|
77136
|
-
return null;
|
|
77137
|
-
const made = adminDirTime(target);
|
|
77138
|
-
if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
|
|
77139
|
-
return null;
|
|
77140
|
-
return row;
|
|
77141
|
-
}
|
|
77142
|
-
function adminDirTime(worktree) {
|
|
77143
|
-
const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
|
|
77144
|
-
if (!dir.ok)
|
|
77145
|
-
return null;
|
|
77146
|
-
try {
|
|
77147
|
-
const stat = fs24.statSync(dir.stdout.trim());
|
|
77148
|
-
return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
|
|
77149
|
-
} catch {
|
|
77150
|
-
return null;
|
|
77151
|
-
}
|
|
77152
|
-
}
|
|
77153
|
-
function findSlice(plan, id) {
|
|
77154
|
-
const slice = plan.slices.find((candidate) => candidate.id === id);
|
|
77155
|
-
return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
|
|
77156
|
-
}
|
|
77157
|
-
function excludeScratch(cwd) {
|
|
77158
|
-
const common = gitCommonDir(cwd);
|
|
77159
|
-
if (!common)
|
|
77160
|
-
return;
|
|
77161
|
-
const file = path44.join(common, "info", "exclude");
|
|
77162
|
-
let text = "";
|
|
77163
|
-
try {
|
|
77164
|
-
text = fs24.readFileSync(file, "utf8");
|
|
77165
|
-
} catch {
|
|
77166
|
-
text = "";
|
|
77167
|
-
}
|
|
77168
|
-
if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
|
|
77169
|
-
return;
|
|
77170
|
-
fs24.mkdirSync(path44.dirname(file), { recursive: true });
|
|
77171
|
-
fs24.appendFileSync(file, `${text && !text.endsWith(`
|
|
77172
|
-
`) ? `
|
|
77173
|
-
` : ""}# workit fanout: per-worker scratch dirs
|
|
77174
|
-
${EXCLUDE_LINE}
|
|
77175
|
-
`);
|
|
77176
|
-
}
|
|
77177
|
-
function ensureScratch(worktree) {
|
|
77178
|
-
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77179
|
-
fs24.mkdirSync(scratch, { recursive: true });
|
|
77180
|
-
return scratch;
|
|
77181
|
-
}
|
|
77182
|
-
function createSliceWorktree(cwd, plan, input) {
|
|
77183
|
-
const found = findSlice(plan, input.slice);
|
|
77184
|
-
if (!found.ok)
|
|
77185
|
-
return found;
|
|
77186
|
-
const slice = found.data;
|
|
77187
|
-
if (!slice.worktree)
|
|
77188
|
-
return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
|
|
77189
|
-
const target = slicePath(cwd, slice);
|
|
77190
|
-
const worktrees = listWorktrees(cwd);
|
|
77191
|
-
const localRef = `refs/heads/${slice.branch}`;
|
|
77192
|
-
const existing = worktrees.find((entry) => samePath(entry.path, target));
|
|
77193
|
-
const base = {
|
|
77194
|
-
fanout: plan.name,
|
|
77195
|
-
slice: slice.id,
|
|
77196
|
-
branch: slice.branch,
|
|
77197
|
-
base: slice.base,
|
|
77198
|
-
path: target
|
|
77199
|
-
};
|
|
77200
|
-
if (existing) {
|
|
77201
|
-
if (existing.branch !== null && existing.branch !== localRef)
|
|
77202
|
-
return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
|
|
77203
|
-
if (!createdRow(cwd, plan, slice, target))
|
|
77204
|
-
return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
|
|
77205
|
-
excludeScratch(cwd);
|
|
77206
|
-
return {
|
|
77207
|
-
ok: true,
|
|
77208
|
-
data: {
|
|
77209
|
-
...base,
|
|
77210
|
-
scratch: ensureScratch(target),
|
|
77211
|
-
head: existing.head ?? "",
|
|
77212
|
-
mode: resolveRef(cwd, localRef) ? "resume" : "new",
|
|
77213
|
-
created: false,
|
|
77214
|
-
notes: ["the worktree already exists; nothing was changed"]
|
|
77215
|
-
}
|
|
77216
|
-
};
|
|
77217
|
-
}
|
|
77218
|
-
const elsewhere = worktrees.find((entry) => entry.branch === localRef);
|
|
77219
|
-
if (elsewhere)
|
|
77220
|
-
return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
|
|
77221
|
-
let dirExisted = false;
|
|
77222
|
-
try {
|
|
77223
|
-
const stat = fs24.statSync(target);
|
|
77224
|
-
if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
|
|
77225
|
-
return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
|
|
77226
|
-
dirExisted = true;
|
|
77227
|
-
} catch (error) {
|
|
77228
|
-
if (error.code !== "ENOENT")
|
|
77229
|
-
return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
|
|
77230
|
-
}
|
|
77231
|
-
const notes = [];
|
|
77232
|
-
let mode = "resume";
|
|
77233
|
-
if (resolveRef(cwd, localRef)) {
|
|
77234
|
-
const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
|
|
77235
|
-
if (!added.ok)
|
|
77236
|
-
return worktreeFailed(added.stderr);
|
|
77237
|
-
} else {
|
|
77238
|
-
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77239
|
-
const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
|
|
77240
|
-
if (resolveRef(cwd, remoteRef)) {
|
|
77241
|
-
const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
|
|
77242
|
-
if (!added.ok)
|
|
77243
|
-
return worktreeFailed(added.stderr);
|
|
77244
|
-
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
77245
|
-
} else {
|
|
77246
|
-
mode = "new";
|
|
77247
|
-
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
77248
|
-
const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
77249
|
-
if (!from)
|
|
77250
|
-
return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
|
|
77251
|
-
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
77252
|
-
if (!added.ok)
|
|
77253
|
-
return worktreeFailed(added.stderr);
|
|
77254
|
-
const branched = gitBranch(target, { name: slice.branch, base: slice.base });
|
|
77255
|
-
if (!branched.ok) {
|
|
77256
|
-
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
77257
|
-
return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
|
|
77258
|
-
}
|
|
77259
|
-
notes.push(...branched.data.notes);
|
|
77260
|
-
}
|
|
77261
|
-
}
|
|
77262
|
-
excludeScratch(cwd);
|
|
77263
|
-
const scratch = ensureScratch(target);
|
|
77264
|
-
const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
|
|
77265
|
-
const baseSha = mode === "new" ? head : null;
|
|
77266
|
-
appendObserved(cwd, {
|
|
77267
|
-
type: "fanout.worktree.created",
|
|
77268
|
-
actor: input.actor,
|
|
77269
|
-
fanout: plan.name,
|
|
77270
|
-
slice: slice.id,
|
|
77271
|
-
branch: slice.branch,
|
|
77272
|
-
path: target,
|
|
77273
|
-
scratch,
|
|
77274
|
-
mode,
|
|
77275
|
-
head,
|
|
77276
|
-
dirExisted,
|
|
77277
|
-
planCreatedAt: plan.createdAt,
|
|
77278
|
-
...baseSha ? { baseSha } : {}
|
|
77279
|
-
});
|
|
77280
|
-
return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
|
|
77281
|
-
}
|
|
77282
|
-
function releaseSliceWorktree(cwd, plan, input) {
|
|
77283
|
-
const found = findSlice(plan, input.slice);
|
|
77284
|
-
if (!found.ok)
|
|
77285
|
-
return found;
|
|
77286
|
-
const slice = found.data;
|
|
77287
|
-
const target = slicePath(cwd, slice);
|
|
77288
|
-
const worktrees = listWorktrees(cwd);
|
|
77289
|
-
const index = worktrees.findIndex((entry) => samePath(entry.path, target));
|
|
77290
|
-
const out = {
|
|
77291
|
-
fanout: plan.name,
|
|
77292
|
-
slice: slice.id,
|
|
77293
|
-
branch: slice.branch,
|
|
77294
|
-
path: target,
|
|
77295
|
-
head: null,
|
|
77296
|
-
dirty: [],
|
|
77297
|
-
forced: input.force,
|
|
77298
|
-
removed: false,
|
|
77299
|
-
unpushed: null,
|
|
77300
|
-
notes: []
|
|
77301
|
-
};
|
|
77302
|
-
if (index < 0) {
|
|
77303
|
-
if (fs24.existsSync(target))
|
|
77304
|
-
return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
|
|
77305
|
-
out.notes.push(`no worktree at ${target}; nothing to release`);
|
|
77306
|
-
return { ok: true, data: out };
|
|
77307
|
-
}
|
|
77308
|
-
const entry = worktrees[index];
|
|
77309
|
-
if (index === 0)
|
|
77310
|
-
return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
|
|
77311
|
-
if (inside2(cwd, entry.path))
|
|
77312
|
-
return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
|
|
77313
|
-
if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
|
|
77314
|
-
return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
|
|
77315
|
-
const made = createdRow(cwd, plan, slice, entry.path);
|
|
77316
|
-
if (!made)
|
|
77317
|
-
return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
|
|
77318
|
-
out.head = entry.head;
|
|
77319
|
-
const status = gitRun4(entry.path, [
|
|
77320
|
-
"status",
|
|
77321
|
-
"--porcelain=v1",
|
|
77322
|
-
"-z",
|
|
77323
|
-
"--untracked-files=all",
|
|
77324
|
-
"--ignore-submodules=none"
|
|
77325
|
-
]);
|
|
77326
|
-
if (!status.ok)
|
|
77327
|
-
return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
|
|
77328
|
-
`)[0]}`, "nothing was removed; inspect the worktree by hand");
|
|
77329
|
-
const entries = porcelainEntries(status.stdout);
|
|
77330
|
-
const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
|
|
77331
|
-
out.dirty = entries.filter((line) => !inScratch(line));
|
|
77332
|
-
out.unpushed = unpushedCount(cwd, slice.branch);
|
|
77333
|
-
const refused = out.dirty.length > 0 && !input.force;
|
|
77334
|
-
const recorded = appendObserved(cwd, {
|
|
77335
|
-
type: "fanout.worktree.released",
|
|
77336
|
-
actor: input.actor,
|
|
77337
|
-
fanout: plan.name,
|
|
77338
|
-
slice: slice.id,
|
|
77339
|
-
branch: slice.branch,
|
|
77340
|
-
path: entry.path,
|
|
77341
|
-
head: entry.head,
|
|
77342
|
-
uncommittedCount: out.dirty.length,
|
|
77343
|
-
uncommitted: rowSample(out.dirty),
|
|
77344
|
-
forced: input.force,
|
|
77345
|
-
outcome: refused ? "refused" : "removed"
|
|
77346
|
-
});
|
|
77347
|
-
if (!recorded.ok)
|
|
77348
|
-
return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
|
|
77349
|
-
if (refused)
|
|
77350
|
-
return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
|
|
77351
|
-
const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
|
|
77352
|
-
let removed = gitRun4(cwd, args, SLOW);
|
|
77353
|
-
if (!removed.ok && entries.some(inScratch)) {
|
|
77354
|
-
removeScratch(entry.path);
|
|
77355
|
-
removed = gitRun4(cwd, args, SLOW);
|
|
77356
|
-
}
|
|
77357
|
-
if (!removed.ok)
|
|
77358
|
-
return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
|
|
77359
|
-
`)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
|
|
77360
|
-
out.removed = true;
|
|
77361
|
-
if (made.dirExisted === true)
|
|
77362
|
-
fs24.mkdirSync(entry.path, { recursive: true });
|
|
77363
|
-
if (out.unpushed)
|
|
77364
|
-
out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
|
|
77365
|
-
return { ok: true, data: out };
|
|
77366
|
-
}
|
|
77367
|
-
function porcelainEntries(stdout) {
|
|
77368
|
-
const fields = nulList2(stdout);
|
|
77369
|
-
const out = [];
|
|
77370
|
-
for (let index = 0;index < fields.length; index += 1) {
|
|
77371
|
-
const entry = fields[index];
|
|
77372
|
-
out.push(entry);
|
|
77373
|
-
if (entry[0] === "R" || entry[0] === "C")
|
|
77374
|
-
index += 1;
|
|
77375
|
-
}
|
|
77376
|
-
return out;
|
|
77377
|
-
}
|
|
77378
|
-
function unpushedCount(cwd, branch) {
|
|
77379
|
-
const remote = pushRemoteName(cwd) ?? "origin";
|
|
77380
|
-
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
77381
|
-
if (!local)
|
|
77382
|
-
return null;
|
|
77383
|
-
const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
77384
|
-
if (!tracking)
|
|
77385
|
-
return null;
|
|
77386
|
-
const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
|
|
77387
|
-
return count.ok ? Number(count.stdout.trim()) : null;
|
|
77388
|
-
}
|
|
77389
|
-
function removeScratch(worktree) {
|
|
77390
|
-
const scratch = path44.join(worktree, SCRATCH_DIR);
|
|
77391
|
-
let stat;
|
|
77392
|
-
try {
|
|
77393
|
-
stat = fs24.lstatSync(scratch);
|
|
77394
|
-
} catch {
|
|
77395
|
-
return;
|
|
77396
|
-
}
|
|
77397
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
77398
|
-
fs24.unlinkSync(scratch);
|
|
77399
|
-
else
|
|
77400
|
-
fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
|
|
77401
|
-
}
|
|
77402
|
-
var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
|
|
77403
|
-
try {
|
|
77404
|
-
return fs24.realpathSync.native(target);
|
|
77405
|
-
} catch {
|
|
77406
|
-
return path44.resolve(target);
|
|
77407
|
-
}
|
|
77408
|
-
}, samePath = (a, b) => {
|
|
77409
|
-
const left = canonical2(a);
|
|
77410
|
-
const right = canonical2(b);
|
|
77411
|
-
return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
77412
|
-
}, inside2 = (child, parent) => {
|
|
77413
|
-
const relative = path44.relative(canonical2(parent), canonical2(child));
|
|
77414
|
-
return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
|
|
77415
|
-
}, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
77416
|
-
`)[0] || "unknown error"}`);
|
|
77417
|
-
var init_fanout_worktree = __esm(() => {
|
|
77418
|
-
init_ops();
|
|
77419
|
-
init_rev();
|
|
77420
|
-
init_ledger();
|
|
77421
|
-
init_fanout();
|
|
77422
|
-
SLOW = GIT_TIMEOUTS.worktree;
|
|
77423
|
-
EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
|
|
77424
|
-
});
|
|
77425
|
-
|
|
77426
77727
|
// packages/workit-cli/src/verbs/fanout.ts
|
|
77427
77728
|
var exports_fanout = {};
|
|
77428
77729
|
__export(exports_fanout, {
|
|
@@ -77477,7 +77778,12 @@ function resolveTrunk(io, raw, flag, track) {
|
|
|
77477
77778
|
return { trunk: derived.tracked || track !== null ? derived.trunk : null };
|
|
77478
77779
|
}
|
|
77479
77780
|
function plan2(argv, io) {
|
|
77480
|
-
const flags = parseFlags(argv, {
|
|
77781
|
+
const flags = parseFlags(argv, {
|
|
77782
|
+
name: "value",
|
|
77783
|
+
trunk: "value",
|
|
77784
|
+
track: "value",
|
|
77785
|
+
"take-lead": "boolean"
|
|
77786
|
+
});
|
|
77481
77787
|
if (typeof flags === "string")
|
|
77482
77788
|
return usage3(io, flags, PLAN_USAGE2);
|
|
77483
77789
|
const track = flags.values.track ?? null;
|
|
@@ -77499,7 +77805,8 @@ function plan2(argv, io) {
|
|
|
77499
77805
|
raw,
|
|
77500
77806
|
name: flags.values.name ?? null,
|
|
77501
77807
|
trunk: trunk.trunk,
|
|
77502
|
-
actor: actorFromEnv(io.env)
|
|
77808
|
+
actor: actorFromEnv(io.env),
|
|
77809
|
+
takeLead: flags.booleans.has("take-lead")
|
|
77503
77810
|
});
|
|
77504
77811
|
if (!result.ok) {
|
|
77505
77812
|
const code = failed(io, result);
|
|
@@ -77507,15 +77814,42 @@ function plan2(argv, io) {
|
|
|
77507
77814
|
return code;
|
|
77508
77815
|
}
|
|
77509
77816
|
return emit(io, ok(result.data), (data) => [
|
|
77510
|
-
`${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"}`,
|
|
77511
77818
|
...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
|
|
77512
77819
|
worktree ${slice.worktree}`),
|
|
77513
77820
|
...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
|
|
77514
77821
|
`waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
|
|
77515
77822
|
`landing order: ${data.landingOrder.join(", ")}`,
|
|
77516
|
-
...data.notes.map((note) => `note: ${note}`)
|
|
77823
|
+
...data.notes.map((note) => `note: ${note}`),
|
|
77824
|
+
...data.warnings.map((warning) => `warning: ${warning}`)
|
|
77517
77825
|
]);
|
|
77518
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
|
+
}
|
|
77519
77853
|
function check2(argv, io) {
|
|
77520
77854
|
const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
|
|
77521
77855
|
if (typeof flags === "string")
|
|
@@ -77617,6 +77951,8 @@ async function run15(argv, io) {
|
|
|
77617
77951
|
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
77618
77952
|
if (sub === "plan")
|
|
77619
77953
|
return plan2(rest, io);
|
|
77954
|
+
if (sub === "brief")
|
|
77955
|
+
return brief(rest, io);
|
|
77620
77956
|
if (sub === "check")
|
|
77621
77957
|
return check2(rest, io);
|
|
77622
77958
|
if (sub === "status")
|
|
@@ -77625,7 +77961,7 @@ async function run15(argv, io) {
|
|
|
77625
77961
|
return worktree(rest, io);
|
|
77626
77962
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
|
|
77627
77963
|
}
|
|
77628
|
-
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
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) => [
|
|
77629
77965
|
`fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
|
|
77630
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 ? `
|
|
77631
77967
|
${slice.reasons.join(`
|
|
@@ -77663,6 +77999,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
|
|
|
77663
77999
|
];
|
|
77664
78000
|
};
|
|
77665
78001
|
var init_fanout2 = __esm(() => {
|
|
78002
|
+
init_fanout_brief();
|
|
77666
78003
|
init_fanout_check();
|
|
77667
78004
|
init_fanout_status();
|
|
77668
78005
|
init_fanout_worktree();
|
|
@@ -77678,7 +78015,7 @@ var exports_ledger = {};
|
|
|
77678
78015
|
__export(exports_ledger, {
|
|
77679
78016
|
run: () => run16
|
|
77680
78017
|
});
|
|
77681
|
-
import { randomBytes as
|
|
78018
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
77682
78019
|
import { parseArgs } from "node:util";
|
|
77683
78020
|
function targetBranch(io, rows, branch, pr) {
|
|
77684
78021
|
if (pr !== undefined) {
|
|
@@ -77724,8 +78061,10 @@ async function run16(argv, io) {
|
|
|
77724
78061
|
const text = rest.join(" ");
|
|
77725
78062
|
if (sub === undefined)
|
|
77726
78063
|
return usage4(io, "missing subcommand");
|
|
77727
|
-
if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
|
|
78064
|
+
if (!["decision", "ruling", "verdict", "standing", "list", "show", "check"].includes(sub))
|
|
77728
78065
|
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
78066
|
+
if (sub === "standing")
|
|
78067
|
+
return standing(io, values, rest);
|
|
77729
78068
|
const ledger = readLedger(io.cwd);
|
|
77730
78069
|
if (!ledger.ok)
|
|
77731
78070
|
return failed2(io, ledger);
|
|
@@ -77803,7 +78142,62 @@ async function run16(argv, io) {
|
|
|
77803
78142
|
`);
|
|
77804
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)` : ""}`);
|
|
77805
78144
|
}
|
|
77806
|
-
|
|
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) => {
|
|
77807
78201
|
const actor = actorFromEnv(io.env);
|
|
77808
78202
|
if (values.session !== undefined && values.as !== undefined)
|
|
77809
78203
|
return new Error("pass --session or --as, not both");
|
|
@@ -77817,7 +78211,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
77817
78211
|
return new Error("--as takes a lowercase role, e.g. verifier");
|
|
77818
78212
|
const prefix = (actor.session ?? actor.host).slice(0, 96);
|
|
77819
78213
|
return {
|
|
77820
|
-
actor: { ...actor, session: `${prefix}:${values.as}:${
|
|
78214
|
+
actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes5(4).toString("hex")}` },
|
|
77821
78215
|
derivedFrom: actor.session
|
|
77822
78216
|
};
|
|
77823
78217
|
}
|
|
@@ -77840,6 +78234,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
77840
78234
|
};
|
|
77841
78235
|
var init_ledger2 = __esm(() => {
|
|
77842
78236
|
init_ledger();
|
|
78237
|
+
init_fanout();
|
|
77843
78238
|
init_rev();
|
|
77844
78239
|
init_implicit_task();
|
|
77845
78240
|
init_output();
|
|
@@ -77860,6 +78255,7 @@ var init_ledger2 = __esm(() => {
|
|
|
77860
78255
|
supersedes: { type: "string" },
|
|
77861
78256
|
session: { type: "string" },
|
|
77862
78257
|
as: { type: "string" },
|
|
78258
|
+
fanout: { type: "string" },
|
|
77863
78259
|
json: { type: "boolean" }
|
|
77864
78260
|
};
|
|
77865
78261
|
SESSION_SAFE2 = /^[A-Za-z0-9_.:@/+-]{1,128}$/;
|
|
@@ -95043,15 +95439,15 @@ var VERBS = [
|
|
|
95043
95439
|
{
|
|
95044
95440
|
name: "fanout",
|
|
95045
95441
|
group: "delivery",
|
|
95046
|
-
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]",
|
|
95047
|
-
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",
|
|
95048
95444
|
load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
|
|
95049
95445
|
},
|
|
95050
95446
|
{
|
|
95051
95447
|
name: "ledger",
|
|
95052
95448
|
group: "delivery",
|
|
95053
|
-
usage: "workit ledger decision|ruling|verdict|list|check ...",
|
|
95054
|
-
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",
|
|
95055
95451
|
load: () => Promise.resolve().then(() => (init_ledger2(), exports_ledger))
|
|
95056
95452
|
},
|
|
95057
95453
|
{
|