@brainervirus/workit-cli 8.10.0 → 8.11.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 +423 -37
  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.10.0",
68
+ version: "8.11.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.10.0",
107
- "@brainervirus/workit-cursor": "^8.10.0",
108
- "@brainervirus/workit-opencode": "^8.10.0",
106
+ "@brainervirus/workit-core": "^8.11.0",
107
+ "@brainervirus/workit-cursor": "^8.11.0",
108
+ "@brainervirus/workit-opencode": "^8.11.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -13404,7 +13404,7 @@ var PREFIXES, branchNames = (workspaceRoot) => {
13404
13404
  return new Set;
13405
13405
  return new Set((r.stdout ?? "").split(`
13406
13406
  `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
13407
- }, detectBranchPolicy = (workspaceRoot) => {
13407
+ }, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
13408
13408
  const names = branchNames(workspaceRoot);
13409
13409
  const develop = names.has("develop");
13410
13410
  const main = names.has("main");
@@ -16808,10 +16808,35 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16808
16808
  ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
16809
16809
  ])
16810
16810
  };
16811
- }, validateBranchNamePolicy = (name, policy, source) => {
16811
+ }, KIND_SYNONYMS, allowedKinds = (allowed) => [
16812
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
16813
+ ], editDistance = (a, b) => {
16814
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
16815
+ for (let i = 1;i <= a.length; i++) {
16816
+ let previous = row[0];
16817
+ row[0] = i;
16818
+ for (let j = 1;j <= b.length; j++) {
16819
+ const saved = row[j];
16820
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
16821
+ previous = saved;
16822
+ }
16823
+ }
16824
+ return row[b.length];
16825
+ }, suggestBranchName = (name, kinds) => {
16826
+ if (!kinds.length)
16827
+ return null;
16828
+ const slash = name.indexOf("/");
16829
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
16830
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
16831
+ const synonym = KIND_SYNONYMS[prefix];
16832
+ const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
16833
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
16834
+ return `${kind}/${rest}`;
16835
+ }, kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", "), validateBranchNamePolicy = (name, policy, source) => {
16812
16836
  const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
16837
+ const kinds = allowedKinds(policy.allowed);
16813
16838
  if (policy.protected.has(name.toLowerCase())) {
16814
- const correction = `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
16839
+ const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
16815
16840
  return {
16816
16841
  ok: false,
16817
16842
  rule: "protected_ref",
@@ -16822,7 +16847,8 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16822
16847
  };
16823
16848
  }
16824
16849
  if (!policy.allowed.some((pattern) => pattern.test(name))) {
16825
- const correction = `choose a branch matching one of: ${allowed.join(", ")}`;
16850
+ const suggestion = suggestBranchName(name, kinds);
16851
+ const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
16826
16852
  return {
16827
16853
  ok: false,
16828
16854
  rule: "allowed_pattern",
@@ -16898,7 +16924,45 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16898
16924
  error: `policy_unavailable: cannot validate commit subject ${JSON.stringify(attempted)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
16899
16925
  };
16900
16926
  }
16901
- }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy, resolveCommitPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "commit", profileName).commitPolicy, policy = (root) => resolveBranchPolicyFor(root), isProtected = (root, name) => policy(root).protected.has(name.toLowerCase()), isProtectedTarget = (root, name) => {
16927
+ }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy, resolveCommitPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "commit", profileName).commitPolicy, BRANCH_KINDS, branchKindOf = (name) => {
16928
+ const prefix = name?.split("/", 1)[0]?.toLowerCase() ?? "";
16929
+ return name?.includes("/") && BRANCH_KINDS.includes(prefix) ? prefix : null;
16930
+ }, gitFlowOf = (root, integration) => {
16931
+ try {
16932
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
16933
+ return null;
16934
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
16935
+ const names = branchNames(root);
16936
+ const production = productionBranchOf(names);
16937
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
16938
+ return { develop, production, developExists: names.has(develop) };
16939
+ } catch {
16940
+ return null;
16941
+ }
16942
+ }, flowRoute = (flow, kind) => kind === "hotfix" ? { kind, base: flow.production, target: flow.production, mergeBack: flow.develop } : kind === "release" ? { kind, base: flow.develop, target: flow.production, mergeBack: flow.develop } : { kind, base: flow.develop, target: flow.develop, mergeBack: null }, releaseBranchesOf = (cwd) => {
16943
+ try {
16944
+ const resolved = resolveBranchPolicyFor(cwd);
16945
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
16946
+ return {
16947
+ preset: resolved.preset,
16948
+ branches: flow ? [flow.production, flow.develop] : [],
16949
+ production: flow ? flow.production : resolved.defaultTargetBranch
16950
+ };
16951
+ } catch {
16952
+ return { preset: "unknown", branches: [], production: null };
16953
+ }
16954
+ }, releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase()), shortBranch = (ref) => ref.replace(/^refs\/heads\//u, "").replace(/^(?:refs\/remotes\/)?origin\//u, ""), flowMismatch = (flow, kind, ref, role) => {
16955
+ if (!flow || !kind || !flow.developExists)
16956
+ return null;
16957
+ const route = flowRoute(flow, kind);
16958
+ const expected = role === "base" ? route.base : route.target;
16959
+ const given = shortBranch(ref).toLowerCase();
16960
+ const longLived = [flow.develop, flow.production].map((name) => name.toLowerCase());
16961
+ const backMerge = role === "target" && route.mergeBack?.toLowerCase() === given;
16962
+ if (!longLived.includes(given) || given === expected.toLowerCase() || backMerge)
16963
+ return null;
16964
+ return role === "base" ? `gitflow_base: git-flow starts ${kind}/* from ${expected}, not ${shortBranch(ref)}` : `gitflow_target: git-flow lands ${kind}/* on ${expected}, not ${shortBranch(ref)}`;
16965
+ }, policy = (root) => resolveBranchPolicyFor(root), isProtected = (root, name) => policy(root).protected.has(name.toLowerCase()), isProtectedTarget = (root, name) => {
16902
16966
  try {
16903
16967
  return isProtected(root, name);
16904
16968
  } catch {
@@ -16942,11 +17006,116 @@ var init_branch = __esm(() => {
16942
17006
  init_workspaces();
16943
17007
  init_vcs_config();
16944
17008
  init_release_tracks();
17009
+ KIND_SYNONYMS = {
17010
+ fix: "bugfix",
17011
+ fixes: "bugfix",
17012
+ bug: "bugfix",
17013
+ bugs: "bugfix",
17014
+ bugfixes: "bugfix",
17015
+ feat: "feature",
17016
+ features: "feature",
17017
+ chore: "feature",
17018
+ docs: "feature",
17019
+ doc: "feature",
17020
+ refactor: "feature",
17021
+ test: "feature",
17022
+ tests: "feature",
17023
+ ci: "feature",
17024
+ build: "feature",
17025
+ perf: "feature",
17026
+ style: "feature",
17027
+ deps: "feature",
17028
+ task: "feature",
17029
+ enhancement: "feature",
17030
+ hot: "hotfix",
17031
+ hotfixes: "hotfix",
17032
+ releases: "release",
17033
+ rel: "release"
17034
+ };
17035
+ BRANCH_KINDS = ["feature", "bugfix", "release", "hotfix"];
17036
+ });
17037
+
17038
+ // packages/workit-core/src/hooks/session-marker.ts
17039
+ var init_session_marker = __esm(() => {
17040
+ init_paths();
17041
+ });
17042
+ // packages/workit-core/src/hooks/raw-git.ts
17043
+ var SHELLS, PREFIXES2, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, LAUNCHERS, PUSH_VALUE_OPTIONS, VALUE_OPTIONS;
17044
+ var init_raw_git = __esm(() => {
17045
+ init_branch();
17046
+ init_workspaces();
17047
+ init_rev();
17048
+ init_ledger();
17049
+ init_paths();
17050
+ init_session_marker();
17051
+ SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
17052
+ PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
17053
+ CD = new Set(["cd", "chdir", "set-location", "sl"]);
17054
+ PUSHD = new Set(["pushd", "push-location"]);
17055
+ POPD = new Set(["popd", "pop-location"]);
17056
+ GIT_VALUE_OPTIONS = new Set([
17057
+ "-c",
17058
+ "--git-dir",
17059
+ "--work-tree",
17060
+ "--namespace",
17061
+ "--exec-path",
17062
+ "--config-env",
17063
+ "--super-prefix"
17064
+ ]);
17065
+ LAUNCHERS = new Set([
17066
+ "npx",
17067
+ "bunx",
17068
+ "pnpx",
17069
+ "-y",
17070
+ "--yes",
17071
+ "pnpm",
17072
+ "dlx",
17073
+ "exec",
17074
+ "env",
17075
+ "sudo",
17076
+ "command"
17077
+ ]);
17078
+ PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
17079
+ VALUE_OPTIONS = {
17080
+ git: new Set([
17081
+ "-m",
17082
+ "--message",
17083
+ "-F",
17084
+ "--file",
17085
+ "-C",
17086
+ "-c",
17087
+ "--reuse-message",
17088
+ "--reedit-message",
17089
+ "--author",
17090
+ "--date",
17091
+ "--trailer",
17092
+ "-t",
17093
+ "--template",
17094
+ "--cleanup",
17095
+ "--fixup",
17096
+ "--squash",
17097
+ "-o",
17098
+ "--push-option",
17099
+ "--repo",
17100
+ "--receive-pack",
17101
+ "--exec"
17102
+ ]),
17103
+ 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"])),
17104
+ 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"]))
17105
+ };
16945
17106
  });
16946
17107
 
16947
17108
  // packages/workit-core/src/core/route-intent.ts
17109
+ var SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, WORKTREE_VALUES;
16948
17110
  var init_route_intent = __esm(() => {
17111
+ init_raw_git();
16949
17112
  init_branch();
17113
+ SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
17114
+ CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
17115
+ WORKTREE_CREATE = new Set(["-b", "-B"]);
17116
+ 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"]));
17117
+ BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
17118
+ WORKTREE_VALUES = new Set(["--reason"]);
16950
17119
  });
16951
17120
 
16952
17121
  // packages/workit-core/src/core/runtime-identity.ts
@@ -72715,7 +72884,7 @@ async function runGrant(argv, io, deps = defaultDeps) {
72715
72884
  return usage2(io, `unknown subcommand "${sub}"`);
72716
72885
  }
72717
72886
  var USAGE2 = "workit grant show [<workspace>] [--all] | grant set <workspace> <kind>=<true|false|verified>… [defaultEndpoint=commit|pr|green|merged] [verification=self|independent] | grant unset <workspace> <kind>… (kinds: push, pr, merge, release, rerun, defaultEndpoint, verification)", AGENT_ENV, agentMarker = (env) => AGENT_ENV.find((name) => env[name]) ?? Object.keys(env).find((name) => name.startsWith("CODEX_") && env[name]), defaultDeps, parseValue = (raw) => raw === "true" || raw === "yes" || raw === "on" ? true : raw === "false" || raw === "no" || raw === "off" ? false : raw === "verified" ? "verified" : null, STOPS_AT, endpointLabel = (view) => view.effectiveEndpoint === view.defaultEndpoint ? view.defaultEndpoint : `${view.defaultEndpoint} (effective: ${view.effectiveEndpoint}${view.endpointReason ? `, ${view.endpointReason}` : ""})`, describe2 = (grants, configured, endpoint, verification) => [
72718
- ...GRANT_KINDS.map((kind) => ` ${kind.padEnd(8)}${String(grants[kind]).padEnd(10)}${configured.includes(kind) ? "configured" : "default"}${kind === "release" ? " (no consumer yet: reserved, not enforced)" : ""}`),
72887
+ ...GRANT_KINDS.map((kind) => ` ${kind.padEnd(8)}${String(grants[kind]).padEnd(10)}${configured.includes(kind) ? "configured" : "default"}${kind === "release" ? " (workit git push --release: git-flow main and develop, landed v* tags)" : ""}`),
72719
72888
  ` default endpoint: ${endpointLabel(endpoint)} (an unnamed request stops at ${STOPS_AT[endpoint.effectiveEndpoint]})`,
72720
72889
  ...endpoint.endpointIssue ? [` note: ${endpoint.endpointIssue}`] : [],
72721
72890
  ` verification: ${verification} (${verification === "independent" ? "a normal-risk behavior change needs a non-author verdict" : "a normal-risk behavior change needs a passing check and the author's --self verdict, shown as self-reviewed"})`
@@ -73161,6 +73330,12 @@ function gitBranch(cwd, input) {
73161
73330
  return failure3("blocked", policy.error, policy.correction);
73162
73331
  if (resolveRef(cwd, `refs/heads/${name}`))
73163
73332
  return failure3("failed", `branch_exists: ${name} already exists`, `git switch ${name}`);
73333
+ const kind = input.kind ?? branchKindOf(name);
73334
+ const flow = resolved.ok !== false && !releaseTrack?.track ? gitFlowOf(cwd, String(resolved.defaultTargetBranch ?? "") || null) : null;
73335
+ const route = flow && kind ? flowRoute(flow, kind) : null;
73336
+ const mismatch = explicitBase ? flowMismatch(flow, kind, explicitBase, "base") : null;
73337
+ if (mismatch && route)
73338
+ return failure3("blocked", mismatch, `workit git branch ${input.kind ? `--kind ${kind} --slug ${input.slug}` : name} # without --base it starts from ${route.base}`);
73164
73339
  let base = explicitBase;
73165
73340
  let defaultTarget = null;
73166
73341
  let defaultBase = null;
@@ -73170,12 +73345,14 @@ function gitBranch(cwd, input) {
73170
73345
  defaultTarget = String(resolved.defaultTargetBranch ?? "") || null;
73171
73346
  defaultBase = String(resolved.baseBranch ?? "") || defaultTarget;
73172
73347
  }
73173
- base ??= defaultBase;
73348
+ base ??= route?.base ?? defaultBase;
73174
73349
  if (!base)
73175
73350
  return failure3("invalid_input", "no default target branch is configured; pass --base <branch>");
73176
73351
  if (base.startsWith("-"))
73177
73352
  return failure3("invalid_input", "--base must not start with -");
73178
73353
  const notes = [...releaseTrack?.warnings ?? []];
73354
+ if (route?.mergeBack)
73355
+ notes.push(`git-flow: ${route.kind}/* lands on ${route.target} (workit pr create targets it), then ${route.target} merges back into ${route.mergeBack}`);
73179
73356
  const remotes = remoteNames(cwd);
73180
73357
  const remote = remotes.includes("origin") ? "origin" : pushRemoteName(cwd);
73181
73358
  const localTip = resolveRef(cwd, `refs/heads/${base}`);
@@ -73218,7 +73395,8 @@ function gitBranch(cwd, input) {
73218
73395
  baseSha,
73219
73396
  carried: dirt !== "clean",
73220
73397
  notes,
73221
- releaseTrack
73398
+ releaseTrack,
73399
+ flow: route
73222
73400
  });
73223
73401
  }
73224
73402
  function lintCommitMessage(cwd, message) {
@@ -73439,7 +73617,7 @@ function pushPreflight(cwd, options = {}) {
73439
73617
  if (!branch)
73440
73618
  return failure3("invalid_input", "HEAD is detached; push a branch");
73441
73619
  if (isProtectedTarget(cwd, branch))
73442
- return failure3("blocked", `protected_branch: ${branch} is protected by the workspace branch policy; workit never pushes to it`, "workit git branch <feature/name> # push a feature branch and open a PR");
73620
+ return failure3("blocked", `protected_branch: ${branch} is protected by the workspace branch policy; workit git push never pushes to it`, releaseBranch(cwd, branch) ? `workit git push --release ${branch} # finishing a release or hotfix; needs the release grant` : "workit git branch <feature/name> # push a feature branch and open a PR");
73443
73621
  const sha = resolveRef(cwd, `refs/heads/${branch}`);
73444
73622
  if (!sha)
73445
73623
  return failure3("not_found", `branch ${branch} has no commit to push`);
@@ -73606,6 +73784,125 @@ ${gitOutput(run)}`);
73606
73784
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
73607
73785
  });
73608
73786
  }
73787
+ function releasePushPreflight(cwd, names) {
73788
+ const progress = operationInProgress(cwd);
73789
+ if (progress)
73790
+ return inProgressFailure(progress.operation, "push");
73791
+ const wanted = [...new Set(names)];
73792
+ if (!wanted.length)
73793
+ return failure3("invalid_input", "name the release refs to push");
73794
+ const { preset, branches, production } = releaseBranchesOf(cwd);
73795
+ const refs = [];
73796
+ for (const raw of wanted) {
73797
+ if (raw.startsWith("-") || raw.includes(":"))
73798
+ return failure3("invalid_input", `${raw} is not a ref name (refspecs are not accepted)`);
73799
+ const tagName = raw.replace(/^refs\/tags\//u, "");
73800
+ const branchName = raw.replace(/^refs\/heads\//u, "");
73801
+ const tag = raw.startsWith("refs/heads/") ? null : resolveRef(cwd, `refs/tags/${tagName}`);
73802
+ const branch = raw.startsWith("refs/tags/") ? null : resolveRef(cwd, `refs/heads/${branchName}`);
73803
+ if (tag && branch)
73804
+ return failure3("invalid_input", `${raw} names both a branch and a tag`, `workit git push --release refs/heads/${raw} # or refs/tags/${raw}`);
73805
+ if (tag) {
73806
+ if (!RELEASE_TAG.test(tagName))
73807
+ return failure3("blocked", `release_push_refused: tag ${tagName} is not a release tag (v<version>)`, "git tag -a v<version> -m <message> # then: workit git push --release v<version>");
73808
+ refs.push({ kind: "tag", name: tagName, ref: `refs/tags/${tagName}`, sha: tag });
73809
+ continue;
73810
+ }
73811
+ if (!branch)
73812
+ return failure3("not_found", `${raw} is not a local branch or tag`);
73813
+ if (!branches.some((name) => name.toLowerCase() === branchName.toLowerCase()))
73814
+ 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>`);
73815
+ refs.push({ kind: "branch", name: branchName, ref: `refs/heads/${branchName}`, sha: branch });
73816
+ }
73817
+ const remote = pushRemoteName(cwd, refs.find((ref) => ref.kind === "branch")?.name ?? null);
73818
+ if (!remote)
73819
+ return failure3("not_found", "no push remote is configured for this repository", "git remote add origin <url>");
73820
+ const raw = pushUrl(cwd, remote);
73821
+ if (!raw)
73822
+ return failure3("blocked", `remote "${remote}" has no single push URL`, `git remote get-url --push --all ${remote} # keep exactly one push URL`);
73823
+ const local = parseRemoteUrl(raw)?.protocol === "file";
73824
+ if (!local && !pushTargetIsStable(cwd, raw))
73825
+ 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");
73826
+ return success2({ refs, remote, url: redactRemote(raw), rawUrl: raw, local, production });
73827
+ }
73828
+ function executeReleasePush(cwd, plan, input) {
73829
+ const previous = new Map;
73830
+ for (const ref of plan.refs) {
73831
+ const before = remoteRefTip(cwd, plan.rawUrl, ref.ref);
73832
+ if (!before.ok)
73833
+ return failure3(before.code, before.error);
73834
+ previous.set(ref.ref, before.sha);
73835
+ if (before.sha === null || before.sha === ref.sha)
73836
+ continue;
73837
+ if (ref.kind === "tag")
73838
+ return failure3("blocked", `tag_exists: ${plan.remote} already has ${ref.name} at ${before.sha.slice(0, 12)}, not ${ref.sha.slice(0, 12)}; a release tag never moves`, `git tag -d ${ref.name} # tag the next version instead`);
73839
+ if (!git5(cwd, ["merge-base", "--is-ancestor", before.sha, ref.sha]).ok)
73840
+ 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}`);
73841
+ }
73842
+ const tags = plan.refs.filter((ref) => ref.kind === "tag");
73843
+ if (tags.length) {
73844
+ let productionTip = null;
73845
+ if (plan.production) {
73846
+ const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.production}`);
73847
+ if (!tip.ok)
73848
+ return failure3(tip.code, tip.error);
73849
+ productionTip = tip.sha;
73850
+ }
73851
+ const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
73852
+ for (const tag of tags) {
73853
+ const released = productionTip !== null && contains(productionTip, tag.sha) || plan.refs.some((ref) => ref.kind === "branch" && contains(ref.sha, tag.sha));
73854
+ if (!released)
73855
+ 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`);
73856
+ }
73857
+ }
73858
+ const pending = plan.refs.filter((ref) => previous.get(ref.ref) !== ref.sha);
73859
+ if (pending.length) {
73860
+ const run = gitNetwork(cwd, [
73861
+ "push",
73862
+ "--porcelain",
73863
+ "--atomic",
73864
+ "--no-follow-tags",
73865
+ "--recurse-submodules=no",
73866
+ plan.remote,
73867
+ ...pending.map((ref) => ref.kind === "tag" ? `${ref.ref}:${ref.ref}` : `${ref.sha}:${ref.ref}`)
73868
+ ], input.timeoutMs ?? PUSH_TIMEOUT_MS);
73869
+ if (!run.ok)
73870
+ return run.timedOut ? failure3("unavailable", `git push to ${plan.url} timed out`) : failure3("failed", `git push to ${plan.url} failed:
73871
+ ${gitOutput(run)}`);
73872
+ }
73873
+ for (const ref of plan.refs) {
73874
+ const after = remoteRefTip(cwd, plan.rawUrl, ref.ref);
73875
+ if (!after.ok)
73876
+ return failure3(after.code, `push sent but not verified: ${after.error}`);
73877
+ if (after.sha !== ref.sha)
73878
+ return failure3("failed", `push_unverified: ${plan.remote} ${ref.ref} is at ${after.sha?.slice(0, 12) ?? "(absent)"}, not ${ref.sha.slice(0, 12)}`);
73879
+ }
73880
+ const refs = plan.refs.map((ref) => ({
73881
+ ...ref,
73882
+ previous: previous.get(ref.ref) ?? null,
73883
+ pushed: pending.includes(ref)
73884
+ }));
73885
+ const row = appendObserved(cwd, {
73886
+ type: "release.pushed",
73887
+ actor: input.actor,
73888
+ remote: plan.remote,
73889
+ url: plan.url,
73890
+ refs: refs.map(({ ref, sha, previous: before, pushed }) => ({
73891
+ ref,
73892
+ sha,
73893
+ previous: before,
73894
+ pushed
73895
+ })),
73896
+ grant: input.grant,
73897
+ ...input.identity ? { identity: input.identity } : {}
73898
+ });
73899
+ return success2({
73900
+ remote: plan.remote,
73901
+ url: plan.url,
73902
+ refs,
73903
+ recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
73904
+ });
73905
+ }
73609
73906
  function deleteRemoteBranch(cwd, target, branch, expectedTip, options = {}) {
73610
73907
  if (target.startsWith("-") || branch.startsWith("-") || !SHA.test(expectedTip))
73611
73908
  return { ok: false, lease: false, error: "invalid branch deletion target" };
@@ -73670,7 +73967,7 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
73670
73967
  ...lines.slice(-half)
73671
73968
  ].join(`
73672
73969
  `);
73673
- }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, 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;
73970
+ }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, 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, RELEASE_TAG;
73674
73971
  var init_ops = __esm(() => {
73675
73972
  init_branch();
73676
73973
  init_commit_flavors();
@@ -73685,6 +73982,7 @@ var init_ops = __esm(() => {
73685
73982
  TRAILER_CONFIG = ["-c", "trailer.separators=:", "-c", "trailer.where=end"];
73686
73983
  SHA = /^[0-9a-f]{40,64}$/u;
73687
73984
  SHA_PREFIX = /^[0-9a-f]{7,39}$/u;
73985
+ RELEASE_TAG = /^v\d/u;
73688
73986
  });
73689
73987
 
73690
73988
  // packages/workit-core/src/forge/reruns.ts
@@ -74224,7 +74522,7 @@ function boundSource(cwd, resolved) {
74224
74522
  return failure3("blocked", remote.sha === null ? `not_pushed: ${branch} is not on ${resolved.remote}` : `not_pushed: ${resolved.remote}/${branch} is at ${remote.sha.slice(0, 12)}, local ${branch} is at ${sha.slice(0, 12)}`, "workit git push");
74225
74523
  return success2({ branch, sha });
74226
74524
  }
74227
- async function verifiedHead(resolved, branch, sha, first, sleep) {
74525
+ async function verifiedHead(resolved, branch, sha, first, sleep, base) {
74228
74526
  let ref = first;
74229
74527
  for (const delay of [0, ...VERIFY_DELAYS_MS]) {
74230
74528
  if (ref.headSha === sha)
@@ -74234,7 +74532,8 @@ async function verifiedHead(resolved, branch, sha, first, sleep) {
74234
74532
  const found = resolved.forge.findPr(branch, {
74235
74533
  owner: resolved.headRepo.split("/")[0] ?? null,
74236
74534
  projectId: resolved.headProjectId,
74237
- sha
74535
+ sha,
74536
+ base
74238
74537
  });
74239
74538
  if (!found.ok)
74240
74539
  return found;
@@ -74259,7 +74558,8 @@ async function createPullRequest(cwd, resolved, input, sleep) {
74259
74558
  const existing = resolved.forge.findPr(branch, {
74260
74559
  owner: resolved.headRepo.split("/")[0] ?? null,
74261
74560
  projectId: resolved.headProjectId,
74262
- sha
74561
+ sha,
74562
+ base: input.base
74263
74563
  });
74264
74564
  if (!existing.ok)
74265
74565
  return existing;
@@ -74282,7 +74582,7 @@ async function createPullRequest(cwd, resolved, input, sleep) {
74282
74582
  ref = made.data;
74283
74583
  created = true;
74284
74584
  }
74285
- const verified = await verifiedHead(resolved, branch, sha, ref, sleep);
74585
+ const verified = await verifiedHead(resolved, branch, sha, ref, sleep, input.base);
74286
74586
  if (!verified.ok)
74287
74587
  return verified;
74288
74588
  const row = appendObserved(cwd, {
@@ -74449,7 +74749,7 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
74449
74749
  const gone = !deleted.ok && remoteRefTip(cwd, resolved.remote, `refs/heads/${branch}`);
74450
74750
  deletedBranch = deleted.ok || gone && gone.ok && gone.sha === null ? true : { error: deleted.error };
74451
74751
  }
74452
- const mergeBack = mergeBackFor(cwd, doc.base);
74752
+ const mergeBack = mergeBackFor(cwd, doc.base, branch);
74453
74753
  const row = appendObserved(cwd, {
74454
74754
  type: "pr.merged",
74455
74755
  actor: input.actor,
@@ -74488,9 +74788,16 @@ var VERIFY_DELAYS_MS, noun = (resolved) => resolved.forge.kind === "github" ? "p
74488
74788
  return `a non-author verifies the head (workit-review); the endpoint is ${endpoint}, so ${AFTER_REVIEW[endpoint]}`;
74489
74789
  }, SESSION_TRAILER, indent = (text) => text.split(`
74490
74790
  `).map((line) => line ? ` ${line}` : line).join(`
74491
- `), mergeBackFor = (cwd, base) => {
74791
+ `), mergeBackFor = (cwd, base, head) => {
74492
74792
  try {
74493
- const owners = workspaceReleaseTracks(cwd).tracks.filter((track) => track.productionBranch === base);
74793
+ const read = workspaceReleaseTracks(cwd);
74794
+ if (!read.tracks.length) {
74795
+ const flow = gitFlowOf(cwd);
74796
+ const kind = branchKindOf(head);
74797
+ const route = flow && kind ? flowRoute(flow, kind) : null;
74798
+ return route?.mergeBack && route.target === base ? { track: "git-flow", branches: [route.mergeBack] } : { track: null, branches: [] };
74799
+ }
74800
+ const owners = read.tracks.filter((track) => track.productionBranch === base);
74494
74801
  if (owners.length !== 1)
74495
74802
  return { track: null, branches: [] };
74496
74803
  return {
@@ -76238,8 +76545,11 @@ function createGitHubForge(options) {
76238
76545
  if (!data.ok)
76239
76546
  return data;
76240
76547
  const nodes = data.data.repository?.pullRequests.nodes ?? [];
76241
- const ours = nodes.filter((node) => node.headRefName === head && (match.owner !== null && node.headRepositoryOwner?.login.toLowerCase() === match.owner.toLowerCase() || match.sha !== null && node.headRefOid === match.sha));
76242
- const pick = ours.find((node) => node.state === "OPEN") ?? ours[0];
76548
+ const ours = nodes.filter((node) => node.headRefName === head && (!match.base || node.baseRefName === undefined || node.baseRefName === match.base) && (match.owner !== null && node.headRepositoryOwner?.login.toLowerCase() === match.owner.toLowerCase() || match.sha !== null && node.headRefOid === match.sha));
76549
+ const open = ours.filter((node) => node.state === "OPEN");
76550
+ if (!match.base && new Set(open.map((node) => node.baseRefName)).size > 1)
76551
+ return failure3("blocked", `ambiguous_pr: ${head} has open pull requests ${open.map((node) => `#${node.number} -> ${node.baseRefName}`).join(", ")}`, `pass --pr <n>, e.g. --pr ${open[0].number}`);
76552
+ const pick = open[0] ?? ours[0];
76243
76553
  if (!pick)
76244
76554
  return success2(null);
76245
76555
  const ref = {
@@ -76459,7 +76769,7 @@ function createGitHubForge(options) {
76459
76769
  }
76460
76770
  };
76461
76771
  }
76462
- var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state headRefName headRefOid headRepositoryOwner { login } } } } }`, READY_MUTATION = `mutation($id: ID!) { markPullRequestReadyForReview(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, DRAFT_MUTATION = `mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, REPLY_MUTATION = `mutation($thread: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body }) { comment { url } } }`, RESOLVE_MUTATION = `mutation($thread: ID!) { resolveReviewThread(input: { threadId: $thread }) { thread { isResolved } } }`, THREAD_QUERY = `query($thread: ID!) { node(id: $thread) { ... on PullRequestReviewThread { isResolved pullRequest { number } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING2, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
76772
+ var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state baseRefName headRefName headRefOid headRepositoryOwner { login } } } } }`, READY_MUTATION = `mutation($id: ID!) { markPullRequestReadyForReview(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, DRAFT_MUTATION = `mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, REPLY_MUTATION = `mutation($thread: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body }) { comment { url } } }`, RESOLVE_MUTATION = `mutation($thread: ID!) { resolveReviewThread(input: { threadId: $thread }) { thread { isResolved } } }`, THREAD_QUERY = `query($thread: ID!) { node(id: $thread) { ... on PullRequestReviewThread { isResolved pullRequest { number } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING2, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
76463
76773
  var init_github = __esm(() => {
76464
76774
  init_exec();
76465
76775
  init_redact();
@@ -76576,8 +76886,11 @@ function createGitLabForge(options) {
76576
76886
  return list;
76577
76887
  if (!Array.isArray(list.data))
76578
76888
  return failure3("failed", "merge request list is not a list");
76579
- const matches = list.data.filter((mr) => mr.source_branch === head && (match.projectId !== null && mr.source_project_id === match.projectId || match.sha !== null && mr.sha === match.sha));
76580
- const pick = matches.find((mr) => mr.state === "opened") ?? matches[0];
76889
+ const matches = list.data.filter((mr) => mr.source_branch === head && (!match.base || mr.target_branch === undefined || mr.target_branch === match.base) && (match.projectId !== null && mr.source_project_id === match.projectId || match.sha !== null && mr.sha === match.sha));
76890
+ const open = matches.filter((mr) => mr.state === "opened");
76891
+ if (!match.base && new Set(open.map((mr) => mr.target_branch)).size > 1)
76892
+ return failure3("blocked", `ambiguous_pr: ${head} has open merge requests ${open.map((mr) => `!${mr.iid} -> ${mr.target_branch}`).join(", ")}`, `pass --pr <n>, e.g. --pr ${open[0].iid}`);
76893
+ const pick = open[0] ?? matches[0];
76581
76894
  return success2(pick ? {
76582
76895
  number: pick.iid,
76583
76896
  url: pick.web_url,
@@ -77388,8 +77701,18 @@ async function create4(argv, io) {
77388
77701
  if (flags.values.base !== undefined && flags.values.track !== undefined)
77389
77702
  return usage3(io, "pass --base or --track, not both (--track picks the base for you)", CREATE_USAGE);
77390
77703
  let base = flags.values.base ?? null;
77704
+ const branch = currentBranch(io.cwd);
77705
+ const kind = branchKindOf(branch);
77391
77706
  let releaseTrack = null;
77392
- if (!base) {
77707
+ let route = null;
77708
+ if (base) {
77709
+ const flow = gitFlowOf(io.cwd);
77710
+ const mismatch = flowMismatch(flow, kind, base, "target");
77711
+ if (mismatch && flow && kind)
77712
+ return emit(io, fail("blocked", mismatch, {
77713
+ unblock: `workit pr create … # without --base it targets ${flowRoute(flow, kind).target}`
77714
+ }));
77715
+ } else {
77393
77716
  const resolved = vcsConfig("resolve", io.cwd, { track: flags.values.track ?? null });
77394
77717
  if (resolved.ok === false)
77395
77718
  return emit(io, fail("blocked", String(resolved.error), {
@@ -77401,10 +77724,13 @@ async function create4(argv, io) {
77401
77724
  unblock: "workit pr create --track <name> … # or --base <branch>"
77402
77725
  }));
77403
77726
  base = String(resolved.defaultTargetBranch ?? "") || null;
77727
+ const flow = releaseTrack?.track ? null : gitFlowOf(io.cwd, base);
77728
+ route = flow && kind ? flowRoute(flow, kind) : null;
77729
+ if (route)
77730
+ base = route.target;
77404
77731
  }
77405
77732
  if (!base)
77406
77733
  return usage3(io, "no default target branch is configured; pass --base <b>", CREATE_USAGE);
77407
- const branch = currentBranch(io.cwd);
77408
77734
  const connected = connect(io, branch);
77409
77735
  if (!connected.ok)
77410
77736
  return forgeFail(io, connected);
@@ -77431,11 +77757,18 @@ async function create4(argv, io) {
77431
77757
  }, forgeDeps.sleep);
77432
77758
  if (!result.ok)
77433
77759
  return forgeFail(io, result);
77434
- const created = releaseTrack ? { ...result.data, releaseTrack } : result.data;
77760
+ const created = {
77761
+ ...result.data,
77762
+ ...releaseTrack ? { releaseTrack } : {},
77763
+ ...route ? { flow: route } : {}
77764
+ };
77435
77765
  return emit(io, ok(created), (data) => [
77436
77766
  `${data.created ? "opened" : "found open"} ${connected.data.forge.kind === "github" ? "PR #" : "MR !"}${data.number}${data.draft && data.created ? " (draft)" : ""}: ${data.branch} -> ${data.base} ${data.url}`,
77437
77767
  ...data.releaseTrack?.name ? [`release track: ${data.releaseTrack.name} (${data.releaseTrack.detail})`] : [],
77438
77768
  ...(data.releaseTrack?.warnings ?? []).map((warning) => `note: ${warning}`),
77769
+ ...route?.mergeBack ? [
77770
+ `merge back: git-flow: once ${data.branch} lands on ${route.target}, bring ${route.target} into ${route.mergeBack} (workit pr create --base ${route.mergeBack} opens the back-merge PR from ${data.branch}, or merge locally and run workit git push --release ${route.mergeBack}); tag the release and push it with workit git push --release <tag>`
77771
+ ] : [],
77439
77772
  `head ${data.head.slice(0, 12)} verified on the forge`,
77440
77773
  ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : [],
77441
77774
  `next: ${data.next}`
@@ -77641,6 +77974,7 @@ var init_pr = __esm(() => {
77641
77974
  init_pr_ops();
77642
77975
  init_report();
77643
77976
  init_rev();
77977
+ init_branch();
77644
77978
  init_ledger();
77645
77979
  init_vcs_config();
77646
77980
  init_output();
@@ -77872,6 +78206,17 @@ __export(exports_git, {
77872
78206
  });
77873
78207
  import { readFileSync as readFileSync22 } from "node:fs";
77874
78208
  import path46 from "node:path";
78209
+ function kindHint(cwd, kind) {
78210
+ let kinds = [...BRANCH_KINDS];
78211
+ try {
78212
+ kinds = allowedKinds(branchPolicySnapshotFor(cwd).namePolicy.allowed);
78213
+ } catch {}
78214
+ const spellable = kinds.filter((name) => BRANCH_KINDS.includes(name));
78215
+ if (spellable.length)
78216
+ return `did you mean --kind ${suggestBranchName(`${kind}/x`, spellable)?.split("/")[0]}?`;
78217
+ const named = suggestBranchName(`${kind}/<slug>`, kinds);
78218
+ return named ? `this workspace allows ${kinds.map((name) => `${name}/`).join(", ")}: pass a name, e.g. workit git branch ${named}` : `this workspace allows any non-protected name: pass one, e.g. workit git branch ${kind}/<slug>`;
78219
+ }
77875
78220
  async function branch(argv, io) {
77876
78221
  const flags = parseFlags(argv, {
77877
78222
  base: "value",
@@ -77887,8 +78232,8 @@ async function branch(argv, io) {
77887
78232
  return usage3(io, `unexpected argument ${flags.positionals[1]}`, BRANCH_USAGE);
77888
78233
  if (kind === undefined !== (slug === undefined))
77889
78234
  return usage3(io, "--kind and --slug go together", BRANCH_USAGE);
77890
- if (kind !== undefined && !["feature", "bugfix", "hotfix"].includes(kind))
77891
- return usage3(io, "--kind must be feature, bugfix or hotfix", BRANCH_USAGE);
78235
+ if (kind !== undefined && !BRANCH_KINDS.includes(kind))
78236
+ return usage3(io, `--kind must be ${BRANCH_KINDS.join(", ")}; ${kindHint(io.cwd, kind)}`, BRANCH_USAGE);
77892
78237
  if (kind !== undefined && flags.positionals.length)
77893
78238
  return usage3(io, "pass a name or --kind/--slug, not both", BRANCH_USAGE);
77894
78239
  if (kind === undefined && !flags.positionals[0])
@@ -78016,10 +78361,17 @@ async function push(argv, io) {
78016
78361
  "set-upstream": "boolean",
78017
78362
  "force-with-lease": "boolean",
78018
78363
  "overwrite-unintegrated": "boolean",
78019
- expect: "value"
78364
+ expect: "value",
78365
+ release: "list"
78020
78366
  });
78021
78367
  if (typeof flags === "string")
78022
78368
  return usage3(io, flags, PUSH_USAGE);
78369
+ const release = flags.lists.release;
78370
+ if (release) {
78371
+ if (flags.booleans.size || flags.values.expect !== undefined || flags.positionals.length)
78372
+ return usage3(io, "--release takes only ref names (repeat it: --release main --release v1.2.0)", PUSH_USAGE);
78373
+ return releasePush(release, io);
78374
+ }
78023
78375
  if (flags.positionals.length)
78024
78376
  return usage3(io, `unexpected argument ${flags.positionals[0]} (workit pushes the current branch)`, PUSH_USAGE);
78025
78377
  const expect = flags.values.expect ?? null;
@@ -78065,6 +78417,39 @@ async function push(argv, io) {
78065
78417
  ...data.upstream ? [`upstream: ${data.remote}/${data.branch}`] : []
78066
78418
  ]);
78067
78419
  }
78420
+ async function releasePush(names, io) {
78421
+ const plan = releasePushPreflight(io.cwd, names);
78422
+ if (!plan.ok)
78423
+ return forgeFail(io, plan);
78424
+ const release = requireGrant(io.cwd, "release");
78425
+ if (!release.allowed)
78426
+ return emit(io, fail("blocked", release.error, {
78427
+ unblock: release.unblock,
78428
+ data: { reason: release.reason }
78429
+ }));
78430
+ const firstBranch = plan.data.refs.find((ref) => ref.kind === "branch")?.name;
78431
+ const identity = plan.data.local ? null : pushIdentity(io, firstBranch ?? plan.data.refs[0].name);
78432
+ if (identity && !identity.ok)
78433
+ return forgeFail(io, identity);
78434
+ const checked = identity?.data ?? null;
78435
+ const pushGrant = requireGrant(io.cwd, "push", { forge: checked?.forge != null });
78436
+ if (!pushGrant.allowed)
78437
+ return emit(io, fail("blocked", pushGrant.error, {
78438
+ unblock: pushGrant.unblock,
78439
+ data: { reason: pushGrant.reason }
78440
+ }));
78441
+ const result = executeReleasePush(io.cwd, plan.data, {
78442
+ actor: actorFromEnv(io.env),
78443
+ grant: release.source,
78444
+ ...checked ? { identity: { ...checked } } : {}
78445
+ });
78446
+ if (!result.ok)
78447
+ return forgeFail(io, result);
78448
+ return emit(io, ok({ ...result.data, identity: checked }), (data) => [
78449
+ ...data.refs.map((ref) => ref.pushed ? `pushed ${ref.name} ${short2(ref.previous)} -> ${short2(ref.sha)} to ${data.remote}; remote tip verified` : `${data.remote} ${ref.name} is already at ${short2(ref.sha)}; remote tip verified`),
78450
+ ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : []
78451
+ ]);
78452
+ }
78068
78453
  async function run13(argv, io) {
78069
78454
  const [sub, ...rest] = argv;
78070
78455
  if (sub === "branch")
@@ -78075,9 +78460,10 @@ async function run13(argv, io) {
78075
78460
  return push(rest, io);
78076
78461
  return usage3(io, sub && !sub.startsWith("-") ? `unknown git subcommand "${sub}"` : "missing git subcommand", USAGE5);
78077
78462
  }
78078
- var BRANCH_USAGE = "workit git branch <name> | --kind feature|bugfix|hotfix --slug <s> [--base <b>] [--track <t>] [--carry] [--json]", COMMIT_USAGE = "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>] [--json]", PUSH_USAGE = "workit git push [--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--json]", USAGE5 = "workit git branch|commit|push ... (workit help git)", short2 = (sha) => sha ? sha.slice(0, 12) : "(none)", COMMIT_SWITCHES;
78463
+ var BRANCH_USAGE = "workit git branch <name> | --kind feature|bugfix|release|hotfix --slug <s> [--base <b>] [--track <t>] [--carry] [--json]", COMMIT_USAGE = "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>] [--json]", PUSH_USAGE = "workit git push [--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…] [--json]", USAGE5 = "workit git branch|commit|push ... (workit help git)", short2 = (sha) => sha ? sha.slice(0, 12) : "(none)", COMMIT_SWITCHES;
78079
78464
  var init_git2 = __esm(() => {
78080
78465
  init_autonomy();
78466
+ init_branch();
78081
78467
  init_ops();
78082
78468
  init_ledger();
78083
78469
  init_implicit_task();
@@ -98571,8 +98957,8 @@ var VERBS = [
98571
98957
  subcommands: [
98572
98958
  {
98573
98959
  name: "branch",
98574
- usage: "workit git branch <name> | git branch --kind feature|bugfix|hotfix --slug <s> [--base <b>] [--track <t>] [--carry]",
98575
- summary: "Create and switch to a branch from the trunk or a release track"
98960
+ usage: "workit git branch <name> | git branch --kind feature|bugfix|release|hotfix --slug <s> [--base <b>] [--track <t>] [--carry]",
98961
+ summary: "Create and switch to a branch from the trunk, the git-flow route of its kind or a release track"
98576
98962
  },
98577
98963
  {
98578
98964
  name: "commit",
@@ -98581,8 +98967,8 @@ var VERBS = [
98581
98967
  },
98582
98968
  {
98583
98969
  name: "push",
98584
- usage: "workit git push [-u|--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]]",
98585
- summary: "Push the current branch and verify the remote tip (never a blind --force)"
98970
+ usage: "workit git push [-u|--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…]",
98971
+ summary: "Push the current branch and verify the remote tip (never a blind --force); --release finishes a git-flow release"
98586
98972
  }
98587
98973
  ],
98588
98974
  examples: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "8.10.0",
3
+ "version": "8.11.0",
4
4
  "private": false,
5
5
  "description": "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
6
6
  "keywords": [
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@babel/parser": "7.29.9",
41
- "@brainervirus/workit-core": "^8.10.0",
42
- "@brainervirus/workit-cursor": "^8.10.0",
43
- "@brainervirus/workit-opencode": "^8.10.0",
41
+ "@brainervirus/workit-core": "^8.11.0",
42
+ "@brainervirus/workit-cursor": "^8.11.0",
43
+ "@brainervirus/workit-opencode": "^8.11.0",
44
44
  "@inkjs/ui": "2.0.0",
45
45
  "ink": "7.1.1",
46
46
  "react": "19.2.8"