@brainervirus/workit-cli 8.12.2 → 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 +1000 -121
  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.2",
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.2",
107
- "@brainervirus/workit-cursor": "^8.12.2",
108
- "@brainervirus/workit-opencode": "^8.12.2",
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();
@@ -17303,17 +17311,49 @@ var init_branch = __esm(() => {
17303
17311
  var init_session_marker = __esm(() => {
17304
17312
  init_paths();
17305
17313
  });
17314
+
17315
+ // packages/workit-core/src/hooks/shell-words.ts
17316
+ var init_shell_words = () => {};
17317
+
17306
17318
  // packages/workit-core/src/hooks/raw-git.ts
17307
- var SHELLS, PREFIXES2, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, LAUNCHERS, PUSH_VALUE_OPTIONS, VALUE_OPTIONS;
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;
17308
17320
  var init_raw_git = __esm(() => {
17309
17321
  init_branch();
17322
+ init_branch_policy();
17310
17323
  init_workspaces();
17311
- init_rev();
17312
17324
  init_ledger();
17313
17325
  init_paths();
17314
17326
  init_session_marker();
17315
- SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
17316
- PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
17327
+ init_shell_words();
17328
+ SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "mksh", "yash", "fish"]);
17329
+ POWERSHELLS = new Set(["pwsh", "powershell"]);
17330
+ EVALS = new Set(["eval", "iex", "invoke-expression"]);
17331
+ WRAPPERS = {
17332
+ sudo: { values: "-u -g -h -p -C -D -R -T -r -t -U --user --group --host --prompt --chdir" },
17333
+ doas: { values: "-u -C" },
17334
+ command: { values: "" },
17335
+ builtin: { values: "" },
17336
+ env: { values: "-u -C --unset --chdir" },
17337
+ nohup: { values: "" },
17338
+ setsid: { values: "" },
17339
+ time: { values: "-f -o --format --output" },
17340
+ exec: { values: "-a" },
17341
+ nice: { values: "-n --adjustment" },
17342
+ ionice: { values: "-c -n -p --class --classdata" },
17343
+ stdbuf: { values: "-i -o -e --input --output --error" },
17344
+ chronic: { values: "" },
17345
+ unbuffer: { values: "" },
17346
+ wsl: { values: "-d -u --cd --distribution --user" },
17347
+ timeout: { values: "-s -k --signal --kill-after", operands: 1 },
17348
+ flock: { values: "-w -E --timeout --conflict-exit-code", operands: 1 },
17349
+ coproc: { values: "" },
17350
+ taskset: { values: "", operands: 1 },
17351
+ watch: { values: "-n -d --interval --differences -q --equexit" },
17352
+ "&": { values: "" }
17353
+ };
17354
+ WRAPPER_VALUES = new Map(Object.entries(WRAPPERS).map(([name, wrapper]) => [name, new Set(wrapper.values.split(" "))]));
17355
+ KEYWORDS = new Set(["if", "then", "else", "elif", "while", "until", "do", "!", "{"]);
17356
+ DECLARATIONS = new Set(["export", "local", "declare", "readonly", "typeset"]);
17317
17357
  CD = new Set(["cd", "chdir", "set-location", "sl"]);
17318
17358
  PUSHD = new Set(["pushd", "push-location"]);
17319
17359
  POPD = new Set(["popd", "pop-location"]);
@@ -17326,6 +17366,17 @@ var init_raw_git = __esm(() => {
17326
17366
  "--config-env",
17327
17367
  "--super-prefix"
17328
17368
  ]);
17369
+ GIT_CONFIG_OPTIONS = new Set(["-c", "--config-env", "--git-dir"]);
17370
+ XARGS_VALUES = new Set(["-n", "-L", "-l", "-P", "-d", "-E", "-e", "-s", "-a", "--arg-file", "--delimiter"].concat([
17371
+ "--max-args",
17372
+ "--max-lines",
17373
+ "--max-procs",
17374
+ "--eof",
17375
+ "--max-chars",
17376
+ "--process-slot-var"
17377
+ ]));
17378
+ PARALLEL_VALUES = new Set(["-j", "--jobs", "-N", "-n", "-S", "--sshlogin", "-P", "--delay"]);
17379
+ RECENT = new Map;
17329
17380
  LAUNCHERS = new Set([
17330
17381
  "npx",
17331
17382
  "bunx",
@@ -17367,19 +17418,29 @@ var init_raw_git = __esm(() => {
17367
17418
  gh: new Set(["-b", "--body", "-F", "--body-file", "-t", "--title", "--subject", "-A", "--author-email"].concat(["-B", "--base", "-H", "--head", "-l", "--label", "-a", "--assignee"]).concat(["-r", "--reviewer", "-m", "--milestone", "-p", "--project", "-T", "--template"]).concat(["--match-head-commit", "-R", "--repo"])),
17368
17419
  glab: new Set(["-m", "--message", "--squash-message", "--sha", "-t", "--title", "-d", "--description"].concat(["-b", "--target-branch", "-s", "--source-branch", "-l", "--label"]).concat(["-a", "--assignee", "--reviewer", "-R", "--repo"]))
17369
17420
  };
17421
+ API_VALUES = new Set(["--method", "--header", "--raw-field", "--field", "--jq", "--template", "--input"].concat([
17422
+ "--hostname",
17423
+ "--preview",
17424
+ "--cache",
17425
+ "--output"
17426
+ ]));
17427
+ API_FIELDS = new Set(["-f", "-F", "--raw-field", "--field"]);
17428
+ GIT_COMMANDS = new Set(("add am annotate apply archive bisect blame branch bugreport bundle cat-file check-attr " + "check-ignore check-mailmap check-ref-format checkout checkout-index cherry cherry-pick " + "citool clean clone column commit commit-graph commit-tree config count-objects credential " + "daemon describe diagnose diff diff-files diff-index diff-tree difftool fast-export " + "fast-import fetch fetch-pack filter-branch fmt-merge-msg for-each-ref for-each-repo " + "format-patch fsck gc get-tar-commit-id grep gui hash-object help hook index-pack init " + "instaweb interpret-trailers lfs log ls-files ls-remote ls-tree mailinfo mailsplit " + "maintenance merge merge-base merge-file merge-index merge-tree mergetool mktag mktree " + "multi-pack-index mv name-rev notes pack-objects pack-refs patch-id prune pull push " + "range-diff read-tree rebase reflog remote repack replace request-pull rerere reset " + "restore rev-list rev-parse revert rm send-email shortlog show show-branch show-ref " + "sparse-checkout stage stash status stripspace submodule subtree switch symbolic-ref tag " + "update-index update-ref var verify-commit verify-pack verify-tag version whatchanged " + "worktree write-tree").split(" "));
17370
17429
  });
17371
17430
 
17372
17431
  // packages/workit-core/src/core/route-intent.ts
17373
- var SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, WORKTREE_VALUES;
17432
+ var SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, SUBTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, WORKTREE_VALUES, FETCH_VALUES;
17374
17433
  var init_route_intent = __esm(() => {
17375
17434
  init_raw_git();
17376
17435
  init_branch();
17377
17436
  SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
17378
17437
  CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
17379
17438
  WORKTREE_CREATE = new Set(["-b", "-B"]);
17439
+ SUBTREE_CREATE = new Set(["-b", "--branch"]);
17380
17440
  BRANCH_NO_CREATE = new Set(["-d", "-D", "--delete", "-l", "--list", "-a", "--all", "-r", "--remotes", "--show-current"].concat(["-v", "-vv", "--verbose", "--contains", "--no-contains", "--merged", "--no-merged"]).concat(["--points-at", "--sort", "--format", "--column", "--no-column", "-u"]).concat(["--set-upstream-to", "--unset-upstream", "--edit-description", "-i", "--ignore-case"]));
17381
17441
  BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
17382
17442
  WORKTREE_VALUES = new Set(["--reason"]);
17443
+ FETCH_VALUES = new Set(["--depth", "--deepen", "--shallow-since", "--shallow-exclude", "--refmap", "-j", "--jobs"].concat(["--upload-pack", "-o", "--server-option", "--negotiation-tip", "--filter"]).concat(["--recurse-submodules-default", "--submodule-prefix", "-s", "--strategy", "-X"]).concat(["--strategy-option"]));
17383
17444
  });
17384
17445
 
17385
17446
  // packages/workit-core/src/core/runtime-identity.ts
@@ -74576,6 +74637,51 @@ ${gitOutput(run)}`);
74576
74637
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
74577
74638
  });
74578
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
+ }
74579
74685
  function releasePushPreflight(cwd, names) {
74580
74686
  const progress = operationInProgress(cwd);
74581
74687
  if (progress)
@@ -74583,7 +74689,16 @@ function releasePushPreflight(cwd, names) {
74583
74689
  const wanted = [...new Set(names)];
74584
74690
  if (!wanted.length)
74585
74691
  return failure3("invalid_input", "name the release refs to push");
74586
- 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>`;
74587
74702
  const refs = [];
74588
74703
  for (const raw of wanted) {
74589
74704
  if (raw.startsWith("-") || raw.includes(":"))
@@ -74595,16 +74710,22 @@ function releasePushPreflight(cwd, names) {
74595
74710
  if (tag && branch)
74596
74711
  return failure3("invalid_input", `${raw} names both a branch and a tag`, `workit git push --release refs/heads/${raw} # or refs/tags/${raw}`);
74597
74712
  if (tag) {
74598
- const prefix = releaseTagPrefix(cwd);
74599
- if (!isReleaseTag(tagName, prefix))
74600
- 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>`);
74601
- 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
+ });
74602
74723
  continue;
74603
74724
  }
74604
74725
  if (!branch)
74605
74726
  return failure3("not_found", `${raw} is not a local branch or tag`);
74606
74727
  if (!branches.some((name) => name.toLowerCase() === branchName.toLowerCase()))
74607
- 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}`);
74608
74729
  refs.push({ kind: "branch", name: branchName, ref: `refs/heads/${branchName}`, sha: branch });
74609
74730
  }
74610
74731
  const remote = pushRemoteName(cwd, refs.find((ref) => ref.kind === "branch")?.name ?? null);
@@ -74616,7 +74737,14 @@ function releasePushPreflight(cwd, names) {
74616
74737
  const local = parseRemoteUrl(raw)?.protocol === "file";
74617
74738
  if (!local && !pushTargetIsStable(cwd, raw))
74618
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");
74619
- 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
+ });
74620
74748
  }
74621
74749
  function executeReleasePush(cwd, plan, input) {
74622
74750
  const previous = new Map;
@@ -74633,20 +74761,20 @@ function executeReleasePush(cwd, plan, input) {
74633
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}`);
74634
74762
  }
74635
74763
  const tags = plan.refs.filter((ref) => ref.kind === "tag");
74636
- if (tags.length) {
74637
- let productionTip = null;
74638
- if (plan.production) {
74639
- 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}`);
74640
74770
  if (!tip.ok)
74641
74771
  return failure3(tip.code, tip.error);
74642
- productionTip = tip.sha;
74643
- }
74644
- const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
74645
- for (const tag of tags) {
74646
- const released = productionTip !== null && contains(productionTip, tag.sha) || plan.refs.some((ref) => ref.kind === "branch" && contains(ref.sha, tag.sha));
74647
- if (!released)
74648
- 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;
74649
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`);
74650
74778
  }
74651
74779
  const pending = plan.refs.filter((ref) => previous.get(ref.ref) !== ref.sha);
74652
74780
  if (pending.length) {
@@ -74760,12 +74888,10 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
74760
74888
  ...lines.slice(-half)
74761
74889
  ].join(`
74762
74890
  `);
74763
- }, 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) => {
74764
- const configured = git5(cwd, ["config", "--get", "gitflow.prefix.versiontag"]);
74765
- return configured.ok ? configured.stdout.trim() : "v";
74766
- }, 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;
74767
74892
  var init_ops = __esm(() => {
74768
74893
  init_branch();
74894
+ init_branch_policy();
74769
74895
  init_commit_flavors();
74770
74896
  init_pr_create();
74771
74897
  init_release_tracks();
@@ -75792,7 +75918,7 @@ function stacksDir(cwd) {
75792
75918
  return { ok: true, data: path44.join(root.value.root, "stacks") };
75793
75919
  }
75794
75920
  function parseStack(raw) {
75795
- if (!isRecord8(raw) || !Array.isArray(raw.branches))
75921
+ if (!isRecord9(raw) || !Array.isArray(raw.branches))
75796
75922
  return null;
75797
75923
  const name = str3(raw.name);
75798
75924
  const trunk = str3(raw.trunk);
@@ -75800,12 +75926,12 @@ function parseStack(raw) {
75800
75926
  return null;
75801
75927
  const branches = [];
75802
75928
  for (const item of raw.branches) {
75803
- if (!isRecord8(item))
75929
+ if (!isRecord9(item))
75804
75930
  return null;
75805
75931
  const branch = str3(item.branch);
75806
75932
  if (!branch)
75807
75933
  return null;
75808
- const merged = isRecord8(item.merged) ? {
75934
+ const merged = isRecord9(item.merged) ? {
75809
75935
  pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
75810
75936
  mergeSha: str3(item.merged.mergeSha),
75811
75937
  at: str3(item.merged.at) ?? ""
@@ -75818,7 +75944,7 @@ function parseStack(raw) {
75818
75944
  lastParentHead: str3(item.lastParentHead),
75819
75945
  patchId: str3(item.patchId),
75820
75946
  merged,
75821
- ...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) ? {
75822
75948
  pendingRestack: {
75823
75949
  from: str3(item.pendingRestack.from),
75824
75950
  tip: str3(item.pendingRestack.tip),
@@ -76868,7 +76994,7 @@ ${run.stdout}`.split(`
76868
76994
  return false;
76869
76995
  }
76870
76996
  return true;
76871
- }, 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) => {
76872
76998
  let raw = null;
76873
76999
  try {
76874
77000
  raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
@@ -77626,6 +77752,24 @@ function createGitHubForge(options) {
77626
77752
  if (node?.pullRequest?.number !== number)
77627
77753
  return success2(null);
77628
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
+ });
77629
77773
  }
77630
77774
  };
77631
77775
  }
@@ -78011,6 +78155,24 @@ ${squash.body}` : squash.subject
78011
78155
  if (!notes.some((note) => note.resolvable === true))
78012
78156
  return success2(null);
78013
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
+ });
78014
78176
  }
78015
78177
  };
78016
78178
  }
@@ -79616,12 +79778,700 @@ var init_verify = __esm(() => {
79616
79778
  SAFE_ARG = /^[0-9A-Za-z@/._~+-]+$/u;
79617
79779
  });
79618
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
+
79619
80469
  // packages/workit-cli/src/verbs/verify-delivery.ts
79620
80470
  var exports_verify_delivery = {};
79621
80471
  __export(exports_verify_delivery, {
79622
- run: () => run14
80472
+ run: () => run15
79623
80473
  });
79624
- async function run14(argv, io) {
80474
+ async function run15(argv, io) {
79625
80475
  const flags = parseFlags(argv, {
79626
80476
  expect: "value",
79627
80477
  branch: "value",
@@ -79631,31 +80481,31 @@ async function run14(argv, io) {
79631
80481
  package: "value"
79632
80482
  });
79633
80483
  if (typeof flags === "string")
79634
- return usage3(io, flags, USAGE6);
80484
+ return usage3(io, flags, USAGE7);
79635
80485
  if (flags.positionals.length > 1)
79636
- return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE6);
80486
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE7);
79637
80487
  const named = flags.positionals[0];
79638
80488
  if (named !== undefined && !ENDPOINTS[named])
79639
- return usage3(io, `unknown endpoint "${named}"`, USAGE6);
80489
+ return usage3(io, `unknown endpoint "${named}"`, USAGE7);
79640
80490
  if (flags.values.expect !== undefined && !ENDPOINTS[flags.values.expect])
79641
- 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);
79642
80492
  if (named && flags.values.expect && ENDPOINTS[named] !== ENDPOINTS[flags.values.expect])
79643
- return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE6);
80493
+ return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE7);
79644
80494
  const expect = ENDPOINTS[named ?? ""] ?? ENDPOINTS[flags.values.expect ?? ""] ?? (flags.values.tag || flags.values.package ? "released" : flags.values.pr ? "pr" : "pushed");
79645
80495
  const pr = positiveInt(flags.values.pr, "--pr");
79646
80496
  if (typeof pr === "string")
79647
- return usage3(io, pr, USAGE6);
80497
+ return usage3(io, pr, USAGE7);
79648
80498
  if (pr && flags.values.branch)
79649
- return usage3(io, "pass --pr or --branch, not both", USAGE6);
80499
+ return usage3(io, "pass --pr or --branch, not both", USAGE7);
79650
80500
  const sha = flags.values.sha ?? null;
79651
80501
  if (sha !== null && !/^[0-9a-f]{7,64}$/u.test(sha))
79652
- 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);
79653
80503
  const actor = actorFromEnv(io.env);
79654
80504
  const branch = flags.values.branch ?? null;
79655
80505
  let result;
79656
80506
  if (expect === "pushed") {
79657
80507
  if (pr)
79658
- return usage3(io, "push is checked by --branch, not --pr", USAGE6);
80508
+ return usage3(io, "push is checked by --branch, not --pr", USAGE7);
79659
80509
  result = verifyPushed(io.cwd, { branch, sha, actor });
79660
80510
  } else if (expect === "released") {
79661
80511
  result = verifyReleased(io.cwd, { tag: flags.values.tag ?? null, pkg: flags.values.package ?? null, sha, actor }, forgeDeps.npm);
@@ -79680,7 +80530,7 @@ async function run14(argv, io) {
79680
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"
79681
80531
  }));
79682
80532
  }
79683
- 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) => [
79684
80534
  `${report.expect}: ${report.delivered ? "delivered" : "NOT delivered"}${report.branch ? ` (${report.branch}${report.sha ? ` @ ${report.sha.slice(0, 12)}` : ""})` : ""}${report.pr ? ` #${report.pr}` : ""}`,
79685
80535
  ...report.observations.map((entry) => ` ${entry.ok ? "ok" : "x "} ${entry.kind}: expected ${entry.expected ?? "-"}, observed ${entry.observed ?? "(none)"}${entry.note ? ` — ${entry.note}` : ""}`)
79686
80536
  ];
@@ -79703,7 +80553,7 @@ var init_verify_delivery = __esm(() => {
79703
80553
  // packages/workit-cli/src/verbs/stack.ts
79704
80554
  var exports_stack = {};
79705
80555
  __export(exports_stack, {
79706
- run: () => run15
80556
+ run: () => run16
79707
80557
  });
79708
80558
  async function plan(argv, io) {
79709
80559
  const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
@@ -79877,7 +80727,7 @@ async function land(argv, io) {
79877
80727
  ...data.dryRun ? [
79878
80728
  data.wouldLand.length ? `would land: ${data.wouldLand.map((item) => `${label2(kind, item.pr)} (${item.branch})`).join(", ")}` : "would land nothing"
79879
80729
  ] : [
79880
- 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"
79881
80731
  ],
79882
80732
  ...data.retargeted.length ? [`retargeted: ${data.retargeted.map((pr) => label2(kind, pr)).join(", ")}`] : [],
79883
80733
  ...data.restacked.length ? [`restacked: ${data.restacked.join(", ")}`] : [],
@@ -79885,7 +80735,7 @@ async function land(argv, io) {
79885
80735
  unblock: ${data.stoppedAt.unblock}` : data.complete ? "stack complete" : ""
79886
80736
  ].filter(Boolean));
79887
80737
  }
79888
- async function run15(argv, io) {
80738
+ async function run16(argv, io) {
79889
80739
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
79890
80740
  if (sub === "plan")
79891
80741
  return plan(rest, io);
@@ -79895,13 +80745,13 @@ async function run15(argv, io) {
79895
80745
  return sync(rest, io);
79896
80746
  if (sub === "land")
79897
80747
  return land(rest, io);
79898
- 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);
79899
80749
  }
79900
- 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) => [
79901
80751
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
79902
80752
  ...data.steps.map((step) => [
79903
80753
  ` ${step.branch} on ${step.parent}:`,
79904
- 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",
79905
80755
  step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
79906
80756
  step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
79907
80757
  ].filter(Boolean).join(" "))
@@ -80064,7 +80914,7 @@ function inScope(plan, slice, file) {
80064
80914
  }
80065
80915
  function parsePlan(raw, defaults, name) {
80066
80916
  const problems = [];
80067
- if (!isRecord9(raw))
80917
+ if (!isRecord10(raw))
80068
80918
  return { ok: false, problems: ["the plan must be a JSON object"] };
80069
80919
  const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
80070
80920
  if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
@@ -80087,8 +80937,8 @@ function parsePlan(raw, defaults, name) {
80087
80937
  const seenIds = new Set;
80088
80938
  const seenBranches = new Map;
80089
80939
  raw.slices.forEach((item, index) => {
80090
- const where = isRecord9(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80091
- if (!isRecord9(item)) {
80940
+ const where = isRecord10(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80941
+ if (!isRecord10(item)) {
80092
80942
  problems.push(`${where}: must be an object`);
80093
80943
  return;
80094
80944
  }
@@ -80406,7 +81256,7 @@ function fanoutsDir(cwd) {
80406
81256
  return { ok: true, data: path48.join(root.value.root, "fanouts") };
80407
81257
  }
80408
81258
  function parseStored(raw) {
80409
- if (!isRecord9(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
81259
+ if (!isRecord10(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
80410
81260
  return null;
80411
81261
  if (!Array.isArray(raw.slices))
80412
81262
  return null;
@@ -80423,7 +81273,7 @@ function parseStored(raw) {
80423
81273
  });
80424
81274
  const slices = [];
80425
81275
  for (const item of raw.slices) {
80426
- if (!isRecord9(item) || typeof item.id !== "string" || typeof item.branch !== "string")
81276
+ if (!isRecord10(item) || typeof item.id !== "string" || typeof item.branch !== "string")
80427
81277
  return null;
80428
81278
  slices.push({
80429
81279
  id: item.id,
@@ -80546,7 +81396,7 @@ function planFanout(cwd, input, now = new Date) {
80546
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");
80547
81397
  if (input.takeLead && isDelegateSession(session))
80548
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");
80549
- 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, {
80550
81400
  trunk: input.trunk ?? defaultTrunk(cwd),
80551
81401
  root: mainCheckout(cwd),
80552
81402
  worktreeRoot: worktreeRoot(cwd)
@@ -80669,7 +81519,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
80669
81519
  error,
80670
81520
  ...unblock ? { unblock } : {},
80671
81521
  ...data ? { data } : {}
80672
- }), 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) => {
80673
81523
  if (typeof value === "string")
80674
81524
  return [value];
80675
81525
  if (Array.isArray(value) && value.every((item) => typeof item === "string"))
@@ -80721,7 +81571,7 @@ var init_fanout = __esm(() => {
80721
81571
  "CHANGELOG.md"
80722
81572
  ];
80723
81573
  compiled = new Map;
80724
- PLACEHOLDER3 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
81574
+ PLACEHOLDER4 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
80725
81575
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
80726
81576
  });
80727
81577
 
@@ -81278,9 +82128,9 @@ function hasOwnCommits2(cwd, branch, start, head) {
81278
82128
  const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
81279
82129
  if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
81280
82130
  continue;
81281
- if (sha !== head && !isAncestor3(cwd, sha, head))
82131
+ if (sha !== head && !isAncestor4(cwd, sha, head))
81282
82132
  continue;
81283
- if (start && (sha === start || isAncestor3(cwd, sha, start)))
82133
+ if (start && (sha === start || isAncestor4(cwd, sha, start)))
81284
82134
  continue;
81285
82135
  return true;
81286
82136
  }
@@ -81290,7 +82140,7 @@ function reverted(cwd, trunk, landedHead, start) {
81290
82140
  const fork = mergeBase(cwd, landedHead, trunk);
81291
82141
  if (fork && fork !== landedHead)
81292
82142
  return unchangedSince(cwd, fork, landedHead, trunk);
81293
- 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();
81294
82144
  return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
81295
82145
  }
81296
82146
  function unchangedSince(cwd, before, after, trunk) {
@@ -81398,9 +82248,9 @@ function detectLanding(ctx, slice, input) {
81398
82248
  }
81399
82249
  return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
81400
82250
  }
81401
- if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
82251
+ if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
81402
82252
  return landed("pr_merged", merged ?? head);
81403
- 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)`;
81404
82254
  return out;
81405
82255
  }
81406
82256
  }
@@ -81417,7 +82267,7 @@ function detectLanding(ctx, slice, input) {
81417
82267
  if (trunkIds.size === 0)
81418
82268
  return out;
81419
82269
  const froms = [fork];
81420
- 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))
81421
82271
  froms.push(input.parentHead);
81422
82272
  for (const from of froms) {
81423
82273
  const id = patchId(ctx.cwd, from, head);
@@ -81433,7 +82283,7 @@ function sliceStart(cwd, rows, fanout, slice) {
81433
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));
81434
82284
  return row?.baseSha ?? null;
81435
82285
  }
81436
- 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;
81437
82287
  var init_fanout_landed = __esm(() => {
81438
82288
  init_rev();
81439
82289
  init_fanout();
@@ -81490,7 +82340,7 @@ function checkFanout(cwd, plan, options = {}) {
81490
82340
  const b = tips.get(second);
81491
82341
  if (landings.get(first)?.landed || landings.get(second)?.landed)
81492
82342
  continue;
81493
- if (!a || !b || isAncestor4(cwd, a, b) || isAncestor4(cwd, b, a))
82343
+ if (!a || !b || isAncestor5(cwd, a, b) || isAncestor5(cwd, b, a))
81494
82344
  continue;
81495
82345
  const check = checks.get(second);
81496
82346
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -81670,7 +82520,7 @@ function unblockFor(check, plan) {
81670
82520
  return `unblock ${check.waitsFor.join(", ")} first`;
81671
82521
  return check.reasons[0] ?? "workit fanout check";
81672
82522
  }
81673
- 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) => ({
81674
82524
  id: slice.id,
81675
82525
  branch: slice.branch,
81676
82526
  head,
@@ -81887,7 +82737,7 @@ var init_fanout_status = __esm(() => {
81887
82737
  // packages/workit-cli/src/verbs/fanout.ts
81888
82738
  var exports_fanout = {};
81889
82739
  __export(exports_fanout, {
81890
- run: () => run16
82740
+ run: () => run17
81891
82741
  });
81892
82742
  import fs27 from "node:fs";
81893
82743
  import path50 from "node:path";
@@ -82083,7 +82933,7 @@ function worktree(argv, io) {
82083
82933
  return failed(io, result);
82084
82934
  return emit(io, ok(result.data), (data) => [
82085
82935
  `${data.created ? "created" : "kept"} worktree for slice ${data.slice} (${data.mode}): ${data.path}`,
82086
- ` branch ${data.branch} <- ${data.base} at ${short5(data.head)}`,
82936
+ ` branch ${data.branch} <- ${data.base} at ${short6(data.head)}`,
82087
82937
  ` scratch ${data.scratch} (put it in the worker brief: temp files go here)`,
82088
82938
  ...data.notes.map((note) => `note: ${note}`)
82089
82939
  ]);
@@ -82107,7 +82957,7 @@ function worktree(argv, io) {
82107
82957
  ...data.notes.map((note) => `note: ${note}`)
82108
82958
  ]);
82109
82959
  }
82110
- async function run16(argv, io) {
82960
+ async function run17(argv, io) {
82111
82961
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
82112
82962
  if (sub === "plan")
82113
82963
  return plan2(rest, io);
@@ -82119,9 +82969,9 @@ async function run16(argv, io) {
82119
82969
  return status3(rest, io);
82120
82970
  if (sub === "worktree")
82121
82971
  return worktree(rest, io);
82122
- 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);
82123
82973
  }
82124
- 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) => [
82125
82975
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
82126
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 ? `
82127
82977
  ${slice.reasons.join(`
@@ -82131,7 +82981,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82131
82981
  `landing order: ${data.landingOrder.join(", ")}`,
82132
82982
  `next: ${data.next}`,
82133
82983
  ...data.notes.map((note) => `note: ${note}`)
82134
- ], short5 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
82984
+ ], short6 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
82135
82985
  const counts = new Map;
82136
82986
  for (const slice of data.slices)
82137
82987
  counts.set(slice.state, (counts.get(slice.state) ?? 0) + 1);
@@ -82139,7 +82989,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82139
82989
  `fanout ${data.name} on ${data.trunk}: ${[...counts].map(([state, count]) => `${count} ${state.replace("_", " ")}`).join(", ")} · forge ${data.forge.available ? "on" : "off"}`,
82140
82990
  ...data.slices.map((slice) => {
82141
82991
  const parts = [
82142
- slice.branchExists ? short5(slice.head) : "no branch",
82992
+ slice.branchExists ? short6(slice.head) : "no branch",
82143
82993
  slice.lastCommitAt && slice.idleMinutes !== null ? `idle ${slice.idleMinutes} min` : null,
82144
82994
  slice.pr ? `PR #${slice.pr.number} ${slice.pr.state}` : null,
82145
82995
  slice.ci === "unknown" ? null : `CI ${slice.ci}`,
@@ -82173,7 +83023,7 @@ var init_fanout2 = __esm(() => {
82173
83023
  // packages/workit-cli/src/verbs/ledger.ts
82174
83024
  var exports_ledger = {};
82175
83025
  __export(exports_ledger, {
82176
- run: () => run17
83026
+ run: () => run18
82177
83027
  });
82178
83028
  import { randomBytes as randomBytes5 } from "node:crypto";
82179
83029
  import { parseArgs } from "node:util";
@@ -82197,7 +83047,7 @@ function targetBranch(io, rows, branch, pr) {
82197
83047
  }
82198
83048
  return { ok: true, value: branch ?? null };
82199
83049
  }
82200
- async function run17(argv, io) {
83050
+ async function run18(argv, io) {
82201
83051
  let parsed;
82202
83052
  try {
82203
83053
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -82389,7 +83239,7 @@ async function standing(io, values, args) {
82389
83239
  return usage4(io, `unexpected argument: ${rest[1]}`);
82390
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}`);
82391
83241
  }
82392
- 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) => {
82393
83243
  const actor = actorFromEnv(io.env);
82394
83244
  if (values.session !== undefined && values.as !== undefined)
82395
83245
  return new Error("pass --session or --as, not both");
@@ -82414,7 +83264,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (wor
82414
83264
  return;
82415
83265
  const parsed = Number(value);
82416
83266
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
82417
- }, 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) => {
82418
83268
  const ledger = readLedger(cwd);
82419
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);
82420
83270
  }, verdictLines = (check) => {
@@ -98793,7 +99643,7 @@ async function runMutation(options) {
98793
99643
  if (!runnable.includes(mutant))
98794
99644
  record({ ...mutant, status: "no-tests", durationMs: 0, tests: [] });
98795
99645
  if (runnable.length === 0)
98796
- return finish2(options, 0, results, counts, started);
99646
+ return finish3(options, 0, results, counts, started);
98797
99647
  removeStaleCopies();
98798
99648
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
98799
99649
  const copyCwd = path53.join(copy, cwdRelative);
@@ -98857,7 +99707,7 @@ async function runMutation(options) {
98857
99707
  restore();
98858
99708
  }
98859
99709
  }
98860
- return finish2(options, baseline.durationMs, results, counts, started);
99710
+ return finish3(options, baseline.durationMs, results, counts, started);
98861
99711
  } finally {
98862
99712
  process.off("SIGINT", onSignal);
98863
99713
  process.off("SIGTERM", onSignal);
@@ -98865,7 +99715,7 @@ async function runMutation(options) {
98865
99715
  cleanup();
98866
99716
  }
98867
99717
  }
98868
- function finish2(options, baselineMs, results, counts, started) {
99718
+ function finish3(options, baselineMs, results, counts, started) {
98869
99719
  const decided = counts.killed + counts.timeout + counts.survived;
98870
99720
  return {
98871
99721
  base: options.base,
@@ -98935,7 +99785,7 @@ var init_mutate = __esm(() => {
98935
99785
  // packages/workit-cli/src/verbs/test-audit.ts
98936
99786
  var exports_test_audit = {};
98937
99787
  __export(exports_test_audit, {
98938
- run: () => run18
99788
+ run: () => run19
98939
99789
  });
98940
99790
  import { existsSync as existsSync22 } from "node:fs";
98941
99791
  import path54 from "node:path";
@@ -99036,11 +99886,11 @@ function human(data) {
99036
99886
  }
99037
99887
  return lines;
99038
99888
  }
99039
- async function run18(argv, io) {
99889
+ async function run19(argv, io) {
99040
99890
  const options = parse7(argv);
99041
99891
  if (typeof options === "string")
99042
99892
  return emit(io, fail("invalid_input", `${options}
99043
- usage: ${USAGE10}`));
99893
+ usage: ${USAGE11}`));
99044
99894
  const top = git7(io.cwd, ["rev-parse", "--show-toplevel"]);
99045
99895
  const repo = top.ok ? path54.resolve(top.stdout.trim()) : null;
99046
99896
  const display = (file) => toPosix(path54.relative(io.cwd, file) || file);
@@ -99126,7 +99976,7 @@ usage: ${USAGE10}`));
99126
99976
  }
99127
99977
  return emit(io, ok(data), human);
99128
99978
  }
99129
- 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, "'\\''")}'`;
99130
99980
  var init_test_audit = __esm(() => {
99131
99981
  init_audit();
99132
99982
  init_mutate();
@@ -99147,12 +99997,12 @@ var init_test_audit = __esm(() => {
99147
99997
  // packages/workit-cli/src/verbs/knowledge.ts
99148
99998
  var exports_knowledge = {};
99149
99999
  __export(exports_knowledge, {
99150
- run: () => run19
100000
+ run: () => run20
99151
100001
  });
99152
- async function run19(argv, io) {
100002
+ async function run20(argv, io) {
99153
100003
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
99154
100004
  if (sub !== "lint" || rest.length > 0)
99155
- return emit(io, fail("invalid_input", `usage: ${USAGE11}`));
100005
+ return emit(io, fail("invalid_input", `usage: ${USAGE12}`));
99156
100006
  const report = lintKnowledge(checkRoot(io.cwd));
99157
100007
  if (report.unreadable.length > 0)
99158
100008
  return emit(io, fail("unavailable", `knowledge lint: cannot read ${report.unreadable.join(", ")}`, {
@@ -99169,7 +100019,7 @@ async function run19(argv, io) {
99169
100019
  `);
99170
100020
  return report.findings.length === 0 ? 0 : 1;
99171
100021
  }
99172
- var USAGE11 = "workit knowledge lint [--json]";
100022
+ var USAGE12 = "workit knowledge lint [--json]";
99173
100023
  var init_knowledge2 = __esm(() => {
99174
100024
  init_check_config();
99175
100025
  init_knowledge();
@@ -99283,14 +100133,14 @@ var init_youtrack_writes = __esm(() => {
99283
100133
  // packages/workit-cli/src/verbs/youtrack.ts
99284
100134
  var exports_youtrack = {};
99285
100135
  __export(exports_youtrack, {
99286
- run: () => run20
100136
+ run: () => run21
99287
100137
  });
99288
100138
  import { readFileSync as readFileSync25 } from "node:fs";
99289
100139
  import path55 from "node:path";
99290
- async function run20(argv, io) {
100140
+ async function run21(argv, io) {
99291
100141
  const [sub, ...rest] = argv;
99292
100142
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
99293
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
100143
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
99294
100144
  const flags = parseFlags(rest, {
99295
100145
  markdown: "value",
99296
100146
  file: "value",
@@ -99299,18 +100149,18 @@ async function run20(argv, io) {
99299
100149
  date: "value"
99300
100150
  });
99301
100151
  if (typeof flags === "string")
99302
- return usage3(io, flags, USAGE12);
100152
+ return usage3(io, flags, USAGE13);
99303
100153
  const [issueId, extra] = flags.positionals;
99304
100154
  if (!issueId || !ISSUE_RE2.test(issueId))
99305
- 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);
99306
100156
  if (extra !== undefined)
99307
- return usage3(io, `unexpected argument ${extra}`, USAGE12);
100157
+ return usage3(io, `unexpected argument ${extra}`, USAGE13);
99308
100158
  const minutes = positiveInt(flags.values.minutes, "--minutes");
99309
100159
  if (typeof minutes === "string")
99310
- return usage3(io, minutes, USAGE12);
100160
+ return usage3(io, minutes, USAGE13);
99311
100161
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
99312
100162
  if ("error" in date)
99313
- return usage3(io, date.error, USAGE12);
100163
+ return usage3(io, date.error, USAGE13);
99314
100164
  const dateMs = date.data.dateMs;
99315
100165
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
99316
100166
  const root = io.cwd;
@@ -99328,24 +100178,24 @@ async function run20(argv, io) {
99328
100178
  };
99329
100179
  if (sub === "note") {
99330
100180
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
99331
- return usage3(io, "pass exactly one of --markdown or --file", USAGE12);
100181
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE13);
99332
100182
  let markdown = flags.values.markdown ?? "";
99333
100183
  if (flags.values.file)
99334
100184
  try {
99335
100185
  markdown = readFileSync25(path55.resolve(io.cwd, flags.values.file), "utf8");
99336
100186
  } catch (error) {
99337
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
100187
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE13);
99338
100188
  }
99339
100189
  if (!markdown.trim())
99340
- return usage3(io, "the note is empty", USAGE12);
100190
+ return usage3(io, "the note is empty", USAGE13);
99341
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);
99342
100192
  if (missing)
99343
100193
  return emit(io, fail("unavailable", missing));
99344
100194
  } else {
99345
100195
  if (!minutes)
99346
- return usage3(io, `${sub} needs --minutes <n>`, USAGE12);
100196
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE13);
99347
100197
  if (sub === "meeting" && !flags.values.text)
99348
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE12);
100198
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE13);
99349
100199
  const text = flags.values.text ?? "";
99350
100200
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
99351
100201
  if (missing)
@@ -99374,7 +100224,7 @@ async function run20(argv, io) {
99374
100224
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
99375
100225
  ]);
99376
100226
  }
99377
- 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) => {
99378
100228
  try {
99379
100229
  return { ok: true, data: unwrap2(value) };
99380
100230
  } catch (error) {
@@ -99398,14 +100248,14 @@ var init_youtrack2 = __esm(() => {
99398
100248
  // packages/workit-cli/src/verbs/changelog.ts
99399
100249
  var exports_changelog = {};
99400
100250
  __export(exports_changelog, {
99401
- run: () => run21
100251
+ run: () => run22
99402
100252
  });
99403
100253
  import { readFileSync as readFileSync26 } from "node:fs";
99404
100254
  import path56 from "node:path";
99405
- async function run21(argv, io) {
100255
+ async function run22(argv, io) {
99406
100256
  const [sub, ...rest] = argv;
99407
100257
  if (sub !== "apply")
99408
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
100258
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE14);
99409
100259
  const flags = parseFlags(rest, {
99410
100260
  entries: "value",
99411
100261
  path: "value",
@@ -99413,18 +100263,18 @@ async function run21(argv, io) {
99413
100263
  preview: "boolean"
99414
100264
  });
99415
100265
  if (typeof flags === "string")
99416
- return usage3(io, flags, USAGE13);
100266
+ return usage3(io, flags, USAGE14);
99417
100267
  if (flags.positionals.length)
99418
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE13);
100268
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE14);
99419
100269
  const normalizeOnly = flags.booleans.has("normalize-only");
99420
100270
  if (!normalizeOnly && !flags.values.entries)
99421
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE13);
100271
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE14);
99422
100272
  let entries;
99423
100273
  if (flags.values.entries)
99424
100274
  try {
99425
100275
  entries = readEntries(flags.values.entries, io.cwd);
99426
100276
  } catch (error) {
99427
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE13);
100277
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE14);
99428
100278
  }
99429
100279
  const input = {
99430
100280
  entries,
@@ -99446,7 +100296,7 @@ async function run21(argv, io) {
99446
100296
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
99447
100297
  ]);
99448
100298
  }
99449
- 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) => {
99450
100300
  const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path56.resolve(cwd, raw.slice(1)), "utf8") : raw;
99451
100301
  return JSON.parse(text);
99452
100302
  };
@@ -99459,12 +100309,12 @@ var init_changelog2 = __esm(() => {
99459
100309
  // packages/workit-cli/src/verbs/handoff.ts
99460
100310
  var exports_handoff = {};
99461
100311
  __export(exports_handoff, {
99462
- run: () => run22
100312
+ run: () => run23
99463
100313
  });
99464
100314
  import { parseArgs as parseArgs2 } from "node:util";
99465
100315
  function human2(brief) {
99466
100316
  const lines = [
99467
- `branch: ${brief.branch ?? "(detached)"} @ ${short6(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
100317
+ `branch: ${brief.branch ?? "(detached)"} @ ${short7(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
99468
100318
  ];
99469
100319
  if (brief.base)
99470
100320
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -99507,11 +100357,11 @@ async function resumeBrief(argv, io) {
99507
100357
  strict: true
99508
100358
  }));
99509
100359
  } catch (error) {
99510
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE14 }));
100360
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE15 }));
99511
100361
  }
99512
100362
  const last = values.last === undefined ? 10 : Number(values.last);
99513
100363
  if (!Number.isInteger(last) || last < 1)
99514
- 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 }));
99515
100365
  const built = buildHandoff(io.cwd, {
99516
100366
  last,
99517
100367
  note: values.note ?? null,
@@ -99529,13 +100379,13 @@ async function resumeBrief(argv, io) {
99529
100379
  }
99530
100380
  return emit(io, ok({ ...data, recorded }), human2);
99531
100381
  }
99532
- async function run22(argv, io) {
100382
+ async function run23(argv, io) {
99533
100383
  if (!argv.some((arg) => arg === "--task" || arg.startsWith("--task=")))
99534
100384
  return resumeBrief(argv, io);
99535
100385
  await Promise.resolve().then(() => init_task());
99536
100386
  return runTaskCommand(["handoff", ...argv]);
99537
100387
  }
99538
- 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";
99539
100389
  var init_handoff = __esm(() => {
99540
100390
  init_ledger();
99541
100391
  init_output();
@@ -99914,6 +100764,35 @@ var VERBS = [
99914
100764
  ],
99915
100765
  load: () => Promise.resolve().then(() => (init_git2(), exports_git))
99916
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
+ }),
99917
100796
  withSubcommands({
99918
100797
  name: "verify-delivery",
99919
100798
  group: "delivery",