@brainervirus/workit-cli 8.10.0 → 8.12.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 +1573 -288
  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.12.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.12.0",
107
+ "@brainervirus/workit-cursor": "^8.12.0",
108
+ "@brainervirus/workit-opencode": "^8.12.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -298,7 +298,7 @@ function loadCheckConfig(cwd) {
298
298
  gates[gate] = names;
299
299
  }
300
300
  }
301
- return { source: CHECKS_FILE, root, checks, gates, error: null };
301
+ return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
302
302
  }
303
303
  function matchesNamedCheck(config, name, argv, cwd) {
304
304
  if (config.error || name === null || cwd !== ".")
@@ -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");
@@ -13690,8 +13690,8 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
13690
13690
  profiles: record(nonBlank, profileSchema).optional()
13691
13691
  }).passthrough();
13692
13692
  return entrySchema;
13693
- }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd) => {
13694
- const file = path12.join(configDir(), "workspaces.json");
13693
+ }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
13694
+ const file = path12.join(dir, "workspaces.json");
13695
13695
  let raw;
13696
13696
  try {
13697
13697
  raw = readFileSync3(file, "utf8");
@@ -13720,10 +13720,27 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
13720
13720
  const matches = matchingEntries(cwd, entries ?? []);
13721
13721
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
13722
13722
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
13723
- return selected ? { file, selected } : null;
13723
+ return selected ? { file, selected, matches } : null;
13724
13724
  }, resolveRuntimeReleaseTracks = (cwd) => {
13725
13725
  const matched = matchRuntimeWorkspace(cwd);
13726
13726
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13727
+ }, workspaceTrailers = (cwd, dir) => {
13728
+ let matched;
13729
+ try {
13730
+ matched = matchRuntimeWorkspace(cwd, dir);
13731
+ } catch {
13732
+ return { enabled: true, workspace: null, from: null };
13733
+ }
13734
+ if (!matched)
13735
+ return { enabled: true, workspace: null, from: null };
13736
+ const { selected, matches } = matched;
13737
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
13738
+ const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
13739
+ return {
13740
+ enabled: source?.trailers !== false,
13741
+ workspace: selected.name,
13742
+ from: source?.name ?? null
13743
+ };
13727
13744
  }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13728
13745
  const matched = matchRuntimeWorkspace(cwd);
13729
13746
  if (!matched)
@@ -13874,6 +13891,7 @@ var init_workspaces = __esm(() => {
13874
13891
  autoApprove: union([boolean2(), array(nonBlank)]).optional(),
13875
13892
  defaultEndpoint: unknown().optional(),
13876
13893
  verification: _enum(["self", "independent"]).optional(),
13894
+ trailers: boolean2().optional(),
13877
13895
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13878
13896
  kind,
13879
13897
  union([boolean2(), literal("verified")]).optional()
@@ -16808,10 +16826,35 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16808
16826
  ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
16809
16827
  ])
16810
16828
  };
16811
- }, validateBranchNamePolicy = (name, policy, source) => {
16829
+ }, KIND_SYNONYMS, allowedKinds = (allowed) => [
16830
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
16831
+ ], editDistance = (a, b) => {
16832
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
16833
+ for (let i = 1;i <= a.length; i++) {
16834
+ let previous = row[0];
16835
+ row[0] = i;
16836
+ for (let j = 1;j <= b.length; j++) {
16837
+ const saved = row[j];
16838
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
16839
+ previous = saved;
16840
+ }
16841
+ }
16842
+ return row[b.length];
16843
+ }, suggestBranchName = (name, kinds) => {
16844
+ if (!kinds.length)
16845
+ return null;
16846
+ const slash = name.indexOf("/");
16847
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
16848
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
16849
+ const synonym = KIND_SYNONYMS[prefix];
16850
+ 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;
16851
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
16852
+ return `${kind}/${rest}`;
16853
+ }, kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", "), validateBranchNamePolicy = (name, policy, source) => {
16812
16854
  const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
16855
+ const kinds = allowedKinds(policy.allowed);
16813
16856
  if (policy.protected.has(name.toLowerCase())) {
16814
- const correction = `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
16857
+ 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
16858
  return {
16816
16859
  ok: false,
16817
16860
  rule: "protected_ref",
@@ -16822,7 +16865,8 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16822
16865
  };
16823
16866
  }
16824
16867
  if (!policy.allowed.some((pattern) => pattern.test(name))) {
16825
- const correction = `choose a branch matching one of: ${allowed.join(", ")}`;
16868
+ const suggestion = suggestBranchName(name, kinds);
16869
+ 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
16870
  return {
16827
16871
  ok: false,
16828
16872
  rule: "allowed_pattern",
@@ -16898,7 +16942,45 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16898
16942
  error: `policy_unavailable: cannot validate commit subject ${JSON.stringify(attempted)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
16899
16943
  };
16900
16944
  }
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) => {
16945
+ }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy, resolveCommitPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "commit", profileName).commitPolicy, BRANCH_KINDS, branchKindOf = (name) => {
16946
+ const prefix = name?.split("/", 1)[0]?.toLowerCase() ?? "";
16947
+ return name?.includes("/") && BRANCH_KINDS.includes(prefix) ? prefix : null;
16948
+ }, gitFlowOf = (root, integration) => {
16949
+ try {
16950
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
16951
+ return null;
16952
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
16953
+ const names = branchNames(root);
16954
+ const production = productionBranchOf(names);
16955
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
16956
+ return { develop, production, developExists: names.has(develop) };
16957
+ } catch {
16958
+ return null;
16959
+ }
16960
+ }, 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) => {
16961
+ try {
16962
+ const resolved = resolveBranchPolicyFor(cwd);
16963
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
16964
+ return {
16965
+ preset: resolved.preset,
16966
+ branches: flow ? [flow.production, flow.develop] : [],
16967
+ production: flow ? flow.production : resolved.defaultTargetBranch
16968
+ };
16969
+ } catch {
16970
+ return { preset: "unknown", branches: [], production: null };
16971
+ }
16972
+ }, 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) => {
16973
+ if (!flow || !kind || !flow.developExists)
16974
+ return null;
16975
+ const route = flowRoute(flow, kind);
16976
+ const expected = role === "base" ? route.base : route.target;
16977
+ const given = shortBranch(ref).toLowerCase();
16978
+ const longLived = [flow.develop, flow.production].map((name) => name.toLowerCase());
16979
+ const backMerge = role === "target" && route.mergeBack?.toLowerCase() === given;
16980
+ if (!longLived.includes(given) || given === expected.toLowerCase() || backMerge)
16981
+ return null;
16982
+ 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)}`;
16983
+ }, policy = (root) => resolveBranchPolicyFor(root), isProtected = (root, name) => policy(root).protected.has(name.toLowerCase()), isProtectedTarget = (root, name) => {
16902
16984
  try {
16903
16985
  return isProtected(root, name);
16904
16986
  } catch {
@@ -16942,11 +17024,116 @@ var init_branch = __esm(() => {
16942
17024
  init_workspaces();
16943
17025
  init_vcs_config();
16944
17026
  init_release_tracks();
17027
+ KIND_SYNONYMS = {
17028
+ fix: "bugfix",
17029
+ fixes: "bugfix",
17030
+ bug: "bugfix",
17031
+ bugs: "bugfix",
17032
+ bugfixes: "bugfix",
17033
+ feat: "feature",
17034
+ features: "feature",
17035
+ chore: "feature",
17036
+ docs: "feature",
17037
+ doc: "feature",
17038
+ refactor: "feature",
17039
+ test: "feature",
17040
+ tests: "feature",
17041
+ ci: "feature",
17042
+ build: "feature",
17043
+ perf: "feature",
17044
+ style: "feature",
17045
+ deps: "feature",
17046
+ task: "feature",
17047
+ enhancement: "feature",
17048
+ hot: "hotfix",
17049
+ hotfixes: "hotfix",
17050
+ releases: "release",
17051
+ rel: "release"
17052
+ };
17053
+ BRANCH_KINDS = ["feature", "bugfix", "release", "hotfix"];
17054
+ });
17055
+
17056
+ // packages/workit-core/src/hooks/session-marker.ts
17057
+ var init_session_marker = __esm(() => {
17058
+ init_paths();
17059
+ });
17060
+ // packages/workit-core/src/hooks/raw-git.ts
17061
+ var SHELLS, PREFIXES2, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, LAUNCHERS, PUSH_VALUE_OPTIONS, VALUE_OPTIONS;
17062
+ var init_raw_git = __esm(() => {
17063
+ init_branch();
17064
+ init_workspaces();
17065
+ init_rev();
17066
+ init_ledger();
17067
+ init_paths();
17068
+ init_session_marker();
17069
+ SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
17070
+ PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
17071
+ CD = new Set(["cd", "chdir", "set-location", "sl"]);
17072
+ PUSHD = new Set(["pushd", "push-location"]);
17073
+ POPD = new Set(["popd", "pop-location"]);
17074
+ GIT_VALUE_OPTIONS = new Set([
17075
+ "-c",
17076
+ "--git-dir",
17077
+ "--work-tree",
17078
+ "--namespace",
17079
+ "--exec-path",
17080
+ "--config-env",
17081
+ "--super-prefix"
17082
+ ]);
17083
+ LAUNCHERS = new Set([
17084
+ "npx",
17085
+ "bunx",
17086
+ "pnpx",
17087
+ "-y",
17088
+ "--yes",
17089
+ "pnpm",
17090
+ "dlx",
17091
+ "exec",
17092
+ "env",
17093
+ "sudo",
17094
+ "command"
17095
+ ]);
17096
+ PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
17097
+ VALUE_OPTIONS = {
17098
+ git: new Set([
17099
+ "-m",
17100
+ "--message",
17101
+ "-F",
17102
+ "--file",
17103
+ "-C",
17104
+ "-c",
17105
+ "--reuse-message",
17106
+ "--reedit-message",
17107
+ "--author",
17108
+ "--date",
17109
+ "--trailer",
17110
+ "-t",
17111
+ "--template",
17112
+ "--cleanup",
17113
+ "--fixup",
17114
+ "--squash",
17115
+ "-o",
17116
+ "--push-option",
17117
+ "--repo",
17118
+ "--receive-pack",
17119
+ "--exec"
17120
+ ]),
17121
+ 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"])),
17122
+ 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"]))
17123
+ };
16945
17124
  });
16946
17125
 
16947
17126
  // packages/workit-core/src/core/route-intent.ts
17127
+ var SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, WORKTREE_VALUES;
16948
17128
  var init_route_intent = __esm(() => {
17129
+ init_raw_git();
16949
17130
  init_branch();
17131
+ SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
17132
+ CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
17133
+ WORKTREE_CREATE = new Set(["-b", "-B"]);
17134
+ 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"]));
17135
+ BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
17136
+ WORKTREE_VALUES = new Set(["--reason"]);
16950
17137
  });
16951
17138
 
16952
17139
  // packages/workit-core/src/core/runtime-identity.ts
@@ -64278,6 +64465,26 @@ function installSessionHook(cwd, env = process.env) {
64278
64465
  detail: `${verb} the Workit ${SESSION_HOOK_NAME} hook at ${status.hookPath}${helper}`
64279
64466
  };
64280
64467
  }
64468
+ function removeSessionHook(cwd, env = process.env) {
64469
+ const status = inspectSessionHook(cwd, env);
64470
+ if (status.state === "not_git")
64471
+ return { action: "skipped", removed: [], status, detail: "not inside a git work tree" };
64472
+ const removed = [];
64473
+ if (workitHookIn(status)) {
64474
+ rmSync2(status.hookPath, { force: true });
64475
+ removed.push(status.hookPath);
64476
+ }
64477
+ if (helperIn(status)) {
64478
+ rmSync2(status.helperPath, { force: true });
64479
+ removed.push(status.helperPath);
64480
+ }
64481
+ return {
64482
+ action: removed.length ? "removed" : "unchanged",
64483
+ removed,
64484
+ status: inspectSessionHook(cwd, env),
64485
+ detail: removed.length ? `removed ${removed.join(" and ")}` : "no Workit session hook or helper to remove"
64486
+ };
64487
+ }
64281
64488
  var SESSION_HOOK_NAME = "prepare-commit-msg", SESSION_HOOK_MARKER = "# workit-session-trailer-hook", HELPER, LEGACY_CHAINED = "commit-msg.workit-chained", SESSION_TRAILER_SCRIPT = `#!/bin/sh
64282
64489
  # Workit (https://github.com/BrainerVirus/workit) wrote this file with
64283
64490
  # \`workit doctor --fix\`. A prepare-commit-msg hook runs it with its own
@@ -64305,6 +64512,19 @@ fi
64305
64512
  [ -n "$session" ] || exit 0
64306
64513
  [ \${#session} -le 128 ] || exit 0
64307
64514
  case "$session" in *[!A-Za-z0-9_.:@/+-]*) exit 0 ;; esac
64515
+ # A workspace with \`trailers: false\` keeps authorship in the ledger only.
64516
+ # \`workit git commit\` decides it already (WORKIT_TRAILERS); otherwise ask
64517
+ # workit, bounded, and add the trailer when it cannot answer.
64518
+ case "\${WORKIT_TRAILERS-}" in
64519
+ off) exit 0 ;;
64520
+ on) ;;
64521
+ *)
64522
+ if command -v workit >/dev/null 2>&1; then
64523
+ t=; ! command -v timeout >/dev/null 2>&1 || t="timeout 5"
64524
+ [ "$($t workit git trailers 2>/dev/null)" != off ] || exit 0
64525
+ fi
64526
+ ;;
64527
+ esac
64308
64528
  add_trailer() {
64309
64529
  [ -f "$1" ] && [ -r "$1" ] || return 0
64310
64530
  body=$(sed '/^# -\\{24\\} >8 -\\{24\\}$/,$d' "$1" | git stripspace --strip-comments)
@@ -64448,7 +64668,7 @@ exit 0
64448
64668
  } finally {
64449
64669
  rmSync2(staged, { force: true });
64450
64670
  }
64451
- };
64671
+ }, workitHookIn = (status) => status.hookPath !== null && (readText2(status.hookPath)?.includes(SESSION_HOOK_MARKER) ?? false), helperIn = (status) => status.helperPath !== null && exists(status.helperPath);
64452
64672
  var init_session_hook = __esm(() => {
64453
64673
  init_rev();
64454
64674
  HELPER = ["workit", "session-trailer.sh"];
@@ -65883,12 +66103,34 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65883
66103
  };
65884
66104
  }
65885
66105
  }, checkSessionHook = (res) => {
65886
- if (!matchedWorkspace(res))
66106
+ const workspace = matchedWorkspace(res);
66107
+ if (!workspace)
65887
66108
  return {
65888
66109
  id: "session_hook",
65889
66110
  status: "pass",
65890
66111
  detail: `not in a Workit workspace — skipping the ${SESSION_HOOK_NAME} session hook`
65891
66112
  };
66113
+ const trailers = workspaceTrailers(res.cwd, res.configDir);
66114
+ if (!trailers.enabled) {
66115
+ const setting = `workspace ${trailers.from ?? workspace.name} sets trailers: false`;
66116
+ const left = inspectSessionHook(res.cwd, res.env);
66117
+ const files = [
66118
+ ...workitHookIn(left) ? [left.hookPath] : [],
66119
+ ...helperIn(left) ? [left.helperPath] : []
66120
+ ];
66121
+ if (files.length)
66122
+ return {
66123
+ id: "session_hook",
66124
+ status: "warn",
66125
+ detail: `${setting}, but Workit's ${files.join(" and ")} ${files.length > 1 ? "are" : "is"} still installed: plain git commits still get a Workit-Session trailer whenever the helper cannot ask workit (it fails open)`,
66126
+ fix: SESSION_HOOK_FIX
66127
+ };
66128
+ return {
66129
+ id: "session_hook",
66130
+ status: "pass",
66131
+ detail: `${setting} (authorship in the ledger only) — no ${SESSION_HOOK_NAME} session hook needed`
66132
+ };
66133
+ }
65892
66134
  const hook = inspectSessionHook(res.cwd, res.env);
65893
66135
  const finding = sessionHookFinding(hook);
65894
66136
  if (!hook.leftover)
@@ -65908,6 +66150,14 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65908
66150
  detail: `${res.cwd} is not in a Workit workspace; no hook installed`,
65909
66151
  workspace: null
65910
66152
  };
66153
+ if (!workspaceTrailers(res.cwd, res.configDir).enabled) {
66154
+ const removal = removeSessionHook(res.cwd, res.env);
66155
+ return {
66156
+ ...removal,
66157
+ detail: `trailers: false; no hook installed (${removal.detail})`,
66158
+ workspace: workspace.name
66159
+ };
66160
+ }
65911
66161
  return { ...installSessionHook(res.cwd, res.env), workspace: workspace.name };
65912
66162
  }, isPlaceholder = (p) => {
65913
66163
  try {
@@ -72346,7 +72596,10 @@ function runCheckCommand(argv, options) {
72346
72596
  const buffer = new TailBuffer(MAX_LOG_BYTES);
72347
72597
  const redactor = new StreamRedactor((text) => buffer.push(text));
72348
72598
  const env = options.env ?? process.env;
72349
- const plan = planSpawn(argv, { platform, env, cwd: options.cwd, shell: options.shell });
72599
+ const wrapped = Boolean(options.wrapper?.length) && platform !== "win32";
72600
+ const command = wrapped ? [...options.wrapper, ...options.shell ? ["/bin/sh", "-c", argv[0]] : argv] : argv;
72601
+ const shell = wrapped ? false : options.shell;
72602
+ const plan = planSpawn(command, { platform, env, cwd: options.cwd, shell });
72350
72603
  return new Promise((resolve) => {
72351
72604
  let settled = false;
72352
72605
  let timedOut = false;
@@ -72409,6 +72662,8 @@ function runCheckCommand(argv, options) {
72409
72662
  finish(null, null, error.code ?? error.message);
72410
72663
  return;
72411
72664
  }
72665
+ if (child.pid)
72666
+ options.onSpawn?.(child.pid);
72412
72667
  if (!windows)
72413
72668
  for (const name of FORWARDED_SIGNALS) {
72414
72669
  const handler = () => {
@@ -72715,7 +72970,7 @@ async function runGrant(argv, io, deps = defaultDeps) {
72715
72970
  return usage2(io, `unknown subcommand "${sub}"`);
72716
72971
  }
72717
72972
  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)" : ""}`),
72973
+ ...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
72974
  ` default endpoint: ${endpointLabel(endpoint)} (an unnamed request stops at ${STOPS_AT[endpoint.effectiveEndpoint]})`,
72720
72975
  ...endpoint.endpointIssue ? [` note: ${endpoint.endpointIssue}`] : [],
72721
72976
  ` 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"})`
@@ -72746,18 +73001,503 @@ var init_grant = __esm(() => {
72746
73001
  };
72747
73002
  });
72748
73003
 
73004
+ // packages/workit-core/src/core/resources.ts
73005
+ import { randomUUID as randomUUID5 } from "node:crypto";
73006
+ import fs20 from "node:fs";
73007
+ import os14 from "node:os";
73008
+ import path40 from "node:path";
73009
+ function memoryValue(value, host) {
73010
+ const text = typeof value === "number" ? String(value) : typeof value === "string" ? value : "";
73011
+ const percent = PERCENT.exec(text.trim());
73012
+ if (percent) {
73013
+ const share = Number(percent[1]);
73014
+ if (share <= 0 || share > 100)
73015
+ return new Error(`memoryMax ${text} must be 1-100%`);
73016
+ return Math.floor(host.totalMemBytes * share / 100);
73017
+ }
73018
+ const size = SIZE.exec(text.trim());
73019
+ if (size && Number(size[1]) > 0)
73020
+ return Math.floor(Number(size[1]) * UNITS[size[2].toUpperCase()]);
73021
+ return new Error(`memoryMax must be a percent of RAM ("40%") or a size ("8G"), got ${text}`);
73022
+ }
73023
+ function cpuValue(value, host) {
73024
+ if (typeof value === "number" && value > 0)
73025
+ return Math.round(value * 100);
73026
+ const percent = typeof value === "string" ? PERCENT.exec(value.trim()) : null;
73027
+ if (percent) {
73028
+ const share = Number(percent[1]);
73029
+ if (share <= 0 || share > 100)
73030
+ return new Error(`cpuQuota ${percent[0]} must be 1-100%`);
73031
+ return Math.max(1, Math.round(host.cpus * share));
73032
+ }
73033
+ return new Error(`cpuQuota must be a percent of the machine ("70%") or a CPU count, got ${String(value)}`);
73034
+ }
73035
+ function resolveResourceSettings(sources, host = hostShape()) {
73036
+ let memory = Math.floor(host.totalMemBytes * DEFAULT_MEMORY_SHARE);
73037
+ let cpu = Math.max(1, Math.round(host.cpus * DEFAULT_CPU_SHARE * 100));
73038
+ const settings = {
73039
+ slots: DEFAULT_SLOTS,
73040
+ heavy: [...DEFAULT_HEAVY_CHECKS],
73041
+ memoryMax: "",
73042
+ cpuQuota: "",
73043
+ waitMs: DEFAULT_WAIT_S * 1000,
73044
+ scope: true
73045
+ };
73046
+ const warnings = [];
73047
+ const ordered = [
73048
+ ...sources.filter((source) => source.trust === "machine"),
73049
+ ...sources.filter((source) => source.trust === "repo")
73050
+ ];
73051
+ for (const { label, trust, value } of ordered) {
73052
+ if (value === undefined || value === null)
73053
+ continue;
73054
+ if (!isRecord7(value)) {
73055
+ warnings.push(`${label}: resources must be an object`);
73056
+ continue;
73057
+ }
73058
+ const repo = trust === "repo";
73059
+ const warn = (field, message) => warnings.push(`${label}: ${field}: ${message}`);
73060
+ const machineOnly = (field) => warn(field, "is machine-wide; set it in your workspaces.json, not in the repo");
73061
+ if (value.slots !== undefined) {
73062
+ const slots = value.slots;
73063
+ if (repo)
73064
+ machineOnly("slots");
73065
+ else if (typeof slots === "number" && Number.isInteger(slots) && slots >= 1 && slots <= MAX_SLOTS)
73066
+ settings.slots = slots;
73067
+ else
73068
+ warn("slots", `must be an integer 1-${MAX_SLOTS}`);
73069
+ }
73070
+ if (value.scope !== undefined) {
73071
+ if (repo)
73072
+ machineOnly("scope");
73073
+ else if (typeof value.scope === "boolean")
73074
+ settings.scope = value.scope;
73075
+ else
73076
+ warn("scope", "must be true or false");
73077
+ }
73078
+ if (value.heavy !== undefined) {
73079
+ const heavy = isRecord7(value.heavy) ? Object.keys(value.heavy) : value.heavy;
73080
+ if (!Array.isArray(heavy) || !heavy.every((item) => typeof item === "string"))
73081
+ warn("heavy", "must be a list of check names");
73082
+ else
73083
+ settings.heavy = repo ? [...new Set([...settings.heavy, ...heavy])] : heavy;
73084
+ }
73085
+ if (value.memoryMax !== undefined) {
73086
+ const next = memoryValue(value.memoryMax, host);
73087
+ if (next instanceof Error)
73088
+ warn("memoryMax", next.message);
73089
+ else if (repo && next > memory)
73090
+ warn("memoryMax", "a repo can only lower it");
73091
+ else
73092
+ memory = next;
73093
+ }
73094
+ if (value.cpuQuota !== undefined) {
73095
+ const next = cpuValue(value.cpuQuota, host);
73096
+ if (next instanceof Error)
73097
+ warn("cpuQuota", next.message);
73098
+ else if (repo && next > cpu)
73099
+ warn("cpuQuota", "a repo can only lower it");
73100
+ else
73101
+ cpu = next;
73102
+ }
73103
+ if (value.wait !== undefined) {
73104
+ if (typeof value.wait === "number" && value.wait >= 0)
73105
+ settings.waitMs = value.wait * 1000;
73106
+ else
73107
+ warn("wait", "must be a number of seconds");
73108
+ }
73109
+ }
73110
+ settings.memoryMax = String(memory);
73111
+ settings.cpuQuota = `${cpu}%`;
73112
+ return { settings, warnings };
73113
+ }
73114
+ function stateDir(env, platform = process.platform) {
73115
+ if (env.WORKFLOW_TOOLKIT_STATE)
73116
+ return env.WORKFLOW_TOOLKIT_STATE;
73117
+ if (env.XDG_STATE_HOME)
73118
+ return path40.join(env.XDG_STATE_HOME, "workit");
73119
+ const home = env.HOME || os14.homedir();
73120
+ if (platform === "darwin")
73121
+ return path40.join(home, "Library", "Application Support", "workit");
73122
+ if (platform === "win32")
73123
+ return path40.join(env.LOCALAPPDATA ?? path40.join(home, "AppData", "Local"), "workit");
73124
+ return path40.join(home, ".local", "state", "workit");
73125
+ }
73126
+ function scopeBusy(unit) {
73127
+ if (process.platform !== "linux" || !SCOPE_UNIT.test(unit))
73128
+ return false;
73129
+ const uid = process.getuid?.() ?? 0;
73130
+ const manager = `/sys/fs/cgroup/user.slice/user-${uid}.slice/user@${uid}.service`;
73131
+ return [`${manager}/app.slice/${unit}`, `${manager}/${unit}`].some((dir) => (readText3(`${dir}/cgroup.procs`) ?? "").trim() !== "");
73132
+ }
73133
+ function isAncestor(pid) {
73134
+ if (process.platform !== "linux")
73135
+ return true;
73136
+ let current = process.pid;
73137
+ for (let step = 0;step < 128 && current > 1; step += 1) {
73138
+ if (current === pid)
73139
+ return true;
73140
+ const ppid = Number(statFields(current)?.[1]);
73141
+ if (!Number.isInteger(ppid) || ppid <= 0)
73142
+ return false;
73143
+ current = ppid;
73144
+ }
73145
+ return current === pid;
73146
+ }
73147
+ function staleReason(lease, probe = {}) {
73148
+ const boot = probe.boot === undefined ? bootId() : probe.boot;
73149
+ if (lease.bootId && boot) {
73150
+ if (lease.bootId !== boot)
73151
+ return "taken before a reboot";
73152
+ } else if (typeof lease.bootTime === "number") {
73153
+ const now = probe.bootTime === undefined ? bootTime() : probe.bootTime;
73154
+ if (now !== null && Math.abs(now - lease.bootTime) > BOOT_SLACK_MS)
73155
+ return "taken before a reboot";
73156
+ }
73157
+ const start = lease.processStart ? (probe.start ?? processStart)(lease.pid) : null;
73158
+ const age = (probe.now ?? Date.now()) - Date.parse(lease.startedAt);
73159
+ if (!start && typeof lease.maxAgeMs === "number" && age > lease.maxAgeMs)
73160
+ return `older than its ${Math.round(lease.maxAgeMs / 1000)} s bound`;
73161
+ const holder = !(probe.alive ?? pidAlive2)(lease.pid) ? `holder pid ${lease.pid} is gone` : start && start !== lease.processStart ? `pid ${lease.pid} now belongs to another process` : null;
73162
+ if (!holder)
73163
+ return null;
73164
+ if (typeof lease.pgid === "number" && lease.pgid > 0 && (probe.group ?? groupAlive)(lease.pgid))
73165
+ return null;
73166
+ if (lease.scope && (probe.scope ?? scopeBusy)(lease.scope))
73167
+ return null;
73168
+ return holder;
73169
+ }
73170
+ function reclaim(file, stale) {
73171
+ const guard = `${file}.reclaim`;
73172
+ try {
73173
+ fs20.mkdirSync(guard);
73174
+ } catch {
73175
+ try {
73176
+ if (Date.now() - fs20.statSync(guard).mtimeMs > GUARD_TTL_MS)
73177
+ fs20.rmSync(guard, { force: true, recursive: true });
73178
+ } catch {}
73179
+ return false;
73180
+ }
73181
+ try {
73182
+ let ageMs = 0;
73183
+ try {
73184
+ ageMs = Date.now() - fs20.statSync(file).mtimeMs;
73185
+ } catch {
73186
+ return true;
73187
+ }
73188
+ if (!stale(parseLease(readText3(file)), ageMs))
73189
+ return false;
73190
+ fs20.rmSync(file, { force: true });
73191
+ return true;
73192
+ } finally {
73193
+ fs20.rmSync(guard, { force: true, recursive: true });
73194
+ }
73195
+ }
73196
+ function tryOnce(options, record, reclaimed) {
73197
+ const holders = [];
73198
+ const text = `${JSON.stringify(record)}
73199
+ `;
73200
+ for (let slot = 0;slot < options.slots; slot += 1) {
73201
+ const file = path40.join(options.dir, `slot-${slot}.json`);
73202
+ for (let attempt = 0;attempt < 2; attempt += 1) {
73203
+ try {
73204
+ const fd = fs20.openSync(file, "wx", 384);
73205
+ try {
73206
+ fs20.writeSync(fd, text);
73207
+ } finally {
73208
+ fs20.closeSync(fd);
73209
+ }
73210
+ return { slot, file };
73211
+ } catch (error) {
73212
+ if (error.code !== "EEXIST")
73213
+ throw error;
73214
+ }
73215
+ const lease = parseLease(readText3(file));
73216
+ let reason = null;
73217
+ const removed = reclaim(file, (current, ageMs) => {
73218
+ reason = current ? staleReason(current, options.probe) : ageMs > UNREADABLE_TTL_MS ? "unreadable" : null;
73219
+ return reason !== null;
73220
+ });
73221
+ if (removed) {
73222
+ if (lease && reason)
73223
+ reclaimed.push({ ...holderOf(lease), reason });
73224
+ continue;
73225
+ }
73226
+ if (lease)
73227
+ holders.push(holderOf(lease));
73228
+ break;
73229
+ }
73230
+ }
73231
+ return { holders };
73232
+ }
73233
+ function removeOwn(file, id) {
73234
+ if (parseLease(readText3(file))?.id === id)
73235
+ fs20.rmSync(file, { force: true });
73236
+ }
73237
+ async function acquireLease(options) {
73238
+ const pid = options.pid ?? process.pid;
73239
+ const boot = bootId();
73240
+ const record = {
73241
+ id: randomUUID5(),
73242
+ pid,
73243
+ processStart: processStart(pid),
73244
+ bootId: boot,
73245
+ bootTime: boot ? null : bootTime(),
73246
+ pgid: null,
73247
+ maxAgeMs: options.maxAgeMs ?? null,
73248
+ check: options.check,
73249
+ cwd: options.cwd,
73250
+ session: options.session,
73251
+ startedAt: new Date().toISOString(),
73252
+ scope: null
73253
+ };
73254
+ try {
73255
+ fs20.mkdirSync(options.dir, { recursive: true, mode: 448 });
73256
+ } catch (error) {
73257
+ return { ok: false, reason: "unavailable", error: error.message };
73258
+ }
73259
+ const started = Date.now();
73260
+ const reclaimed = [];
73261
+ let waitedOn = [];
73262
+ for (;; ) {
73263
+ let outcome;
73264
+ try {
73265
+ outcome = tryOnce(options, record, reclaimed);
73266
+ } catch (error) {
73267
+ return { ok: false, reason: "unavailable", error: error.message };
73268
+ }
73269
+ if ("slot" in outcome) {
73270
+ const { file, slot } = outcome;
73271
+ let released = false;
73272
+ const release = () => {
73273
+ if (released)
73274
+ return;
73275
+ released = true;
73276
+ process.off("exit", release);
73277
+ removeOwn(file, record.id);
73278
+ };
73279
+ process.on("exit", release);
73280
+ return {
73281
+ ok: true,
73282
+ lease: {
73283
+ record,
73284
+ slot,
73285
+ file,
73286
+ waitedOn,
73287
+ waitedMs: waitedOn.length ? Date.now() - started : 0,
73288
+ reclaimed,
73289
+ release
73290
+ }
73291
+ };
73292
+ }
73293
+ const waited = Date.now() - started;
73294
+ if (!waitedOn.length) {
73295
+ waitedOn = outcome.holders;
73296
+ options.onWait?.(outcome.holders);
73297
+ }
73298
+ if (waited >= options.waitMs)
73299
+ return { ok: false, reason: "timeout", holders: outcome.holders, waitedMs: waited };
73300
+ await sleep(Math.min(options.pollMs ?? 2000, Math.max(1, options.waitMs - waited)));
73301
+ }
73302
+ }
73303
+ function updateLease(lease, patch) {
73304
+ Object.assign(lease.record, patch);
73305
+ try {
73306
+ if (parseLease(readText3(lease.file))?.id !== lease.record.id)
73307
+ return;
73308
+ const temp = `${lease.file}.${process.pid}.tmp`;
73309
+ fs20.writeFileSync(temp, `${JSON.stringify(lease.record)}
73310
+ `, { mode: 384 });
73311
+ fs20.renameSync(temp, lease.file);
73312
+ } catch {}
73313
+ }
73314
+ function heldLease(dir, id, options = {}) {
73315
+ if (!id || !/^[0-9a-f-]{36}$/u.test(id))
73316
+ return null;
73317
+ let names;
73318
+ try {
73319
+ names = fs20.readdirSync(dir);
73320
+ } catch {
73321
+ return null;
73322
+ }
73323
+ for (const name of names) {
73324
+ if (!/^slot-\d+\.json$/u.test(name))
73325
+ continue;
73326
+ const lease = parseLease(readText3(path40.join(dir, name)));
73327
+ if (lease?.id === id && !staleReason(lease, options.probe) && (options.ancestor ?? isAncestor)(lease.pid))
73328
+ return lease;
73329
+ }
73330
+ return null;
73331
+ }
73332
+ function describeHolder(holder, now = Date.now()) {
73333
+ const seconds = Math.max(0, Math.round((now - Date.parse(holder.startedAt)) / 1000));
73334
+ const age = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
73335
+ return `${holder.check}, pid ${holder.pid}${holder.session ? `, session ${holder.session}` : ""}, ${age}, ${holder.cwd}`;
73336
+ }
73337
+ function scopeSupport(env, platform = process.platform, read = readText3) {
73338
+ if (platform !== "linux")
73339
+ return { ok: false, reason: `no systemd scopes on ${platform}` };
73340
+ if (!onPath("systemd-run", env))
73341
+ return { ok: false, reason: "systemd-run is not on PATH" };
73342
+ const uid = process.getuid?.() ?? 0;
73343
+ const runtime = env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
73344
+ if (!fs20.existsSync(path40.join(runtime, "systemd", "private")))
73345
+ return { ok: false, reason: "no user systemd manager is running" };
73346
+ const controllers = read(`/sys/fs/cgroup/user.slice/user-${uid}.slice/user@${uid}.service/cgroup.controllers`);
73347
+ if (controllers === null)
73348
+ return { ok: false, reason: "cgroup v2 is not mounted for the user manager" };
73349
+ const missing = ["memory", "cpu"].filter((name) => !controllers.split(/\s+/u).includes(name));
73350
+ if (missing.length)
73351
+ return { ok: false, reason: `the user manager is not delegated ${missing.join(" and ")}` };
73352
+ return { ok: true };
73353
+ }
73354
+ function scopePrefix(unit, settings) {
73355
+ return [
73356
+ "systemd-run",
73357
+ "--user",
73358
+ "--scope",
73359
+ "--quiet",
73360
+ "--collect",
73361
+ `--unit=${unit}`,
73362
+ "-p",
73363
+ `MemoryMax=${settings.memoryMax}`,
73364
+ "-p",
73365
+ `CPUQuota=${settings.cpuQuota}`,
73366
+ "--"
73367
+ ];
73368
+ }
73369
+ async function runScoped(run, wrapper, sinks) {
73370
+ if (!wrapper)
73371
+ return { result: await run(null, sinks), fallback: null };
73372
+ let state = "deciding";
73373
+ let firstLine = "";
73374
+ const held = [];
73375
+ const flush = () => {
73376
+ state = "passing";
73377
+ for (const [sink, text] of held.splice(0))
73378
+ sinks[sink](text);
73379
+ };
73380
+ const result = await run(wrapper, {
73381
+ onStdout: (text) => {
73382
+ if (state === "passing")
73383
+ return sinks.onStdout(text);
73384
+ held.push(["onStdout", text]);
73385
+ if (state === "deciding")
73386
+ flush();
73387
+ },
73388
+ onStderr: (text) => {
73389
+ if (state === "passing")
73390
+ return sinks.onStderr(text);
73391
+ held.push(["onStderr", text]);
73392
+ if (state === "held")
73393
+ return;
73394
+ firstLine += text;
73395
+ const newline = firstLine.indexOf(`
73396
+ `);
73397
+ if (newline < 0 && firstLine.length < 4096)
73398
+ return;
73399
+ if (SCOPE_START_FAILURE.test(firstLine.slice(0, newline < 0 ? undefined : newline)))
73400
+ state = "held";
73401
+ else
73402
+ flush();
73403
+ }
73404
+ });
73405
+ const lines = tailLines(result.log, 2);
73406
+ if (state !== "passing" && result.exitCode !== 0 && lines.length === 1 && SCOPE_START_FAILURE.test(lines[0]))
73407
+ return { result: await run(null, sinks), fallback: lines[0] };
73408
+ flush();
73409
+ return { result, fallback: null };
73410
+ }
73411
+ function firstNotice(env, session, key, now = Date.now()) {
73412
+ const id = (session ?? `ppid-${process.ppid}`).replace(/[^A-Za-z0-9_.-]/gu, "_").slice(0, 120);
73413
+ const dir = path40.join(stateDir(env), "notices");
73414
+ try {
73415
+ fs20.mkdirSync(dir, { recursive: true, mode: 448 });
73416
+ fs20.writeFileSync(path40.join(dir, `${key}-${id}`), `${new Date(now).toISOString()}
73417
+ `, {
73418
+ flag: "wx",
73419
+ mode: 384
73420
+ });
73421
+ } catch (error) {
73422
+ return error.code !== "EEXIST";
73423
+ }
73424
+ try {
73425
+ for (const name of fs20.readdirSync(dir)) {
73426
+ const file = path40.join(dir, name);
73427
+ if (now - fs20.statSync(file).mtimeMs > NOTICE_TTL_MS)
73428
+ fs20.rmSync(file, { force: true });
73429
+ }
73430
+ } catch {}
73431
+ return true;
73432
+ }
73433
+ var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 600, DEFAULT_MEMORY_SHARE = 0.4, DEFAULT_CPU_SHARE = 0.7, MAX_SLOTS = 64, UNREADABLE_TTL_MS = 30000, GUARD_TTL_MS = 30000, hostShape = () => ({
73434
+ totalMemBytes: os14.totalmem(),
73435
+ cpus: Math.max(1, os14.availableParallelism?.() ?? os14.cpus().length)
73436
+ }), isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SIZE, PERCENT, UNITS, leaseDir = (env) => path40.join(stateDir(env), "leases"), readText3 = (file) => {
73437
+ try {
73438
+ return fs20.readFileSync(file, "utf8");
73439
+ } catch {
73440
+ return null;
73441
+ }
73442
+ }, bootId = () => readText3("/proc/sys/kernel/random/boot_id")?.trim() || null, bootTime = () => Math.round((Date.now() - os14.uptime() * 1000) / 1000) * 1000, BOOT_SLACK_MS = 120000, statFields = (pid) => {
73443
+ const stat = readText3(`/proc/${pid}/stat`);
73444
+ return stat ? stat.slice(stat.lastIndexOf(")") + 2).split(" ") : null;
73445
+ }, processStart = (pid) => statFields(pid)?.[19] ?? null, pidAlive2 = (pid) => {
73446
+ try {
73447
+ process.kill(pid, 0);
73448
+ return true;
73449
+ } catch (error) {
73450
+ return error.code === "EPERM";
73451
+ }
73452
+ }, groupAlive = (pgid) => process.platform === "win32" ? pidAlive2(pgid) : pidAlive2(-pgid), SCOPE_UNIT, parseLease = (text) => {
73453
+ if (!text)
73454
+ return null;
73455
+ try {
73456
+ const value = JSON.parse(text);
73457
+ return isRecord7(value) && typeof value.id === "string" && typeof value.pid === "number" ? value : null;
73458
+ } catch {
73459
+ return null;
73460
+ }
73461
+ }, holderOf = (lease) => ({
73462
+ pid: lease.pid,
73463
+ check: lease.check,
73464
+ cwd: lease.cwd,
73465
+ session: lease.session,
73466
+ startedAt: lease.startedAt
73467
+ }), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), onPath = (command, env) => (env.PATH ?? "").split(path40.delimiter).filter(Boolean).some((dir) => {
73468
+ try {
73469
+ fs20.accessSync(path40.join(dir, command), fs20.constants.X_OK);
73470
+ return true;
73471
+ } catch {
73472
+ return false;
73473
+ }
73474
+ }), SCOPE_START_FAILURE, NOTICE_TTL_MS;
73475
+ var init_resources = __esm(() => {
73476
+ init_checks3();
73477
+ DEFAULT_HEAVY_CHECKS = ["test", "build", "typecheck"];
73478
+ SIZE = /^(\d+(?:\.\d+)?)([KMGT]?)$/iu;
73479
+ PERCENT = /^(\d+(?:\.\d+)?)%$/u;
73480
+ UNITS = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
73481
+ SCOPE_UNIT = /^[\w.@-]+\.scope$/u;
73482
+ SCOPE_START_FAILURE = /^Failed to (?:connect to bus|create bus connection|start transient scope unit|parse \S+?=)/u;
73483
+ NOTICE_TTL_MS = 7 * 86400000;
73484
+ });
73485
+
72749
73486
  // packages/workit-cli/src/verbs/check.ts
72750
73487
  var exports_check = {};
72751
73488
  __export(exports_check, {
72752
73489
  run: () => run10
72753
73490
  });
72754
- import path40 from "node:path";
73491
+ import os15 from "node:os";
73492
+ import path41 from "node:path";
72755
73493
  function parse5(argv) {
72756
73494
  const split = argv.indexOf("--");
72757
73495
  const head = split >= 0 ? argv.slice(0, split) : [...argv];
72758
73496
  const options = {
72759
73497
  name: null,
72760
73498
  shell: false,
73499
+ heavy: false,
73500
+ noLease: false,
72761
73501
  timeoutMs: 0,
72762
73502
  task: null,
72763
73503
  base: null,
@@ -72777,6 +73517,13 @@ function parse5(argv) {
72777
73517
  options.shell = true;
72778
73518
  continue;
72779
73519
  }
73520
+ if (flag === "--heavy" || flag === "--no-lease") {
73521
+ if (flag === "--heavy")
73522
+ options.heavy = true;
73523
+ else
73524
+ options.noLease = true;
73525
+ continue;
73526
+ }
72780
73527
  if (flag === "--name" || flag === "--task" || flag === "--base" || flag === "--timeout") {
72781
73528
  const got = value();
72782
73529
  if (got instanceof Error)
@@ -72900,6 +73647,90 @@ function boundedArgv(argv) {
72900
73647
  const truncated = cut.length !== argv.length || cut.some((item, index) => item !== argv[index]);
72901
73648
  return { argv: cut, truncated };
72902
73649
  }
73650
+ async function workspaceResources(io) {
73651
+ const env = io.env;
73652
+ const dir = env.WORKFLOW_TOOLKIT_CONFIG ?? env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path41.join(env.XDG_CONFIG_HOME || path41.join(env.HOME || os15.homedir(), ".config"), "workit");
73653
+ try {
73654
+ await Promise.resolve().then(() => init_workspaces());
73655
+ const workspace = resolveWorkspaceFrom(io.cwd, dir, env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined);
73656
+ return { value: workspace?.resources, warning: null };
73657
+ } catch (error) {
73658
+ return { value: undefined, warning: `workspaces.json: ${error.message}` };
73659
+ }
73660
+ }
73661
+ async function prepareHeavy(io, settings, label, runCwd, timeoutMs) {
73662
+ const session = hostSessionFromEnv(io.env).session;
73663
+ const dir = leaseDir(io.env);
73664
+ const outer = heldLease(dir, io.env.WORKIT_LEASE);
73665
+ if (outer)
73666
+ return {
73667
+ lease: null,
73668
+ leaseReport: { slot: null, waitedMs: 0, holder: null, reclaimed: [], inherited: true },
73669
+ scope: { unit: outer.scope, reason: "inherited from the outer heavy check" },
73670
+ wrapper: null,
73671
+ env: io.env
73672
+ };
73673
+ const acquired = await acquireLease({
73674
+ dir,
73675
+ slots: settings.slots,
73676
+ check: label,
73677
+ cwd: runCwd,
73678
+ session,
73679
+ waitMs: settings.waitMs,
73680
+ pollMs: 1000,
73681
+ maxAgeMs: (timeoutMs || MAX_UNVERIFIED_RUN_MS) + 60000,
73682
+ onWait: (holders) => io.stderr(`workit: waiting for the heavy-check lease (${settings.slots} slot${settings.slots === 1 ? "" : "s"}): held by ${holders.map((holder) => describeHolder(holder)).join("; ")}
73683
+ `)
73684
+ });
73685
+ if (!acquired.ok && acquired.reason === "timeout") {
73686
+ const holders = acquired.holders.map((holder) => describeHolder(holder)).join("; ");
73687
+ emit(io, fail("busy", `the heavy-check lease stayed busy for ${Math.round(acquired.waitedMs / 1000)} s`, {
73688
+ data: { lease: { waitedMs: acquired.waitedMs, holders: acquired.holders } },
73689
+ unblock: `wait for ${holders || "the holders"} to finish, then run it again`
73690
+ }));
73691
+ return 4;
73692
+ }
73693
+ let lease = null;
73694
+ if (acquired.ok)
73695
+ lease = acquired.lease;
73696
+ else
73697
+ io.stderr(`workit: running without the heavy-check lease: ${acquired.error}
73698
+ `);
73699
+ for (const stale of lease?.reclaimed ?? [])
73700
+ io.stderr(`workit: reclaimed a stale heavy-check lease (${stale.reason}): ${describeHolder(stale)}
73701
+ `);
73702
+ const leaseReport = lease ? {
73703
+ slot: lease.slot,
73704
+ waitedMs: lease.waitedMs,
73705
+ holder: lease.waitedOn[0] ?? null,
73706
+ reclaimed: lease.reclaimed,
73707
+ inherited: false
73708
+ } : null;
73709
+ const env = lease ? { ...io.env, WORKIT_LEASE: lease.record.id } : io.env;
73710
+ const support = settings.scope ? scopeSupport(io.env) : { ok: false, reason: "resources.scope is false" };
73711
+ if (!support.ok) {
73712
+ if (settings.scope && firstNotice(io.env, session, "no-scope"))
73713
+ io.stderr(`workit: heavy checks run without a memory/CPU limit here: ${support.reason} (said once per session)
73714
+ `);
73715
+ return {
73716
+ lease,
73717
+ leaseReport,
73718
+ scope: { unit: null, reason: support.reason },
73719
+ wrapper: null,
73720
+ env
73721
+ };
73722
+ }
73723
+ const unit = `workit-check-${(lease?.record.id ?? "nolease0").slice(0, 8)}-${process.pid}`;
73724
+ if (lease)
73725
+ updateLease(lease, { scope: `${unit}.scope` });
73726
+ return {
73727
+ lease,
73728
+ leaseReport,
73729
+ scope: { unit: `${unit}.scope`, memoryMax: settings.memoryMax, cpuQuota: settings.cpuQuota },
73730
+ wrapper: scopePrefix(unit, settings),
73731
+ env
73732
+ };
73733
+ }
72903
73734
  async function run10(argv, io) {
72904
73735
  const options = parse5(argv);
72905
73736
  if (options instanceof Error)
@@ -72934,21 +73765,52 @@ async function run10(argv, io) {
72934
73765
  if (options.shell && command.length !== 1)
72935
73766
  return emit(io, fail("invalid_input", '--shell takes the command as one string: workit check --shell -- "<cmd>"'));
72936
73767
  }
72937
- const relCwd = posix(path40.relative(top, runCwd));
73768
+ const relCwd = posix(path41.relative(top, runCwd));
72938
73769
  const configured = !options.shell && matchesNamedCheck(config, name, command, relCwd);
72939
73770
  const actor = actorFromEnv(io.env);
72940
73771
  const branchKey = codeKey(io.cwd, { base: options.base });
72941
73772
  const detached = branchKey.tree === null ? worktreeTree(io.cwd) : null;
72942
73773
  const key = detached ? { ...branchKey, tree: detached.key, dirty: detached.dirty } : branchKey;
72943
73774
  const signal = key.tree === null ? null : worktreeSignal(io.cwd);
72944
- const result = await runCheckCommand(command, {
72945
- cwd: runCwd,
72946
- shell: options.shell,
72947
- timeoutMs: options.timeoutMs,
72948
- env: io.env,
72949
- onStdout: io.json ? io.stderr : io.stdout,
72950
- onStderr: io.stderr
72951
- });
73775
+ let heavy = null;
73776
+ if (!options.noLease && (options.heavy || options.command === null)) {
73777
+ const workspace = await workspaceResources(io);
73778
+ const { settings, warnings } = resolveResourceSettings([
73779
+ { label: "workspaces.json", trust: "machine", value: workspace.value },
73780
+ { label: CHECKS_FILE, trust: "repo", value: config.resources }
73781
+ ]);
73782
+ for (const warning of [workspace.warning, ...warnings])
73783
+ if (warning)
73784
+ io.stderr(`workit: resources: ${warning}
73785
+ `);
73786
+ if (options.heavy || name !== null && settings.heavy.includes(name)) {
73787
+ const prepared = await prepareHeavy(io, settings, name ?? command.join(" "), runCwd, options.timeoutMs);
73788
+ if (typeof prepared === "number")
73789
+ return prepared;
73790
+ heavy = prepared;
73791
+ }
73792
+ }
73793
+ const lease = heavy?.lease ?? null;
73794
+ let result;
73795
+ try {
73796
+ const scoped = await runScoped((wrapper, sinks) => runCheckCommand(command, {
73797
+ cwd: runCwd,
73798
+ shell: options.shell,
73799
+ timeoutMs: options.timeoutMs,
73800
+ env: heavy?.env ?? io.env,
73801
+ ...sinks,
73802
+ ...wrapper ? { wrapper } : {},
73803
+ ...lease ? { onSpawn: (pid) => updateLease(lease, { pgid: pid }) } : {}
73804
+ }), heavy?.wrapper ?? null, { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr });
73805
+ result = scoped.result;
73806
+ if (heavy && scoped.fallback) {
73807
+ io.stderr(`workit: the memory/CPU scope could not start (${scoped.fallback}); ran without it
73808
+ `);
73809
+ heavy.scope = { unit: null, reason: `systemd-run failed: ${scoped.fallback}` };
73810
+ }
73811
+ } finally {
73812
+ lease?.release();
73813
+ }
72952
73814
  const treeAfter = key.tree === null ? null : worktreeTree(io.cwd)?.key ?? null;
72953
73815
  const modifiedWorktree = key.tree !== null && treeAfter !== key.tree;
72954
73816
  const store = storeRoot(io.cwd);
@@ -73029,6 +73891,8 @@ async function run10(argv, io) {
73029
73891
  outputBytes: result.outputBytes,
73030
73892
  logTruncated: result.truncated,
73031
73893
  task: task.task,
73894
+ lease: heavy?.leaseReport ?? null,
73895
+ scope: heavy?.scope ?? null,
73032
73896
  ...task.note ? { taskNote: task.note } : {},
73033
73897
  satisfies: task.satisfies,
73034
73898
  stillUnsatisfied: task.stillUnsatisfied,
@@ -73058,7 +73922,7 @@ async function run10(argv, io) {
73058
73922
  return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
73059
73923
  return 0;
73060
73924
  }
73061
- var USAGE3 = "workit check <name> | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path40.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
73925
+ var USAGE3 = "workit check <name> [--no-lease] | workit check [--name <n>] [--shell] [--heavy] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", MAX_UNVERIFIED_RUN_MS, RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path41.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
73062
73926
  var init_check = __esm(() => {
73063
73927
  init_check_config();
73064
73928
  init_checks3();
@@ -73067,6 +73931,8 @@ var init_check = __esm(() => {
73067
73931
  init_ledger();
73068
73932
  init_output();
73069
73933
  init_host_session();
73934
+ init_resources();
73935
+ MAX_UNVERIFIED_RUN_MS = 6 * 3600000;
73070
73936
  ENGINE_CODES2 = {
73071
73937
  busy: "busy",
73072
73938
  not_found: "not_found",
@@ -73082,7 +73948,7 @@ var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock)
73082
73948
  // packages/workit-core/src/git/ops.ts
73083
73949
  import { spawnSync as spawnSync18 } from "node:child_process";
73084
73950
  import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
73085
- import path41 from "node:path";
73951
+ import path42 from "node:path";
73086
73952
  function operationInProgress(cwd) {
73087
73953
  const markers = [
73088
73954
  ["rebase-merge", "rebase"],
@@ -73095,18 +73961,18 @@ function operationInProgress(cwd) {
73095
73961
  if (!run.ok)
73096
73962
  return null;
73097
73963
  const found = run.stdout.split(`
73098
- `).map((line) => path41.resolve(cwd, line.trim()));
73964
+ `).map((line) => path42.resolve(cwd, line.trim()));
73099
73965
  const conflicted = () => git5(cwd, ["ls-files", "-u"]).stdout.trim() !== "";
73100
73966
  for (const [index, [, operation]] of markers.entries()) {
73101
73967
  const marker = found[index];
73102
73968
  if (!marker || !existsSync19(marker))
73103
73969
  continue;
73104
- if (index === 1 && existsSync19(path41.join(marker, "applying")))
73970
+ if (index === 1 && existsSync19(path42.join(marker, "applying")))
73105
73971
  return { operation: "am", commitAllowed: false, branch: null };
73106
- if (index === 0 && existsSync19(path41.join(marker, "amend")) && !conflicted()) {
73972
+ if (index === 0 && existsSync19(path42.join(marker, "amend")) && !conflicted()) {
73107
73973
  let headName = "";
73108
73974
  try {
73109
- headName = readFileSync19(path41.join(marker, "head-name"), "utf8").trim();
73975
+ headName = readFileSync19(path42.join(marker, "head-name"), "utf8").trim();
73110
73976
  } catch {}
73111
73977
  const branch = headName.startsWith("refs/heads/") ? headName.slice("refs/heads/".length) : null;
73112
73978
  return { operation, commitAllowed: branch !== null, branch };
@@ -73161,6 +74027,12 @@ function gitBranch(cwd, input) {
73161
74027
  return failure3("blocked", policy.error, policy.correction);
73162
74028
  if (resolveRef(cwd, `refs/heads/${name}`))
73163
74029
  return failure3("failed", `branch_exists: ${name} already exists`, `git switch ${name}`);
74030
+ const kind = input.kind ?? branchKindOf(name);
74031
+ const flow = resolved.ok !== false && !releaseTrack?.track ? gitFlowOf(cwd, String(resolved.defaultTargetBranch ?? "") || null) : null;
74032
+ const route = flow && kind ? flowRoute(flow, kind) : null;
74033
+ const mismatch = explicitBase ? flowMismatch(flow, kind, explicitBase, "base") : null;
74034
+ if (mismatch && route)
74035
+ return failure3("blocked", mismatch, `workit git branch ${input.kind ? `--kind ${kind} --slug ${input.slug}` : name} # without --base it starts from ${route.base}`);
73164
74036
  let base = explicitBase;
73165
74037
  let defaultTarget = null;
73166
74038
  let defaultBase = null;
@@ -73170,12 +74042,14 @@ function gitBranch(cwd, input) {
73170
74042
  defaultTarget = String(resolved.defaultTargetBranch ?? "") || null;
73171
74043
  defaultBase = String(resolved.baseBranch ?? "") || defaultTarget;
73172
74044
  }
73173
- base ??= defaultBase;
74045
+ base ??= route?.base ?? defaultBase;
73174
74046
  if (!base)
73175
74047
  return failure3("invalid_input", "no default target branch is configured; pass --base <branch>");
73176
74048
  if (base.startsWith("-"))
73177
74049
  return failure3("invalid_input", "--base must not start with -");
73178
74050
  const notes = [...releaseTrack?.warnings ?? []];
74051
+ if (route?.mergeBack)
74052
+ notes.push(`git-flow: ${route.kind}/* lands on ${route.target} (workit pr create targets it), then ${route.target} merges back into ${route.mergeBack}`);
73179
74053
  const remotes = remoteNames(cwd);
73180
74054
  const remote = remotes.includes("origin") ? "origin" : pushRemoteName(cwd);
73181
74055
  const localTip = resolveRef(cwd, `refs/heads/${base}`);
@@ -73218,7 +74092,8 @@ function gitBranch(cwd, input) {
73218
74092
  baseSha,
73219
74093
  carried: dirt !== "clean",
73220
74094
  notes,
73221
- releaseTrack
74095
+ releaseTrack,
74096
+ flow: route
73222
74097
  });
73223
74098
  }
73224
74099
  function lintCommitMessage(cwd, message) {
@@ -73247,6 +74122,40 @@ function lintCommitMessage(cwd, message) {
73247
74122
  correction: `use a ${detected} commit subject`
73248
74123
  };
73249
74124
  }
74125
+ function stripSessionTrailers(text, options) {
74126
+ const end = text.length - (/\s*$/u.exec(text)?.[0].length ?? 0);
74127
+ const lines = text.slice(0, end).split(`
74128
+ `);
74129
+ let start = lines.length;
74130
+ while (start > 0 && lines[start - 1]?.trim() !== "")
74131
+ start -= 1;
74132
+ if (start === 0 && options.subject)
74133
+ return text;
74134
+ const block = lines.slice(start);
74135
+ if (!block.length || !TRAILER_LINE.test(block[0] ?? ""))
74136
+ return text;
74137
+ if (!block.every((line) => TRAILER_LINE.test(line) || /^\s+\S/u.test(line)))
74138
+ return text;
74139
+ const kept = [];
74140
+ let dropping = false;
74141
+ for (const line of block) {
74142
+ if (TRAILER_LINE.test(line))
74143
+ dropping = SESSION_KEY.test(line);
74144
+ if (!dropping)
74145
+ kept.push(line);
74146
+ }
74147
+ if (kept.length === block.length)
74148
+ return text;
74149
+ let head = lines.slice(0, start);
74150
+ if (!kept.length) {
74151
+ let cut = head.length;
74152
+ while (cut > 0 && head[cut - 1]?.trim() === "")
74153
+ cut -= 1;
74154
+ head = head.slice(0, cut);
74155
+ }
74156
+ return [...head, ...kept].join(`
74157
+ `) + text.slice(end);
74158
+ }
73250
74159
  function messageTrailers(cwd, message) {
73251
74160
  const run = git5(cwd, [...TRAILER_CONFIG, "interpret-trailers", "--parse", "--no-divider"], {
73252
74161
  input: `${message}
@@ -73306,6 +74215,12 @@ function gitCommit(cwd, input) {
73306
74215
  const given = input.message?.trim() ?? "";
73307
74216
  if (!given && !input.amend)
73308
74217
  return failure3("invalid_input", "a commit message is required (-m <msg> or -F <file>)");
74218
+ const extra = (input.trailers ?? []).map((trailer) => trailer.trim());
74219
+ const malformed = extra.find((trailer) => !EXTRA_TRAILER.test(trailer));
74220
+ if (malformed !== undefined)
74221
+ return failure3("invalid_input", `--trailer ${JSON.stringify(malformed)} is not one "Key: value" line`, `workit git commit -m '<msg>' --trailer "Co-Authored-By: Name <email>"`);
74222
+ if (extra.some((trailer) => /^workit-session:/iu.test(trailer)))
74223
+ return failure3("invalid_input", "--trailer cannot set Workit-Session: workit writes it from the acting session");
73309
74224
  const paths = input.paths ?? [];
73310
74225
  if (input.all && paths.length)
73311
74226
  return failure3("invalid_input", "pass --all or paths, not both");
@@ -73327,6 +74242,7 @@ function gitCommit(cwd, input) {
73327
74242
  }
73328
74243
  let message = given;
73329
74244
  let carried = [];
74245
+ let previousTrailers = [];
73330
74246
  if (given) {
73331
74247
  const lint = lintCommitMessage(cwd, given);
73332
74248
  if (!lint.ok)
@@ -73336,8 +74252,9 @@ function gitCommit(cwd, input) {
73336
74252
  const kept = git5(cwd, ["log", "-1", "--format=%B", previous]);
73337
74253
  if (!kept.ok)
73338
74254
  return failure3("failed", `could not read the message of ${branch}: ${gitError(kept)}`);
74255
+ previousTrailers = messageTrailers(cwd, kept.stdout.trim());
73339
74256
  if (given)
73340
- carried = messageTrailers(cwd, kept.stdout.trim());
74257
+ carried = previousTrailers;
73341
74258
  else
73342
74259
  message = kept.stdout.trim();
73343
74260
  }
@@ -73351,12 +74268,31 @@ function gitCommit(cwd, input) {
73351
74268
  args.push("--amend");
73352
74269
  if (input.allowEmpty || emptyAmend)
73353
74270
  args.push("--allow-empty");
73354
- args.push("-m", withTrailers(cwd, message, [...carried, ...session ? [`Workit-Session: ${session}`] : []]));
74271
+ const trailersOn = workspaceTrailers(cwd).enabled;
74272
+ const sessionTrailer = Boolean(session) && trailersOn;
74273
+ if (!trailersOn) {
74274
+ carried = carried.filter((trailer) => !SESSION_KEY.test(trailer));
74275
+ if (!given)
74276
+ message = stripSessionTrailers(message, { subject: true });
74277
+ }
74278
+ const finalMessage = withTrailers(cwd, message, [
74279
+ ...carried,
74280
+ ...extra,
74281
+ ...sessionTrailer ? [`Workit-Session: ${session}`] : []
74282
+ ]);
74283
+ const allowed = new Set([
74284
+ ...trailersOn ? sessionValues(previousTrailers) : [],
74285
+ ...sessionTrailer && session ? [session] : []
74286
+ ]);
74287
+ const forged = sessionValues(messageTrailers(cwd, finalMessage)).find((value) => !allowed.has(value));
74288
+ if (forged !== undefined)
74289
+ return failure3("invalid_input", `forged_session_trailer: the message would carry "Workit-Session: ${forged}", which workit did not add; workit writes that trailer itself`, "drop the Workit-Session line (or the trailer.<alias>.key config that expands into it)");
74290
+ args.push("-m", finalMessage);
73355
74291
  if (paths.length) {
73356
74292
  if (paths.some((value) => value.startsWith("-") || !value.trim()))
73357
74293
  return failure3("invalid_input", "paths must not be empty or start with -");
73358
74294
  const tracked = new Set(nulList(git5(cwd, ["ls-files", "-z", "--", ...paths]).stdout));
73359
- const addable = paths.filter((value) => existsSync19(path41.resolve(cwd, value)) || [...tracked].some((file) => file === value || file.startsWith(`${value}/`)));
74295
+ const addable = paths.filter((value) => existsSync19(path42.resolve(cwd, value)) || [...tracked].some((file) => file === value || file.startsWith(`${value}/`)));
73360
74296
  if (addable.length) {
73361
74297
  const added = git5(cwd, ["add", "-A", "--", ...addable]);
73362
74298
  if (!added.ok)
@@ -73382,7 +74318,7 @@ function gitCommit(cwd, input) {
73382
74318
  }
73383
74319
  const committed = git5(cwd, args, {
73384
74320
  timeoutMs: COMMIT_TIMEOUT_MS,
73385
- env: input.env
74321
+ env: { ...input.env ?? process.env, WORKIT_TRAILERS: trailersOn ? "on" : "off" }
73386
74322
  });
73387
74323
  if (!committed.ok)
73388
74324
  return failure3("failed", `git commit failed (a hook or git refused it):
@@ -73426,6 +74362,7 @@ ${gitOutput(committed)}`);
73426
74362
  files,
73427
74363
  leftDirty,
73428
74364
  session,
74365
+ sessionTrailer,
73429
74366
  amended: previous,
73430
74367
  notes,
73431
74368
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
@@ -73439,7 +74376,7 @@ function pushPreflight(cwd, options = {}) {
73439
74376
  if (!branch)
73440
74377
  return failure3("invalid_input", "HEAD is detached; push a branch");
73441
74378
  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");
74379
+ 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
74380
  const sha = resolveRef(cwd, `refs/heads/${branch}`);
73444
74381
  if (!sha)
73445
74382
  return failure3("not_found", `branch ${branch} has no commit to push`);
@@ -73606,6 +74543,125 @@ ${gitOutput(run)}`);
73606
74543
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
73607
74544
  });
73608
74545
  }
74546
+ function releasePushPreflight(cwd, names) {
74547
+ const progress = operationInProgress(cwd);
74548
+ if (progress)
74549
+ return inProgressFailure(progress.operation, "push");
74550
+ const wanted = [...new Set(names)];
74551
+ if (!wanted.length)
74552
+ return failure3("invalid_input", "name the release refs to push");
74553
+ const { preset, branches, production } = releaseBranchesOf(cwd);
74554
+ const refs = [];
74555
+ for (const raw of wanted) {
74556
+ if (raw.startsWith("-") || raw.includes(":"))
74557
+ return failure3("invalid_input", `${raw} is not a ref name (refspecs are not accepted)`);
74558
+ const tagName = raw.replace(/^refs\/tags\//u, "");
74559
+ const branchName = raw.replace(/^refs\/heads\//u, "");
74560
+ const tag = raw.startsWith("refs/heads/") ? null : resolveRef(cwd, `refs/tags/${tagName}`);
74561
+ const branch = raw.startsWith("refs/tags/") ? null : resolveRef(cwd, `refs/heads/${branchName}`);
74562
+ if (tag && branch)
74563
+ return failure3("invalid_input", `${raw} names both a branch and a tag`, `workit git push --release refs/heads/${raw} # or refs/tags/${raw}`);
74564
+ if (tag) {
74565
+ if (!RELEASE_TAG.test(tagName))
74566
+ 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>");
74567
+ refs.push({ kind: "tag", name: tagName, ref: `refs/tags/${tagName}`, sha: tag });
74568
+ continue;
74569
+ }
74570
+ if (!branch)
74571
+ return failure3("not_found", `${raw} is not a local branch or tag`);
74572
+ if (!branches.some((name) => name.toLowerCase() === branchName.toLowerCase()))
74573
+ 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>`);
74574
+ refs.push({ kind: "branch", name: branchName, ref: `refs/heads/${branchName}`, sha: branch });
74575
+ }
74576
+ const remote = pushRemoteName(cwd, refs.find((ref) => ref.kind === "branch")?.name ?? null);
74577
+ if (!remote)
74578
+ return failure3("not_found", "no push remote is configured for this repository", "git remote add origin <url>");
74579
+ const raw = pushUrl(cwd, remote);
74580
+ if (!raw)
74581
+ return failure3("blocked", `remote "${remote}" has no single push URL`, `git remote get-url --push --all ${remote} # keep exactly one push URL`);
74582
+ const local = parseRemoteUrl(raw)?.protocol === "file";
74583
+ if (!local && !pushTargetIsStable(cwd, raw))
74584
+ 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");
74585
+ return success2({ refs, remote, url: redactRemote(raw), rawUrl: raw, local, production });
74586
+ }
74587
+ function executeReleasePush(cwd, plan, input) {
74588
+ const previous = new Map;
74589
+ for (const ref of plan.refs) {
74590
+ const before = remoteRefTip(cwd, plan.rawUrl, ref.ref);
74591
+ if (!before.ok)
74592
+ return failure3(before.code, before.error);
74593
+ previous.set(ref.ref, before.sha);
74594
+ if (before.sha === null || before.sha === ref.sha)
74595
+ continue;
74596
+ if (ref.kind === "tag")
74597
+ 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`);
74598
+ if (!git5(cwd, ["merge-base", "--is-ancestor", before.sha, ref.sha]).ok)
74599
+ 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}`);
74600
+ }
74601
+ const tags = plan.refs.filter((ref) => ref.kind === "tag");
74602
+ if (tags.length) {
74603
+ let productionTip = null;
74604
+ if (plan.production) {
74605
+ const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.production}`);
74606
+ if (!tip.ok)
74607
+ return failure3(tip.code, tip.error);
74608
+ productionTip = tip.sha;
74609
+ }
74610
+ const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
74611
+ for (const tag of tags) {
74612
+ const released = productionTip !== null && contains(productionTip, tag.sha) || plan.refs.some((ref) => ref.kind === "branch" && contains(ref.sha, tag.sha));
74613
+ if (!released)
74614
+ 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`);
74615
+ }
74616
+ }
74617
+ const pending = plan.refs.filter((ref) => previous.get(ref.ref) !== ref.sha);
74618
+ if (pending.length) {
74619
+ const run = gitNetwork(cwd, [
74620
+ "push",
74621
+ "--porcelain",
74622
+ "--atomic",
74623
+ "--no-follow-tags",
74624
+ "--recurse-submodules=no",
74625
+ plan.remote,
74626
+ ...pending.map((ref) => ref.kind === "tag" ? `${ref.ref}:${ref.ref}` : `${ref.sha}:${ref.ref}`)
74627
+ ], input.timeoutMs ?? PUSH_TIMEOUT_MS);
74628
+ if (!run.ok)
74629
+ return run.timedOut ? failure3("unavailable", `git push to ${plan.url} timed out`) : failure3("failed", `git push to ${plan.url} failed:
74630
+ ${gitOutput(run)}`);
74631
+ }
74632
+ for (const ref of plan.refs) {
74633
+ const after = remoteRefTip(cwd, plan.rawUrl, ref.ref);
74634
+ if (!after.ok)
74635
+ return failure3(after.code, `push sent but not verified: ${after.error}`);
74636
+ if (after.sha !== ref.sha)
74637
+ return failure3("failed", `push_unverified: ${plan.remote} ${ref.ref} is at ${after.sha?.slice(0, 12) ?? "(absent)"}, not ${ref.sha.slice(0, 12)}`);
74638
+ }
74639
+ const refs = plan.refs.map((ref) => ({
74640
+ ...ref,
74641
+ previous: previous.get(ref.ref) ?? null,
74642
+ pushed: pending.includes(ref)
74643
+ }));
74644
+ const row = appendObserved(cwd, {
74645
+ type: "release.pushed",
74646
+ actor: input.actor,
74647
+ remote: plan.remote,
74648
+ url: plan.url,
74649
+ refs: refs.map(({ ref, sha, previous: before, pushed }) => ({
74650
+ ref,
74651
+ sha,
74652
+ previous: before,
74653
+ pushed
74654
+ })),
74655
+ grant: input.grant,
74656
+ ...input.identity ? { identity: input.identity } : {}
74657
+ });
74658
+ return success2({
74659
+ remote: plan.remote,
74660
+ url: plan.url,
74661
+ refs,
74662
+ recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
74663
+ });
74664
+ }
73609
74665
  function deleteRemoteBranch(cwd, target, branch, expectedTip, options = {}) {
73610
74666
  if (target.startsWith("-") || branch.startsWith("-") || !SHA.test(expectedTip))
73611
74667
  return { ok: false, lease: false, error: "invalid branch deletion target" };
@@ -73670,29 +74726,34 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
73670
74726
  ...lines.slice(-half)
73671
74727
  ].join(`
73672
74728
  `);
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;
74729
+ }, 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, RELEASE_TAG;
73674
74730
  var init_ops = __esm(() => {
73675
74731
  init_branch();
73676
74732
  init_commit_flavors();
73677
74733
  init_pr_create();
73678
74734
  init_release_tracks();
73679
74735
  init_vcs_config();
74736
+ init_workspaces();
73680
74737
  init_redact();
73681
74738
  init_ledger();
73682
74739
  init_rev();
73683
74740
  COMMIT_TIMEOUT_MS = 10 * 60000;
73684
74741
  SESSION_SAFE = /^[\w.:@/+-]{1,128}$/u;
74742
+ EXTRA_TRAILER = /^[A-Za-z0-9][A-Za-z0-9-]*: *\S[^\n\r]*$/u;
74743
+ SESSION_KEY = /^workit-session\s*:/iu;
74744
+ TRAILER_LINE = /^[A-Za-z0-9][A-Za-z0-9-]*\s*:/u;
73685
74745
  TRAILER_CONFIG = ["-c", "trailer.separators=:", "-c", "trailer.where=end"];
73686
74746
  SHA = /^[0-9a-f]{40,64}$/u;
73687
74747
  SHA_PREFIX = /^[0-9a-f]{7,39}$/u;
74748
+ RELEASE_TAG = /^v\d/u;
73688
74749
  });
73689
74750
 
73690
74751
  // packages/workit-core/src/forge/reruns.ts
73691
- import fs20 from "node:fs";
73692
- import path42 from "node:path";
74752
+ import fs21 from "node:fs";
74753
+ import path43 from "node:path";
73693
74754
  function rerunLogPath(cwd) {
73694
74755
  const common = gitCommonDir(cwd);
73695
- return common ? path42.join(common, "workit", "ci-reruns.jsonl") : null;
74756
+ return common ? path43.join(common, "workit", "ci-reruns.jsonl") : null;
73696
74757
  }
73697
74758
  function rerunCounts(cwd, key) {
73698
74759
  const counts = new Map;
@@ -73701,7 +74762,7 @@ function rerunCounts(cwd, key) {
73701
74762
  return counts;
73702
74763
  let text;
73703
74764
  try {
73704
- text = fs20.readFileSync(file, "utf8");
74765
+ text = fs21.readFileSync(file, "utf8");
73705
74766
  } catch {
73706
74767
  return counts;
73707
74768
  }
@@ -73727,8 +74788,8 @@ function recordReruns(cwd, rows) {
73727
74788
  if (!file)
73728
74789
  return false;
73729
74790
  try {
73730
- fs20.mkdirSync(path42.dirname(file), { recursive: true });
73731
- fs20.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
74791
+ fs21.mkdirSync(path43.dirname(file), { recursive: true });
74792
+ fs21.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
73732
74793
  `).join(""));
73733
74794
  return true;
73734
74795
  } catch {
@@ -73740,18 +74801,18 @@ function withRerunLock(cwd, fn) {
73740
74801
  if (!file)
73741
74802
  return fn();
73742
74803
  const lock = `${file.slice(0, -".jsonl".length)}.lock`;
73743
- fs20.mkdirSync(path42.dirname(lock), { recursive: true });
74804
+ fs21.mkdirSync(path43.dirname(lock), { recursive: true });
73744
74805
  const until = Date.now() + LOCK_WAIT_MS2;
73745
74806
  for (;; ) {
73746
74807
  try {
73747
- fs20.closeSync(fs20.openSync(lock, "wx"));
74808
+ fs21.closeSync(fs21.openSync(lock, "wx"));
73748
74809
  break;
73749
74810
  } catch (error) {
73750
74811
  if (error.code !== "EEXIST")
73751
74812
  throw error;
73752
74813
  try {
73753
- if (Date.now() - fs20.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
73754
- fs20.rmSync(lock, { force: true });
74814
+ if (Date.now() - fs21.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
74815
+ fs21.rmSync(lock, { force: true });
73755
74816
  continue;
73756
74817
  }
73757
74818
  } catch {
@@ -73765,7 +74826,7 @@ function withRerunLock(cwd, fn) {
73765
74826
  try {
73766
74827
  return fn();
73767
74828
  } finally {
73768
- fs20.rmSync(lock, { force: true });
74829
+ fs21.rmSync(lock, { force: true });
73769
74830
  }
73770
74831
  }
73771
74832
  var isRow = (value) => {
@@ -74224,7 +75285,7 @@ function boundSource(cwd, resolved) {
74224
75285
  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
75286
  return success2({ branch, sha });
74226
75287
  }
74227
- async function verifiedHead(resolved, branch, sha, first, sleep) {
75288
+ async function verifiedHead(resolved, branch, sha, first, sleep, base) {
74228
75289
  let ref = first;
74229
75290
  for (const delay of [0, ...VERIFY_DELAYS_MS]) {
74230
75291
  if (ref.headSha === sha)
@@ -74234,7 +75295,8 @@ async function verifiedHead(resolved, branch, sha, first, sleep) {
74234
75295
  const found = resolved.forge.findPr(branch, {
74235
75296
  owner: resolved.headRepo.split("/")[0] ?? null,
74236
75297
  projectId: resolved.headProjectId,
74237
- sha
75298
+ sha,
75299
+ base
74238
75300
  });
74239
75301
  if (!found.ok)
74240
75302
  return found;
@@ -74259,7 +75321,8 @@ async function createPullRequest(cwd, resolved, input, sleep) {
74259
75321
  const existing = resolved.forge.findPr(branch, {
74260
75322
  owner: resolved.headRepo.split("/")[0] ?? null,
74261
75323
  projectId: resolved.headProjectId,
74262
- sha
75324
+ sha,
75325
+ base: input.base
74263
75326
  });
74264
75327
  if (!existing.ok)
74265
75328
  return existing;
@@ -74282,7 +75345,7 @@ async function createPullRequest(cwd, resolved, input, sleep) {
74282
75345
  ref = made.data;
74283
75346
  created = true;
74284
75347
  }
74285
- const verified = await verifiedHead(resolved, branch, sha, ref, sleep);
75348
+ const verified = await verifiedHead(resolved, branch, sha, ref, sleep, input.base);
74286
75349
  if (!verified.ok)
74287
75350
  return verified;
74288
75351
  const row = appendObserved(cwd, {
@@ -74347,6 +75410,56 @@ function nextHint(next, number) {
74347
75410
  return null;
74348
75411
  }
74349
75412
  }
75413
+ function squashMessageFor(cwd, resolved, input, doc) {
75414
+ if (input.method !== "squash")
75415
+ return null;
75416
+ if (input.subject === undefined && input.body === undefined && workspaceTrailers(cwd).enabled)
75417
+ return null;
75418
+ let subject = input.subject?.trim() ?? "";
75419
+ if (!subject) {
75420
+ const meta = resolved.forge.prMeta(doc.number);
75421
+ if (!meta.ok)
75422
+ return meta;
75423
+ subject = resolved.forge.kind === "github" ? `${meta.data.title} (#${doc.number})` : meta.data.title;
75424
+ }
75425
+ const body = input.body === undefined ? squashCommitList(cwd, resolved, doc.base, doc.head.sha) : stripSessionTrailers(input.body, { subject: false });
75426
+ return success2({ subject, body });
75427
+ }
75428
+ function squashCommitList(cwd, resolved, base, head) {
75429
+ const from = resolveRef(cwd, `refs/remotes/${resolved.baseRemote}/${base}`) ?? resolveRef(cwd, `refs/heads/${base}`);
75430
+ if (!from)
75431
+ return "";
75432
+ const log = spawnSync19("git", ["log", "--reverse", "--no-merges", "--format=%B%x1e", `${from}..${head}`], {
75433
+ cwd,
75434
+ encoding: "utf8",
75435
+ timeout: GIT_TIMEOUTS.local,
75436
+ killSignal: "SIGKILL",
75437
+ maxBuffer: 64 * 1024 * 1024
75438
+ });
75439
+ if (log.status !== 0)
75440
+ return "";
75441
+ const entries = log.stdout.split("\x1E").map((message) => stripSessionTrailers(message.replace(/^\n+/u, ""), { subject: true })).filter((message) => message.trim()).map((message) => {
75442
+ const [subject = "", ...rest] = message.trimEnd().split(`
75443
+ `);
75444
+ const body = rest.join(`
75445
+ `).replace(/^\n+/u, "");
75446
+ return body ? `* ${subject}
75447
+
75448
+ ${body}` : `* ${subject}`;
75449
+ });
75450
+ const listed = [];
75451
+ let size = 0;
75452
+ for (const entry of entries) {
75453
+ if (listed.length >= SQUASH_LIST_MAX_COMMITS || size + entry.length > SQUASH_LIST_MAX_CHARS)
75454
+ break;
75455
+ listed.push(entry);
75456
+ size += entry.length + 2;
75457
+ }
75458
+ const more = entries.length - listed.length;
75459
+ return [...listed, ...more ? [`… and ${more} more commit${more === 1 ? "" : "s"}`] : []].join(`
75460
+
75461
+ `);
75462
+ }
74350
75463
  async function mergePullRequest(cwd, resolved, input, sleep) {
74351
75464
  const grant = requireGrant(cwd, "merge");
74352
75465
  if (!grant.allowed)
@@ -74423,7 +75536,14 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
74423
75536
  return refuse(`head_mismatch: the verdict check reads ${branch} at ${check.head?.slice(0, 12) ?? "(missing)"} but ${label} is at ${head.slice(0, 12)}`, `git fetch && git switch ${branch} && git merge --ff-only @{u} # then re-verify that head`, { reason: "head_mismatch", verdict: summary });
74424
75537
  else if (!covered)
74425
75538
  return refuse(`NEEDS_VERDICT: ${label} has no accepted independent verdict for ${head.slice(0, 12)} (${check.accepted.reasons.join(", ")})`, `an independent session verifies the head and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${branch}${grant.allowUnverified ? `; or, only if the user asks to merge unverified: workit pr merge --unverified --reason "<why>"` : ""}`, { reason: "needs_verdict", verdict: summary });
74426
- const merged = resolved.forge.merge(doc.number, { sha: head, method: input.method });
75539
+ const squashMessage = squashMessageFor(cwd, resolved, input, doc);
75540
+ if (squashMessage && !squashMessage.ok)
75541
+ return squashMessage;
75542
+ const merged = resolved.forge.merge(doc.number, {
75543
+ sha: head,
75544
+ method: input.method,
75545
+ ...squashMessage ? { squashMessage: squashMessage.data } : {}
75546
+ });
74427
75547
  if (!merged.ok)
74428
75548
  return merged;
74429
75549
  let state = "open";
@@ -74449,7 +75569,7 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
74449
75569
  const gone = !deleted.ok && remoteRefTip(cwd, resolved.remote, `refs/heads/${branch}`);
74450
75570
  deletedBranch = deleted.ok || gone && gone.ok && gone.sha === null ? true : { error: deleted.error };
74451
75571
  }
74452
- const mergeBack = mergeBackFor(cwd, doc.base);
75572
+ const mergeBack = mergeBackFor(cwd, doc.base, branch);
74453
75573
  const row = appendObserved(cwd, {
74454
75574
  type: "pr.merged",
74455
75575
  actor: input.actor,
@@ -74488,9 +75608,16 @@ var VERIFY_DELAYS_MS, noun = (resolved) => resolved.forge.kind === "github" ? "p
74488
75608
  return `a non-author verifies the head (workit-review); the endpoint is ${endpoint}, so ${AFTER_REVIEW[endpoint]}`;
74489
75609
  }, SESSION_TRAILER, indent = (text) => text.split(`
74490
75610
  `).map((line) => line ? ` ${line}` : line).join(`
74491
- `), mergeBackFor = (cwd, base) => {
75611
+ `), mergeBackFor = (cwd, base, head) => {
74492
75612
  try {
74493
- const owners = workspaceReleaseTracks(cwd).tracks.filter((track) => track.productionBranch === base);
75613
+ const read = workspaceReleaseTracks(cwd);
75614
+ if (!read.tracks.length) {
75615
+ const flow = gitFlowOf(cwd);
75616
+ const kind = branchKindOf(head);
75617
+ const route = flow && kind ? flowRoute(flow, kind) : null;
75618
+ return route?.mergeBack && route.target === base ? { track: "git-flow", branches: [route.mergeBack] } : { track: null, branches: [] };
75619
+ }
75620
+ const owners = read.tracks.filter((track) => track.productionBranch === base);
74494
75621
  if (owners.length !== 1)
74495
75622
  return { track: null, branches: [] };
74496
75623
  return {
@@ -74506,11 +75633,12 @@ var VERIFY_DELAYS_MS, noun = (resolved) => resolved.forge.kind === "github" ? "p
74506
75633
  error,
74507
75634
  unblock,
74508
75635
  refusal
74509
- });
75636
+ }), SQUASH_LIST_MAX_COMMITS = 100, SQUASH_LIST_MAX_CHARS = 60000;
74510
75637
  var init_pr_ops = __esm(() => {
74511
75638
  init_autonomy();
74512
75639
  init_branch();
74513
75640
  init_vcs_config();
75641
+ init_workspaces();
74514
75642
  init_release_tracks();
74515
75643
  init_ops();
74516
75644
  init_rev();
@@ -74529,8 +75657,8 @@ var init_pr_ops = __esm(() => {
74529
75657
  // packages/workit-core/src/stack.ts
74530
75658
  import { spawnSync as spawnSync20 } from "node:child_process";
74531
75659
  import { createHash as createHash17, randomBytes as randomBytes3 } from "node:crypto";
74532
- import fs21 from "node:fs";
74533
- import path43 from "node:path";
75660
+ import fs22 from "node:fs";
75661
+ import path44 from "node:path";
74534
75662
  function worktreeOf(cwd, branch) {
74535
75663
  const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
74536
75664
  if (!list.ok)
@@ -74553,7 +75681,7 @@ function rebasingBranch2(dir) {
74553
75681
  if (!found.ok)
74554
75682
  continue;
74555
75683
  try {
74556
- return fs21.readFileSync(found.stdout.trim(), "utf8").trim();
75684
+ return fs22.readFileSync(found.stdout.trim(), "utf8").trim();
74557
75685
  } catch {}
74558
75686
  }
74559
75687
  return null;
@@ -74561,7 +75689,7 @@ function rebasingBranch2(dir) {
74561
75689
  function rebaseInProgress(dir) {
74562
75690
  for (const name of ["rebase-merge", "rebase-apply"]) {
74563
75691
  const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
74564
- if (found.ok && fs21.existsSync(found.stdout.trim()))
75692
+ if (found.ok && fs22.existsSync(found.stdout.trim()))
74565
75693
  return true;
74566
75694
  }
74567
75695
  return false;
@@ -74573,8 +75701,8 @@ function gitRestack(cwd, input) {
74573
75701
  const scratch = scratchDir(cwd);
74574
75702
  if (!scratch)
74575
75703
  return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
74576
- fs21.mkdirSync(scratch, { recursive: true });
74577
- dir = path43.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
75704
+ fs22.mkdirSync(scratch, { recursive: true });
75705
+ dir = path44.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74578
75706
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
74579
75707
  if (!added.ok)
74580
75708
  return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
@@ -74612,10 +75740,10 @@ function gitRestack(cwd, input) {
74612
75740
  }
74613
75741
  function pruneScratch(cwd) {
74614
75742
  const scratch = scratchDir(cwd);
74615
- if (!scratch || !fs21.existsSync(scratch))
75743
+ if (!scratch || !fs22.existsSync(scratch))
74616
75744
  return;
74617
- for (const name of fs21.readdirSync(scratch)) {
74618
- const dir = path43.join(scratch, name);
75745
+ for (const name of fs22.readdirSync(scratch)) {
75746
+ const dir = path44.join(scratch, name);
74619
75747
  if (!rebaseInProgress(dir) && !trackedDirt(dir))
74620
75748
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
74621
75749
  }
@@ -74625,10 +75753,10 @@ function stacksDir(cwd) {
74625
75753
  const root = storeRoot(cwd);
74626
75754
  if (!root.ok)
74627
75755
  return stackFail(root.code, root.error, root.unblock);
74628
- return { ok: true, data: path43.join(root.value.root, "stacks") };
75756
+ return { ok: true, data: path44.join(root.value.root, "stacks") };
74629
75757
  }
74630
75758
  function parseStack(raw) {
74631
- if (!isRecord7(raw) || !Array.isArray(raw.branches))
75759
+ if (!isRecord8(raw) || !Array.isArray(raw.branches))
74632
75760
  return null;
74633
75761
  const name = str3(raw.name);
74634
75762
  const trunk = str3(raw.trunk);
@@ -74636,12 +75764,12 @@ function parseStack(raw) {
74636
75764
  return null;
74637
75765
  const branches = [];
74638
75766
  for (const item of raw.branches) {
74639
- if (!isRecord7(item))
75767
+ if (!isRecord8(item))
74640
75768
  return null;
74641
75769
  const branch = str3(item.branch);
74642
75770
  if (!branch)
74643
75771
  return null;
74644
- const merged = isRecord7(item.merged) ? {
75772
+ const merged = isRecord8(item.merged) ? {
74645
75773
  pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
74646
75774
  mergeSha: str3(item.merged.mergeSha),
74647
75775
  at: str3(item.merged.at) ?? ""
@@ -74654,7 +75782,7 @@ function parseStack(raw) {
74654
75782
  lastParentHead: str3(item.lastParentHead),
74655
75783
  patchId: str3(item.patchId),
74656
75784
  merged,
74657
- ...isRecord7(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
75785
+ ...isRecord8(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
74658
75786
  pendingRestack: {
74659
75787
  from: str3(item.pendingRestack.from),
74660
75788
  tip: str3(item.pendingRestack.tip),
@@ -74677,10 +75805,10 @@ function readStack(cwd, name) {
74677
75805
  const dir = stacksDir(cwd);
74678
75806
  if (!dir.ok)
74679
75807
  return dir;
74680
- const file = path43.join(dir.data, fileFor(name));
75808
+ const file = path44.join(dir.data, fileFor(name));
74681
75809
  let text;
74682
75810
  try {
74683
- text = fs21.readFileSync(file, "utf8");
75811
+ text = fs22.readFileSync(file, "utf8");
74684
75812
  } catch (error) {
74685
75813
  if (error.code === "ENOENT")
74686
75814
  return { ok: true, data: null };
@@ -74698,17 +75826,17 @@ function writeStack(cwd, stack, now = new Date) {
74698
75826
  const dir = stacksDir(cwd);
74699
75827
  if (!dir.ok)
74700
75828
  return dir;
74701
- const file = path43.join(dir.data, fileFor(stack.name));
75829
+ const file = path44.join(dir.data, fileFor(stack.name));
74702
75830
  const temp = `${file}.${process.pid}.tmp`;
74703
75831
  try {
74704
- fs21.mkdirSync(dir.data, { recursive: true });
75832
+ fs22.mkdirSync(dir.data, { recursive: true });
74705
75833
  stack.updatedAt = now.toISOString();
74706
- fs21.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
75834
+ fs22.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
74707
75835
  `);
74708
- fs21.renameSync(temp, file);
75836
+ fs22.renameSync(temp, file);
74709
75837
  return { ok: true, data: undefined };
74710
75838
  } catch (error) {
74711
- fs21.rmSync(temp, { force: true });
75839
+ fs22.rmSync(temp, { force: true });
74712
75840
  return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
74713
75841
  }
74714
75842
  }
@@ -74718,7 +75846,7 @@ function listStacks(cwd) {
74718
75846
  return dir;
74719
75847
  let names = [];
74720
75848
  try {
74721
- names = fs21.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
75849
+ names = fs22.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
74722
75850
  } catch {
74723
75851
  return { ok: true, data: [] };
74724
75852
  }
@@ -74726,7 +75854,7 @@ function listStacks(cwd) {
74726
75854
  for (const name of names) {
74727
75855
  let parsed = null;
74728
75856
  try {
74729
- parsed = parseStack(JSON.parse(fs21.readFileSync(path43.join(dir.data, name), "utf8")));
75857
+ parsed = parseStack(JSON.parse(fs22.readFileSync(path44.join(dir.data, name), "utf8")));
74730
75858
  } catch {
74731
75859
  parsed = null;
74732
75860
  }
@@ -74773,7 +75901,7 @@ function stackLockStale(raw, age) {
74773
75901
  }
74774
75902
  function takeLock(lock) {
74775
75903
  try {
74776
- fs21.mkdirSync(lock);
75904
+ fs22.mkdirSync(lock);
74777
75905
  } catch (error) {
74778
75906
  if (error.code === "EEXIST")
74779
75907
  return null;
@@ -74785,24 +75913,24 @@ function takeLock(lock) {
74785
75913
  host: localLockHost(),
74786
75914
  nonce: randomBytes3(8).toString("hex")
74787
75915
  };
74788
- fs21.writeFileSync(path43.join(lock, OWNER), JSON.stringify(owner));
75916
+ fs22.writeFileSync(path44.join(lock, OWNER), JSON.stringify(owner));
74789
75917
  return owner;
74790
75918
  }
74791
75919
  function reapLock(lock, observed) {
74792
75920
  const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
74793
75921
  try {
74794
- fs21.renameSync(lock, tomb);
75922
+ fs22.renameSync(lock, tomb);
74795
75923
  } catch {
74796
75924
  return false;
74797
75925
  }
74798
75926
  if (readOwner(tomb).raw === observed) {
74799
- fs21.rmSync(tomb, { recursive: true, force: true });
75927
+ fs22.rmSync(tomb, { recursive: true, force: true });
74800
75928
  return true;
74801
75929
  }
74802
75930
  try {
74803
- fs21.renameSync(tomb, lock);
75931
+ fs22.renameSync(tomb, lock);
74804
75932
  } catch {
74805
- fs21.rmSync(tomb, { recursive: true, force: true });
75933
+ fs22.rmSync(tomb, { recursive: true, force: true });
74806
75934
  }
74807
75935
  return false;
74808
75936
  }
@@ -74812,8 +75940,8 @@ function releaseLock(lock, owner) {
74812
75940
  return;
74813
75941
  const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
74814
75942
  try {
74815
- fs21.renameSync(lock, tomb);
74816
- fs21.rmSync(tomb, { recursive: true, force: true });
75943
+ fs22.renameSync(lock, tomb);
75944
+ fs22.rmSync(tomb, { recursive: true, force: true });
74817
75945
  } catch {}
74818
75946
  }
74819
75947
  async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
@@ -74822,9 +75950,9 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74822
75950
  return dir;
74823
75951
  if (!validName(name))
74824
75952
  return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
74825
- const lock = path43.join(dir.data, `${fileFor(name)}.lock`);
75953
+ const lock = path44.join(dir.data, `${fileFor(name)}.lock`);
74826
75954
  try {
74827
- fs21.mkdirSync(dir.data, { recursive: true });
75955
+ fs22.mkdirSync(dir.data, { recursive: true });
74828
75956
  } catch (error) {
74829
75957
  return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
74830
75958
  }
@@ -74852,7 +75980,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74852
75980
  const touch = () => {
74853
75981
  try {
74854
75982
  const now = new Date;
74855
- fs21.utimesSync(path43.join(lock, OWNER), now, now);
75983
+ fs22.utimesSync(path44.join(lock, OWNER), now, now);
74856
75984
  } catch {}
74857
75985
  };
74858
75986
  const beat = setInterval(touch, HEARTBEAT_MS);
@@ -74885,7 +76013,7 @@ function deriveChain(cwd, trunk, base) {
74885
76013
  const [name, sha] = line.trim().split(" ");
74886
76014
  if (!name || !sha || name === branch || name === trunk)
74887
76015
  continue;
74888
- if (sha === head || isAncestor(cwd, sha, base) || !isAncestor(cwd, sha, head))
76016
+ if (sha === head || isAncestor2(cwd, sha, base) || !isAncestor2(cwd, sha, head))
74889
76017
  continue;
74890
76018
  const count = gitRun3(cwd, ["rev-list", "--count", `${base}..${sha}`]);
74891
76019
  below.push({ branch: name, depth: Number(count.stdout.trim()) || 0 });
@@ -74931,17 +76059,17 @@ function planStack(cwd, resolved, input, existing) {
74931
76059
  const status = resolved.forge.prStatus(before.pr);
74932
76060
  still = status.ok && status.data.state === "merged";
74933
76061
  } else
74934
- still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor(cwd, tip, trunkSha);
76062
+ still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor2(cwd, tip, trunkSha);
74935
76063
  if (still) {
74936
76064
  entries.push(before);
74937
76065
  continue;
74938
76066
  }
74939
76067
  notes.push(`${branch} was recorded as merged but is not; it is back in the stack`);
74940
76068
  }
74941
- const onParent = isAncestor(cwd, parentSha, tip);
76069
+ const onParent = isAncestor2(cwd, parentSha, tip);
74942
76070
  let builtOn = parentSha;
74943
76071
  if (!onParent) {
74944
- if (before?.lastParentHead && isAncestor(cwd, before.lastParentHead, tip))
76072
+ if (before?.lastParentHead && isAncestor2(cwd, before.lastParentHead, tip))
74945
76073
  builtOn = before.lastParentHead;
74946
76074
  else {
74947
76075
  const old = oldParentVersion(cwd, parent, parentSha, tip) ?? oldParentLookalike(cwd, parentSha, tip);
@@ -75016,7 +76144,7 @@ function planStack(cwd, resolved, input, existing) {
75016
76144
  function hasOwnCommits(cwd, tip, base) {
75017
76145
  if (!base)
75018
76146
  return false;
75019
- return tip !== base && !isAncestor(cwd, tip, base);
76147
+ return tip !== base && !isAncestor2(cwd, tip, base);
75020
76148
  }
75021
76149
  function oldParentVersion(cwd, parent, parentSha, child) {
75022
76150
  if (!safeRef2(parent))
@@ -75028,7 +76156,7 @@ function oldParentVersion(cwd, parent, parentSha, child) {
75028
76156
  `).map((line) => line.trim())) {
75029
76157
  if (!sha || sha === parentSha)
75030
76158
  continue;
75031
- if (isAncestor(cwd, sha, child) && !isAncestor(cwd, sha, parentSha))
76159
+ if (isAncestor2(cwd, sha, child) && !isAncestor2(cwd, sha, parentSha))
75032
76160
  return sha;
75033
76161
  }
75034
76162
  return null;
@@ -75162,7 +76290,7 @@ function stackStatus(ctx, stack) {
75162
76290
  pr: entry.pr,
75163
76291
  merged,
75164
76292
  head,
75165
- onParent: merged || !head || !parentSha ? null : isAncestor(ctx.cwd, parentSha, head),
76293
+ onParent: merged || !head || !parentSha ? null : isAncestor2(ctx.cwd, parentSha, head),
75166
76294
  prState: doc?.state ?? null,
75167
76295
  prBase: doc?.base ?? null,
75168
76296
  baseMatches: doc && !merged ? doc.base === parentName : null,
@@ -75225,7 +76353,7 @@ function detectMerged(ctx, cwd, stack, trunkSha, state) {
75225
76353
  merged = read.data.state === "merged";
75226
76354
  } else {
75227
76355
  const tip = branchTip(cwd, entry.branch);
75228
- merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor(cwd, tip, trunkSha);
76356
+ merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor2(cwd, tip, trunkSha);
75229
76357
  }
75230
76358
  if (merged) {
75231
76359
  entry.merged = { pr: entry.pr, mergeSha: null, at: new Date().toISOString() };
@@ -75246,7 +76374,7 @@ function pushBranch(cwd, actor, branch, priorTip) {
75246
76374
  const grant = requireGrant(cwd, "push", { forge: !plan.data.local });
75247
76375
  if (!grant.allowed)
75248
76376
  return stackFail("blocked", grant.error, grant.unblock, { reason: grant.reason });
75249
- const force = remote.sha !== null && !isAncestor(cwd, remote.sha, plan.data.sha);
76377
+ const force = remote.sha !== null && !isAncestor2(cwd, remote.sha, plan.data.sha);
75250
76378
  const expect = !force ? null : remote.sha === priorTip ? priorTip : recordedRemoteTip(cwd, plan.data) ?? null;
75251
76379
  const pushed = executePush(cwd, plan.data, { forceWithLease: force, expect, actor });
75252
76380
  if (!pushed.ok)
@@ -75323,9 +76451,9 @@ function syncStack(cwd, ctx, stack, options, actor) {
75323
76451
  carried: null
75324
76452
  };
75325
76453
  newTip = null;
75326
- } else if (!isAncestor(cwd, parentTip, tip)) {
76454
+ } else if (!isAncestor2(cwd, parentTip, tip)) {
75327
76455
  const pending = entry.pendingRestack ?? null;
75328
- const from = pending && isAncestor(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
76456
+ const from = pending && isAncestor2(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor2(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
75329
76457
  if (options.dryRun) {
75330
76458
  step.restack = { from, onto: parentTip, head: null, carried: null };
75331
76459
  newTip = null;
@@ -75373,7 +76501,7 @@ function syncStack(cwd, ctx, stack, options, actor) {
75373
76501
  }
75374
76502
  }
75375
76503
  if (!options.dryRun && newTip) {
75376
- const check = entry.pendingRestack && parentTip && isAncestor(cwd, parentTip, newTip) ? entry.pendingRestack : null;
76504
+ const check = entry.pendingRestack && parentTip && isAncestor2(cwd, parentTip, newTip) ? entry.pendingRestack : null;
75377
76505
  let forced = false;
75378
76506
  if (check && parentTip) {
75379
76507
  const before = {
@@ -75687,12 +76815,12 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75687
76815
  stderr: run.error ? String(run.error.message) : run.stderr ?? "",
75688
76816
  timedOut: failure?.code === "ETIMEDOUT"
75689
76817
  };
75690
- }, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun3(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
76818
+ }, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor2 = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun3(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
75691
76819
  const status = gitRun3(dir, ["status", "--porcelain", "--untracked-files=no"]);
75692
76820
  return !status.ok || status.stdout.trim() !== "";
75693
76821
  }, scratchDir = (cwd) => {
75694
76822
  const dir = stacksDir(cwd);
75695
- return dir.ok ? path43.join(dir.data, "worktrees") : null;
76823
+ return dir.ok ? path44.join(dir.data, "worktrees") : null;
75696
76824
  }, firstLine = (run) => (`${run.stderr}
75697
76825
  ${run.stdout}`.split(`
75698
76826
  `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash17("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
@@ -75704,17 +76832,17 @@ ${run.stdout}`.split(`
75704
76832
  return false;
75705
76833
  }
75706
76834
  return true;
75707
- }, isRecord7 = (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) => {
76835
+ }, 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) => {
75708
76836
  let raw = null;
75709
76837
  try {
75710
- raw = fs21.readFileSync(path43.join(lock, OWNER), "utf8");
76838
+ raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
75711
76839
  } catch {
75712
76840
  raw = null;
75713
76841
  }
75714
76842
  let age = null;
75715
- for (const target of [path43.join(lock, OWNER), lock])
76843
+ for (const target of [path44.join(lock, OWNER), lock])
75716
76844
  try {
75717
- age = Date.now() - fs21.statSync(target).mtimeMs;
76845
+ age = Date.now() - fs22.statSync(target).mtimeMs;
75718
76846
  break;
75719
76847
  } catch {}
75720
76848
  return { raw, age };
@@ -75931,16 +77059,16 @@ var init_pr_edit = __esm(() => {
75931
77059
 
75932
77060
  // packages/workit-core/src/forge/exec.ts
75933
77061
  import { spawnSync as spawnSync22 } from "node:child_process";
75934
- import fs22 from "node:fs";
75935
- import path44 from "node:path";
77062
+ import fs23 from "node:fs";
77063
+ import path45 from "node:path";
75936
77064
  function findExecutable(bin, env) {
75937
- const dirs = (env.PATH ?? env.Path ?? "").split(path44.delimiter).filter(Boolean);
77065
+ const dirs = (env.PATH ?? env.Path ?? "").split(path45.delimiter).filter(Boolean);
75938
77066
  const exts = process.platform === "win32" ? [".exe"] : [""];
75939
77067
  for (const dir of dirs)
75940
77068
  for (const ext of exts) {
75941
- const candidate = path44.join(dir, `${bin}${ext}`);
77069
+ const candidate = path45.join(dir, `${bin}${ext}`);
75942
77070
  try {
75943
- const stat = fs22.statSync(candidate);
77071
+ const stat = fs23.statSync(candidate);
75944
77072
  if (stat.isFile())
75945
77073
  return candidate;
75946
77074
  } catch {}
@@ -75972,7 +77100,7 @@ function systemRunner(kind, apiHost, env = process.env) {
75972
77100
  timeout: options.timeoutMs,
75973
77101
  killSignal: "SIGKILL",
75974
77102
  windowsHide: true,
75975
- stdio: ["ignore", "pipe", "pipe"]
77103
+ ...options.input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }
75976
77104
  });
75977
77105
  const error = result.error;
75978
77106
  return {
@@ -76048,8 +77176,8 @@ function apiText(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.lo
76048
77176
  return cliFailure(bin, apiHost, run, what, timeoutMs);
76049
77177
  return { ok: true, data: run.stdout };
76050
77178
  }
76051
- function apiWrite(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api) {
76052
- const run = runner(bin, args, { timeoutMs });
77179
+ function apiWrite(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api, input) {
77180
+ const run = runner(bin, args, input === undefined ? { timeoutMs } : { timeoutMs, input });
76053
77181
  if (run.status !== 0) {
76054
77182
  const text = redactText(`${run.stderr}
76055
77183
  ${run.stdout}`).trim();
@@ -76238,8 +77366,11 @@ function createGitHubForge(options) {
76238
77366
  if (!data.ok)
76239
77367
  return data;
76240
77368
  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];
77369
+ 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));
77370
+ const open = ours.filter((node) => node.state === "OPEN");
77371
+ if (!match.base && new Set(open.map((node) => node.baseRefName)).size > 1)
77372
+ 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}`);
77373
+ const pick = open[0] ?? ours[0];
76243
77374
  if (!pick)
76244
77375
  return success2(null);
76245
77376
  const ref = {
@@ -76348,16 +77479,19 @@ function createGitHubForge(options) {
76348
77479
  });
76349
77480
  },
76350
77481
  merge(number, mergeOptions) {
77482
+ const squash = mergeOptions.method === "squash" ? mergeOptions.squashMessage : undefined;
76351
77483
  const merged = apiWrite(runner, "gh", apiHost, [
76352
77484
  "api",
76353
77485
  "-X",
76354
77486
  "PUT",
76355
77487
  `repos/${repo}/pulls/${number}/merge`,
76356
- "-f",
76357
- `sha=${mergeOptions.sha}`,
76358
- "-f",
76359
- `merge_method=${mergeOptions.method}`
76360
- ], `merge of pull request #${number}`);
77488
+ ...squash ? ["--input", "-"] : ["-f", `sha=${mergeOptions.sha}`, "-f", `merge_method=${mergeOptions.method}`]
77489
+ ], `merge of pull request #${number}`, undefined, squash ? JSON.stringify({
77490
+ sha: mergeOptions.sha,
77491
+ merge_method: mergeOptions.method,
77492
+ commit_title: squash.subject,
77493
+ commit_message: squash.body
77494
+ }) : undefined);
76361
77495
  if (!merged.ok)
76362
77496
  return merged;
76363
77497
  if (merged.data.merged === false)
@@ -76459,7 +77593,7 @@ function createGitHubForge(options) {
76459
77593
  }
76460
77594
  };
76461
77595
  }
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("/");
77596
+ 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
77597
  var init_github = __esm(() => {
76464
77598
  init_exec();
76465
77599
  init_redact();
@@ -76576,8 +77710,11 @@ function createGitLabForge(options) {
76576
77710
  return list;
76577
77711
  if (!Array.isArray(list.data))
76578
77712
  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];
77713
+ 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));
77714
+ const open = matches.filter((mr) => mr.state === "opened");
77715
+ if (!match.base && new Set(open.map((mr) => mr.target_branch)).size > 1)
77716
+ 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}`);
77717
+ const pick = open[0] ?? matches[0];
76581
77718
  return success2(pick ? {
76582
77719
  number: pick.iid,
76583
77720
  url: pick.web_url,
@@ -76712,6 +77849,7 @@ function createGitLabForge(options) {
76712
77849
  });
76713
77850
  },
76714
77851
  merge(iid, mergeOptions) {
77852
+ const squash = mergeOptions.method === "squash" ? mergeOptions.squashMessage : undefined;
76715
77853
  if (mergeOptions.method === "rebase")
76716
77854
  return failure3("invalid_input", "GitLab merges with the project's merge method; pass --method squash or merge");
76717
77855
  const merged = apiWrite(runner, "glab", apiHost, [
@@ -76719,11 +77857,19 @@ function createGitLabForge(options) {
76719
77857
  "-X",
76720
77858
  "PUT",
76721
77859
  `${project}/merge_requests/${iid}/merge`,
76722
- "-f",
76723
- `sha=${mergeOptions.sha}`,
76724
- "-F",
76725
- `squash=${mergeOptions.method === "squash" ? "true" : "false"}`
76726
- ], `merge of merge request !${iid}`);
77860
+ ...squash ? ["--input", "-"] : [
77861
+ "-f",
77862
+ `sha=${mergeOptions.sha}`,
77863
+ "-F",
77864
+ `squash=${mergeOptions.method === "squash" ? "true" : "false"}`
77865
+ ]
77866
+ ], `merge of merge request !${iid}`, undefined, squash ? JSON.stringify({
77867
+ sha: mergeOptions.sha,
77868
+ squash: true,
77869
+ squash_commit_message: squash.body ? `${squash.subject}
77870
+
77871
+ ${squash.body}` : squash.subject
77872
+ }) : undefined);
76727
77873
  if (!merged.ok)
76728
77874
  return merged;
76729
77875
  const value = merged.data;
@@ -76868,7 +78014,7 @@ var init_gitlab = __esm(() => {
76868
78014
  });
76869
78015
 
76870
78016
  // packages/workit-core/src/forge/resolve.ts
76871
- import fs23 from "node:fs";
78017
+ import fs24 from "node:fs";
76872
78018
  function memo4(scope, key, compute, now = Date.now) {
76873
78019
  let entries = memoScopes.get(scope.owner);
76874
78020
  if (!entries)
@@ -76913,7 +78059,7 @@ function resolveCredential(workspace, derived, run, scope) {
76913
78059
  return failure3("blocked", `token_host_unverified: push host ${derived.host} only looks like ${kind}; workspace ${workspace?.name ?? "?"} vcs.tokenFile is not sent to it`, `if ${apiHost} is your ${kind} server, set ${kind}.host in ~/.config/workit/vcs.json (or vcs.host for the workspace); otherwise fix the push remote`);
76914
78060
  let token = "";
76915
78061
  try {
76916
- token = fs23.readFileSync(tokenFile.trim(), "utf8").trim();
78062
+ token = fs24.readFileSync(tokenFile.trim(), "utf8").trim();
76917
78063
  } catch {
76918
78064
  return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
76919
78065
  }
@@ -77111,16 +78257,16 @@ function parseDuration2(value) {
77111
78257
  return null;
77112
78258
  let total = 0;
77113
78259
  for (const [, amount, unit] of parts) {
77114
- const factor = UNITS.find(([pattern]) => pattern.test(unit))?.[1];
78260
+ const factor = UNITS2.find(([pattern]) => pattern.test(unit))?.[1];
77115
78261
  if (factor === undefined)
77116
78262
  return null;
77117
78263
  total += Number(amount) * factor;
77118
78264
  }
77119
78265
  return Math.round(total);
77120
78266
  }
77121
- var UNITS;
78267
+ var UNITS2;
77122
78268
  var init_duration = __esm(() => {
77123
- UNITS = [
78269
+ UNITS2 = [
77124
78270
  [/^(?:ms|milliseconds?)$/u, 1],
77125
78271
  [/^(?:s|secs?|seconds?)$/u, 1000],
77126
78272
  [/^(?:m|mins?|minutes?)$/u, 60000],
@@ -77285,11 +78431,11 @@ __export(exports_pr, {
77285
78431
  run: () => run11
77286
78432
  });
77287
78433
  import { readFileSync as readFileSync21 } from "node:fs";
77288
- import path45 from "node:path";
78434
+ import path46 from "node:path";
77289
78435
  function readBodyFile(io, file) {
77290
78436
  try {
77291
78437
  return {
77292
- body: file === "-" ? forgeDeps.readStdin() : readFileSync21(path45.resolve(io.cwd, file), "utf8")
78438
+ body: file === "-" ? forgeDeps.readStdin() : readFileSync21(path46.resolve(io.cwd, file), "utf8")
77293
78439
  };
77294
78440
  } catch {
77295
78441
  return `cannot read --body-file ${file}`;
@@ -77388,8 +78534,18 @@ async function create4(argv, io) {
77388
78534
  if (flags.values.base !== undefined && flags.values.track !== undefined)
77389
78535
  return usage3(io, "pass --base or --track, not both (--track picks the base for you)", CREATE_USAGE);
77390
78536
  let base = flags.values.base ?? null;
78537
+ const branch = currentBranch(io.cwd);
78538
+ const kind = branchKindOf(branch);
77391
78539
  let releaseTrack = null;
77392
- if (!base) {
78540
+ let route = null;
78541
+ if (base) {
78542
+ const flow = gitFlowOf(io.cwd);
78543
+ const mismatch = flowMismatch(flow, kind, base, "target");
78544
+ if (mismatch && flow && kind)
78545
+ return emit(io, fail("blocked", mismatch, {
78546
+ unblock: `workit pr create … # without --base it targets ${flowRoute(flow, kind).target}`
78547
+ }));
78548
+ } else {
77393
78549
  const resolved = vcsConfig("resolve", io.cwd, { track: flags.values.track ?? null });
77394
78550
  if (resolved.ok === false)
77395
78551
  return emit(io, fail("blocked", String(resolved.error), {
@@ -77401,10 +78557,13 @@ async function create4(argv, io) {
77401
78557
  unblock: "workit pr create --track <name> … # or --base <branch>"
77402
78558
  }));
77403
78559
  base = String(resolved.defaultTargetBranch ?? "") || null;
78560
+ const flow = releaseTrack?.track ? null : gitFlowOf(io.cwd, base);
78561
+ route = flow && kind ? flowRoute(flow, kind) : null;
78562
+ if (route)
78563
+ base = route.target;
77404
78564
  }
77405
78565
  if (!base)
77406
78566
  return usage3(io, "no default target branch is configured; pass --base <b>", CREATE_USAGE);
77407
- const branch = currentBranch(io.cwd);
77408
78567
  const connected = connect(io, branch);
77409
78568
  if (!connected.ok)
77410
78569
  return forgeFail(io, connected);
@@ -77431,11 +78590,18 @@ async function create4(argv, io) {
77431
78590
  }, forgeDeps.sleep);
77432
78591
  if (!result.ok)
77433
78592
  return forgeFail(io, result);
77434
- const created = releaseTrack ? { ...result.data, releaseTrack } : result.data;
78593
+ const created = {
78594
+ ...result.data,
78595
+ ...releaseTrack ? { releaseTrack } : {},
78596
+ ...route ? { flow: route } : {}
78597
+ };
77435
78598
  return emit(io, ok(created), (data) => [
77436
78599
  `${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
78600
  ...data.releaseTrack?.name ? [`release track: ${data.releaseTrack.name} (${data.releaseTrack.detail})`] : [],
77438
78601
  ...(data.releaseTrack?.warnings ?? []).map((warning) => `note: ${warning}`),
78602
+ ...route?.mergeBack ? [
78603
+ `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>`
78604
+ ] : [],
77439
78605
  `head ${data.head.slice(0, 12)} verified on the forge`,
77440
78606
  ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : [],
77441
78607
  `next: ${data.next}`
@@ -77447,7 +78613,10 @@ async function merge3(argv, io) {
77447
78613
  method: "value",
77448
78614
  "delete-branch": "boolean",
77449
78615
  unverified: "boolean",
77450
- reason: "value"
78616
+ reason: "value",
78617
+ subject: "value",
78618
+ body: "value",
78619
+ "body-file": "value"
77451
78620
  });
77452
78621
  if (typeof flags === "string")
77453
78622
  return usage3(io, flags, MERGE_USAGE);
@@ -77462,6 +78631,21 @@ async function merge3(argv, io) {
77462
78631
  const method = flags.values.method ?? "squash";
77463
78632
  if (!METHODS2.has(method))
77464
78633
  return usage3(io, "--method must be squash, merge or rebase", MERGE_USAGE);
78634
+ const { subject } = flags.values;
78635
+ if (flags.values.body !== undefined && flags.values["body-file"] !== undefined)
78636
+ return usage3(io, "pass --body or --body-file, not both", MERGE_USAGE);
78637
+ let body = flags.values.body;
78638
+ if (flags.values["body-file"] !== undefined) {
78639
+ const read = readBodyFile(io, flags.values["body-file"]);
78640
+ if (typeof read === "string")
78641
+ return usage3(io, read, MERGE_USAGE);
78642
+ body = read.body;
78643
+ }
78644
+ if ((subject !== undefined || body !== undefined) && method !== "squash")
78645
+ return usage3(io, "--subject and --body set a squash message; use --method squash", MERGE_USAGE);
78646
+ if (subject !== undefined && (!subject.trim() || subject.includes(`
78647
+ `)))
78648
+ return usage3(io, "--subject must be one non-empty line", MERGE_USAGE);
77465
78649
  const connected = connect(io);
77466
78650
  if (!connected.ok)
77467
78651
  return forgeFail(io, connected);
@@ -77470,7 +78654,9 @@ async function merge3(argv, io) {
77470
78654
  method,
77471
78655
  deleteBranch: flags.booleans.has("delete-branch"),
77472
78656
  actor: actorFromEnv(io.env),
77473
- ...unverified ? { unverified } : {}
78657
+ ...unverified ? { unverified } : {},
78658
+ ...subject !== undefined ? { subject } : {},
78659
+ ...body !== undefined ? { body } : {}
77474
78660
  }, forgeDeps.sleep);
77475
78661
  if (!result.ok)
77476
78662
  return emit(io, fail(result.code, result.error, {
@@ -77634,13 +78820,14 @@ async function run11(argv, io) {
77634
78820
  return reply(rest, io);
77635
78821
  return usage3(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", USAGE4);
77636
78822
  }
77637
- var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", CREATE_USAGE = "workit pr create [--base <b> | --track <t>] (--title <t> [--body <text> | --body-file <f|->] | --fill) [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--delete-branch] [--unverified --reason <why>] [--json]", READY_USAGE = "workit pr ready [--pr <n>] [--undo] [--json]", EDIT_USAGE = "workit pr edit [--pr <n>] [--title <t>] [--body-file <f|->] [--add-label <l>]… [--remove-label <l>]… [--add-reviewer <login>]… [--base <b>] [--json]", THREADS_USAGE = "workit pr threads [--pr <n>] [--json]", REPLY_USAGE = "workit pr reply [--pr <n>] --thread <id> [--body-file <f|->] [--resolve] [--json]", USAGE4 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}`, METHODS2;
78823
+ var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", CREATE_USAGE = "workit pr create [--base <b> | --track <t>] (--title <t> [--body <text> | --body-file <f|->] | --fill) [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch] [--unverified --reason <why>] [--json]", READY_USAGE = "workit pr ready [--pr <n>] [--undo] [--json]", EDIT_USAGE = "workit pr edit [--pr <n>] [--title <t>] [--body-file <f|->] [--add-label <l>]… [--remove-label <l>]… [--add-reviewer <login>]… [--base <b>] [--json]", THREADS_USAGE = "workit pr threads [--pr <n>] [--json]", REPLY_USAGE = "workit pr reply [--pr <n>] --thread <id> [--body-file <f|->] [--resolve] [--json]", USAGE4 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}`, METHODS2;
77638
78824
  var init_pr = __esm(() => {
77639
78825
  init_pr_body();
77640
78826
  init_pr_edit();
77641
78827
  init_pr_ops();
77642
78828
  init_report();
77643
78829
  init_rev();
78830
+ init_branch();
77644
78831
  init_ledger();
77645
78832
  init_vcs_config();
77646
78833
  init_output();
@@ -77871,7 +79058,18 @@ __export(exports_git, {
77871
79058
  run: () => run13
77872
79059
  });
77873
79060
  import { readFileSync as readFileSync22 } from "node:fs";
77874
- import path46 from "node:path";
79061
+ import path47 from "node:path";
79062
+ function kindHint(cwd, kind) {
79063
+ let kinds = [...BRANCH_KINDS];
79064
+ try {
79065
+ kinds = allowedKinds(branchPolicySnapshotFor(cwd).namePolicy.allowed);
79066
+ } catch {}
79067
+ const spellable = kinds.filter((name) => BRANCH_KINDS.includes(name));
79068
+ if (spellable.length)
79069
+ return `did you mean --kind ${suggestBranchName(`${kind}/x`, spellable)?.split("/")[0]}?`;
79070
+ const named = suggestBranchName(`${kind}/<slug>`, kinds);
79071
+ 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>`;
79072
+ }
77875
79073
  async function branch(argv, io) {
77876
79074
  const flags = parseFlags(argv, {
77877
79075
  base: "value",
@@ -77887,8 +79085,8 @@ async function branch(argv, io) {
77887
79085
  return usage3(io, `unexpected argument ${flags.positionals[1]}`, BRANCH_USAGE);
77888
79086
  if (kind === undefined !== (slug === undefined))
77889
79087
  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);
79088
+ if (kind !== undefined && !BRANCH_KINDS.includes(kind))
79089
+ return usage3(io, `--kind must be ${BRANCH_KINDS.join(", ")}; ${kindHint(io.cwd, kind)}`, BRANCH_USAGE);
77892
79090
  if (kind !== undefined && flags.positionals.length)
77893
79091
  return usage3(io, "pass a name or --kind/--slug, not both", BRANCH_USAGE);
77894
79092
  if (kind === undefined && !flags.positionals[0])
@@ -77911,6 +79109,7 @@ function parseCommit(argv) {
77911
79109
  const out = {
77912
79110
  messages: [],
77913
79111
  files: [],
79112
+ trailers: [],
77914
79113
  all: false,
77915
79114
  amend: false,
77916
79115
  noEdit: false,
@@ -77932,14 +79131,15 @@ function parseCommit(argv) {
77932
79131
  }
77933
79132
  const valued = [
77934
79133
  ["-m", "--message", out.messages],
77935
- ["-F", "--file", out.files]
77936
- ].find(([shortName, longName]) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`) || arg.startsWith(shortName) && arg.length > 2);
79134
+ ["-F", "--file", out.files],
79135
+ [null, "--trailer", out.trailers]
79136
+ ].find(([shortName, longName]) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`) || shortName !== null && arg.startsWith(shortName) && arg.length > 2);
77937
79137
  if (valued) {
77938
79138
  const [shortName, longName, into] = valued;
77939
79139
  if (arg === shortName || arg === longName) {
77940
79140
  const value = argv[++index];
77941
79141
  if (value === undefined)
77942
- return `${arg} requires a ${shortName === "-m" ? "message" : "file"}`;
79142
+ return `${arg} requires a ${shortName === "-m" ? "message" : shortName === "-F" ? "file" : "Key: value trailer"}`;
77943
79143
  into.push(value);
77944
79144
  } else
77945
79145
  into.push(arg.startsWith(`${longName}=`) ? arg.slice(longName.length + 1) : arg.slice(2));
@@ -77962,7 +79162,7 @@ function commitMessage(parsed, io) {
77962
79162
 
77963
79163
  `);
77964
79164
  try {
77965
- return file === "-" ? readFileSync22(0, "utf8") : readFileSync22(path46.resolve(io.cwd, file), "utf8");
79165
+ return file === "-" ? readFileSync22(0, "utf8") : readFileSync22(path47.resolve(io.cwd, file), "utf8");
77966
79166
  } catch (error) {
77967
79167
  return {
77968
79168
  error: `cannot read -F ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -77993,6 +79193,7 @@ async function commit(argv, io) {
77993
79193
  paths: parsed.paths,
77994
79194
  amend: parsed.amend,
77995
79195
  allowEmpty: parsed.allowEmpty,
79196
+ trailers: parsed.trailers,
77996
79197
  actor: actorFromEnv(io.env),
77997
79198
  env: io.env
77998
79199
  });
@@ -78016,10 +79217,17 @@ async function push(argv, io) {
78016
79217
  "set-upstream": "boolean",
78017
79218
  "force-with-lease": "boolean",
78018
79219
  "overwrite-unintegrated": "boolean",
78019
- expect: "value"
79220
+ expect: "value",
79221
+ release: "list"
78020
79222
  });
78021
79223
  if (typeof flags === "string")
78022
79224
  return usage3(io, flags, PUSH_USAGE);
79225
+ const release = flags.lists.release;
79226
+ if (release) {
79227
+ if (flags.booleans.size || flags.values.expect !== undefined || flags.positionals.length)
79228
+ return usage3(io, "--release takes only ref names (repeat it: --release main --release v1.2.0)", PUSH_USAGE);
79229
+ return releasePush(release, io);
79230
+ }
78023
79231
  if (flags.positionals.length)
78024
79232
  return usage3(io, `unexpected argument ${flags.positionals[0]} (workit pushes the current branch)`, PUSH_USAGE);
78025
79233
  const expect = flags.values.expect ?? null;
@@ -78065,6 +79273,46 @@ async function push(argv, io) {
78065
79273
  ...data.upstream ? [`upstream: ${data.remote}/${data.branch}`] : []
78066
79274
  ]);
78067
79275
  }
79276
+ async function releasePush(names, io) {
79277
+ const plan = releasePushPreflight(io.cwd, names);
79278
+ if (!plan.ok)
79279
+ return forgeFail(io, plan);
79280
+ const release = requireGrant(io.cwd, "release");
79281
+ if (!release.allowed)
79282
+ return emit(io, fail("blocked", release.error, {
79283
+ unblock: release.unblock,
79284
+ data: { reason: release.reason }
79285
+ }));
79286
+ const firstBranch = plan.data.refs.find((ref) => ref.kind === "branch")?.name;
79287
+ const identity = plan.data.local ? null : pushIdentity(io, firstBranch ?? plan.data.refs[0].name);
79288
+ if (identity && !identity.ok)
79289
+ return forgeFail(io, identity);
79290
+ const checked = identity?.data ?? null;
79291
+ const pushGrant = requireGrant(io.cwd, "push", { forge: checked?.forge != null });
79292
+ if (!pushGrant.allowed)
79293
+ return emit(io, fail("blocked", pushGrant.error, {
79294
+ unblock: pushGrant.unblock,
79295
+ data: { reason: pushGrant.reason }
79296
+ }));
79297
+ const result = executeReleasePush(io.cwd, plan.data, {
79298
+ actor: actorFromEnv(io.env),
79299
+ grant: release.source,
79300
+ ...checked ? { identity: { ...checked } } : {}
79301
+ });
79302
+ if (!result.ok)
79303
+ return forgeFail(io, result);
79304
+ return emit(io, ok({ ...result.data, identity: checked }), (data) => [
79305
+ ...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`),
79306
+ ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : []
79307
+ ]);
79308
+ }
79309
+ function trailers(argv, io) {
79310
+ const extra = argv.find((arg) => arg !== "--json");
79311
+ if (extra !== undefined)
79312
+ return usage3(io, `unexpected argument ${extra}`, "workit git trailers");
79313
+ const state = workspaceTrailers(io.cwd);
79314
+ return emit(io, ok(state), (data) => data.enabled ? "on" : "off");
79315
+ }
78068
79316
  async function run13(argv, io) {
78069
79317
  const [sub, ...rest] = argv;
78070
79318
  if (sub === "branch")
@@ -78073,12 +79321,16 @@ async function run13(argv, io) {
78073
79321
  return commit(rest, io);
78074
79322
  if (sub === "push")
78075
79323
  return push(rest, io);
79324
+ if (sub === "trailers")
79325
+ return trailers(rest, io);
78076
79326
  return usage3(io, sub && !sub.startsWith("-") ? `unknown git subcommand "${sub}"` : "missing git subcommand", USAGE5);
78077
79327
  }
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;
79328
+ 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|-> [--trailer <key: value>]… [--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|trailers ... (workit help git)", short2 = (sha) => sha ? sha.slice(0, 12) : "(none)", COMMIT_SWITCHES;
78079
79329
  var init_git2 = __esm(() => {
78080
79330
  init_autonomy();
79331
+ init_branch();
78081
79332
  init_ops();
79333
+ init_workspaces();
78082
79334
  init_ledger();
78083
79335
  init_implicit_task();
78084
79336
  init_output();
@@ -78629,8 +79881,8 @@ var init_stack2 = __esm(() => {
78629
79881
  // packages/workit-core/src/fanout.ts
78630
79882
  import { spawnSync as spawnSync24 } from "node:child_process";
78631
79883
  import { createHash as createHash18 } from "node:crypto";
78632
- import fs24 from "node:fs";
78633
- import path47 from "node:path";
79884
+ import fs25 from "node:fs";
79885
+ import path48 from "node:path";
78634
79886
  function expandBraces(pattern, limit = 64) {
78635
79887
  for (let index = 0;index < pattern.length; index += 1) {
78636
79888
  if (pattern[index] === "\\") {
@@ -78776,7 +80028,7 @@ function inScope(plan, slice, file) {
78776
80028
  }
78777
80029
  function parsePlan(raw, defaults, name) {
78778
80030
  const problems = [];
78779
- if (!isRecord8(raw))
80031
+ if (!isRecord9(raw))
78780
80032
  return { ok: false, problems: ["the plan must be a JSON object"] };
78781
80033
  const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
78782
80034
  if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
@@ -78799,8 +80051,8 @@ function parsePlan(raw, defaults, name) {
78799
80051
  const seenIds = new Set;
78800
80052
  const seenBranches = new Map;
78801
80053
  raw.slices.forEach((item, index) => {
78802
- const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
78803
- if (!isRecord8(item)) {
80054
+ const where = isRecord9(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80055
+ if (!isRecord9(item)) {
78804
80056
  problems.push(`${where}: must be an object`);
78805
80057
  return;
78806
80058
  }
@@ -78864,7 +80116,7 @@ function parsePlan(raw, defaults, name) {
78864
80116
  id,
78865
80117
  branch,
78866
80118
  base: optional("base") ?? "",
78867
- worktree: path47.resolve(defaults.root, optional("worktree") ?? path47.join(defaults.worktreeRoot, id)),
80119
+ worktree: path48.resolve(defaults.root, optional("worktree") ?? path48.join(defaults.worktreeRoot, id)),
78868
80120
  tier,
78869
80121
  dependsOn: [...new Set(dependsOn ?? [])],
78870
80122
  scope,
@@ -79093,11 +80345,11 @@ function defaultTrunk(cwd) {
79093
80345
  }
79094
80346
  function mainCheckout(cwd) {
79095
80347
  const common = gitCommonDir(cwd);
79096
- return common && path47.basename(common) === ".git" ? path47.dirname(common) : path47.resolve(cwd);
80348
+ return common && path48.basename(common) === ".git" ? path48.dirname(common) : path48.resolve(cwd);
79097
80349
  }
79098
80350
  function worktreeRoot(cwd) {
79099
80351
  const main = mainCheckout(cwd);
79100
- return path47.join(path47.dirname(main), `${path47.basename(main)}-wt`);
80352
+ return path48.join(path48.dirname(main), `${path48.basename(main)}-wt`);
79101
80353
  }
79102
80354
  function sliceHash(slice) {
79103
80355
  const {
@@ -79115,10 +80367,10 @@ function fanoutsDir(cwd) {
79115
80367
  const root = storeRoot(cwd);
79116
80368
  if (!root.ok)
79117
80369
  return fanoutFail(root.code, root.error, root.unblock);
79118
- return { ok: true, data: path47.join(root.value.root, "fanouts") };
80370
+ return { ok: true, data: path48.join(root.value.root, "fanouts") };
79119
80371
  }
79120
80372
  function parseStored(raw) {
79121
- if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
80373
+ if (!isRecord9(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
79122
80374
  return null;
79123
80375
  if (!Array.isArray(raw.slices))
79124
80376
  return null;
@@ -79135,7 +80387,7 @@ function parseStored(raw) {
79135
80387
  });
79136
80388
  const slices = [];
79137
80389
  for (const item of raw.slices) {
79138
- if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
80390
+ if (!isRecord9(item) || typeof item.id !== "string" || typeof item.branch !== "string")
79139
80391
  return null;
79140
80392
  slices.push({
79141
80393
  id: item.id,
@@ -79181,10 +80433,10 @@ function readFanout(cwd, name) {
79181
80433
  const dir = fanoutsDir(cwd);
79182
80434
  if (!dir.ok)
79183
80435
  return dir;
79184
- const file = path47.join(dir.data, stackFileName(name));
80436
+ const file = path48.join(dir.data, stackFileName(name));
79185
80437
  let parsed = null;
79186
80438
  try {
79187
- parsed = parseStored(JSON.parse(fs24.readFileSync(file, "utf8")));
80439
+ parsed = parseStored(JSON.parse(fs25.readFileSync(file, "utf8")));
79188
80440
  } catch (error) {
79189
80441
  if (error.code === "ENOENT")
79190
80442
  return { ok: true, data: null };
@@ -79198,7 +80450,7 @@ function listFanouts(cwd) {
79198
80450
  return dir;
79199
80451
  let names = [];
79200
80452
  try {
79201
- names = fs24.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
80453
+ names = fs25.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
79202
80454
  } catch {
79203
80455
  return { ok: true, data: [] };
79204
80456
  }
@@ -79206,7 +80458,7 @@ function listFanouts(cwd) {
79206
80458
  for (const entry of names) {
79207
80459
  let parsed = null;
79208
80460
  try {
79209
- parsed = parseStored(JSON.parse(fs24.readFileSync(path47.join(dir.data, entry), "utf8")));
80461
+ parsed = parseStored(JSON.parse(fs25.readFileSync(path48.join(dir.data, entry), "utf8")));
79210
80462
  } catch {
79211
80463
  parsed = null;
79212
80464
  }
@@ -79236,16 +80488,16 @@ function writeFanout(cwd, plan) {
79236
80488
  const dir = fanoutsDir(cwd);
79237
80489
  if (!dir.ok)
79238
80490
  return dir;
79239
- const file = path47.join(dir.data, stackFileName(plan.name));
80491
+ const file = path48.join(dir.data, stackFileName(plan.name));
79240
80492
  const temp = `${file}.${process.pid}.tmp`;
79241
80493
  try {
79242
- fs24.mkdirSync(dir.data, { recursive: true });
79243
- fs24.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
80494
+ fs25.mkdirSync(dir.data, { recursive: true });
80495
+ fs25.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
79244
80496
  `);
79245
- fs24.renameSync(temp, file);
80497
+ fs25.renameSync(temp, file);
79246
80498
  return { ok: true, data: file };
79247
80499
  } catch (error) {
79248
- fs24.rmSync(temp, { force: true });
80500
+ fs25.rmSync(temp, { force: true });
79249
80501
  return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
79250
80502
  }
79251
80503
  }
@@ -79258,7 +80510,7 @@ function planFanout(cwd, input, now = new Date) {
79258
80510
  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");
79259
80511
  if (input.takeLead && isDelegateSession(session))
79260
80512
  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");
79261
- const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
80513
+ const parsed = parsePlan(input.trunk && isRecord9(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
79262
80514
  trunk: input.trunk ?? defaultTrunk(cwd),
79263
80515
  root: mainCheckout(cwd),
79264
80516
  worktreeRoot: worktreeRoot(cwd)
@@ -79381,7 +80633,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
79381
80633
  error,
79382
80634
  ...unblock ? { unblock } : {},
79383
80635
  ...data ? { data } : {}
79384
- }), 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)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER3, filled = (text) => text.trim() !== "" && !PLACEHOLDER3.test(text.trim()), textList = (value) => {
80636
+ }), 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) => {
79385
80637
  if (typeof value === "string")
79386
80638
  return [value];
79387
80639
  if (Array.isArray(value) && value.every((item) => typeof item === "string"))
@@ -79439,8 +80691,8 @@ var init_fanout = __esm(() => {
79439
80691
 
79440
80692
  // packages/workit-core/src/fanout-worktree.ts
79441
80693
  import { randomBytes as randomBytes4 } from "node:crypto";
79442
- import fs25 from "node:fs";
79443
- import path48 from "node:path";
80694
+ import fs26 from "node:fs";
80695
+ import path49 from "node:path";
79444
80696
  function rowSample(lines) {
79445
80697
  const out = [];
79446
80698
  let bytes = 0;
@@ -79500,7 +80752,7 @@ function adminDir(worktree) {
79500
80752
  const file = dir.stdout.trim();
79501
80753
  let token = null;
79502
80754
  try {
79503
- token = fs25.readFileSync(path48.join(file, TOKEN_FILE), "utf8").trim() || null;
80755
+ token = fs26.readFileSync(path49.join(file, TOKEN_FILE), "utf8").trim() || null;
79504
80756
  } catch {
79505
80757
  token = null;
79506
80758
  }
@@ -79508,7 +80760,7 @@ function adminDir(worktree) {
79508
80760
  const stat = worktreeDeps.stat(file);
79509
80761
  return {
79510
80762
  path: file,
79511
- id: path48.basename(file),
80763
+ id: path49.basename(file),
79512
80764
  ino: stat.ino.toString(),
79513
80765
  token,
79514
80766
  birthMs: stat.birthtimeNs > 0n ? Number(stat.birthtimeMs) : null,
@@ -79524,7 +80776,7 @@ function markAdminDir(worktree) {
79524
80776
  return null;
79525
80777
  const token = randomBytes4(16).toString("hex");
79526
80778
  try {
79527
- fs25.writeFileSync(path48.join(admin.path, TOKEN_FILE), `${token}
80779
+ fs26.writeFileSync(path49.join(admin.path, TOKEN_FILE), `${token}
79528
80780
  `);
79529
80781
  } catch {
79530
80782
  return null;
@@ -79539,25 +80791,25 @@ function excludeScratch(cwd) {
79539
80791
  const common = gitCommonDir(cwd);
79540
80792
  if (!common)
79541
80793
  return;
79542
- const file = path48.join(common, "info", "exclude");
80794
+ const file = path49.join(common, "info", "exclude");
79543
80795
  let text = "";
79544
80796
  try {
79545
- text = fs25.readFileSync(file, "utf8");
80797
+ text = fs26.readFileSync(file, "utf8");
79546
80798
  } catch {
79547
80799
  text = "";
79548
80800
  }
79549
80801
  if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
79550
80802
  return;
79551
- fs25.mkdirSync(path48.dirname(file), { recursive: true });
79552
- fs25.appendFileSync(file, `${text && !text.endsWith(`
80803
+ fs26.mkdirSync(path49.dirname(file), { recursive: true });
80804
+ fs26.appendFileSync(file, `${text && !text.endsWith(`
79553
80805
  `) ? `
79554
80806
  ` : ""}# workit fanout: per-worker scratch dirs
79555
80807
  ${EXCLUDE_LINE}
79556
80808
  `);
79557
80809
  }
79558
80810
  function ensureScratch(worktree) {
79559
- const scratch = path48.join(worktree, SCRATCH_DIR);
79560
- fs25.mkdirSync(scratch, { recursive: true });
80811
+ const scratch = path49.join(worktree, SCRATCH_DIR);
80812
+ fs26.mkdirSync(scratch, { recursive: true });
79561
80813
  return scratch;
79562
80814
  }
79563
80815
  function createSliceWorktree(cwd, plan, input) {
@@ -79601,8 +80853,8 @@ function createSliceWorktree(cwd, plan, input) {
79601
80853
  return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
79602
80854
  let dirExisted = false;
79603
80855
  try {
79604
- const stat = fs25.statSync(target);
79605
- if (!stat.isDirectory() || fs25.readdirSync(target).length > 0)
80856
+ const stat = fs26.statSync(target);
80857
+ if (!stat.isDirectory() || fs26.readdirSync(target).length > 0)
79606
80858
  return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
79607
80859
  dirExisted = true;
79608
80860
  } catch (error) {
@@ -79684,7 +80936,7 @@ function releaseSliceWorktree(cwd, plan, input) {
79684
80936
  notes: []
79685
80937
  };
79686
80938
  if (index < 0) {
79687
- if (fs25.existsSync(target))
80939
+ if (fs26.existsSync(target))
79688
80940
  return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
79689
80941
  out.notes.push(`no worktree at ${target}; nothing to release`);
79690
80942
  return { ok: true, data: out };
@@ -79746,7 +80998,7 @@ function releaseSliceWorktree(cwd, plan, input) {
79746
80998
  `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
79747
80999
  out.removed = true;
79748
81000
  if (made.dirExisted === true)
79749
- fs25.mkdirSync(entry.path, { recursive: true });
81001
+ fs26.mkdirSync(entry.path, { recursive: true });
79750
81002
  if (out.unpushed)
79751
81003
  out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
79752
81004
  return { ok: true, data: out };
@@ -79774,32 +81026,32 @@ function unpushedCount(cwd, branch) {
79774
81026
  return count.ok ? Number(count.stdout.trim()) : null;
79775
81027
  }
79776
81028
  function removeScratch(worktree) {
79777
- const scratch = path48.join(worktree, SCRATCH_DIR);
81029
+ const scratch = path49.join(worktree, SCRATCH_DIR);
79778
81030
  let stat;
79779
81031
  try {
79780
- stat = fs25.lstatSync(scratch);
81032
+ stat = fs26.lstatSync(scratch);
79781
81033
  } catch {
79782
81034
  return;
79783
81035
  }
79784
81036
  if (stat.isSymbolicLink() || !stat.isDirectory())
79785
- fs25.unlinkSync(scratch);
81037
+ fs26.unlinkSync(scratch);
79786
81038
  else
79787
- fs25.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
81039
+ fs26.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
79788
81040
  }
79789
81041
  var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
79790
81042
  try {
79791
- return fs25.realpathSync.native(target);
81043
+ return fs26.realpathSync.native(target);
79792
81044
  } catch {
79793
- return path48.resolve(target);
81045
+ return path49.resolve(target);
79794
81046
  }
79795
81047
  }, samePath = (a, b) => {
79796
81048
  const left = canonical2(a);
79797
81049
  const right = canonical2(b);
79798
81050
  return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
79799
81051
  }, inside3 = (child, parent) => {
79800
- const relative = path48.relative(canonical2(parent), canonical2(child));
79801
- return relative === "" || !relative.startsWith("..") && !path48.isAbsolute(relative);
79802
- }, slicePath = (cwd, slice) => path48.isAbsolute(slice.worktree) ? slice.worktree : path48.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
81052
+ const relative = path49.relative(canonical2(parent), canonical2(child));
81053
+ return relative === "" || !relative.startsWith("..") && !path49.isAbsolute(relative);
81054
+ }, slicePath = (cwd, slice) => path49.isAbsolute(slice.worktree) ? slice.worktree : path49.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
79803
81055
  `)[0] || "unknown error"}`);
79804
81056
  var init_fanout_worktree = __esm(() => {
79805
81057
  init_ops();
@@ -79809,7 +81061,7 @@ var init_fanout_worktree = __esm(() => {
79809
81061
  SLOW = GIT_TIMEOUTS.worktree;
79810
81062
  EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
79811
81063
  worktreeDeps = {
79812
- stat: (file) => fs25.statSync(file, { bigint: true })
81064
+ stat: (file) => fs26.statSync(file, { bigint: true })
79813
81065
  };
79814
81066
  });
79815
81067
 
@@ -79850,7 +81102,9 @@ function renderBrief(cwd, plan, input) {
79850
81102
  id: row.id,
79851
81103
  what: String(row.what)
79852
81104
  }));
79853
- const session = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
81105
+ const identity = identityOf(input.actor.host);
81106
+ const minted = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
81107
+ const session = identity === "declared" ? minted : null;
79854
81108
  const fanIn = plan.fanIn === "integration" ? [
79855
81109
  `FAN-IN: integration branch ${plan.trunk}. Before you report, merge its tip into your branch`,
79856
81110
  ` (\`git merge --no-edit ${plan.trunk}\`), resolve conflicts inside SCOPE and re-run VERIFY, so the`,
@@ -79879,8 +81133,8 @@ function renderBrief(cwd, plan, input) {
79879
81133
  ...REPORT,
79880
81134
  "STANDING:",
79881
81135
  ...standing.length ? standing.map((order) => ` - ${order.what}`) : [" - none recorded beyond this brief"],
79882
- ` export WORKIT_SESSION_ID=${session}`,
79883
- ...rules(mode, branch, slice.base)
81136
+ ...identityLines(identity, minted),
81137
+ ...rules(identity, mode, branch, slice.base)
79884
81138
  ];
79885
81139
  return {
79886
81140
  ok: true,
@@ -79901,13 +81155,38 @@ function renderBrief(cwd, plan, input) {
79901
81155
  }
79902
81156
  };
79903
81157
  }
79904
- var DEFAULT_TIMEBOX = "30 minutes", REPORT, rules = (mode, branch, base) => [
79905
- "RULES:",
79906
- mode === "new" ? ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).` : ` 1. First command: \`git switch ${branch}\`, then continue from its head.`,
79907
- ' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
79908
- " 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
79909
- " 4. Never record a verdict on your own work."
79910
- ], sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
81158
+ var DEFAULT_TIMEBOX = "30 minutes", REPORT, identityOf = (host) => {
81159
+ const family = host?.replace(/_(?:cli|desktop)$/, "") ?? "";
81160
+ if (family === "claude_code")
81161
+ return "issued";
81162
+ if (family === "opencode" || family === "pi" || family === "codex")
81163
+ return "derived";
81164
+ return "declared";
81165
+ }, identityLines = (identity, session) => {
81166
+ if (identity === "issued")
81167
+ return [
81168
+ " Session: your env already has WORKIT_SESSION_ID (the lead's id; SubagentStart issues ids only to verifiers and reviewers). Use it as is, in your Workit-Session trailer and workit commands; never export or hand-mint one."
81169
+ ];
81170
+ if (identity === "derived")
81171
+ return [
81172
+ " Session: your host already sets WORKIT_SESSION_ID in your shell; export nothing and never hand-mint one."
81173
+ ];
81174
+ return [` export WORKIT_SESSION_ID=${session}`];
81175
+ }, rules = (identity, mode, branch, base) => {
81176
+ const plain = identity === "issued";
81177
+ let first = ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).`;
81178
+ if (mode === "resume")
81179
+ first = ` 1. First command: \`git switch ${branch}\`, then continue from its head.`;
81180
+ else if (plain)
81181
+ first = ` 1. First command: \`git fetch origin && git switch -c ${branch} origin/${base}\` (plain git: worktree isolation refuses every \`workit git\` form).`;
81182
+ return [
81183
+ "RULES:",
81184
+ first,
81185
+ ' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
81186
+ plain ? ' 3. Commit with plain `git commit -m <msg> --trailer "Workit-Session: <the WORKIT_SESSION_ID in your env>" --trailer "<your host attribution trailer>" -- <paths in SCOPE>`; push only if this brief says so.' : " 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
81187
+ " 4. Never record a verdict on your own work."
81188
+ ];
81189
+ }, sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
79911
81190
  var init_fanout_brief = __esm(() => {
79912
81191
  init_ledger();
79913
81192
  init_fanout();
@@ -79963,9 +81242,9 @@ function hasOwnCommits2(cwd, branch, start, head) {
79963
81242
  const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
79964
81243
  if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
79965
81244
  continue;
79966
- if (sha !== head && !isAncestor2(cwd, sha, head))
81245
+ if (sha !== head && !isAncestor3(cwd, sha, head))
79967
81246
  continue;
79968
- if (start && (sha === start || isAncestor2(cwd, sha, start)))
81247
+ if (start && (sha === start || isAncestor3(cwd, sha, start)))
79969
81248
  continue;
79970
81249
  return true;
79971
81250
  }
@@ -79975,7 +81254,7 @@ function reverted(cwd, trunk, landedHead, start) {
79975
81254
  const fork = mergeBase(cwd, landedHead, trunk);
79976
81255
  if (fork && fork !== landedHead)
79977
81256
  return unchangedSince(cwd, fork, landedHead, trunk);
79978
- const before = start && start !== landedHead && isAncestor2(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
81257
+ const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
79979
81258
  return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
79980
81259
  }
79981
81260
  function unchangedSince(cwd, before, after, trunk) {
@@ -80083,9 +81362,9 @@ function detectLanding(ctx, slice, input) {
80083
81362
  }
80084
81363
  return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
80085
81364
  }
80086
- if (!merged || head === merged || isAncestor2(ctx.cwd, head, merged))
81365
+ if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
80087
81366
  return landed("pr_merged", merged ?? head);
80088
- out.note = isAncestor2(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)`;
81367
+ 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)`;
80089
81368
  return out;
80090
81369
  }
80091
81370
  }
@@ -80102,7 +81381,7 @@ function detectLanding(ctx, slice, input) {
80102
81381
  if (trunkIds.size === 0)
80103
81382
  return out;
80104
81383
  const froms = [fork];
80105
- if (input.parentHead && input.parentHead !== head && isAncestor2(ctx.cwd, input.parentHead, head))
81384
+ if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
80106
81385
  froms.push(input.parentHead);
80107
81386
  for (const from of froms) {
80108
81387
  const id = patchId(ctx.cwd, from, head);
@@ -80118,7 +81397,7 @@ function sliceStart(cwd, rows, fanout, slice) {
80118
81397
  const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
80119
81398
  return row?.baseSha ?? null;
80120
81399
  }
80121
- var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
81400
+ var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
80122
81401
  var init_fanout_landed = __esm(() => {
80123
81402
  init_rev();
80124
81403
  init_fanout();
@@ -80175,7 +81454,7 @@ function checkFanout(cwd, plan, options = {}) {
80175
81454
  const b = tips.get(second);
80176
81455
  if (landings.get(first)?.landed || landings.get(second)?.landed)
80177
81456
  continue;
80178
- if (!a || !b || isAncestor3(cwd, a, b) || isAncestor3(cwd, b, a))
81457
+ if (!a || !b || isAncestor4(cwd, a, b) || isAncestor4(cwd, b, a))
80179
81458
  continue;
80180
81459
  const check = checks.get(second);
80181
81460
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -80355,7 +81634,7 @@ function unblockFor(check, plan) {
80355
81634
  return `unblock ${check.waitsFor.join(", ")} first`;
80356
81635
  return check.reasons[0] ?? "workit fanout check";
80357
81636
  }
80358
- var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor3 = (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) => ({
81637
+ 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) => ({
80359
81638
  id: slice.id,
80360
81639
  branch: slice.branch,
80361
81640
  head,
@@ -80574,8 +81853,8 @@ var exports_fanout = {};
80574
81853
  __export(exports_fanout, {
80575
81854
  run: () => run16
80576
81855
  });
80577
- import fs26 from "node:fs";
80578
- import path49 from "node:path";
81856
+ import fs27 from "node:fs";
81857
+ import path50 from "node:path";
80579
81858
  function forgeFor(io, offline) {
80580
81859
  if (offline)
80581
81860
  return { forge: null, error: "--offline" };
@@ -80636,10 +81915,10 @@ function plan2(argv, io) {
80636
81915
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
80637
81916
  if (flags.positionals.length !== 1)
80638
81917
  return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
80639
- const file = path49.resolve(io.cwd, flags.positionals[0]);
81918
+ const file = path50.resolve(io.cwd, flags.positionals[0]);
80640
81919
  let raw;
80641
81920
  try {
80642
- raw = JSON.parse(fs26.readFileSync(file, "utf8"));
81921
+ raw = JSON.parse(fs27.readFileSync(file, "utf8"));
80643
81922
  } catch (error) {
80644
81923
  return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
80645
81924
  }
@@ -96124,7 +97403,7 @@ var init_ast = __esm(() => {
96124
97403
 
96125
97404
  // packages/workit-cli/src/test-audit/rules.ts
96126
97405
  import { builtinModules } from "node:module";
96127
- import path50 from "node:path";
97406
+ import path51 from "node:path";
96128
97407
  function testCallee(call) {
96129
97408
  let callee = call.callee;
96130
97409
  if (callee.type === "CallExpression")
@@ -96617,12 +97896,12 @@ function checkConstantsOnly(ctx, emit) {
96617
97896
  }
96618
97897
  function checkOverMocking(ctx, emit) {
96619
97898
  const { program } = ctx.parsed;
96620
- const unit = path50.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
97899
+ const unit = path51.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
96621
97900
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
96622
97901
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
96623
97902
  for (const call of mocked) {
96624
97903
  const spec = call.arguments[0].value;
96625
- const base = path50.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
97904
+ const base = path51.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
96626
97905
  if (base === unit)
96627
97906
  emit({
96628
97907
  rule: "over-mocking",
@@ -97012,7 +98291,7 @@ var init_rules = __esm(() => {
97012
98291
  // packages/workit-cli/src/test-audit/audit.ts
97013
98292
  import { spawnSync as spawnSync26 } from "node:child_process";
97014
98293
  import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync23, statSync as statSync8 } from "node:fs";
97015
- import path51 from "node:path";
98294
+ import path52 from "node:path";
97016
98295
  function git7(cwd, args) {
97017
98296
  const run = spawnSync26("git", ["-c", "core.quotepath=off", ...args], {
97018
98297
  cwd,
@@ -97025,7 +98304,7 @@ function walkTests(dir, out) {
97025
98304
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
97026
98305
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
97027
98306
  continue;
97028
- const full = path51.join(dir, entry.name);
98307
+ const full = path52.join(dir, entry.name);
97029
98308
  if (entry.isDirectory()) {
97030
98309
  if (!SKIP_DIRS.has(entry.name))
97031
98310
  walkTests(full, out);
@@ -97037,7 +98316,7 @@ function expandPaths(cwd, inputs) {
97037
98316
  const files = [];
97038
98317
  const missing = [];
97039
98318
  for (const input of inputs.length > 0 ? inputs : ["."]) {
97040
- const full = path51.resolve(cwd, input);
98319
+ const full = path52.resolve(cwd, input);
97041
98320
  if (!existsSync20(full))
97042
98321
  missing.push(input);
97043
98322
  else if (statSync8(full).isDirectory())
@@ -97074,11 +98353,11 @@ function changedLines(cwd, base) {
97074
98353
  for (let offset = 0;offset < count; offset += 1)
97075
98354
  lines.add(start + offset);
97076
98355
  }
97077
- changed.set(path51.join(root, file), lines);
98356
+ changed.set(path52.join(root, file), lines);
97078
98357
  }
97079
98358
  const untracked = git7(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
97080
98359
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
97081
- changed.set(path51.join(root, file), "all");
98360
+ changed.set(path52.join(root, file), "all");
97082
98361
  return changed;
97083
98362
  }
97084
98363
  function auditFiles(files, display) {
@@ -97140,7 +98419,7 @@ function auditFiles(files, display) {
97140
98419
  durationMs: Math.round(performance.now() - started)
97141
98420
  };
97142
98421
  }
97143
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path51.sep).join("/"), SEVERITY_ORDER, ruleIds;
98422
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path52.sep).join("/"), SEVERITY_ORDER, ruleIds;
97144
98423
  var init_audit = __esm(() => {
97145
98424
  init_ast();
97146
98425
  init_rules();
@@ -97169,8 +98448,8 @@ import {
97169
98448
  writeFileSync as writeFileSync11,
97170
98449
  copyFileSync as copyFileSync6
97171
98450
  } from "node:fs";
97172
- import os14 from "node:os";
97173
- import path52 from "node:path";
98451
+ import os16 from "node:os";
98452
+ import path53 from "node:path";
97174
98453
  function mutantsFor(source, file, lines) {
97175
98454
  const parsed = parseSource(source, file);
97176
98455
  const out = [];
@@ -97243,7 +98522,7 @@ function relatedTests(source, testFiles) {
97243
98522
  for (const match of text.matchAll(SPECIFIER)) {
97244
98523
  const spec = match[1];
97245
98524
  if (spec.startsWith(".")) {
97246
- if (stripExt(path52.posix.join(path52.posix.dirname(file), spec)) === target)
98525
+ if (stripExt(path53.posix.join(path53.posix.dirname(file), spec)) === target)
97247
98526
  return true;
97248
98527
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
97249
98528
  return true;
@@ -97251,7 +98530,7 @@ function relatedTests(source, testFiles) {
97251
98530
  return false;
97252
98531
  }).map((entry) => entry.file);
97253
98532
  }
97254
- function removeStaleCopies(tmp = os14.tmpdir()) {
98533
+ function removeStaleCopies(tmp = os16.tmpdir()) {
97255
98534
  let removed = 0;
97256
98535
  let names = [];
97257
98536
  try {
@@ -97263,12 +98542,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
97263
98542
  for (const name of names) {
97264
98543
  if (!name.startsWith(TEMP_PREFIX))
97265
98544
  continue;
97266
- const dir = path52.join(tmp, name);
98545
+ const dir = path53.join(tmp, name);
97267
98546
  try {
97268
98547
  const stat = lstatSync8(dir);
97269
98548
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
97270
98549
  continue;
97271
- const pid = Number(readFileSync24(path52.join(dir, PID_FILE), "utf8"));
98550
+ const pid = Number(readFileSync24(path53.join(dir, PID_FILE), "utf8"));
97272
98551
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
97273
98552
  continue;
97274
98553
  } catch (error) {
@@ -97285,8 +98564,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
97285
98564
  function linkNodeModules(origin, target, repo, copy) {
97286
98565
  mkdirSync10(target, { recursive: true });
97287
98566
  for (const entry of readdirSync10(origin, { withFileTypes: true })) {
97288
- const from = path52.join(origin, entry.name);
97289
- const to = path52.join(target, entry.name);
98567
+ const from = path53.join(origin, entry.name);
98568
+ const to = path53.join(target, entry.name);
97290
98569
  if (entry.name.startsWith("@") && entry.isDirectory()) {
97291
98570
  linkNodeModules(from, to, repo, copy);
97292
98571
  continue;
@@ -97302,9 +98581,9 @@ function linkNodeModules(origin, target, repo, copy) {
97302
98581
  } catch {
97303
98582
  continue;
97304
98583
  }
97305
- const relative = path52.relative(repo, real);
97306
- if (!relative.startsWith("..") && !path52.isAbsolute(relative) && !relative.split(path52.sep).includes("node_modules")) {
97307
- symlinkSync(path52.join(copy, relative), to, "junction");
98584
+ const relative = path53.relative(repo, real);
98585
+ if (!relative.startsWith("..") && !path53.isAbsolute(relative) && !relative.split(path53.sep).includes("node_modules")) {
98586
+ symlinkSync(path53.join(copy, relative), to, "junction");
97308
98587
  continue;
97309
98588
  }
97310
98589
  }
@@ -97320,7 +98599,7 @@ function copySymlink(repo, tree, from, to) {
97320
98599
  }
97321
98600
  const realRepo = realpathSync9(repo);
97322
98601
  if (real && inside4(realRepo, real)) {
97323
- symlinkSync(path52.relative(path52.dirname(to), path52.join(tree, path52.relative(realRepo, real))), to);
98602
+ symlinkSync(path53.relative(path53.dirname(to), path53.join(tree, path53.relative(realRepo, real))), to);
97324
98603
  return;
97325
98604
  }
97326
98605
  if (real && statSync9(real).isFile())
@@ -97329,10 +98608,10 @@ function copySymlink(repo, tree, from, to) {
97329
98608
  symlinkSync(readlinkSync3(from), to);
97330
98609
  }
97331
98610
  function writeInside(copy, file, content) {
97332
- const target = path52.join(copy, file);
98611
+ const target = path53.join(copy, file);
97333
98612
  try {
97334
98613
  const stat = lstatSync8(target);
97335
- if (!stat.isFile() || !inside4(realpathSync9(copy), realpathSync9(path52.dirname(target))))
98614
+ if (!stat.isFile() || !inside4(realpathSync9(copy), realpathSync9(path53.dirname(target))))
97336
98615
  return false;
97337
98616
  } catch {
97338
98617
  return false;
@@ -97343,9 +98622,9 @@ function writeInside(copy, file, content) {
97343
98622
  return true;
97344
98623
  }
97345
98624
  function copyWorkingTree(repo) {
97346
- const dir = mkdtempSync4(path52.join(os14.tmpdir(), TEMP_PREFIX));
97347
- writeFileSync11(path52.join(dir, PID_FILE), String(process.pid));
97348
- const tree = path52.join(dir, "tree");
98625
+ const dir = mkdtempSync4(path53.join(os16.tmpdir(), TEMP_PREFIX));
98626
+ writeFileSync11(path53.join(dir, PID_FILE), String(process.pid));
98627
+ const tree = path53.join(dir, "tree");
97349
98628
  const head = git7(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
97350
98629
  if (head && git7(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
97351
98630
  const branch = git7(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -97362,29 +98641,29 @@ function copyWorkingTree(repo) {
97362
98641
  for (const relative of listed) {
97363
98642
  if (!relative)
97364
98643
  continue;
97365
- const from = path52.join(repo, relative);
98644
+ const from = path53.join(repo, relative);
97366
98645
  let stat;
97367
98646
  try {
97368
98647
  stat = lstatSync8(from);
97369
98648
  } catch {
97370
98649
  continue;
97371
98650
  }
97372
- const to = path52.join(tree, relative);
97373
- mkdirSync10(path52.dirname(to), { recursive: true });
98651
+ const to = path53.join(tree, relative);
98652
+ mkdirSync10(path53.dirname(to), { recursive: true });
97374
98653
  if (stat.isSymbolicLink())
97375
98654
  copySymlink(repo, tree, from, to);
97376
98655
  else if (stat.isFile())
97377
98656
  copyFileSync6(from, to);
97378
- let parent = path52.dirname(relative);
98657
+ let parent = path53.dirname(relative);
97379
98658
  while (parent !== "." && !dirs.has(parent)) {
97380
98659
  dirs.add(parent);
97381
- parent = path52.dirname(parent);
98660
+ parent = path53.dirname(parent);
97382
98661
  }
97383
98662
  }
97384
98663
  for (const parent of dirs) {
97385
- const modules = path52.join(repo, parent, "node_modules");
97386
- if (existsSync21(modules) && !existsSync21(path52.join(tree, parent, "node_modules")))
97387
- linkNodeModules(modules, path52.join(tree, parent, "node_modules"), repo, tree);
98664
+ const modules = path53.join(repo, parent, "node_modules");
98665
+ if (existsSync21(modules) && !existsSync21(path53.join(tree, parent, "node_modules")))
98666
+ linkNodeModules(modules, path53.join(tree, parent, "node_modules"), repo, tree);
97388
98667
  }
97389
98668
  return { dir, tree };
97390
98669
  }
@@ -97423,14 +98702,14 @@ async function runMutation(options) {
97423
98702
  const tracked = git7(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
97424
98703
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
97425
98704
  try {
97426
- return { file, text: readFileSync24(path52.join(repo, file), "utf8") };
98705
+ return { file, text: readFileSync24(path53.join(repo, file), "utf8") };
97427
98706
  } catch {
97428
98707
  return { file, text: "" };
97429
98708
  }
97430
98709
  });
97431
98710
  const candidates = [];
97432
98711
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
97433
- const relative = toPosix(path52.relative(repo, absolute));
98712
+ const relative = toPosix(path53.relative(repo, absolute));
97434
98713
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
97435
98714
  continue;
97436
98715
  if (NOT_SOURCE.test(relative) || !existsSync21(absolute))
@@ -97444,9 +98723,9 @@ async function runMutation(options) {
97444
98723
  for (const mutant of mutants)
97445
98724
  if (!related.has(mutant.file))
97446
98725
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
97447
- const cwdRelative = path52.relative(repo, options.cwd);
98726
+ const cwdRelative = path53.relative(repo, options.cwd);
97448
98727
  const asArg = (file) => {
97449
- const relative = toPosix(path52.relative(cwdRelative, file));
98728
+ const relative = toPosix(path53.relative(cwdRelative, file));
97450
98729
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
97451
98730
  };
97452
98731
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -97481,7 +98760,7 @@ async function runMutation(options) {
97481
98760
  return finish2(options, 0, results, counts, started);
97482
98761
  removeStaleCopies();
97483
98762
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
97484
- const copyCwd = path52.join(copy, cwdRelative);
98763
+ const copyCwd = path53.join(copy, cwdRelative);
97485
98764
  let current = null;
97486
98765
  const restore = () => {
97487
98766
  if (current)
@@ -97516,7 +98795,7 @@ async function runMutation(options) {
97516
98795
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
97517
98796
  continue;
97518
98797
  }
97519
- const original = readFileSync24(path52.join(copy, mutant.file), "utf8");
98798
+ const original = readFileSync24(path53.join(copy, mutant.file), "utf8");
97520
98799
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
97521
98800
  current = { file: mutant.file, original };
97522
98801
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -97573,8 +98852,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
97573
98852
  return error.code === "EPERM";
97574
98853
  }
97575
98854
  }, inside4 = (root, file) => {
97576
- const relative = path52.relative(root, file);
97577
- return relative === "" || !relative.startsWith("..") && !path52.isAbsolute(relative);
98855
+ const relative = path53.relative(root, file);
98856
+ return relative === "" || !relative.startsWith("..") && !path53.isAbsolute(relative);
97578
98857
  }, active, killTree = (child) => {
97579
98858
  try {
97580
98859
  if (child.pid && process.platform !== "win32")
@@ -97623,7 +98902,7 @@ __export(exports_test_audit, {
97623
98902
  run: () => run18
97624
98903
  });
97625
98904
  import { existsSync as existsSync22 } from "node:fs";
97626
- import path53 from "node:path";
98905
+ import path54 from "node:path";
97627
98906
  function parse7(argv) {
97628
98907
  const options = {
97629
98908
  paths: [],
@@ -97727,8 +99006,8 @@ async function run18(argv, io) {
97727
99006
  return emit(io, fail("invalid_input", `${options}
97728
99007
  usage: ${USAGE10}`));
97729
99008
  const top = git7(io.cwd, ["rev-parse", "--show-toplevel"]);
97730
- const repo = top.ok ? path53.resolve(top.stdout.trim()) : null;
97731
- const display = (file) => toPosix(path53.relative(io.cwd, file) || file);
99009
+ const repo = top.ok ? path54.resolve(top.stdout.trim()) : null;
99010
+ const display = (file) => toPosix(path54.relative(io.cwd, file) || file);
97732
99011
  let base = null;
97733
99012
  let changed = null;
97734
99013
  if (options.diff || options.mutate) {
@@ -97971,7 +99250,7 @@ __export(exports_youtrack, {
97971
99250
  run: () => run20
97972
99251
  });
97973
99252
  import { readFileSync as readFileSync25 } from "node:fs";
97974
- import path54 from "node:path";
99253
+ import path55 from "node:path";
97975
99254
  async function run20(argv, io) {
97976
99255
  const [sub, ...rest] = argv;
97977
99256
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
@@ -98017,7 +99296,7 @@ async function run20(argv, io) {
98017
99296
  let markdown = flags.values.markdown ?? "";
98018
99297
  if (flags.values.file)
98019
99298
  try {
98020
- markdown = readFileSync25(path54.resolve(io.cwd, flags.values.file), "utf8");
99299
+ markdown = readFileSync25(path55.resolve(io.cwd, flags.values.file), "utf8");
98021
99300
  } catch (error) {
98022
99301
  return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
98023
99302
  }
@@ -98086,7 +99365,7 @@ __export(exports_changelog, {
98086
99365
  run: () => run21
98087
99366
  });
98088
99367
  import { readFileSync as readFileSync26 } from "node:fs";
98089
- import path55 from "node:path";
99368
+ import path56 from "node:path";
98090
99369
  async function run21(argv, io) {
98091
99370
  const [sub, ...rest] = argv;
98092
99371
  if (sub !== "apply")
@@ -98132,7 +99411,7 @@ async function run21(argv, io) {
98132
99411
  ]);
98133
99412
  }
98134
99413
  var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
98135
- const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path55.resolve(cwd, raw.slice(1)), "utf8") : raw;
99414
+ const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path56.resolve(cwd, raw.slice(1)), "utf8") : raw;
98136
99415
  return JSON.parse(text);
98137
99416
  };
98138
99417
  var init_changelog2 = __esm(() => {
@@ -98228,7 +99507,7 @@ var init_handoff = __esm(() => {
98228
99507
 
98229
99508
  // packages/workit-cli/src/main.ts
98230
99509
  init_package();
98231
- import path56 from "node:path";
99510
+ import path57 from "node:path";
98232
99511
 
98233
99512
  // packages/workit-cli/src/help.ts
98234
99513
  var HELP_FLAGS = new Set(["--help", "-h"]);
@@ -98493,7 +99772,7 @@ var VERBS = [
98493
99772
  {
98494
99773
  name: "check",
98495
99774
  group: "delivery",
98496
- usage: "workit check <name> [--task <id>] [--base <ref>] | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--base <ref>] -- <cmd…>",
99775
+ usage: "workit check <name> [--task <id>] [--base <ref>] [--no-lease] | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy] [--no-lease] -- <cmd…>",
98497
99776
  summary: "Run a configured or ad-hoc check and record CLI-observed evidence (exit = the command's)",
98498
99777
  examples: ["workit check test", "workit check --name unit -- bun test test/unit"],
98499
99778
  load: () => Promise.resolve().then(() => (init_check(), exports_check))
@@ -98535,8 +99814,8 @@ var VERBS = [
98535
99814
  },
98536
99815
  {
98537
99816
  name: "merge",
98538
- usage: "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--delete-branch] [--unverified --reason <why>]",
98539
- summary: "Merge when READY, verified by a non-author and granted"
99817
+ usage: "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch] [--unverified --reason <why>]",
99818
+ summary: "Merge when READY, verified by a non-author and granted; --subject/--body set the squash message"
98540
99819
  }
98541
99820
  ],
98542
99821
  examples: [
@@ -98571,23 +99850,29 @@ var VERBS = [
98571
99850
  subcommands: [
98572
99851
  {
98573
99852
  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"
99853
+ usage: "workit git branch <name> | git branch --kind feature|bugfix|release|hotfix --slug <s> [--base <b>] [--track <t>] [--carry]",
99854
+ summary: "Create and switch to a branch from the trunk, the git-flow route of its kind or a release track"
98576
99855
  },
98577
99856
  {
98578
99857
  name: "commit",
98579
- usage: "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>]",
98580
- summary: "Commit (or --amend) with a linted Conventional Commit message and the session trailer"
99858
+ usage: "workit git commit -m|--message <msg>… | -F|--file <file|-> [--trailer <key: value>]… [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>]",
99859
+ summary: "Commit (or --amend) with a linted Conventional Commit message, the session trailer (unless the workspace sets trailers: false) and any --trailer"
98581
99860
  },
98582
99861
  {
98583
99862
  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)"
99863
+ usage: "workit git push [-u|--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…]",
99864
+ summary: "Push the current branch and verify the remote tip (never a blind --force); --release finishes a git-flow release"
99865
+ },
99866
+ {
99867
+ name: "trailers",
99868
+ usage: "workit git trailers",
99869
+ summary: "Print on or off: whether commits in this workspace get the Workit-Session trailer"
98586
99870
  }
98587
99871
  ],
98588
99872
  examples: [
98589
99873
  "workit git branch --kind feature --slug help-everywhere",
98590
99874
  'workit git commit -m "fix(cli): answer help flags first" --all',
99875
+ 'workit git commit -m "feat: x" --trailer "Co-Authored-By: Name <email>" --all',
98591
99876
  "workit git commit --amend --no-edit -- src/forgotten.ts",
98592
99877
  "workit git push --set-upstream"
98593
99878
  ],
@@ -98874,7 +100159,7 @@ var defaultIo = (json) => ({
98874
100159
  });
98875
100160
  async function main(argv, overrides = {}, options = {}) {
98876
100161
  const parsed = parseGlobals(argv);
98877
- const cwd = path56.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
100162
+ const cwd = path57.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
98878
100163
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
98879
100164
  if (parsed.error)
98880
100165
  return emit(io, fail("invalid_input", parsed.error));