@brainervirus/workit-cli 8.12.3 → 8.13.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.
Files changed (2) hide show
  1. package/dist/index.js +943 -116
  2. package/package.json +4 -4
package/dist/index.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: "8.12.3",
68
+ version: "8.13.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": "^8.12.3",
107
- "@brainervirus/workit-cursor": "^8.12.3",
108
- "@brainervirus/workit-opencode": "^8.12.3",
106
+ "@brainervirus/workit-core": "^8.13.0",
107
+ "@brainervirus/workit-cursor": "^8.13.0",
108
+ "@brainervirus/workit-opencode": "^8.13.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -13697,6 +13697,14 @@ var PREFIXES, branchNames = (workspaceRoot) => {
13697
13697
  }, repoBranchPreset = (workspaceRoot) => {
13698
13698
  const detected = detectBranchPolicy(workspaceRoot);
13699
13699
  return detected.developBranch || detected.allowed.length ? detected.preset : null;
13700
+ }, releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
13701
+ if (typeof policyPrefix === "string")
13702
+ return policyPrefix.trim();
13703
+ const r = spawnSync5("git", ["config", "--get", "gitflow.prefix.versiontag"], {
13704
+ cwd: workspaceRoot,
13705
+ encoding: "utf8"
13706
+ });
13707
+ return r.status === 0 ? (r.stdout ?? "").trim() : "v";
13700
13708
  };
13701
13709
  var init_branch_policy = __esm(() => {
13702
13710
  init_config();
@@ -17311,6 +17319,7 @@ var init_shell_words = () => {};
17311
17319
  var SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLARATIONS, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, GIT_CONFIG_OPTIONS, XARGS_VALUES, PARALLEL_VALUES, RECENT, LAUNCHERS, PUSH_VALUE_OPTIONS, VALUE_OPTIONS, API_VALUES, API_FIELDS, GIT_COMMANDS;
17312
17320
  var init_raw_git = __esm(() => {
17313
17321
  init_branch();
17322
+ init_branch_policy();
17314
17323
  init_workspaces();
17315
17324
  init_ledger();
17316
17325
  init_paths();
@@ -74628,6 +74637,51 @@ ${gitOutput(run)}`);
74628
74637
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
74629
74638
  });
74630
74639
  }
74640
+ function releaseLinesOf(cwd) {
74641
+ const { preset, branches, production } = releaseBranchesOf(cwd);
74642
+ let policyPrefix;
74643
+ try {
74644
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
74645
+ policyPrefix = selected?.releaseTagPrefix;
74646
+ } catch {
74647
+ policyPrefix = undefined;
74648
+ }
74649
+ const prefix = releaseTagPrefixOf(cwd, policyPrefix);
74650
+ const trunk = production ?? "main";
74651
+ const lines = [
74652
+ {
74653
+ track: null,
74654
+ production: branches[0] ?? trunk,
74655
+ integration: branches[1] ?? trunk,
74656
+ mergeBack: branches[1] ? [branches[1]] : [],
74657
+ tagPrefix: prefix,
74658
+ gitflow: branches.length > 0,
74659
+ naming: {}
74660
+ }
74661
+ ];
74662
+ let raw;
74663
+ try {
74664
+ raw = resolveRuntimeReleaseTracks(cwd)?.releaseTracks;
74665
+ } catch {
74666
+ return lines;
74667
+ }
74668
+ for (const track of readReleaseTracks(raw).tracks) {
74669
+ const value = isRecord8(raw) ? raw[track.name] : undefined;
74670
+ const entry = isRecord8(value) ? value : {};
74671
+ const strategy = typeof entry.strategy === "string" ? entry.strategy : preset;
74672
+ const namespace = typeof entry.tagNamespace === "string" ? entry.tagNamespace.trim() : "";
74673
+ lines.push({
74674
+ track: track.name,
74675
+ production: track.productionBranch,
74676
+ integration: track.integrationBranch,
74677
+ mergeBack: track.mergeBackBranches.length ? track.mergeBackBranches : [track.integrationBranch],
74678
+ tagPrefix: `${namespace}${prefix}`,
74679
+ gitflow: strategy === "gitflow",
74680
+ naming: { release: track.naming.release, hotfix: track.naming.hotfix }
74681
+ });
74682
+ }
74683
+ return lines;
74684
+ }
74631
74685
  function releasePushPreflight(cwd, names) {
74632
74686
  const progress = operationInProgress(cwd);
74633
74687
  if (progress)
@@ -74635,7 +74689,16 @@ function releasePushPreflight(cwd, names) {
74635
74689
  const wanted = [...new Set(names)];
74636
74690
  if (!wanted.length)
74637
74691
  return failure3("invalid_input", "name the release refs to push");
74638
- const { preset, branches, production } = releaseBranchesOf(cwd);
74692
+ const { preset, production } = releaseBranchesOf(cwd);
74693
+ const lines = releaseLinesOf(cwd);
74694
+ const tracks = lines.filter((line) => line.track !== null && line.gitflow);
74695
+ const branches = [
74696
+ ...new Set([
74697
+ ...releaseBranchesOf(cwd).branches,
74698
+ ...tracks.flatMap((line) => [line.production, line.integration, ...line.mergeBack])
74699
+ ])
74700
+ ];
74701
+ const tagShape = `${lines[0].tagPrefix}<version>`;
74639
74702
  const refs = [];
74640
74703
  for (const raw of wanted) {
74641
74704
  if (raw.startsWith("-") || raw.includes(":"))
@@ -74647,16 +74710,22 @@ function releasePushPreflight(cwd, names) {
74647
74710
  if (tag && branch)
74648
74711
  return failure3("invalid_input", `${raw} names both a branch and a tag`, `workit git push --release refs/heads/${raw} # or refs/tags/${raw}`);
74649
74712
  if (tag) {
74650
- const prefix = releaseTagPrefix(cwd);
74651
- if (!isReleaseTag(tagName, prefix))
74652
- return failure3("blocked", `release_push_refused: tag ${tagName} is not a release tag (${prefix}<version>, from ${prefix === "v" ? "the default" : "gitflow.prefix.versiontag"})`, `git tag -a ${prefix}<version> -m <message> # then: workit git push --release ${prefix}<version>`);
74653
- refs.push({ kind: "tag", name: tagName, ref: `refs/tags/${tagName}`, sha: tag });
74713
+ const line = lineOfTag(tagName, lines);
74714
+ if (!line)
74715
+ return failure3("blocked", `release_push_refused: tag ${tagName} is not a release tag (${tagShape})`, `git tag -a ${tagShape} -m <message> # then: workit git push --release ${tagShape}`);
74716
+ refs.push({
74717
+ kind: "tag",
74718
+ name: tagName,
74719
+ ref: `refs/tags/${tagName}`,
74720
+ sha: tag,
74721
+ production: line.track === null ? production : line.production
74722
+ });
74654
74723
  continue;
74655
74724
  }
74656
74725
  if (!branch)
74657
74726
  return failure3("not_found", `${raw} is not a local branch or tag`);
74658
74727
  if (!branches.some((name) => name.toLowerCase() === branchName.toLowerCase()))
74659
- return failure3("blocked", branches.length ? `release_push_refused: ${branchName} is not a git-flow release branch (${branches.join(", ")})` : `release_push_refused: the ${preset} policy lands ${branchName} only through a reviewed PR; workit git push --release pushes v<version> tags there, not branches`, branches.length ? "workit git push # a feature, bugfix, release or hotfix branch, then: workit pr create" : `workit pr create --base ${branchName} # then tag the merged commit and: workit git push --release v<version>`);
74728
+ return failure3("blocked", branches.length ? `release_push_refused: ${branchName} is not a git-flow release branch (${branches.join(", ")})` : `release_push_refused: the ${preset} policy lands ${branchName} only through a reviewed PR; workit git push --release pushes ${tagShape} tags there, not branches`, branches.length ? "workit git push # a feature, bugfix, release or hotfix branch, then: workit pr create" : `workit pr create --base ${branchName} # then tag the merged commit and: workit git push --release ${tagShape}`);
74660
74729
  refs.push({ kind: "branch", name: branchName, ref: `refs/heads/${branchName}`, sha: branch });
74661
74730
  }
74662
74731
  const remote = pushRemoteName(cwd, refs.find((ref) => ref.kind === "branch")?.name ?? null);
@@ -74668,7 +74737,14 @@ function releasePushPreflight(cwd, names) {
74668
74737
  const local = parseRemoteUrl(raw)?.protocol === "file";
74669
74738
  if (!local && !pushTargetIsStable(cwd, raw))
74670
74739
  return failure3("blocked", `push_target_rewritten: the push URL of "${remote}" is rewritten by a url.*.insteadOf rule, so the destination is not the one checked`, "git config --get-regexp '^url\\.' # remove the rewrite for this remote");
74671
- return success2({ refs, remote, url: redactRemote(raw), rawUrl: raw, local, production });
74740
+ return success2({
74741
+ refs,
74742
+ remote,
74743
+ url: redactRemote(raw),
74744
+ rawUrl: raw,
74745
+ local,
74746
+ production
74747
+ });
74672
74748
  }
74673
74749
  function executeReleasePush(cwd, plan, input) {
74674
74750
  const previous = new Map;
@@ -74685,20 +74761,20 @@ function executeReleasePush(cwd, plan, input) {
74685
74761
  return failure3("failed", `non_fast_forward: ${plan.remote}/${ref.name} (${before.sha.slice(0, 12)}) is not part of local ${ref.name}; a release push never rewrites it`, `git fetch ${plan.remote} ${ref.name} && git switch ${ref.name} && git merge --ff-only ${plan.remote}/${ref.name} # then: workit git push --release ${ref.name}`);
74686
74762
  }
74687
74763
  const tags = plan.refs.filter((ref) => ref.kind === "tag");
74688
- if (tags.length) {
74689
- let productionTip = null;
74690
- if (plan.production) {
74691
- const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.production}`);
74764
+ const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
74765
+ for (const tag of tags) {
74766
+ const production = tag.production ?? plan.production;
74767
+ let remoteTip = null;
74768
+ if (production) {
74769
+ const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${production}`);
74692
74770
  if (!tip.ok)
74693
74771
  return failure3(tip.code, tip.error);
74694
- productionTip = tip.sha;
74695
- }
74696
- const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
74697
- for (const tag of tags) {
74698
- const released = productionTip !== null && contains(productionTip, tag.sha) || plan.refs.some((ref) => ref.kind === "branch" && contains(ref.sha, tag.sha));
74699
- if (!released)
74700
- return failure3("blocked", `tag_unreleased: ${tag.name} (${tag.sha.slice(0, 12)}) is not on ${plan.remote}/${plan.production ?? "the production branch"}${plan.refs.some((ref) => ref.kind === "branch") ? " or a branch in this push" : ""}; a release tag marks a commit that already landed`, `land it through a PR first (workit pr create), git fetch ${plan.remote}, then tag the merged commit`);
74772
+ remoteTip = tip.sha;
74701
74773
  }
74774
+ const pushed = plan.refs.find((ref) => ref.kind === "branch" && ref.name.toLowerCase() === production?.toLowerCase());
74775
+ const released = remoteTip !== null && contains(remoteTip, tag.sha) || pushed !== undefined && contains(pushed.sha, tag.sha);
74776
+ if (!released)
74777
+ return failure3("blocked", `tag_unreleased: ${tag.name} (${tag.sha.slice(0, 12)}) is not on ${plan.remote}/${production ?? "the production branch"}${pushed ? ` or the ${production} in this push` : ""}; a release tag marks a commit that already landed on its line's production branch`, `land it through a PR first (workit pr create), git fetch ${plan.remote}, then tag the merged commit`);
74702
74778
  }
74703
74779
  const pending = plan.refs.filter((ref) => previous.get(ref.ref) !== ref.sha);
74704
74780
  if (pending.length) {
@@ -74812,12 +74888,10 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
74812
74888
  ...lines.slice(-half)
74813
74889
  ].join(`
74814
74890
  `);
74815
- }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, EXTRA_TRAILER, SESSION_KEY, TRAILER_LINE, sessionValues = (trailers) => trailers.filter((trailer) => SESSION_KEY.test(trailer)).map((trailer) => trailer.slice(trailer.indexOf(":") + 1).trim()), TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA, SHA_PREFIX, releaseTagPrefix = (cwd) => {
74816
- const configured = git5(cwd, ["config", "--get", "gitflow.prefix.versiontag"]);
74817
- return configured.ok ? configured.stdout.trim() : "v";
74818
- }, isReleaseTag = (name, prefix) => name.startsWith(prefix) && /^\d/u.test(name.slice(prefix.length));
74891
+ }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, EXTRA_TRAILER, SESSION_KEY, TRAILER_LINE, sessionValues = (trailers) => trailers.filter((trailer) => SESSION_KEY.test(trailer)).map((trailer) => trailer.slice(trailer.indexOf(":") + 1).trim()), TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA, SHA_PREFIX, isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), lineOfTag = (tag, lines) => lines.filter((line) => tag.startsWith(line.tagPrefix) && /^\d/u.test(tag.slice(line.tagPrefix.length))).toSorted((a, b) => b.tagPrefix.length - a.tagPrefix.length)[0] ?? null;
74819
74892
  var init_ops = __esm(() => {
74820
74893
  init_branch();
74894
+ init_branch_policy();
74821
74895
  init_commit_flavors();
74822
74896
  init_pr_create();
74823
74897
  init_release_tracks();
@@ -75844,7 +75918,7 @@ function stacksDir(cwd) {
75844
75918
  return { ok: true, data: path44.join(root.value.root, "stacks") };
75845
75919
  }
75846
75920
  function parseStack(raw) {
75847
- if (!isRecord8(raw) || !Array.isArray(raw.branches))
75921
+ if (!isRecord9(raw) || !Array.isArray(raw.branches))
75848
75922
  return null;
75849
75923
  const name = str3(raw.name);
75850
75924
  const trunk = str3(raw.trunk);
@@ -75852,12 +75926,12 @@ function parseStack(raw) {
75852
75926
  return null;
75853
75927
  const branches = [];
75854
75928
  for (const item of raw.branches) {
75855
- if (!isRecord8(item))
75929
+ if (!isRecord9(item))
75856
75930
  return null;
75857
75931
  const branch = str3(item.branch);
75858
75932
  if (!branch)
75859
75933
  return null;
75860
- const merged = isRecord8(item.merged) ? {
75934
+ const merged = isRecord9(item.merged) ? {
75861
75935
  pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
75862
75936
  mergeSha: str3(item.merged.mergeSha),
75863
75937
  at: str3(item.merged.at) ?? ""
@@ -75870,7 +75944,7 @@ function parseStack(raw) {
75870
75944
  lastParentHead: str3(item.lastParentHead),
75871
75945
  patchId: str3(item.patchId),
75872
75946
  merged,
75873
- ...isRecord8(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
75947
+ ...isRecord9(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
75874
75948
  pendingRestack: {
75875
75949
  from: str3(item.pendingRestack.from),
75876
75950
  tip: str3(item.pendingRestack.tip),
@@ -76920,7 +76994,7 @@ ${run.stdout}`.split(`
76920
76994
  return false;
76921
76995
  }
76922
76996
  return true;
76923
- }, isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str3 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
76997
+ }, isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str3 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
76924
76998
  let raw = null;
76925
76999
  try {
76926
77000
  raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
@@ -77678,6 +77752,24 @@ function createGitHubForge(options) {
77678
77752
  if (node?.pullRequest?.number !== number)
77679
77753
  return success2(null);
77680
77754
  return success2(node.isResolved ? "resolved" : "open");
77755
+ },
77756
+ findRelease(tag) {
77757
+ const found = rest(`repos/${repo}/releases/tags/${encodeRef(tag)}`, `release ${tag}`);
77758
+ if (!found.ok)
77759
+ return found.code === "not_found" ? success2(null) : found;
77760
+ return success2({
77761
+ tag: found.data.tag_name ?? tag,
77762
+ url: safeUrl(found.data.html_url ?? null)
77763
+ });
77764
+ },
77765
+ createRelease(input) {
77766
+ const created = apiWrite(runner, "gh", apiHost, ["api", "-X", "POST", `repos/${repo}/releases`, "--input", "-"], `release ${input.tag}`, undefined, JSON.stringify({ tag_name: input.tag, name: input.name, body: input.body }));
77767
+ if (!created.ok)
77768
+ return created;
77769
+ return success2({
77770
+ tag: created.data.tag_name ?? input.tag,
77771
+ url: safeUrl(created.data.html_url ?? null)
77772
+ });
77681
77773
  }
77682
77774
  };
77683
77775
  }
@@ -78063,6 +78155,24 @@ ${squash.body}` : squash.subject
78063
78155
  if (!notes.some((note) => note.resolvable === true))
78064
78156
  return success2(null);
78065
78157
  return success2(notes.some((note) => note.resolvable === true && note.resolved !== true) ? "open" : "resolved");
78158
+ },
78159
+ findRelease(tag) {
78160
+ const found = apiJson(runner, "glab", apiHost, api(`${project}/releases/${encodeURIComponent(tag)}`), `release ${tag}`);
78161
+ if (!found.ok)
78162
+ return found.code === "not_found" ? success2(null) : found;
78163
+ return success2({
78164
+ tag: found.data.tag_name ?? tag,
78165
+ url: safeUrl2(found.data["_links"]?.self ?? null)
78166
+ });
78167
+ },
78168
+ createRelease(input) {
78169
+ const created = apiWrite(runner, "glab", apiHost, ["api", "-X", "POST", `${project}/releases`, "--input", "-"], `release ${input.tag}`, undefined, JSON.stringify({ tag_name: input.tag, name: input.name, description: input.body }));
78170
+ if (!created.ok)
78171
+ return created;
78172
+ return success2({
78173
+ tag: created.data.tag_name ?? input.tag,
78174
+ url: safeUrl2(created.data["_links"]?.self ?? null)
78175
+ });
78066
78176
  }
78067
78177
  };
78068
78178
  }
@@ -79668,12 +79778,700 @@ var init_verify = __esm(() => {
79668
79778
  SAFE_ARG = /^[0-9A-Za-z@/._~+-]+$/u;
79669
79779
  });
79670
79780
 
79781
+ // packages/workit-core/src/git/release.ts
79782
+ function lineFor(cwd, track) {
79783
+ const lines = releaseLinesOf(cwd);
79784
+ let name = track;
79785
+ if (name === null && lines.length > 1) {
79786
+ const resolved = vcsConfig("resolve", cwd);
79787
+ if (resolved.ok === false)
79788
+ return failure3("blocked", String(resolved.error));
79789
+ if (resolved.releaseTrack?.blocking)
79790
+ return failure3("blocked", String(resolved.releaseTrack.blocking), "pass --track <name>");
79791
+ name = resolved.releaseTrack?.track?.name ?? null;
79792
+ }
79793
+ const line = lines.find((entry) => entry.track === name);
79794
+ if (!line)
79795
+ return failure3("invalid_input", `no release track ${JSON.stringify(name)} (configured: ${lines.map((entry) => entry.track).filter(Boolean).join(", ") || "none"})`);
79796
+ return success2(line);
79797
+ }
79798
+ function releaseStart(cwd, input) {
79799
+ const version = input.version.trim();
79800
+ if (!validVersion(version))
79801
+ return failure3("invalid_input", `${JSON.stringify(version)} is not a version (e.g. 1.4.0)`);
79802
+ const kind = input.hotfix ? "hotfix" : "release";
79803
+ const lineResult = lineFor(cwd, input.track ?? null);
79804
+ if (!lineResult.ok)
79805
+ return lineResult;
79806
+ const line = lineResult.data;
79807
+ const template = templateOf(line, kind);
79808
+ const existing = git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
79809
+ `).filter((name) => name && versionIn(template, name) !== null);
79810
+ if (existing.length)
79811
+ return failure3("blocked", `release_exists: ${existing[0]} is still open; git-flow finishes one ${kind} before starting the next`, `workit release finish # on ${existing[0]}`);
79812
+ const tag = `${line.tagPrefix}${version}`;
79813
+ if (resolveRef(cwd, `refs/tags/${tag}`))
79814
+ return failure3("blocked", `tag_exists: ${tag} already exists`, "start the next version instead");
79815
+ const base = kind === "hotfix" ? line.production : line.integration;
79816
+ const remote = pushRemoteName(cwd, base);
79817
+ if (remote) {
79818
+ fetchRefs(cwd, remote, [`+refs/heads/${base}:refs/remotes/${remote}/${base}`]);
79819
+ const local = resolveRef(cwd, `refs/heads/${base}`);
79820
+ const tracked = resolveRef(cwd, `refs/remotes/${remote}/${base}`);
79821
+ if (local && tracked && local !== tracked && !git5(cwd, ["merge-base", "--is-ancestor", local, tracked]).ok)
79822
+ return failure3("blocked", `base_unpushed: local ${base} has commits ${remote}/${base} lacks; the ${kind} would start without them`, `land them on ${remote}/${base} first, or: git switch ${base} && git reset --keep ${remote}/${base}`);
79823
+ }
79824
+ const branched = gitBranch(cwd, {
79825
+ name: branchFor(line, kind, version),
79826
+ base,
79827
+ ...line.track ? { track: line.track } : {}
79828
+ });
79829
+ if (!branched.ok)
79830
+ return branched;
79831
+ record2(cwd, input.actor, {
79832
+ type: "release.started",
79833
+ version,
79834
+ kind,
79835
+ branch: branched.data.branch,
79836
+ head: branched.data.baseSha,
79837
+ base,
79838
+ track: line.track,
79839
+ tag
79840
+ });
79841
+ return success2({
79842
+ version,
79843
+ kind,
79844
+ branch: branched.data.branch,
79845
+ base,
79846
+ baseSha: branched.data.baseSha,
79847
+ track: line.track,
79848
+ tag,
79849
+ notes: branched.data.notes
79850
+ });
79851
+ }
79852
+ function locate(cwd, version, track) {
79853
+ const lines = releaseLinesOf(cwd).filter((line) => track === null || line.track === track);
79854
+ if (!lines.length)
79855
+ return failure3("invalid_input", `no release track ${JSON.stringify(track)}`);
79856
+ const rows = releaseRows(cwd);
79857
+ const started = rows.filter((row) => row.type === "release.started");
79858
+ const finished = new Set(rows.filter((row) => row.type === "release.finished").map((row) => String(row.branch)));
79859
+ const startedRow = (branch) => started.findLast((row) => row.branch === branch && !finished.has(branch));
79860
+ const lineOf = (branch, candidates) => {
79861
+ const row = startedRow(branch);
79862
+ const byRow = row ? candidates.find((line) => line.track === (row.track ?? null)) : undefined;
79863
+ if (byRow)
79864
+ return byRow;
79865
+ const resolved = vcsConfig("resolve", cwd, { branch });
79866
+ const name = resolved.ok === false ? null : resolved.releaseTrack?.track?.name ?? null;
79867
+ return candidates.find((line) => line.track === name) ?? candidates[0];
79868
+ };
79869
+ const match = (branch) => {
79870
+ const hits = [];
79871
+ for (const line of lines)
79872
+ for (const kind of KINDS) {
79873
+ const found = versionIn(templateOf(line, kind), branch);
79874
+ if (found)
79875
+ hits.push({ line, kind, version: found });
79876
+ }
79877
+ if (!hits.length)
79878
+ return null;
79879
+ const line = lineOf(branch, hits.map((hit) => hit.line));
79880
+ const hit = hits.find((entry) => entry.line === line) ?? hits[0];
79881
+ return { ...hit, branch };
79882
+ };
79883
+ if (version !== null) {
79884
+ if (!validVersion(version))
79885
+ return failure3("invalid_input", `${JSON.stringify(version)} is not a version`);
79886
+ for (const line of lines)
79887
+ for (const kind of KINDS) {
79888
+ const branch = branchFor(line, kind, version);
79889
+ if (resolveRef(cwd, `refs/heads/${branch}`) || startedRow(branch)) {
79890
+ const located = match(branch);
79891
+ if (located)
79892
+ return success2(located);
79893
+ }
79894
+ }
79895
+ return failure3("not_found", `no release or hotfix branch for ${version} here, and none this ledger started`);
79896
+ }
79897
+ const current = currentBranch(cwd);
79898
+ const here = current ? match(current) : null;
79899
+ if (here)
79900
+ return success2(here);
79901
+ const open = started.findLast((row) => !finished.has(String(row.branch)));
79902
+ if (open) {
79903
+ const located = match(String(open.branch));
79904
+ if (located)
79905
+ return success2(located);
79906
+ }
79907
+ const branches = git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
79908
+ `).map((name) => name ? match(name) : null).filter((entry) => entry !== null);
79909
+ if (branches.length === 1)
79910
+ return success2(branches[0]);
79911
+ return failure3(branches.length ? "invalid_input" : "not_found", branches.length ? `several releases are open (${branches.map((entry) => entry.branch).join(", ")}); name one` : "no release or hotfix branch to finish", branches.length ? `workit release finish ${branches[0].version}` : "workit release start <version>");
79912
+ }
79913
+ function changelogSection(cwd, ref, version) {
79914
+ const shown = git5(cwd, ["show", `${ref}:CHANGELOG.md`]);
79915
+ if (!shown.ok)
79916
+ return null;
79917
+ const lines = shown.stdout.split(/\r?\n/u);
79918
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
79919
+ const heading = new RegExp(`^(#{1,3})\\s+\\[?v?${escaped}\\]?(?:[\\s(]|$)`, "u");
79920
+ const start = lines.findIndex((line) => heading.test(line));
79921
+ if (start < 0)
79922
+ return null;
79923
+ const level = heading.exec(lines[start])?.[1].length ?? 2;
79924
+ let end = lines.length;
79925
+ for (let index = start + 1;index < lines.length; index += 1) {
79926
+ const hashes = /^(#+)\s/u.exec(lines[index])?.[1].length;
79927
+ if (hashes !== undefined && hashes <= level) {
79928
+ end = index;
79929
+ break;
79930
+ }
79931
+ }
79932
+ const body = lines.slice(start + 1, end).join(`
79933
+ `).trim();
79934
+ return body || null;
79935
+ }
79936
+ function planReleaseFinish(cwd, input) {
79937
+ const progress = operationInProgress(cwd);
79938
+ if (progress)
79939
+ return inProgressFailure(progress.operation, "finish the release");
79940
+ const located = locate(cwd, input.version?.trim() || null, input.track ?? null);
79941
+ if (!located.ok)
79942
+ return located;
79943
+ const { line, kind, version, branch } = located.data;
79944
+ const notes = [];
79945
+ const dirty = git5(cwd, ["status", "--porcelain", "--untracked-files=no"]).stdout.trim();
79946
+ if (dirty)
79947
+ return failure3("blocked", "dirty_worktree: tracked files have uncommitted changes; a release finish merges on a clean tree", "workit git commit -m '<msg>' -- <paths> # on the release branch, or stash them");
79948
+ const tag = `${line.tagPrefix}${version}`;
79949
+ const rows = releaseRows(cwd).filter((row) => row.branch === branch || row.release === branch);
79950
+ const recordedOpen = rows.findLast((row) => row.type === "release.hotfix_target");
79951
+ const openRelease = kind !== "hotfix" ? null : typeof recordedOpen?.target === "string" ? recordedOpen.target : git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
79952
+ `).find((name) => name && versionIn(templateOf(line, "release"), name) !== null) ?? null;
79953
+ const mergeBack = openRelease ? [...new Set(line.mergeBack.map((name) => name === line.integration ? openRelease : name))] : line.mergeBack;
79954
+ if (openRelease)
79955
+ notes.push(`${openRelease} is open: the hotfix merges back into it instead of ${line.integration}`);
79956
+ const remote = pushRemoteName(cwd, line.production);
79957
+ if (remote) {
79958
+ const wanted = [line.production, ...mergeBack, branch];
79959
+ for (const name of wanted)
79960
+ fetchRefs(cwd, remote, [`+refs/heads/${name}:refs/remotes/${remote}/${name}`]);
79961
+ }
79962
+ const branchTip = resolveRef(cwd, `refs/heads/${branch}`);
79963
+ const remoteBranch = remote ? resolveRef(cwd, `refs/remotes/${remote}/${branch}`) : null;
79964
+ const mergedRow = rows.findLast((row) => row.type === "release.merged");
79965
+ const deletedRow = rows.findLast((row) => row.type === "release.branch_deleted");
79966
+ const recordedTip = [mergedRow?.releaseTip, deletedRow?.head].find((value) => typeof value === "string");
79967
+ const releaseTip = branchTip ?? recordedTip ?? null;
79968
+ if (!releaseTip)
79969
+ return failure3("not_found", `${branch} is gone and this ledger did not record its merge; nothing to resume`);
79970
+ for (const name of [line.production, ...mergeBack]) {
79971
+ const local = resolveRef(cwd, `refs/heads/${name}`);
79972
+ const tracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${name}`) : null;
79973
+ if (!local && tracked) {
79974
+ const created = git5(cwd, ["branch", "--no-track", name, tracked]);
79975
+ if (!created.ok)
79976
+ return failure3("failed", `could not create local ${name}: ${gitError(created)}`);
79977
+ notes.push(`created local ${name} from ${remote}/${name}`);
79978
+ continue;
79979
+ }
79980
+ if (!local)
79981
+ return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
79982
+ if (!tracked || name === openRelease)
79983
+ continue;
79984
+ if (!isAncestor3(cwd, tracked, local))
79985
+ return failure3("blocked", `branch_behind: local ${name} is behind or diverged from ${remote}/${name}; git-flow finishes only onto up-to-date branches`, `git switch ${name} && git merge --ff-only ${remote}/${name} # then: workit release finish ${version}`);
79986
+ const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
79987
+ const foreign = git5(cwd, [
79988
+ "rev-list",
79989
+ local,
79990
+ `^${tracked}`,
79991
+ `^${releaseTip}`,
79992
+ ...productionTracked ? [`^${productionTracked}`] : []
79993
+ ]).stdout.split(`
79994
+ `).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor3(cwd, releaseTip, sha)));
79995
+ if (foreign.length)
79996
+ return failure3("blocked", `branch_ahead: local ${name} has ${foreign.length} commit(s) ${remote}/${name} lacks that are not part of ${branch}; the finish push would publish them`, `land them through review first, or: git switch ${name} && git reset --keep ${remote}/${name} # then: workit release finish ${version}`);
79997
+ }
79998
+ if (branchTip && remoteBranch && !isAncestor3(cwd, remoteBranch, branchTip))
79999
+ return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
80000
+ const production = resolveRef(cwd, `refs/heads/${line.production}`);
80001
+ const merged = isAncestor3(cwd, releaseTip, production);
80002
+ const localTag = resolveRef(cwd, `refs/tags/${tag}`);
80003
+ if (localTag && !(isAncestor3(cwd, localTag, production) && isAncestor3(cwd, releaseTip, localTag)))
80004
+ return failure3("blocked", `tag_exists: ${tag} (${localTag.slice(0, 12)}) is not this release's merge on ${line.production}`, "a release tag never moves; finish under the next version");
80005
+ if (remote) {
80006
+ const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
80007
+ if (remoteTag.ok && remoteTag.sha && remoteTag.sha !== localTag)
80008
+ return failure3("blocked", `tag_exists: ${remote} already has ${tag} at ${remoteTag.sha.slice(0, 12)}`, "a release tag never moves; finish under the next version");
80009
+ }
80010
+ const recordedMerge = typeof mergedRow?.merge === "string" ? mergedRow.merge : null;
80011
+ const tagTarget = localTag ?? (merged ? recordedMerge ?? production : null);
80012
+ const state = {
80013
+ merged,
80014
+ tagged: localTag !== null,
80015
+ mergedBack: Object.fromEntries(mergeBack.map((name) => [
80016
+ name,
80017
+ localTag !== null && isAncestor3(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
80018
+ ])),
80019
+ branchDeleted: branchTip === null
80020
+ };
80021
+ return success2({
80022
+ version,
80023
+ kind,
80024
+ branch,
80025
+ track: line.track,
80026
+ tag,
80027
+ production: line.production,
80028
+ mergeBack,
80029
+ pushBranches: [line.production, ...mergeBack.filter((name) => name !== openRelease)],
80030
+ openRelease,
80031
+ gitflow: line.gitflow,
80032
+ remote,
80033
+ releaseTip,
80034
+ published: remoteBranch,
80035
+ tagTarget,
80036
+ mergeRecorded: mergedRow !== undefined,
80037
+ state,
80038
+ changelog: changelogSection(cwd, branchTip ?? production, version),
80039
+ notes,
80040
+ warnings: integrationPrWarning(cwd, line.production)
80041
+ });
80042
+ }
80043
+ function integrationPrWarning(cwd, production) {
80044
+ try {
80045
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
80046
+ return selected?.integration === "pr" ? [
80047
+ `the workspace integration mode is pr: the remote may require pull requests on ${production}, which would reject the finish push`
80048
+ ] : [];
80049
+ } catch {
80050
+ return [];
80051
+ }
80052
+ }
80053
+ function finishLocally(cwd, plan, actor) {
80054
+ const ran = [];
80055
+ const base = { version: plan.version, release: plan.branch, tag: plan.tag, track: plan.track };
80056
+ let tagTarget = plan.tagTarget;
80057
+ if (plan.openRelease && !releaseRows(cwd).some((row) => row.type === "release.hotfix_target" && row.release === plan.branch))
80058
+ record2(cwd, actor, {
80059
+ type: "release.hotfix_target",
80060
+ ...base,
80061
+ branch: plan.branch,
80062
+ target: plan.openRelease
80063
+ });
80064
+ if (plan.state.merged && !plan.mergeRecorded && tagTarget)
80065
+ record2(cwd, actor, {
80066
+ type: "release.merged",
80067
+ ...base,
80068
+ branch: plan.production,
80069
+ head: tagTarget,
80070
+ merge: tagTarget,
80071
+ releaseTip: plan.releaseTip,
80072
+ observed: true
80073
+ });
80074
+ if (!plan.state.merged) {
80075
+ const on = switchTo(cwd, plan.production);
80076
+ if (!on.ok)
80077
+ return on;
80078
+ const merged = mergeNoFf(cwd, plan.branch, plan.production, plan.version);
80079
+ if (!merged.ok)
80080
+ return merged;
80081
+ tagTarget = merged.data;
80082
+ ran.push("merge");
80083
+ record2(cwd, actor, {
80084
+ type: "release.merged",
80085
+ ...base,
80086
+ branch: plan.production,
80087
+ head: merged.data,
80088
+ merge: merged.data,
80089
+ releaseTip: plan.releaseTip
80090
+ });
80091
+ }
80092
+ if (!tagTarget)
80093
+ return failure3("failed", `cannot tell which ${plan.production} commit to tag`);
80094
+ if (!plan.state.tagged) {
80095
+ const message = plan.changelog ?? `Release ${plan.version}`;
80096
+ const tagged = git5(cwd, [
80097
+ "tag",
80098
+ "-a",
80099
+ "--cleanup=verbatim",
80100
+ plan.tag,
80101
+ "-m",
80102
+ message,
80103
+ tagTarget
80104
+ ]);
80105
+ if (!tagged.ok)
80106
+ return failure3("failed", `git tag ${plan.tag} failed: ${gitError(tagged)}`);
80107
+ ran.push("tag");
80108
+ record2(cwd, actor, {
80109
+ type: "release.tagged",
80110
+ ...base,
80111
+ branch: plan.production,
80112
+ head: tagTarget
80113
+ });
80114
+ }
80115
+ for (const name of plan.mergeBack) {
80116
+ if (plan.state.mergedBack[name])
80117
+ continue;
80118
+ const on = switchTo(cwd, name);
80119
+ if (!on.ok)
80120
+ return on;
80121
+ const merged = mergeNoFf(cwd, plan.tag, name, plan.version);
80122
+ if (!merged.ok)
80123
+ return merged;
80124
+ ran.push(`merge-back ${name}`);
80125
+ record2(cwd, actor, { type: "release.merged_back", ...base, branch: name, head: merged.data });
80126
+ }
80127
+ if (!plan.state.branchDeleted) {
80128
+ if (currentBranch(cwd) === plan.branch) {
80129
+ const on = switchTo(cwd, plan.mergeBack.at(-1) ?? plan.production);
80130
+ if (!on.ok)
80131
+ return on;
80132
+ }
80133
+ const deleted = git5(cwd, ["branch", "-d", plan.branch]);
80134
+ if (!deleted.ok)
80135
+ return failure3("failed", `git branch -d ${plan.branch} failed: ${gitError(deleted)}`, plan.published ? `workit release publish ${plan.version} # its remote copy lacks commits; then: workit release finish ${plan.version}` : undefined);
80136
+ ran.push("delete-branch");
80137
+ record2(cwd, actor, {
80138
+ type: "release.branch_deleted",
80139
+ ...base,
80140
+ branch: plan.branch,
80141
+ head: plan.releaseTip
80142
+ });
80143
+ }
80144
+ return success2({ ran, tagTarget });
80145
+ }
80146
+ function recordReleaseStep(cwd, actor, plan, row) {
80147
+ record2(cwd, actor, {
80148
+ version: plan.version,
80149
+ release: plan.branch,
80150
+ tag: plan.tag,
80151
+ track: plan.track,
80152
+ ...row
80153
+ });
80154
+ }
80155
+ function releaseToPublish(cwd, version) {
80156
+ const located = locate(cwd, version, null);
80157
+ if (!located.ok)
80158
+ return located;
80159
+ if (!resolveRef(cwd, `refs/heads/${located.data.branch}`))
80160
+ return failure3("not_found", `${located.data.branch} is not a local branch`);
80161
+ return located;
80162
+ }
80163
+ var VERSION, PLACEHOLDER3, validVersion = (version) => VERSION.test(version) && !version.includes("..") && !version.endsWith(".lock"), templateOf = (line, kind) => {
80164
+ const template = line.naming[kind];
80165
+ if (!template)
80166
+ return `${kind}/{version}`;
80167
+ return /\{[^}]*\}/u.test(template) ? template : `${template.replace(/\/?\*?$/u, "")}/{version}`;
80168
+ }, branchFor = (line, kind, version) => trackBranchName({ naming: line.naming }, kind, version) ?? `${kind}/${version}`, versionIn = (template, branch) => {
80169
+ const pattern = template.split(PLACEHOLDER3).map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("(.+)");
80170
+ const match = new RegExp(`^${pattern}$`, "u").exec(branch);
80171
+ return match?.[1] && validVersion(match[1]) ? match[1] : null;
80172
+ }, releaseRows = (cwd) => {
80173
+ const ledger = readLedger(cwd);
80174
+ if (!ledger.ok)
80175
+ return [];
80176
+ return ledger.value.rows.filter((row) => row.type.startsWith("release."));
80177
+ }, record2 = (cwd, actor, row) => {
80178
+ appendObserved(cwd, { ...row, actor });
80179
+ }, KINDS, isAncestor3 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
80180
+ if (currentBranch(cwd) === branch)
80181
+ return success2(undefined);
80182
+ const switched = git5(cwd, ["switch", "-q", branch]);
80183
+ return switched.ok ? success2(undefined) : failure3("failed", `git switch ${branch} failed: ${gitError(switched)}`);
80184
+ }, mergeNoFf = (cwd, what, into, version) => {
80185
+ const merged = git5(cwd, ["merge", "--no-ff", "--no-edit", what], { timeoutMs: 10 * 60000 });
80186
+ if (!merged.ok) {
80187
+ const conflicted = git5(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).ok;
80188
+ return failure3("failed", conflicted ? `merge_conflict: merging ${what} into ${into} conflicts` : `git merge ${what} into ${into} failed: ${gitError(merged)}`, conflicted ? `resolve the conflicts, git add <files>, git merge --continue, then: workit release finish ${version}` : undefined);
80189
+ }
80190
+ return success2(git5(cwd, ["rev-parse", "HEAD"]).stdout.trim());
80191
+ }, switchToRelease = (cwd, plan) => switchTo(cwd, plan.branch);
80192
+ var init_release = __esm(() => {
80193
+ init_ledger();
80194
+ init_vcs_config();
80195
+ init_release_tracks();
80196
+ init_workspaces();
80197
+ init_ops();
80198
+ init_rev();
80199
+ VERSION = /^[0-9A-Za-z][0-9A-Za-z._+-]{0,99}$/u;
80200
+ PLACEHOLDER3 = /\{[^}]*\}/gu;
80201
+ KINDS = ["release", "hotfix"];
80202
+ });
80203
+
80204
+ // packages/workit-cli/src/verbs/release.ts
80205
+ var exports_release = {};
80206
+ __export(exports_release, {
80207
+ run: () => run14
80208
+ });
80209
+ async function subVerb(load, argv, io) {
80210
+ let out = "";
80211
+ let err = "";
80212
+ const verb = await load();
80213
+ await verb.run(argv, {
80214
+ ...io,
80215
+ json: true,
80216
+ stdout: (text) => void (out += text),
80217
+ stderr: (text) => void (err += text)
80218
+ });
80219
+ try {
80220
+ return JSON.parse(out);
80221
+ } catch {
80222
+ return fail("failed", (out + err).trim() || `${argv.join(" ")} printed nothing`);
80223
+ }
80224
+ }
80225
+ async function start(argv, io) {
80226
+ const flags = parseFlags(argv, { hotfix: "boolean", track: "value" });
80227
+ if (typeof flags === "string")
80228
+ return usage3(io, flags, START_USAGE);
80229
+ if (flags.positionals.length !== 1)
80230
+ return usage3(io, flags.positionals.length ? "pass one version" : "missing <version>", START_USAGE);
80231
+ const result = releaseStart(io.cwd, {
80232
+ version: flags.positionals[0],
80233
+ hotfix: flags.booleans.has("hotfix"),
80234
+ track: flags.values.track ?? null,
80235
+ actor: actorFromEnv(io.env)
80236
+ });
80237
+ if (!result.ok)
80238
+ return forgeFail(io, result);
80239
+ return emit(io, ok(result.data), (data) => [
80240
+ `started ${data.branch} from ${data.base} (${short4(data.baseSha)}), local only; it will be tagged ${data.tag}`,
80241
+ `next: commit the version bump, changelog and docs on ${data.branch} (workit git commit), then: workit release finish`,
80242
+ ...data.notes.map((note) => `note: ${note}`)
80243
+ ]);
80244
+ }
80245
+ async function releasePr(io, plan, dryRun) {
80246
+ const title = `chore(release): ${plan.version}`;
80247
+ if (dryRun)
80248
+ return emit(io, ok({
80249
+ ...plan,
80250
+ steps: [`push ${plan.branch}`, `open a PR ${plan.branch} -> ${plan.production}: ${title}`]
80251
+ }), () => [
80252
+ `would push ${plan.branch} and open a release PR into ${plan.production} (not git-flow: the trunk moves only through review)`
80253
+ ]);
80254
+ const switched = switchToRelease(io.cwd, plan);
80255
+ if (!switched.ok)
80256
+ return forgeFail(io, switched);
80257
+ const pushed = await subVerb(() => Promise.resolve().then(() => (init_git2(), exports_git)), ["push", "--set-upstream"], io);
80258
+ if (!pushed.ok)
80259
+ return emit(io, pushed);
80260
+ const pr = await subVerb(() => Promise.resolve().then(() => (init_pr(), exports_pr)), [
80261
+ "create",
80262
+ "--base",
80263
+ plan.production,
80264
+ "--title",
80265
+ title,
80266
+ `--body=${plan.changelog ?? `Release ${plan.version}`}`
80267
+ ], io);
80268
+ if (!pr.ok)
80269
+ return emit(io, pr);
80270
+ recordReleaseStep(io.cwd, actorFromEnv(io.env), plan, {
80271
+ type: "release.pr_opened",
80272
+ branch: plan.branch,
80273
+ pr: pr.data.number ?? null
80274
+ });
80275
+ return emit(io, ok({ ...plan, pr: pr.data }), () => [
80276
+ `opened the release PR ${String(pr.data.url ?? "")} into ${plan.production}`,
80277
+ `next: once it merges, tag the merged commit ${plan.tag} and: workit git push --release ${plan.tag}`
80278
+ ]);
80279
+ }
80280
+ async function finish2(argv, io) {
80281
+ const flags = parseFlags(argv, { track: "value", "dry-run": "boolean" });
80282
+ if (typeof flags === "string")
80283
+ return usage3(io, flags, FINISH_USAGE);
80284
+ if (flags.positionals.length > 1)
80285
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, FINISH_USAGE);
80286
+ const dryRun = flags.booleans.has("dry-run");
80287
+ const planned = planReleaseFinish(io.cwd, {
80288
+ version: flags.positionals[0] ?? null,
80289
+ track: flags.values.track ?? null
80290
+ });
80291
+ if (!planned.ok)
80292
+ return forgeFail(io, planned);
80293
+ const plan = planned.data;
80294
+ if (!plan.gitflow) {
80295
+ if (!dryRun && plan.state.branchDeleted)
80296
+ return emit(io, fail("not_found", `${plan.branch} is gone; nothing to open a PR from`));
80297
+ return releasePr(io, plan, dryRun);
80298
+ }
80299
+ const actor = actorFromEnv(io.env);
80300
+ const local = isLocalRemote(io.cwd, plan.remote);
80301
+ const release = requireGrant(io.cwd, "release");
80302
+ const push = requireGrant(io.cwd, "push", { forge: !local });
80303
+ const steps = stepsOf(plan);
80304
+ for (const warning of plan.warnings)
80305
+ io.stderr(`warning: ${warning}
80306
+ `);
80307
+ const warned = (envelope) => plan.warnings.length ? { ...envelope, warnings: plan.warnings } : envelope;
80308
+ if (dryRun) {
80309
+ const missing = [release, push].find((grant) => !grant.allowed);
80310
+ const data = { ...plan, steps, grants: { release: release.allowed, push: push.allowed } };
80311
+ if (missing && !missing.allowed)
80312
+ return emit(io, warned(fail("blocked", missing.error, { unblock: missing.unblock, data })));
80313
+ return emit(io, warned(ok(data)), () => [
80314
+ `finish ${plan.branch} -> ${plan.production} as ${plan.tag}:`,
80315
+ ...steps.map((entry) => ` ${entry.done ? "done" : "todo"} ${entry.step}`)
80316
+ ]);
80317
+ }
80318
+ if (!release.allowed)
80319
+ return blocked(io, release);
80320
+ if (!push.allowed)
80321
+ return blocked(io, push);
80322
+ const resume = `workit release finish ${plan.version}${plan.track ? ` --track ${plan.track}` : ""}`;
80323
+ const merged = finishLocally(io.cwd, plan, actor);
80324
+ if (!merged.ok)
80325
+ return forgeFail(io, merged, { step: "merge", resume });
80326
+ const notes = [...plan.notes];
80327
+ const pushed = await subVerb(() => Promise.resolve().then(() => (init_git2(), exports_git)), ["push", ...[...plan.pushBranches, plan.tag].flatMap((ref) => ["--release", ref])], io);
80328
+ if (!pushed.ok)
80329
+ return emit(io, { ...pushed, data: { ...pushed.data, step: "push", resume } });
80330
+ if (plan.published && plan.remote) {
80331
+ const removed = deleteRemoteBranch(io.cwd, plan.remote, plan.branch, plan.published);
80332
+ notes.push(removed.ok ? `deleted ${plan.remote}/${plan.branch}` : `kept ${plan.remote}/${plan.branch}: ${removed.error}`);
80333
+ if (removed.ok)
80334
+ recordReleaseStep(io.cwd, actor, plan, {
80335
+ type: "release.remote_branch_deleted",
80336
+ branch: plan.branch,
80337
+ head: plan.published
80338
+ });
80339
+ }
80340
+ let forgeRelease = null;
80341
+ if (local)
80342
+ notes.push(`${plan.remote ?? "the remote"} is a local path: no forge release`);
80343
+ else {
80344
+ const connected = connect(io);
80345
+ if (!connected.ok)
80346
+ return forgeFail(io, connected, { step: "forge-release", resume });
80347
+ const forge = connected.data.forge;
80348
+ const found = forge.findRelease(plan.tag);
80349
+ if (!found.ok)
80350
+ return forgeFail(io, found, { step: "forge-release", resume });
80351
+ if (found.data)
80352
+ forgeRelease = { url: found.data.url, created: false };
80353
+ else {
80354
+ const created = forge.createRelease({
80355
+ tag: plan.tag,
80356
+ name: plan.tag,
80357
+ body: plan.changelog ?? `Release ${plan.version}`
80358
+ });
80359
+ if (!created.ok)
80360
+ return forgeFail(io, created, { step: "forge-release", resume });
80361
+ forgeRelease = { url: created.data.url, created: true };
80362
+ }
80363
+ recordReleaseStep(io.cwd, actor, plan, {
80364
+ type: "release.forge_released",
80365
+ branch: plan.production,
80366
+ url: forgeRelease.url,
80367
+ created: forgeRelease.created
80368
+ });
80369
+ }
80370
+ const verified = verifyReleased(io.cwd, { tag: plan.tag, actor }, forgeDeps.npm);
80371
+ if (!verified.ok)
80372
+ return forgeFail(io, verified, { step: "verify", resume });
80373
+ if (!verified.data.delivered)
80374
+ return emit(io, fail("failed", `release_unverified: ${plan.tag} is not on ${plan.remote ?? "the remote"}`, {
80375
+ data: { step: "verify", resume, report: verified.data },
80376
+ unblock: resume
80377
+ }));
80378
+ recordReleaseStep(io.cwd, actor, plan, {
80379
+ type: "release.finished",
80380
+ branch: plan.branch,
80381
+ head: merged.data.tagTarget
80382
+ });
80383
+ return emit(io, warned(ok({ ...plan, ran: merged.data.ran, push: pushed.data, forgeRelease, notes })), () => [
80384
+ `finished ${plan.branch}: ${plan.production} and ${plan.mergeBack.join(", ")} merged, ${plan.tag} tagged on ${short4(merged.data.tagTarget)}`,
80385
+ `pushed ${[...plan.pushBranches, plan.tag].join(", ")} to ${plan.remote ?? "the remote"}; remote tips verified`,
80386
+ ...forgeRelease ? [
80387
+ `forge release ${plan.tag}${forgeRelease.created ? " created" : " already there"}${forgeRelease.url ? `: ${forgeRelease.url}` : ""}`
80388
+ ] : [],
80389
+ ...notes.map((note) => `note: ${note}`)
80390
+ ]);
80391
+ }
80392
+ async function publish(argv, io) {
80393
+ const flags = parseFlags(argv, {});
80394
+ if (typeof flags === "string")
80395
+ return usage3(io, flags, PUBLISH_USAGE);
80396
+ if (flags.positionals.length > 1)
80397
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, PUBLISH_USAGE);
80398
+ const located = releaseToPublish(io.cwd, flags.positionals[0] ?? null);
80399
+ if (!located.ok)
80400
+ return forgeFail(io, located);
80401
+ const plan = pushPreflight(io.cwd, { branch: located.data.branch });
80402
+ if (!plan.ok)
80403
+ return forgeFail(io, plan);
80404
+ const identity = plan.data.local ? null : pushIdentity(io, plan.data.branch);
80405
+ if (identity && !identity.ok)
80406
+ return forgeFail(io, identity);
80407
+ const checked = identity?.data ?? null;
80408
+ const grant = requireGrant(io.cwd, "push", { forge: checked?.forge != null });
80409
+ if (!grant.allowed)
80410
+ return blocked(io, grant);
80411
+ const actor = actorFromEnv(io.env);
80412
+ const result = executePush(io.cwd, plan.data, {
80413
+ setUpstream: true,
80414
+ actor,
80415
+ ...checked ? { identity: { ...checked } } : {}
80416
+ });
80417
+ if (!result.ok)
80418
+ return forgeFail(io, result);
80419
+ appendObserved(io.cwd, {
80420
+ type: "release.published",
80421
+ actor,
80422
+ version: located.data.version,
80423
+ branch: located.data.branch,
80424
+ head: result.data.sha
80425
+ });
80426
+ return emit(io, ok({ ...result.data, version: located.data.version }), (data) => [
80427
+ `published ${data.branch} (${short4(data.sha)}) to ${data.remote}; remote tip verified`
80428
+ ]);
80429
+ }
80430
+ async function run14(argv, io) {
80431
+ const [sub, ...rest] = argv;
80432
+ if (sub === "start")
80433
+ return start(rest, io);
80434
+ if (sub === "finish")
80435
+ return finish2(rest, io);
80436
+ if (sub === "publish")
80437
+ return publish(rest, io);
80438
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown release subcommand "${sub}"` : "missing release subcommand", USAGE6);
80439
+ }
80440
+ var START_USAGE = "workit release start <version> [--hotfix] [--track <t>] [--json]", FINISH_USAGE = "workit release finish [<version>] [--track <t>] [--dry-run] [--json]", PUBLISH_USAGE = "workit release publish [<version>] [--json]", USAGE6 = "workit release start|finish|publish ... (workit help release)", short4 = (sha) => sha ? sha.slice(0, 12) : "(none)", blocked = (io, grant) => emit(io, fail("blocked", grant.error, { unblock: grant.unblock, data: { reason: grant.reason } })), isLocalRemote = (cwd, remote) => {
80441
+ const url = remote ? pushUrl(cwd, remote) : null;
80442
+ return !url || parseRemoteUrl(url)?.protocol === "file";
80443
+ }, stepsOf = (plan) => [
80444
+ { step: `merge ${plan.branch} into ${plan.production} (--no-ff)`, done: plan.state.merged },
80445
+ { step: `tag ${plan.tag} (annotated)`, done: plan.state.tagged },
80446
+ ...plan.mergeBack.map((name) => ({
80447
+ step: `merge ${plan.tag} into ${name} (--no-ff)`,
80448
+ done: plan.state.mergedBack[name] === true
80449
+ })),
80450
+ { step: `delete local ${plan.branch}`, done: plan.state.branchDeleted },
80451
+ {
80452
+ step: `push ${[...plan.pushBranches, plan.tag].join(", ")} (atomic, fast-forward only)`,
80453
+ done: false
80454
+ },
80455
+ { step: `forge release ${plan.tag}`, done: false },
80456
+ { step: `verify ${plan.tag} on the remote`, done: false }
80457
+ ];
80458
+ var init_release2 = __esm(() => {
80459
+ init_autonomy();
80460
+ init_verify();
80461
+ init_ops();
80462
+ init_release();
80463
+ init_rev();
80464
+ init_ledger();
80465
+ init_output();
80466
+ init_forge_common();
80467
+ });
80468
+
79671
80469
  // packages/workit-cli/src/verbs/verify-delivery.ts
79672
80470
  var exports_verify_delivery = {};
79673
80471
  __export(exports_verify_delivery, {
79674
- run: () => run14
80472
+ run: () => run15
79675
80473
  });
79676
- async function run14(argv, io) {
80474
+ async function run15(argv, io) {
79677
80475
  const flags = parseFlags(argv, {
79678
80476
  expect: "value",
79679
80477
  branch: "value",
@@ -79683,31 +80481,31 @@ async function run14(argv, io) {
79683
80481
  package: "value"
79684
80482
  });
79685
80483
  if (typeof flags === "string")
79686
- return usage3(io, flags, USAGE6);
80484
+ return usage3(io, flags, USAGE7);
79687
80485
  if (flags.positionals.length > 1)
79688
- return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE6);
80486
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE7);
79689
80487
  const named = flags.positionals[0];
79690
80488
  if (named !== undefined && !ENDPOINTS[named])
79691
- return usage3(io, `unknown endpoint "${named}"`, USAGE6);
80489
+ return usage3(io, `unknown endpoint "${named}"`, USAGE7);
79692
80490
  if (flags.values.expect !== undefined && !ENDPOINTS[flags.values.expect])
79693
- return usage3(io, "--expect must be pushed, pr, merged or released", USAGE6);
80491
+ return usage3(io, "--expect must be pushed, pr, merged or released", USAGE7);
79694
80492
  if (named && flags.values.expect && ENDPOINTS[named] !== ENDPOINTS[flags.values.expect])
79695
- return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE6);
80493
+ return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE7);
79696
80494
  const expect = ENDPOINTS[named ?? ""] ?? ENDPOINTS[flags.values.expect ?? ""] ?? (flags.values.tag || flags.values.package ? "released" : flags.values.pr ? "pr" : "pushed");
79697
80495
  const pr = positiveInt(flags.values.pr, "--pr");
79698
80496
  if (typeof pr === "string")
79699
- return usage3(io, pr, USAGE6);
80497
+ return usage3(io, pr, USAGE7);
79700
80498
  if (pr && flags.values.branch)
79701
- return usage3(io, "pass --pr or --branch, not both", USAGE6);
80499
+ return usage3(io, "pass --pr or --branch, not both", USAGE7);
79702
80500
  const sha = flags.values.sha ?? null;
79703
80501
  if (sha !== null && !/^[0-9a-f]{7,64}$/u.test(sha))
79704
- return usage3(io, "--sha must be a commit sha (7-64 hex chars)", USAGE6);
80502
+ return usage3(io, "--sha must be a commit sha (7-64 hex chars)", USAGE7);
79705
80503
  const actor = actorFromEnv(io.env);
79706
80504
  const branch = flags.values.branch ?? null;
79707
80505
  let result;
79708
80506
  if (expect === "pushed") {
79709
80507
  if (pr)
79710
- return usage3(io, "push is checked by --branch, not --pr", USAGE6);
80508
+ return usage3(io, "push is checked by --branch, not --pr", USAGE7);
79711
80509
  result = verifyPushed(io.cwd, { branch, sha, actor });
79712
80510
  } else if (expect === "released") {
79713
80511
  result = verifyReleased(io.cwd, { tag: flags.values.tag ?? null, pkg: flags.values.package ?? null, sha, actor }, forgeDeps.npm);
@@ -79732,7 +80530,7 @@ async function run14(argv, io) {
79732
80530
  unblock: expect === "pushed" ? "workit git push" : expect === "pr" ? "workit git push && workit pr create --fill" : expect === "merged" ? "workit pr status # then workit pr merge when READY and granted" : "check the release workflow; the tag/package is not published"
79733
80531
  }));
79734
80532
  }
79735
- var USAGE6 = "workit verify-delivery [push|pr|merge|release] [--expect pushed|pr|merged|released] [--branch <b>] [--pr <n>] [--sha <sha>] [--tag <t>] [--package <name[@version]>] [--json]", ENDPOINTS, render2 = (report) => [
80533
+ var USAGE7 = "workit verify-delivery [push|pr|merge|release] [--expect pushed|pr|merged|released] [--branch <b>] [--pr <n>] [--sha <sha>] [--tag <t>] [--package <name[@version]>] [--json]", ENDPOINTS, render2 = (report) => [
79736
80534
  `${report.expect}: ${report.delivered ? "delivered" : "NOT delivered"}${report.branch ? ` (${report.branch}${report.sha ? ` @ ${report.sha.slice(0, 12)}` : ""})` : ""}${report.pr ? ` #${report.pr}` : ""}`,
79737
80535
  ...report.observations.map((entry) => ` ${entry.ok ? "ok" : "x "} ${entry.kind}: expected ${entry.expected ?? "-"}, observed ${entry.observed ?? "(none)"}${entry.note ? ` — ${entry.note}` : ""}`)
79738
80536
  ];
@@ -79755,7 +80553,7 @@ var init_verify_delivery = __esm(() => {
79755
80553
  // packages/workit-cli/src/verbs/stack.ts
79756
80554
  var exports_stack = {};
79757
80555
  __export(exports_stack, {
79758
- run: () => run15
80556
+ run: () => run16
79759
80557
  });
79760
80558
  async function plan(argv, io) {
79761
80559
  const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
@@ -79929,7 +80727,7 @@ async function land(argv, io) {
79929
80727
  ...data.dryRun ? [
79930
80728
  data.wouldLand.length ? `would land: ${data.wouldLand.map((item) => `${label2(kind, item.pr)} (${item.branch})`).join(", ")}` : "would land nothing"
79931
80729
  ] : [
79932
- data.landed.length ? `landed: ${data.landed.map((item) => `${label2(kind, item.pr)} (${item.branch}) -> ${short4(item.mergeSha)}${item.unverified ? ` UNVERIFIED (ledger ${item.unverified})` : ""}`).join(", ")}` : "landed nothing"
80730
+ data.landed.length ? `landed: ${data.landed.map((item) => `${label2(kind, item.pr)} (${item.branch}) -> ${short5(item.mergeSha)}${item.unverified ? ` UNVERIFIED (ledger ${item.unverified})` : ""}`).join(", ")}` : "landed nothing"
79933
80731
  ],
79934
80732
  ...data.retargeted.length ? [`retargeted: ${data.retargeted.map((pr) => label2(kind, pr)).join(", ")}`] : [],
79935
80733
  ...data.restacked.length ? [`restacked: ${data.restacked.join(", ")}`] : [],
@@ -79937,7 +80735,7 @@ async function land(argv, io) {
79937
80735
  unblock: ${data.stoppedAt.unblock}` : data.complete ? "stack complete" : ""
79938
80736
  ].filter(Boolean));
79939
80737
  }
79940
- async function run15(argv, io) {
80738
+ async function run16(argv, io) {
79941
80739
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
79942
80740
  if (sub === "plan")
79943
80741
  return plan(rest, io);
@@ -79947,13 +80745,13 @@ async function run15(argv, io) {
79947
80745
  return sync(rest, io);
79948
80746
  if (sub === "land")
79949
80747
  return land(rest, io);
79950
- return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
80748
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE8);
79951
80749
  }
79952
- var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label2 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
80750
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE8 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label2 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
79953
80751
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
79954
80752
  ...data.steps.map((step) => [
79955
80753
  ` ${step.branch} on ${step.parent}:`,
79956
- step.restack ? `restacked ${short4(step.restack.from)} -> ${short4(step.restack.onto)}${step.restack.head ? ` = ${short4(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
80754
+ step.restack ? `restacked ${short5(step.restack.from)} -> ${short5(step.restack.onto)}${step.restack.head ? ` = ${short5(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
79957
80755
  step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
79958
80756
  step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
79959
80757
  ].filter(Boolean).join(" "))
@@ -80116,7 +80914,7 @@ function inScope(plan, slice, file) {
80116
80914
  }
80117
80915
  function parsePlan(raw, defaults, name) {
80118
80916
  const problems = [];
80119
- if (!isRecord9(raw))
80917
+ if (!isRecord10(raw))
80120
80918
  return { ok: false, problems: ["the plan must be a JSON object"] };
80121
80919
  const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
80122
80920
  if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
@@ -80139,8 +80937,8 @@ function parsePlan(raw, defaults, name) {
80139
80937
  const seenIds = new Set;
80140
80938
  const seenBranches = new Map;
80141
80939
  raw.slices.forEach((item, index) => {
80142
- const where = isRecord9(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80143
- if (!isRecord9(item)) {
80940
+ const where = isRecord10(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80941
+ if (!isRecord10(item)) {
80144
80942
  problems.push(`${where}: must be an object`);
80145
80943
  return;
80146
80944
  }
@@ -80458,7 +81256,7 @@ function fanoutsDir(cwd) {
80458
81256
  return { ok: true, data: path48.join(root.value.root, "fanouts") };
80459
81257
  }
80460
81258
  function parseStored(raw) {
80461
- if (!isRecord9(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
81259
+ if (!isRecord10(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
80462
81260
  return null;
80463
81261
  if (!Array.isArray(raw.slices))
80464
81262
  return null;
@@ -80475,7 +81273,7 @@ function parseStored(raw) {
80475
81273
  });
80476
81274
  const slices = [];
80477
81275
  for (const item of raw.slices) {
80478
- if (!isRecord9(item) || typeof item.id !== "string" || typeof item.branch !== "string")
81276
+ if (!isRecord10(item) || typeof item.id !== "string" || typeof item.branch !== "string")
80479
81277
  return null;
80480
81278
  slices.push({
80481
81279
  id: item.id,
@@ -80598,7 +81396,7 @@ function planFanout(cwd, input, now = new Date) {
80598
81396
  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");
80599
81397
  if (input.takeLead && isDelegateSession(session))
80600
81398
  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");
80601
- const parsed = parsePlan(input.trunk && isRecord9(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
81399
+ const parsed = parsePlan(input.trunk && isRecord10(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
80602
81400
  trunk: input.trunk ?? defaultTrunk(cwd),
80603
81401
  root: mainCheckout(cwd),
80604
81402
  worktreeRoot: worktreeRoot(cwd)
@@ -80721,7 +81519,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
80721
81519
  error,
80722
81520
  ...unblock ? { unblock } : {},
80723
81521
  ...data ? { data } : {}
80724
- }), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER3, filled = (text) => text.trim() !== "" && !PLACEHOLDER3.test(text.trim()), textList = (value) => {
81522
+ }), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER4, filled = (text) => text.trim() !== "" && !PLACEHOLDER4.test(text.trim()), textList = (value) => {
80725
81523
  if (typeof value === "string")
80726
81524
  return [value];
80727
81525
  if (Array.isArray(value) && value.every((item) => typeof item === "string"))
@@ -80773,7 +81571,7 @@ var init_fanout = __esm(() => {
80773
81571
  "CHANGELOG.md"
80774
81572
  ];
80775
81573
  compiled = new Map;
80776
- PLACEHOLDER3 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
81574
+ PLACEHOLDER4 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
80777
81575
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
80778
81576
  });
80779
81577
 
@@ -81330,9 +82128,9 @@ function hasOwnCommits2(cwd, branch, start, head) {
81330
82128
  const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
81331
82129
  if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
81332
82130
  continue;
81333
- if (sha !== head && !isAncestor3(cwd, sha, head))
82131
+ if (sha !== head && !isAncestor4(cwd, sha, head))
81334
82132
  continue;
81335
- if (start && (sha === start || isAncestor3(cwd, sha, start)))
82133
+ if (start && (sha === start || isAncestor4(cwd, sha, start)))
81336
82134
  continue;
81337
82135
  return true;
81338
82136
  }
@@ -81342,7 +82140,7 @@ function reverted(cwd, trunk, landedHead, start) {
81342
82140
  const fork = mergeBase(cwd, landedHead, trunk);
81343
82141
  if (fork && fork !== landedHead)
81344
82142
  return unchangedSince(cwd, fork, landedHead, trunk);
81345
- const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
82143
+ const before = start && start !== landedHead && isAncestor4(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
81346
82144
  return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
81347
82145
  }
81348
82146
  function unchangedSince(cwd, before, after, trunk) {
@@ -81450,9 +82248,9 @@ function detectLanding(ctx, slice, input) {
81450
82248
  }
81451
82249
  return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
81452
82250
  }
81453
- if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
82251
+ if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
81454
82252
  return landed("pr_merged", merged ?? head);
81455
- out.note = isAncestor3(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
82253
+ out.note = isAncestor4(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
81456
82254
  return out;
81457
82255
  }
81458
82256
  }
@@ -81469,7 +82267,7 @@ function detectLanding(ctx, slice, input) {
81469
82267
  if (trunkIds.size === 0)
81470
82268
  return out;
81471
82269
  const froms = [fork];
81472
- if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
82270
+ if (input.parentHead && input.parentHead !== head && isAncestor4(ctx.cwd, input.parentHead, head))
81473
82271
  froms.push(input.parentHead);
81474
82272
  for (const from of froms) {
81475
82273
  const id = patchId(ctx.cwd, from, head);
@@ -81485,7 +82283,7 @@ function sliceStart(cwd, rows, fanout, slice) {
81485
82283
  const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
81486
82284
  return row?.baseSha ?? null;
81487
82285
  }
81488
- var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
82286
+ var isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
81489
82287
  var init_fanout_landed = __esm(() => {
81490
82288
  init_rev();
81491
82289
  init_fanout();
@@ -81542,7 +82340,7 @@ function checkFanout(cwd, plan, options = {}) {
81542
82340
  const b = tips.get(second);
81543
82341
  if (landings.get(first)?.landed || landings.get(second)?.landed)
81544
82342
  continue;
81545
- if (!a || !b || isAncestor4(cwd, a, b) || isAncestor4(cwd, b, a))
82343
+ if (!a || !b || isAncestor5(cwd, a, b) || isAncestor5(cwd, b, a))
81546
82344
  continue;
81547
82345
  const check = checks.get(second);
81548
82346
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -81722,7 +82520,7 @@ function unblockFor(check, plan) {
81722
82520
  return `unblock ${check.waitsFor.join(", ")} first`;
81723
82521
  return check.reasons[0] ?? "workit fanout check";
81724
82522
  }
81725
- var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
82523
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor5 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
81726
82524
  id: slice.id,
81727
82525
  branch: slice.branch,
81728
82526
  head,
@@ -81939,7 +82737,7 @@ var init_fanout_status = __esm(() => {
81939
82737
  // packages/workit-cli/src/verbs/fanout.ts
81940
82738
  var exports_fanout = {};
81941
82739
  __export(exports_fanout, {
81942
- run: () => run16
82740
+ run: () => run17
81943
82741
  });
81944
82742
  import fs27 from "node:fs";
81945
82743
  import path50 from "node:path";
@@ -82135,7 +82933,7 @@ function worktree(argv, io) {
82135
82933
  return failed(io, result);
82136
82934
  return emit(io, ok(result.data), (data) => [
82137
82935
  `${data.created ? "created" : "kept"} worktree for slice ${data.slice} (${data.mode}): ${data.path}`,
82138
- ` branch ${data.branch} <- ${data.base} at ${short5(data.head)}`,
82936
+ ` branch ${data.branch} <- ${data.base} at ${short6(data.head)}`,
82139
82937
  ` scratch ${data.scratch} (put it in the worker brief: temp files go here)`,
82140
82938
  ...data.notes.map((note) => `note: ${note}`)
82141
82939
  ]);
@@ -82159,7 +82957,7 @@ function worktree(argv, io) {
82159
82957
  ...data.notes.map((note) => `note: ${note}`)
82160
82958
  ]);
82161
82959
  }
82162
- async function run16(argv, io) {
82960
+ async function run17(argv, io) {
82163
82961
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
82164
82962
  if (sub === "plan")
82165
82963
  return plan2(rest, io);
@@ -82171,9 +82969,9 @@ async function run16(argv, io) {
82171
82969
  return status3(rest, io);
82172
82970
  if (sub === "worktree")
82173
82971
  return worktree(rest, io);
82174
- return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
82972
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE9);
82175
82973
  }
82176
- 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) => [
82974
+ 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]", USAGE9 = "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) => [
82177
82975
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
82178
82976
  ...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 ? `
82179
82977
  ${slice.reasons.join(`
@@ -82183,7 +82981,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82183
82981
  `landing order: ${data.landingOrder.join(", ")}`,
82184
82982
  `next: ${data.next}`,
82185
82983
  ...data.notes.map((note) => `note: ${note}`)
82186
- ], short5 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
82984
+ ], short6 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
82187
82985
  const counts = new Map;
82188
82986
  for (const slice of data.slices)
82189
82987
  counts.set(slice.state, (counts.get(slice.state) ?? 0) + 1);
@@ -82191,7 +82989,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82191
82989
  `fanout ${data.name} on ${data.trunk}: ${[...counts].map(([state, count]) => `${count} ${state.replace("_", " ")}`).join(", ")} · forge ${data.forge.available ? "on" : "off"}`,
82192
82990
  ...data.slices.map((slice) => {
82193
82991
  const parts = [
82194
- slice.branchExists ? short5(slice.head) : "no branch",
82992
+ slice.branchExists ? short6(slice.head) : "no branch",
82195
82993
  slice.lastCommitAt && slice.idleMinutes !== null ? `idle ${slice.idleMinutes} min` : null,
82196
82994
  slice.pr ? `PR #${slice.pr.number} ${slice.pr.state}` : null,
82197
82995
  slice.ci === "unknown" ? null : `CI ${slice.ci}`,
@@ -82225,7 +83023,7 @@ var init_fanout2 = __esm(() => {
82225
83023
  // packages/workit-cli/src/verbs/ledger.ts
82226
83024
  var exports_ledger = {};
82227
83025
  __export(exports_ledger, {
82228
- run: () => run17
83026
+ run: () => run18
82229
83027
  });
82230
83028
  import { randomBytes as randomBytes5 } from "node:crypto";
82231
83029
  import { parseArgs } from "node:util";
@@ -82249,7 +83047,7 @@ function targetBranch(io, rows, branch, pr) {
82249
83047
  }
82250
83048
  return { ok: true, value: branch ?? null };
82251
83049
  }
82252
- async function run17(argv, io) {
83050
+ async function run18(argv, io) {
82253
83051
  let parsed;
82254
83052
  try {
82255
83053
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -82441,7 +83239,7 @@ async function standing(io, values, args) {
82441
83239
  return usage4(io, `unexpected argument: ${rest[1]}`);
82442
83240
  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}`);
82443
83241
  }
82444
- 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) => {
83242
+ var USAGE10 = "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) => {
82445
83243
  const actor = actorFromEnv(io.env);
82446
83244
  if (values.session !== undefined && values.as !== undefined)
82447
83245
  return new Error("pass --session or --as, not both");
@@ -82466,7 +83264,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (wor
82466
83264
  return;
82467
83265
  const parsed = Number(value);
82468
83266
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
82469
- }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), passedTestOn = (cwd, verdict) => {
83267
+ }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE10 })), passedTestOn = (cwd, verdict) => {
82470
83268
  const ledger = readLedger(cwd);
82471
83269
  return ledger.ok && ledger.value.rows.some((row) => row.type === "check" && row.observer === "workit_cli" && row.name === "test" && row.result === "passed" && row.dirty !== true && row.branch === verdict.branch && row.head === verdict.head);
82472
83270
  }, verdictLines = (check) => {
@@ -98845,7 +99643,7 @@ async function runMutation(options) {
98845
99643
  if (!runnable.includes(mutant))
98846
99644
  record({ ...mutant, status: "no-tests", durationMs: 0, tests: [] });
98847
99645
  if (runnable.length === 0)
98848
- return finish2(options, 0, results, counts, started);
99646
+ return finish3(options, 0, results, counts, started);
98849
99647
  removeStaleCopies();
98850
99648
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
98851
99649
  const copyCwd = path53.join(copy, cwdRelative);
@@ -98909,7 +99707,7 @@ async function runMutation(options) {
98909
99707
  restore();
98910
99708
  }
98911
99709
  }
98912
- return finish2(options, baseline.durationMs, results, counts, started);
99710
+ return finish3(options, baseline.durationMs, results, counts, started);
98913
99711
  } finally {
98914
99712
  process.off("SIGINT", onSignal);
98915
99713
  process.off("SIGTERM", onSignal);
@@ -98917,7 +99715,7 @@ async function runMutation(options) {
98917
99715
  cleanup();
98918
99716
  }
98919
99717
  }
98920
- function finish2(options, baselineMs, results, counts, started) {
99718
+ function finish3(options, baselineMs, results, counts, started) {
98921
99719
  const decided = counts.killed + counts.timeout + counts.survived;
98922
99720
  return {
98923
99721
  base: options.base,
@@ -98987,7 +99785,7 @@ var init_mutate = __esm(() => {
98987
99785
  // packages/workit-cli/src/verbs/test-audit.ts
98988
99786
  var exports_test_audit = {};
98989
99787
  __export(exports_test_audit, {
98990
- run: () => run18
99788
+ run: () => run19
98991
99789
  });
98992
99790
  import { existsSync as existsSync22 } from "node:fs";
98993
99791
  import path54 from "node:path";
@@ -99088,11 +99886,11 @@ function human(data) {
99088
99886
  }
99089
99887
  return lines;
99090
99888
  }
99091
- async function run18(argv, io) {
99889
+ async function run19(argv, io) {
99092
99890
  const options = parse7(argv);
99093
99891
  if (typeof options === "string")
99094
99892
  return emit(io, fail("invalid_input", `${options}
99095
- usage: ${USAGE10}`));
99893
+ usage: ${USAGE11}`));
99096
99894
  const top = git7(io.cwd, ["rev-parse", "--show-toplevel"]);
99097
99895
  const repo = top.ok ? path54.resolve(top.stdout.trim()) : null;
99098
99896
  const display = (file) => toPosix(path54.relative(io.cwd, file) || file);
@@ -99178,7 +99976,7 @@ usage: ${USAGE10}`));
99178
99976
  }
99179
99977
  return emit(io, ok(data), human);
99180
99978
  }
99181
- var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
99979
+ var USAGE11 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
99182
99980
  var init_test_audit = __esm(() => {
99183
99981
  init_audit();
99184
99982
  init_mutate();
@@ -99199,12 +99997,12 @@ var init_test_audit = __esm(() => {
99199
99997
  // packages/workit-cli/src/verbs/knowledge.ts
99200
99998
  var exports_knowledge = {};
99201
99999
  __export(exports_knowledge, {
99202
- run: () => run19
100000
+ run: () => run20
99203
100001
  });
99204
- async function run19(argv, io) {
100002
+ async function run20(argv, io) {
99205
100003
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
99206
100004
  if (sub !== "lint" || rest.length > 0)
99207
- return emit(io, fail("invalid_input", `usage: ${USAGE11}`));
100005
+ return emit(io, fail("invalid_input", `usage: ${USAGE12}`));
99208
100006
  const report = lintKnowledge(checkRoot(io.cwd));
99209
100007
  if (report.unreadable.length > 0)
99210
100008
  return emit(io, fail("unavailable", `knowledge lint: cannot read ${report.unreadable.join(", ")}`, {
@@ -99221,7 +100019,7 @@ async function run19(argv, io) {
99221
100019
  `);
99222
100020
  return report.findings.length === 0 ? 0 : 1;
99223
100021
  }
99224
- var USAGE11 = "workit knowledge lint [--json]";
100022
+ var USAGE12 = "workit knowledge lint [--json]";
99225
100023
  var init_knowledge2 = __esm(() => {
99226
100024
  init_check_config();
99227
100025
  init_knowledge();
@@ -99335,14 +100133,14 @@ var init_youtrack_writes = __esm(() => {
99335
100133
  // packages/workit-cli/src/verbs/youtrack.ts
99336
100134
  var exports_youtrack = {};
99337
100135
  __export(exports_youtrack, {
99338
- run: () => run20
100136
+ run: () => run21
99339
100137
  });
99340
100138
  import { readFileSync as readFileSync25 } from "node:fs";
99341
100139
  import path55 from "node:path";
99342
- async function run20(argv, io) {
100140
+ async function run21(argv, io) {
99343
100141
  const [sub, ...rest] = argv;
99344
100142
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
99345
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
100143
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
99346
100144
  const flags = parseFlags(rest, {
99347
100145
  markdown: "value",
99348
100146
  file: "value",
@@ -99351,18 +100149,18 @@ async function run20(argv, io) {
99351
100149
  date: "value"
99352
100150
  });
99353
100151
  if (typeof flags === "string")
99354
- return usage3(io, flags, USAGE12);
100152
+ return usage3(io, flags, USAGE13);
99355
100153
  const [issueId, extra] = flags.positionals;
99356
100154
  if (!issueId || !ISSUE_RE2.test(issueId))
99357
- return usage3(io, "an issue id like ABC-123 is required", USAGE12);
100155
+ return usage3(io, "an issue id like ABC-123 is required", USAGE13);
99358
100156
  if (extra !== undefined)
99359
- return usage3(io, `unexpected argument ${extra}`, USAGE12);
100157
+ return usage3(io, `unexpected argument ${extra}`, USAGE13);
99360
100158
  const minutes = positiveInt(flags.values.minutes, "--minutes");
99361
100159
  if (typeof minutes === "string")
99362
- return usage3(io, minutes, USAGE12);
100160
+ return usage3(io, minutes, USAGE13);
99363
100161
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
99364
100162
  if ("error" in date)
99365
- return usage3(io, date.error, USAGE12);
100163
+ return usage3(io, date.error, USAGE13);
99366
100164
  const dateMs = date.data.dateMs;
99367
100165
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
99368
100166
  const root = io.cwd;
@@ -99380,24 +100178,24 @@ async function run20(argv, io) {
99380
100178
  };
99381
100179
  if (sub === "note") {
99382
100180
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
99383
- return usage3(io, "pass exactly one of --markdown or --file", USAGE12);
100181
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE13);
99384
100182
  let markdown = flags.values.markdown ?? "";
99385
100183
  if (flags.values.file)
99386
100184
  try {
99387
100185
  markdown = readFileSync25(path55.resolve(io.cwd, flags.values.file), "utf8");
99388
100186
  } catch (error) {
99389
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
100187
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE13);
99390
100188
  }
99391
100189
  if (!markdown.trim())
99392
- return usage3(io, "the note is empty", USAGE12);
100190
+ return usage3(io, "the note is empty", USAGE13);
99393
100191
  const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
99394
100192
  if (missing)
99395
100193
  return emit(io, fail("unavailable", missing));
99396
100194
  } else {
99397
100195
  if (!minutes)
99398
- return usage3(io, `${sub} needs --minutes <n>`, USAGE12);
100196
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE13);
99399
100197
  if (sub === "meeting" && !flags.values.text)
99400
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE12);
100198
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE13);
99401
100199
  const text = flags.values.text ?? "";
99402
100200
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
99403
100201
  if (missing)
@@ -99426,7 +100224,7 @@ async function run20(argv, io) {
99426
100224
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
99427
100225
  ]);
99428
100226
  }
99429
- var USAGE12 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
100227
+ var USAGE13 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
99430
100228
  try {
99431
100229
  return { ok: true, data: unwrap2(value) };
99432
100230
  } catch (error) {
@@ -99450,14 +100248,14 @@ var init_youtrack2 = __esm(() => {
99450
100248
  // packages/workit-cli/src/verbs/changelog.ts
99451
100249
  var exports_changelog = {};
99452
100250
  __export(exports_changelog, {
99453
- run: () => run21
100251
+ run: () => run22
99454
100252
  });
99455
100253
  import { readFileSync as readFileSync26 } from "node:fs";
99456
100254
  import path56 from "node:path";
99457
- async function run21(argv, io) {
100255
+ async function run22(argv, io) {
99458
100256
  const [sub, ...rest] = argv;
99459
100257
  if (sub !== "apply")
99460
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
100258
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE14);
99461
100259
  const flags = parseFlags(rest, {
99462
100260
  entries: "value",
99463
100261
  path: "value",
@@ -99465,18 +100263,18 @@ async function run21(argv, io) {
99465
100263
  preview: "boolean"
99466
100264
  });
99467
100265
  if (typeof flags === "string")
99468
- return usage3(io, flags, USAGE13);
100266
+ return usage3(io, flags, USAGE14);
99469
100267
  if (flags.positionals.length)
99470
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE13);
100268
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE14);
99471
100269
  const normalizeOnly = flags.booleans.has("normalize-only");
99472
100270
  if (!normalizeOnly && !flags.values.entries)
99473
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE13);
100271
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE14);
99474
100272
  let entries;
99475
100273
  if (flags.values.entries)
99476
100274
  try {
99477
100275
  entries = readEntries(flags.values.entries, io.cwd);
99478
100276
  } catch (error) {
99479
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE13);
100277
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE14);
99480
100278
  }
99481
100279
  const input = {
99482
100280
  entries,
@@ -99498,7 +100296,7 @@ async function run21(argv, io) {
99498
100296
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
99499
100297
  ]);
99500
100298
  }
99501
- var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
100299
+ var USAGE14 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
99502
100300
  const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path56.resolve(cwd, raw.slice(1)), "utf8") : raw;
99503
100301
  return JSON.parse(text);
99504
100302
  };
@@ -99511,12 +100309,12 @@ var init_changelog2 = __esm(() => {
99511
100309
  // packages/workit-cli/src/verbs/handoff.ts
99512
100310
  var exports_handoff = {};
99513
100311
  __export(exports_handoff, {
99514
- run: () => run22
100312
+ run: () => run23
99515
100313
  });
99516
100314
  import { parseArgs as parseArgs2 } from "node:util";
99517
100315
  function human2(brief) {
99518
100316
  const lines = [
99519
- `branch: ${brief.branch ?? "(detached)"} @ ${short6(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
100317
+ `branch: ${brief.branch ?? "(detached)"} @ ${short7(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
99520
100318
  ];
99521
100319
  if (brief.base)
99522
100320
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -99559,11 +100357,11 @@ async function resumeBrief(argv, io) {
99559
100357
  strict: true
99560
100358
  }));
99561
100359
  } catch (error) {
99562
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE14 }));
100360
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE15 }));
99563
100361
  }
99564
100362
  const last = values.last === undefined ? 10 : Number(values.last);
99565
100363
  if (!Number.isInteger(last) || last < 1)
99566
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE14 }));
100364
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE15 }));
99567
100365
  const built = buildHandoff(io.cwd, {
99568
100366
  last,
99569
100367
  note: values.note ?? null,
@@ -99581,13 +100379,13 @@ async function resumeBrief(argv, io) {
99581
100379
  }
99582
100380
  return emit(io, ok({ ...data, recorded }), human2);
99583
100381
  }
99584
- async function run22(argv, io) {
100382
+ async function run23(argv, io) {
99585
100383
  if (!argv.some((arg) => arg === "--task" || arg.startsWith("--task=")))
99586
100384
  return resumeBrief(argv, io);
99587
100385
  await Promise.resolve().then(() => init_task());
99588
100386
  return runTaskCommand(["handoff", ...argv]);
99589
100387
  }
99590
- var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short6 = (sha) => sha ? sha.slice(0, 12) : "no commit";
100388
+ var USAGE15 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short7 = (sha) => sha ? sha.slice(0, 12) : "no commit";
99591
100389
  var init_handoff = __esm(() => {
99592
100390
  init_ledger();
99593
100391
  init_output();
@@ -99966,6 +100764,35 @@ var VERBS = [
99966
100764
  ],
99967
100765
  load: () => Promise.resolve().then(() => (init_git2(), exports_git))
99968
100766
  }),
100767
+ withSubcommands({
100768
+ name: "release",
100769
+ group: "delivery",
100770
+ summary: "git-flow releases without the git-flow binary: start a local release branch, finish it (merge, tag, merge back, push, forge release), publish it only when asked",
100771
+ subcommands: [
100772
+ {
100773
+ name: "start",
100774
+ usage: "workit release start <version> [--hotfix] [--track <t>]",
100775
+ summary: "Cut release/<version> (hotfix/<version> from production) locally; never pushes it"
100776
+ },
100777
+ {
100778
+ name: "finish",
100779
+ usage: "workit release finish [<version>] [--track <t>] [--dry-run]",
100780
+ summary: "Merge into production, tag, merge back, delete the branch, push production, merge-back branches and the tag, publish the forge release; resumes an interrupted finish"
100781
+ },
100782
+ {
100783
+ name: "publish",
100784
+ usage: "workit release publish [<version>]",
100785
+ summary: "Push the release branch (opt-in: others commit to it or CI runs on it)"
100786
+ }
100787
+ ],
100788
+ examples: [
100789
+ "workit release start 1.4.0",
100790
+ 'workit git commit -m "chore(release): 1.4.0" -- package.json CHANGELOG.md',
100791
+ "workit release finish --dry-run",
100792
+ "workit release finish"
100793
+ ],
100794
+ load: () => Promise.resolve().then(() => (init_release2(), exports_release))
100795
+ }),
99969
100796
  withSubcommands({
99970
100797
  name: "verify-delivery",
99971
100798
  group: "delivery",