@brainervirus/workit-claude-code 8.0.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/workit.js CHANGED
@@ -65,7 +65,7 @@ var package_default;
65
65
  var init_package = __esm(() => {
66
66
  package_default = {
67
67
  name: "@brainervirus/workit-cli",
68
- version: "8.0.1",
68
+ version: "8.1.1",
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.0.1",
107
- "@brainervirus/workit-cursor": "^8.0.1",
108
- "@brainervirus/workit-opencode": "^8.0.1",
106
+ "@brainervirus/workit-core": "^8.1.1",
107
+ "@brainervirus/workit-cursor": "^8.1.1",
108
+ "@brainervirus/workit-opencode": "^8.1.1",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -12831,12 +12831,13 @@ function pushForge(cwd, options = {}) {
12831
12831
  };
12832
12832
  const forge = deriveForge(url, options);
12833
12833
  if (!forge) {
12834
- const host = parseRemoteUrl(url)?.host || null;
12834
+ const parsed = parseRemoteUrl(url);
12835
+ const host = parsed?.host || null;
12835
12836
  return {
12836
12837
  ok: false,
12837
12838
  code: "unavailable",
12838
- error: host ? `cannot tell whether push host "${host}" is GitHub or GitLab` : `push URL ${redactRemote(url)} of remote "${remote}" is not a GitHub/GitLab URL`,
12839
- unblock: host ? `map the alias in ~/.ssh/config (Host ${host} / HostName github.com|gitlab.com) or set github.host / gitlab.host in ~/.config/workit/vcs.json` : `git remote set-url --push ${remote} <ssh-or-https-url>`
12839
+ error: host ? parsed?.protocol === "ssh" && !host.includes(".") ? `unsupported_forge: could not resolve ssh alias "${host}" to GitHub or GitLab; PR and CI verbs need one (workit git push still pushes with git)` : parsed?.protocol === "ssh" ? `unsupported_forge: ssh host "${host}" is not a GitHub/GitLab host or an alias workit can resolve; PR and CI verbs need one (workit git push still pushes with git)` : `unsupported_forge: cannot tell whether push host "${host}" is GitHub or GitLab; PR and CI verbs need one (workit git push still pushes with git)` : `push URL ${redactRemote(url)} of remote "${remote}" is not a GitHub/GitLab URL`,
12840
+ unblock: host ? `if ${host} is an ssh alias of GitHub/GitLab, add "Host ${host}" with "HostName github.com" (or gitlab.com) to ~/.ssh/config; if it is a self-hosted GitHub/GitLab, set github.host or gitlab.host in ~/.config/workit/vcs.json; other forges (Bitbucket, Gitea…) have no PR/CI verbs` : `git remote set-url --push ${remote} <ssh-or-https-url>`
12840
12841
  };
12841
12842
  }
12842
12843
  return { ok: true, remote, url: redactRemote(url), forge };
@@ -12847,8 +12848,8 @@ function forgeConflict(derived, configuredProvider) {
12847
12848
  return null;
12848
12849
  return {
12849
12850
  code: "blocked",
12850
- error: `forge_mismatch: push remote ${derived.host} is ${derived.kind} but the workspace vcs.provider is ${configured}`,
12851
- unblock: `set vcs.provider to "${derived.kind}" for this repo in ~/.config/workit/workspaces.json (workit-github-override), or push to a ${configured} remote`
12851
+ error: `forge_mismatch: push remote ${derived.host} is ${derived.kind} but the workspace vcs.provider is ${configured}, so its account and grants do not apply; PR and CI verbs stop here (workit git push still pushes with git)`,
12852
+ unblock: `add a workspace whose glob matches only this repo, with "vcs": {"provider": "${derived.kind}"} (and its account), to ~/.config/workit/workspaces.json; the most specific glob wins`
12852
12853
  };
12853
12854
  }
12854
12855
  var MAX_BUFFER, GIT_TIMEOUTS, git2 = (cwd, args, options = {}) => {
@@ -14919,60 +14920,6 @@ var init_task_context = __esm(() => {
14919
14920
  privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
14920
14921
  });
14921
14922
 
14922
- // packages/workit-core/src/core/commit-flavors.ts
14923
- var CONVENTIONAL_RE, TICKET_RE, GITMOJI_RE, matchCommitFlavor = (message, flavor, pattern) => {
14924
- const subject = message.split(`
14925
- `, 1)[0] ?? "";
14926
- switch (flavor) {
14927
- case "conventional":
14928
- return CONVENTIONAL_RE.test(subject);
14929
- case "gitmoji":
14930
- return GITMOJI_RE.test(subject);
14931
- case "ticket-prefix":
14932
- return TICKET_RE.test(subject);
14933
- case "freeform":
14934
- return subject.trim().length > 0;
14935
- case "custom":
14936
- if (!pattern)
14937
- return false;
14938
- try {
14939
- return new RegExp(pattern).test(subject);
14940
- } catch {
14941
- return false;
14942
- }
14943
- case "auto":
14944
- return subject.trim().length > 0;
14945
- }
14946
- }, DETECTABLE, detectCommitFlavor = (subjects, threshold = 0.7) => {
14947
- if (subjects.length === 0)
14948
- return { flavor: null, confidence: 0 };
14949
- const votes = new Map;
14950
- for (const subject of subjects) {
14951
- const hit = DETECTABLE.find((flavor) => matchCommitFlavor(subject, flavor));
14952
- if (hit)
14953
- votes.set(hit, (votes.get(hit) ?? 0) + 1);
14954
- }
14955
- let best = null;
14956
- let count = 0;
14957
- for (const [flavor, votesFor] of votes) {
14958
- if (votesFor > count) {
14959
- best = flavor;
14960
- count = votesFor;
14961
- }
14962
- }
14963
- const confidence = count / subjects.length;
14964
- return best !== null && confidence >= threshold ? { flavor: best, confidence } : { flavor: null, confidence };
14965
- };
14966
- var init_commit_flavors = __esm(() => {
14967
- CONVENTIONAL_RE = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+/;
14968
- TICKET_RE = /^[A-Z]{2,}-\d+\b/;
14969
- GITMOJI_RE = /^\p{Extended_Pictographic}/u;
14970
- DETECTABLE = ["conventional", "gitmoji", "ticket-prefix"];
14971
- });
14972
-
14973
- // packages/workit-core/src/core/git.ts
14974
- var init_git = () => {};
14975
-
14976
14923
  // packages/workit-core/src/core/boundary.ts
14977
14924
  var EVENT, errorDetail = (err) => {
14978
14925
  if (err instanceof Error) {
@@ -15057,7 +15004,7 @@ var diagnosticLogger, setDiagnosticLogger = (logger) => {
15057
15004
  if (!migrationFailed)
15058
15005
  migratedDir = dir;
15059
15006
  return dir;
15060
- }, configDir = () => ensureConfigDir(), LOCALE_RE, DEFAULTS, readSafe = (p) => {
15007
+ }, configDir = () => ensureConfigDir(), LOCALE_RE, DEFAULTS, unchosenBranchPolicy, readSafe = (p) => {
15061
15008
  try {
15062
15009
  return readFileSync3(p, "utf8");
15063
15010
  } catch {
@@ -15079,22 +15026,21 @@ var diagnosticLogger, setDiagnosticLogger = (logger) => {
15079
15026
  const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
15080
15027
  const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
15081
15028
  const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
15082
- return {
15083
- status: "valid",
15084
- path: file,
15085
- config: {
15086
- locale,
15087
- localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
15088
- branchPolicy: mergePreset(preset, {
15089
- allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
15090
- protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
15091
- }, DEFAULTS),
15092
- commitPolicy: {
15093
- preset: commitPreset,
15094
- ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
15095
- }
15029
+ const config = {
15030
+ locale,
15031
+ localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
15032
+ branchPolicy: mergePreset(preset, {
15033
+ allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
15034
+ protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
15035
+ }, DEFAULTS),
15036
+ commitPolicy: {
15037
+ preset: commitPreset,
15038
+ ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
15096
15039
  }
15097
15040
  };
15041
+ if (!Object.hasOwn(PRESETS, input.branchPolicy?.preset))
15042
+ unchosenBranchPolicy.add(config);
15043
+ return { status: "valid", path: file, config };
15098
15044
  }, readConfigTyped = (dir) => {
15099
15045
  const file = path13.join(dir ?? configDir(), "config.json");
15100
15046
  return parseConfigResult(readSafe(file), file);
@@ -15108,9 +15054,11 @@ var diagnosticLogger, setDiagnosticLogger = (logger) => {
15108
15054
  if (result.status === "malformed")
15109
15055
  throw new Error(result.error);
15110
15056
  return result.config ?? DEFAULTS;
15111
- }, vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path13.join(configDir(), "vcs.json"), branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"), resolveBranchPolicy = (config, workspace) => {
15057
+ }, vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path13.join(configDir(), "vcs.json"), branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"), resolveBranchPolicy = (config, workspace, repoPreset) => {
15112
15058
  const wp = workspace?.branchPolicy ?? {};
15113
- const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
15059
+ const workspacePreset = Object.hasOwn(PRESETS, wp.preset);
15060
+ const detected = !workspacePreset && unchosenBranchPolicy.has(config) ? repoPreset?.() ?? null : null;
15061
+ const preset = workspacePreset ? wp.preset : detected ?? config.branchPolicy?.preset ?? "gitflow";
15114
15062
  const merged = mergePreset(preset, {
15115
15063
  allowed: wp.allowed,
15116
15064
  protectedNames: wp.protected
@@ -15119,9 +15067,10 @@ var diagnosticLogger, setDiagnosticLogger = (logger) => {
15119
15067
  return {
15120
15068
  preset,
15121
15069
  allowed,
15122
- protected: new Set(merged.protected.map((p) => p.toLowerCase())),
15070
+ protected: new Set([...merged.protected, ...detected ? PRESETS.gitflow.protected : []].map((p) => p.toLowerCase())),
15123
15071
  integration: wp.integration === "merge" ? "merge" : "pr",
15124
- defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
15072
+ defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop",
15073
+ detected: detected !== null
15125
15074
  };
15126
15075
  }, COMMIT_PRESETS, resolveCommitPolicy = (config, workspace) => {
15127
15076
  const wp = workspace?.commitPolicy ?? {};
@@ -15155,6 +15104,7 @@ var init_config = __esm(() => {
15155
15104
  },
15156
15105
  commitPolicy: { preset: "conventional" }
15157
15106
  };
15107
+ unchosenBranchPolicy = new WeakSet([DEFAULTS]);
15158
15108
  COMMIT_PRESETS = [
15159
15109
  "conventional",
15160
15110
  "gitmoji",
@@ -15165,7 +15115,128 @@ var init_config = __esm(() => {
15165
15115
  ];
15166
15116
  });
15167
15117
 
15118
+ // packages/workit-core/src/core/branch-policy.ts
15119
+ import { spawnSync as spawnSync7 } from "node:child_process";
15120
+ var PREFIXES, branchNames = (workspaceRoot) => {
15121
+ const r = spawnSync7("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"], { cwd: workspaceRoot, encoding: "utf8" });
15122
+ if (r.status !== 0)
15123
+ return new Set;
15124
+ return new Set((r.stdout ?? "").split(`
15125
+ `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
15126
+ }, detectBranchPolicy = (workspaceRoot) => {
15127
+ const names = branchNames(workspaceRoot);
15128
+ const develop = names.has("develop");
15129
+ const main = names.has("main");
15130
+ const master = names.has("master");
15131
+ const root = main ? "main" : master ? "master" : null;
15132
+ if (develop) {
15133
+ return {
15134
+ preset: "gitflow",
15135
+ developBranch: "develop",
15136
+ integration: "merge",
15137
+ protected: root ? [root, "develop"] : ["develop"],
15138
+ allowed: [...PRESETS.gitflow.allowed],
15139
+ prefixes: PREFIXES
15140
+ };
15141
+ }
15142
+ if (root === "main") {
15143
+ return {
15144
+ preset: "github-flow",
15145
+ developBranch: null,
15146
+ integration: "pr",
15147
+ protected: [root],
15148
+ allowed: ["*"],
15149
+ prefixes: PREFIXES
15150
+ };
15151
+ }
15152
+ if (root === "master") {
15153
+ return {
15154
+ preset: "trunk-based",
15155
+ developBranch: null,
15156
+ integration: "pr",
15157
+ protected: [root],
15158
+ allowed: ["*"],
15159
+ prefixes: PREFIXES
15160
+ };
15161
+ }
15162
+ return {
15163
+ preset: "gitflow",
15164
+ developBranch: null,
15165
+ integration: "merge",
15166
+ protected: [],
15167
+ allowed: [],
15168
+ prefixes: PREFIXES
15169
+ };
15170
+ }, repoBranchPreset = (workspaceRoot) => {
15171
+ const detected = detectBranchPolicy(workspaceRoot);
15172
+ return detected.developBranch || detected.allowed.length ? detected.preset : null;
15173
+ };
15174
+ var init_branch_policy = __esm(() => {
15175
+ init_config();
15176
+ PREFIXES = {
15177
+ feature: "feature/*",
15178
+ bugfix: "bugfix/*",
15179
+ release: "release/*",
15180
+ hotfix: "hotfix/*"
15181
+ };
15182
+ });
15183
+
15184
+ // packages/workit-core/src/core/commit-flavors.ts
15185
+ var CONVENTIONAL_RE, TICKET_RE, GITMOJI_RE, matchCommitFlavor = (message, flavor, pattern) => {
15186
+ const subject = message.split(`
15187
+ `, 1)[0] ?? "";
15188
+ switch (flavor) {
15189
+ case "conventional":
15190
+ return CONVENTIONAL_RE.test(subject);
15191
+ case "gitmoji":
15192
+ return GITMOJI_RE.test(subject);
15193
+ case "ticket-prefix":
15194
+ return TICKET_RE.test(subject);
15195
+ case "freeform":
15196
+ return subject.trim().length > 0;
15197
+ case "custom":
15198
+ if (!pattern)
15199
+ return false;
15200
+ try {
15201
+ return new RegExp(pattern).test(subject);
15202
+ } catch {
15203
+ return false;
15204
+ }
15205
+ case "auto":
15206
+ return subject.trim().length > 0;
15207
+ }
15208
+ }, DETECTABLE, detectCommitFlavor = (subjects, threshold = 0.7) => {
15209
+ if (subjects.length === 0)
15210
+ return { flavor: null, confidence: 0 };
15211
+ const votes = new Map;
15212
+ for (const subject of subjects) {
15213
+ const hit = DETECTABLE.find((flavor) => matchCommitFlavor(subject, flavor));
15214
+ if (hit)
15215
+ votes.set(hit, (votes.get(hit) ?? 0) + 1);
15216
+ }
15217
+ let best = null;
15218
+ let count = 0;
15219
+ for (const [flavor, votesFor] of votes) {
15220
+ if (votesFor > count) {
15221
+ best = flavor;
15222
+ count = votesFor;
15223
+ }
15224
+ }
15225
+ const confidence = count / subjects.length;
15226
+ return best !== null && confidence >= threshold ? { flavor: best, confidence } : { flavor: null, confidence };
15227
+ };
15228
+ var init_commit_flavors = __esm(() => {
15229
+ CONVENTIONAL_RE = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+/;
15230
+ TICKET_RE = /^[A-Z]{2,}-\d+\b/;
15231
+ GITMOJI_RE = /^\p{Extended_Pictographic}/u;
15232
+ DETECTABLE = ["conventional", "gitmoji", "ticket-prefix"];
15233
+ });
15234
+
15235
+ // packages/workit-core/src/core/git.ts
15236
+ var init_git = () => {};
15237
+
15168
15238
  // packages/workit-core/src/core/workspaces.ts
15239
+ import { spawnSync as spawnSync8 } from "node:child_process";
15169
15240
  import { createHash as createHash8 } from "node:crypto";
15170
15241
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
15171
15242
  import path14 from "node:path";
@@ -15174,7 +15245,7 @@ function workspaceDefaultTarget(cwd, globalDefault) {
15174
15245
  const wp = ws?.branchPolicy ?? {};
15175
15246
  const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
15176
15247
  const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
15177
- const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null).defaultTargetBranch;
15248
+ const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
15178
15249
  return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
15179
15250
  }
15180
15251
  var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash8("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
@@ -15293,6 +15364,46 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15293
15364
  };
15294
15365
  }
15295
15366
  return { ok: true };
15367
+ }, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
15368
+ const cached = mainCheckouts.get(cwd);
15369
+ if (cached !== undefined)
15370
+ return cached;
15371
+ const env = { ...process.env };
15372
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
15373
+ delete env[name];
15374
+ const git = (args) => {
15375
+ const run = spawnSync8("git", args, {
15376
+ cwd,
15377
+ env,
15378
+ encoding: "utf8",
15379
+ timeout: 5000,
15380
+ windowsHide: true
15381
+ });
15382
+ return run.status === 0 ? run.stdout : null;
15383
+ };
15384
+ let main = null;
15385
+ const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
15386
+ const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
15387
+ if (gitDir && common && path14.resolve(gitDir) !== path14.resolve(common)) {
15388
+ const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
15389
+ main = listed?.trim() || (path14.basename(common) === ".git" ? path14.dirname(common) : common);
15390
+ }
15391
+ mainCheckouts.set(cwd, main);
15392
+ return main;
15393
+ }, matchingEntries = (cwd, entries) => {
15394
+ const match = (where) => {
15395
+ const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
15396
+ return entries.filter((entry) => {
15397
+ const glob = entry.glob.replaceAll("\\", "/");
15398
+ const canonical = canonicalGlob(glob);
15399
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
15400
+ });
15401
+ };
15402
+ const direct = match(cwd);
15403
+ if (direct.length)
15404
+ return direct;
15405
+ const main = mainCheckoutOf(cwd);
15406
+ return main ? match(main) : direct;
15296
15407
  }, realpathOf = (p) => {
15297
15408
  const real = realpathSync3.native ?? realpathSync3;
15298
15409
  try {
@@ -15314,41 +15425,27 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15314
15425
  if (named.length === 1)
15315
15426
  return named[0];
15316
15427
  if (named.length > 1)
15317
- throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
15428
+ throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
15318
15429
  if (matches.length > 0) {
15319
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
15430
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
15320
15431
  }
15321
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
15432
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
15322
15433
  }
15323
15434
  if (matches.length < 2)
15324
15435
  return matches[0];
15325
15436
  const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
15326
15437
  const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
15327
15438
  if (mostSpecific.length > 1) {
15328
- throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
15439
+ throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
15329
15440
  }
15330
15441
  return mostSpecific[0];
15331
15442
  }, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
15332
- const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
15333
- const matches = [];
15334
- for (const ws of entries) {
15335
- const glob = ws.glob.replaceAll("\\", "/");
15336
- const canonical = canonicalGlob(glob);
15337
- let matched = false;
15338
- for (const target of targets) {
15339
- if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
15340
- matched = true;
15341
- break;
15342
- }
15343
- }
15344
- if (matched)
15345
- matches.push(ws);
15346
- }
15443
+ const matches = matchingEntries(cwd, entries);
15347
15444
  return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
15348
15445
  }, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
15349
15446
  const result = readWorkspacesResult(dir);
15350
15447
  if (result.status === "malformed" || result.status === "invalid")
15351
- throw new Error(result.error);
15448
+ throw new WorkspaceConfigError(result.error);
15352
15449
  return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
15353
15450
  }, runtimeWorkspacePolicySchema = (kind) => {
15354
15451
  const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
@@ -15374,31 +15471,26 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15374
15471
  } catch (error) {
15375
15472
  if (error.code === "ENOENT")
15376
15473
  return null;
15377
- throw new Error(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15474
+ throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15378
15475
  }
15379
15476
  let parsed;
15380
15477
  try {
15381
15478
  parsed = JSON.parse(raw);
15382
15479
  } catch {
15383
- throw new Error(`${file} is not valid JSON`);
15480
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
15384
15481
  }
15385
15482
  const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
15386
15483
  if (!validated.success) {
15387
15484
  const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
15388
- throw new Error(`${file} has invalid workspace matching data: ${detail}`);
15485
+ throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
15389
15486
  }
15390
15487
  const entries = validated.data.workspaces;
15391
15488
  for (const entry of entries ?? []) {
15392
15489
  const glob = validateWorkspaceGlob(entry.glob);
15393
15490
  if (!glob.ok)
15394
- throw new Error(`${file} workspace ${entry.name}: ${glob.error}`);
15491
+ throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
15395
15492
  }
15396
- const targets = [cwd, realpathOf(cwd)].map((value) => value.replaceAll("\\", "/"));
15397
- const matches = (entries ?? []).filter((entry) => {
15398
- const glob = entry.glob.replaceAll("\\", "/");
15399
- const canonical = canonicalGlob(glob);
15400
- return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
15401
- });
15493
+ const matches = matchingEntries(cwd, entries ?? []);
15402
15494
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
15403
15495
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
15404
15496
  return selected ? { file, selected } : null;
@@ -15413,7 +15505,7 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15413
15505
  const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
15414
15506
  if (!policy.success) {
15415
15507
  const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
15416
- throw new Error(`${file} has invalid ${kind} policy configuration: ${detail}`);
15508
+ throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
15417
15509
  }
15418
15510
  return policy.data;
15419
15511
  }, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
@@ -15423,10 +15515,10 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15423
15515
  const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
15424
15516
  const profileName = requestedProfile?.trim() || candidate.defaultProfile;
15425
15517
  if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
15426
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
15518
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
15427
15519
  const profile = profileName ? candidate.profiles?.[profileName] : undefined;
15428
15520
  if (profileName && !profile)
15429
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
15521
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
15430
15522
  const profilePolicy = profile?.[key];
15431
15523
  const workspacePolicy = candidate[key];
15432
15524
  return {
@@ -15456,7 +15548,7 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15456
15548
  commitPolicy: profile?.commitPolicy ? `profile:${profileName}` : workspace.commitPolicy ? `workspace:${workspace.name}` : "user-default"
15457
15549
  }
15458
15550
  };
15459
- }, resolveWorkspacePolicy = (config, workspace, profileName) => {
15551
+ }, resolveWorkspacePolicy = (config, workspace, profileName, repoPreset) => {
15460
15552
  const selected = workspace ? resolveWorkspaceProfile(workspace, profileName) : {
15461
15553
  status: "resolved",
15462
15554
  profileName: null,
@@ -15469,7 +15561,7 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15469
15561
  status: "resolved",
15470
15562
  workspace: selected.workspace,
15471
15563
  profileName: selected.profileName,
15472
- branchPolicy: resolveBranchPolicy(config, selected.workspace),
15564
+ branchPolicy: resolveBranchPolicy(config, selected.workspace, repoPreset),
15473
15565
  commitPolicy: resolveCommitPolicy(config, selected.workspace),
15474
15566
  provenance: selected.provenance
15475
15567
  };
@@ -15477,6 +15569,7 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
15477
15569
  var init_workspaces = __esm(() => {
15478
15570
  init_zod();
15479
15571
  init_config();
15572
+ init_branch_policy();
15480
15573
  nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
15481
15574
  branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
15482
15575
  commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
@@ -15596,6 +15689,10 @@ var init_workspaces = __esm(() => {
15596
15689
  });
15597
15690
  UNSUPPORTED_GLOB = /[?[\]{}]/;
15598
15691
  UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
15692
+ WorkspaceConfigError = class WorkspaceConfigError extends Error {
15693
+ name = "WorkspaceConfigError";
15694
+ };
15695
+ mainCheckouts = new Map;
15599
15696
  runtimeWorkspaceIndexSchema = object({
15600
15697
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
15601
15698
  }).passthrough();
@@ -15603,7 +15700,7 @@ var init_workspaces = __esm(() => {
15603
15700
 
15604
15701
  // packages/workit-core/src/core/release-tracks.ts
15605
15702
  import { createHash as createHash9 } from "node:crypto";
15606
- import { spawnSync as spawnSync7 } from "node:child_process";
15703
+ import { spawnSync as spawnSync9 } from "node:child_process";
15607
15704
  import { readFileSync as readFileSync5 } from "node:fs";
15608
15705
  function readReleaseTracks(raw) {
15609
15706
  const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
@@ -16005,7 +16102,7 @@ var ReleaseTrackError, KNOWN_FIELDS, BRANCH_FIELDS, PLACEHOLDER, HAS_PLACEHOLDER
16005
16102
  const remote = remotes.find((name) => bare.startsWith(`${name}/`));
16006
16103
  return remote ? bare.slice(remote.length + 1) : bare;
16007
16104
  }, MAX_RECORDED_HOPS = 10, git4 = (cwd, args) => {
16008
- const run = spawnSync7("git", args, {
16105
+ const run = spawnSync9("git", args, {
16009
16106
  cwd,
16010
16107
  encoding: "utf8",
16011
16108
  timeout: 20000,
@@ -16066,7 +16163,7 @@ var init_release_tracks = __esm(() => {
16066
16163
  // packages/workit-core/src/core/vcs-config.ts
16067
16164
  import fs12 from "node:fs";
16068
16165
  import path15 from "node:path";
16069
- import { spawnSync as spawnSync8 } from "node:child_process";
16166
+ import { spawnSync as spawnSync10 } from "node:child_process";
16070
16167
  function vcsConfig(mode, cwd, options = {}) {
16071
16168
  const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
16072
16169
  const wsVcs = ws?.vcs ?? {};
@@ -16149,7 +16246,7 @@ function vcsConfig(mode, cwd, options = {}) {
16149
16246
  return out;
16150
16247
  }
16151
16248
  var workspacesPath = () => path15.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
16152
- const r = spawnSync8("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
16249
+ const r = spawnSync10("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
16153
16250
  if (r.status !== 0)
16154
16251
  return null;
16155
16252
  const url = (r.stdout ?? "").trim();
@@ -16199,7 +16296,7 @@ function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
16199
16296
  const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
16200
16297
  const config = readConfig();
16201
16298
  return kind === "branch" ? {
16202
- branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null)),
16299
+ branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(workspaceRoot))),
16203
16300
  provenance: { branchPolicy: selected.source }
16204
16301
  } : {
16205
16302
  commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
@@ -16356,6 +16453,7 @@ var withReleaseTracks = (workspaceRoot, policy) => {
16356
16453
  return dirty ? "carry" : "clean";
16357
16454
  };
16358
16455
  var init_branch = __esm(() => {
16456
+ init_branch_policy();
16359
16457
  init_commit_flavors();
16360
16458
  init_git();
16361
16459
  init_config();
@@ -18464,7 +18562,7 @@ var init_pr_body = __esm(() => {
18464
18562
 
18465
18563
  // packages/workit-core/src/core/pr-create.ts
18466
18564
  import path19 from "node:path";
18467
- import { spawnSync as spawnSync9 } from "node:child_process";
18565
+ import { spawnSync as spawnSync11 } from "node:child_process";
18468
18566
  import { pathToFileURL } from "node:url";
18469
18567
  var safePushUrl = (remote) => {
18470
18568
  const raw = remote.trim();
@@ -18492,7 +18590,7 @@ var safePushUrl = (remote) => {
18492
18590
  const target = safePushUrl(remote);
18493
18591
  if (!target)
18494
18592
  return false;
18495
- const result = spawnSync9("git", ["config", "--get-regexp", "^url\\..*\\.(insteadof|pushinsteadof)$"], { cwd, encoding: "utf8" });
18593
+ const result = spawnSync11("git", ["config", "--get-regexp", "^url\\..*\\.(insteadof|pushinsteadof)$"], { cwd, encoding: "utf8" });
18496
18594
  if (result.status !== 0 && result.status !== 1)
18497
18595
  return false;
18498
18596
  const prefixes = (result.stdout ?? "").split(/\r?\n/u).map((line) => line.trim().slice(line.trim().indexOf(" ") + 1)).filter(Boolean);
@@ -62780,7 +62878,7 @@ var init_support_matrix = __esm(() => {
62780
62878
  });
62781
62879
 
62782
62880
  // packages/workit-cli/src/admin/host-install.ts
62783
- import { spawnSync as spawnSync10 } from "node:child_process";
62881
+ import { spawnSync as spawnSync12 } from "node:child_process";
62784
62882
  import {
62785
62883
  accessSync,
62786
62884
  constants as constants2,
@@ -63051,7 +63149,7 @@ function runHostCommand(step, context = {}) {
63051
63149
  stderr: `Cannot safely execute batch shim ${step.command}; resolve its Node.js CLI entrypoint and retry`
63052
63150
  };
63053
63151
  }
63054
- const result = spawnSync10(step.command, step.args, {
63152
+ const result = spawnSync12(step.command, step.args, {
63055
63153
  cwd: step.cwd,
63056
63154
  env: (() => {
63057
63155
  const home = context.home ?? context.env?.HOME ?? os6.homedir();
@@ -63256,7 +63354,7 @@ var init_skill_manifests = __esm(() => {
63256
63354
  });
63257
63355
 
63258
63356
  // packages/workit-cli/src/admin/doctor.ts
63259
- import { spawnSync as spawnSync11 } from "node:child_process";
63357
+ import { spawnSync as spawnSync13 } from "node:child_process";
63260
63358
  import {
63261
63359
  existsSync as existsSync13,
63262
63360
  lstatSync as lstatSync4,
@@ -63340,7 +63438,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
63340
63438
  }
63341
63439
  return false;
63342
63440
  }, versionOf = (bin, env, timeout) => {
63343
- const r = spawnSync11(bin, ["--version"], { encoding: "utf8", env, timeout });
63441
+ const r = spawnSync13(bin, ["--version"], { encoding: "utf8", env, timeout });
63344
63442
  if (r.error)
63345
63443
  return null;
63346
63444
  return (r.stdout ?? "").trim();
@@ -63589,7 +63687,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
63589
63687
  `)) {
63590
63688
  return false;
63591
63689
  }
63592
- return spawnSync11(runtime, ["--check", entry], { encoding: "utf8", env }).status === 0;
63690
+ return spawnSync13(runtime, ["--check", entry], { encoding: "utf8", env }).status === 0;
63593
63691
  } catch {
63594
63692
  return false;
63595
63693
  }
@@ -63803,7 +63901,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
63803
63901
  if (!cmd)
63804
63902
  return null;
63805
63903
  try {
63806
- const r = spawnSync11(cmd, ["view", pkgName, "version"], {
63904
+ const r = spawnSync13(cmd, ["view", pkgName, "version"], {
63807
63905
  encoding: "utf8",
63808
63906
  timeout: 20000,
63809
63907
  env: res.env
@@ -64087,6 +64185,16 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64087
64185
  detail: `malformed config: ${bad.join(", ")}`,
64088
64186
  fix: `Repair the malformed config in ${bad[0]}`
64089
64187
  };
64188
+ }, branchPresetLine = (res, workspace) => {
64189
+ try {
64190
+ const resolved = resolveWorkspacePolicy(readConfigFromDir(res.configDir), workspace, undefined, () => repoBranchPreset(res.cwd));
64191
+ if (resolved.status === "invalid")
64192
+ return [];
64193
+ const { preset, detected } = resolved.branchPolicy;
64194
+ return [`preset: ${detected ? `detected (${preset})` : preset}`];
64195
+ } catch {
64196
+ return [];
64197
+ }
64090
64198
  }, checkWorkspaceMismatch = (res) => {
64091
64199
  const result = readWorkspacesResult(res.configDir);
64092
64200
  const file = result.path;
@@ -64094,7 +64202,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64094
64202
  return {
64095
64203
  id: "workspace_mismatch",
64096
64204
  status: "pass",
64097
- detail: "no workspaces configured"
64205
+ detail: ["no workspaces configured", ...branchPresetLine(res, null)].join("; ")
64098
64206
  };
64099
64207
  if (result.status === "malformed" || result.status === "invalid")
64100
64208
  return {
@@ -64118,7 +64226,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64118
64226
  const matched = `current directory matches workspace "${match.name}"`;
64119
64227
  const tracks = releaseTracksReport(res.cwd);
64120
64228
  const lines = describeReleaseTracks(tracks);
64121
- const detail = [matched, ...lines].join("; ");
64229
+ const detail = [matched, ...branchPresetLine(res, match), ...lines].join("; ");
64122
64230
  if (tracks.error)
64123
64231
  return {
64124
64232
  id: "workspace_mismatch",
@@ -64232,7 +64340,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64232
64340
  }, defaultIdentityProbes = (env) => ({
64233
64341
  origin: (cwd) => {
64234
64342
  try {
64235
- const out = spawnSync11("git", ["remote", "get-url", "origin"], {
64343
+ const out = spawnSync13("git", ["remote", "get-url", "origin"], {
64236
64344
  cwd,
64237
64345
  encoding: "utf8",
64238
64346
  env
@@ -64244,7 +64352,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64244
64352
  },
64245
64353
  ghLogin: (e) => {
64246
64354
  try {
64247
- const out = spawnSync11("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", env: e });
64355
+ const out = spawnSync13("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", env: e });
64248
64356
  const login = (out.stdout ?? "").trim();
64249
64357
  return out.status === 0 && login ? login : null;
64250
64358
  } catch {
@@ -64253,7 +64361,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64253
64361
  },
64254
64362
  glabLogin: (e) => {
64255
64363
  try {
64256
- const out = spawnSync11("glab", ["api", "user", "--jq", ".username"], {
64364
+ const out = spawnSync13("glab", ["api", "user", "--jq", ".username"], {
64257
64365
  encoding: "utf8",
64258
64366
  env: e
64259
64367
  });
@@ -64265,7 +64373,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64265
64373
  },
64266
64374
  sshLogin: (host) => {
64267
64375
  try {
64268
- const out = spawnSync11("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "-T", `git@${host}`], {
64376
+ const out = spawnSync13("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "-T", `git@${host}`], {
64269
64377
  encoding: "utf8",
64270
64378
  env
64271
64379
  });
@@ -64460,6 +64568,7 @@ var init_doctor = __esm(() => {
64460
64568
  init_runtime_identity();
64461
64569
  init_boundary();
64462
64570
  init_release_tracks();
64571
+ init_branch_policy();
64463
64572
  init_config();
64464
64573
  init_package_root();
64465
64574
  init_registration();
@@ -65473,69 +65582,6 @@ var init_setup = __esm(() => {
65473
65582
  SETUP_OVERRIDES = [...YT_OVERRIDES, ...VCS_OVERRIDES];
65474
65583
  });
65475
65584
 
65476
- // packages/workit-core/src/core/branch-policy.ts
65477
- import { spawnSync as spawnSync12 } from "node:child_process";
65478
- var PREFIXES, detectBranchPolicy = (workspaceRoot) => {
65479
- const branchExists = (name) => {
65480
- const r = spawnSync12("git", ["branch", "--list", name], {
65481
- cwd: workspaceRoot,
65482
- encoding: "utf8"
65483
- });
65484
- return r.status === 0 && (r.stdout ?? "").trim() !== "";
65485
- };
65486
- const develop = branchExists("develop");
65487
- const main = branchExists("main");
65488
- const master = branchExists("master");
65489
- const root = main ? "main" : master ? "master" : null;
65490
- if (develop) {
65491
- return {
65492
- preset: "gitflow",
65493
- developBranch: "develop",
65494
- integration: "merge",
65495
- protected: root ? [root, "develop"] : ["develop"],
65496
- allowed: [...PRESETS.gitflow.allowed],
65497
- prefixes: PREFIXES
65498
- };
65499
- }
65500
- if (root === "main") {
65501
- return {
65502
- preset: "github-flow",
65503
- developBranch: null,
65504
- integration: "pr",
65505
- protected: [root],
65506
- allowed: ["*"],
65507
- prefixes: PREFIXES
65508
- };
65509
- }
65510
- if (root === "master") {
65511
- return {
65512
- preset: "trunk-based",
65513
- developBranch: null,
65514
- integration: "pr",
65515
- protected: [root],
65516
- allowed: ["*"],
65517
- prefixes: PREFIXES
65518
- };
65519
- }
65520
- return {
65521
- preset: "gitflow",
65522
- developBranch: null,
65523
- integration: "merge",
65524
- protected: [],
65525
- allowed: [],
65526
- prefixes: PREFIXES
65527
- };
65528
- };
65529
- var init_branch_policy = __esm(() => {
65530
- init_config();
65531
- PREFIXES = {
65532
- feature: "feature/*",
65533
- bugfix: "bugfix/*",
65534
- release: "release/*",
65535
- hotfix: "hotfix/*"
65536
- };
65537
- });
65538
-
65539
65585
  // packages/workit-core/src/core/workspace-branch-policy.ts
65540
65586
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
65541
65587
  import path31 from "node:path";
@@ -68624,7 +68670,7 @@ function Screen({
68624
68670
  };
68625
68671
  const checkout = resolveBasePath(draft.values);
68626
68672
  const workspace = resolveWorkspaceFromEntries(checkout, draft.values.workspaces);
68627
- const resolved = resolveWorkspacePolicy(config, workspace);
68673
+ const resolved = resolveWorkspacePolicy(config, workspace, undefined, () => repoBranchPreset(checkout));
68628
68674
  if (resolved.status === "invalid")
68629
68675
  return { error: resolved.error };
68630
68676
  return {
@@ -69485,7 +69531,7 @@ var init_init = __esm(() => {
69485
69531
  });
69486
69532
 
69487
69533
  // packages/workit-cli/src/upgrade.ts
69488
- import { spawnSync as spawnSync13 } from "node:child_process";
69534
+ import { spawnSync as spawnSync14 } from "node:child_process";
69489
69535
  import {
69490
69536
  existsSync as existsSync17,
69491
69537
  mkdtempSync as mkdtempSync3,
@@ -69841,7 +69887,7 @@ async function runLaunchCommand(argv, deps = {}) {
69841
69887
  }
69842
69888
  if (deps.run)
69843
69889
  return deps.run(host, hostArgs).status ?? 1;
69844
- return spawnSync13(launch.command, launch.args, {
69890
+ return spawnSync14(launch.command, launch.args, {
69845
69891
  stdio: "inherit",
69846
69892
  env
69847
69893
  }).status ?? 1;
@@ -70023,7 +70069,7 @@ var init_launch = __esm(() => {
70023
70069
  });
70024
70070
 
70025
70071
  // packages/workit-core/src/knowledge.ts
70026
- import { spawnSync as spawnSync14 } from "node:child_process";
70072
+ import { spawnSync as spawnSync15 } from "node:child_process";
70027
70073
  import fs17 from "node:fs";
70028
70074
  import path36 from "node:path";
70029
70075
  function lintKnowledge(root) {
@@ -70144,7 +70190,7 @@ var AGENTS_BYTE_BUDGET, STEERING_FILES, NEED_BASED_FILES, STANDARDS_FILE = "CODI
70144
70190
  return "unreadable";
70145
70191
  }
70146
70192
  }, trackedTopLevel = (root) => {
70147
- const run = spawnSync14("git", ["ls-files", "-z", "--cached"], {
70193
+ const run = spawnSync15("git", ["ls-files", "-z", "--cached"], {
70148
70194
  cwd: root,
70149
70195
  encoding: "utf8",
70150
70196
  maxBuffer: 256 * 1024 * 1024,
@@ -70523,7 +70569,7 @@ var init_redact = __esm(() => {
70523
70569
  });
70524
70570
 
70525
70571
  // packages/workit-core/src/checks.ts
70526
- import { spawn, spawnSync as spawnSync15 } from "node:child_process";
70572
+ import { spawn, spawnSync as spawnSync16 } from "node:child_process";
70527
70573
  import { createHash as createHash14 } from "node:crypto";
70528
70574
  import fs18 from "node:fs";
70529
70575
  import os13 from "node:os";
@@ -70702,7 +70748,7 @@ function runCheckCommand(argv, options) {
70702
70748
  if (!child?.pid)
70703
70749
  return;
70704
70750
  if (windows)
70705
- spawnSync15("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
70751
+ spawnSync16("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
70706
70752
  stdio: "ignore",
70707
70753
  windowsHide: true
70708
70754
  });
@@ -71424,7 +71470,47 @@ var init_check = __esm(() => {
71424
71470
  var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
71425
71471
 
71426
71472
  // packages/workit-core/src/git/ops.ts
71427
- import { spawnSync as spawnSync16 } from "node:child_process";
71473
+ import { spawnSync as spawnSync17 } from "node:child_process";
71474
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
71475
+ import path39 from "node:path";
71476
+ function operationInProgress(cwd) {
71477
+ const markers = [
71478
+ ["rebase-merge", "rebase"],
71479
+ ["rebase-apply", "rebase"],
71480
+ ["MERGE_HEAD", "merge"],
71481
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
71482
+ ["REVERT_HEAD", "revert"]
71483
+ ];
71484
+ const run = git5(cwd, ["rev-parse", ...markers.flatMap(([name]) => ["--git-path", name])]);
71485
+ if (!run.ok)
71486
+ return null;
71487
+ const found = run.stdout.split(`
71488
+ `).map((line) => path39.resolve(cwd, line.trim()));
71489
+ const conflicted = () => git5(cwd, ["ls-files", "-u"]).stdout.trim() !== "";
71490
+ for (const [index, [, operation]] of markers.entries()) {
71491
+ const marker = found[index];
71492
+ if (!marker || !existsSync18(marker))
71493
+ continue;
71494
+ if (index === 1 && existsSync18(path39.join(marker, "applying")))
71495
+ return { operation: "am", commitAllowed: false, branch: null };
71496
+ if (index === 0 && existsSync18(path39.join(marker, "amend")) && !conflicted()) {
71497
+ let headName = "";
71498
+ try {
71499
+ headName = readFileSync18(path39.join(marker, "head-name"), "utf8").trim();
71500
+ } catch {}
71501
+ const branch = headName.startsWith("refs/heads/") ? headName.slice("refs/heads/".length) : null;
71502
+ return { operation, commitAllowed: branch !== null, branch };
71503
+ }
71504
+ if ((operation === "revert" || operation === "merge") && !conflicted())
71505
+ return { operation, commitAllowed: true, branch: null };
71506
+ return { operation, commitAllowed: false, branch: null };
71507
+ }
71508
+ return null;
71509
+ }
71510
+ function inProgressFailure(operation, verb) {
71511
+ const command = operation === "am" ? "git am" : `git ${operation}`;
71512
+ return failure3("blocked", `${operation.replace("-", "_")}_in_progress: a ${operation === "am" ? "git am" : operation} is in progress; finish or abort it before workit can ${verb}`, `resolve the conflicts, git add <files>, then ${command} --continue # or ${command} --abort to return to where you started`);
71513
+ }
71428
71514
  function dirtEntries(cwd) {
71429
71515
  const run = git5(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
71430
71516
  if (!run.ok)
@@ -71444,6 +71530,9 @@ function dirtEntries(cwd) {
71444
71530
  function gitBranch(cwd, input) {
71445
71531
  if (!headSha(cwd) && currentBranch(cwd) === null)
71446
71532
  return failure3("invalid_input", "not inside a git repository with a commit");
71533
+ const progress = operationInProgress(cwd);
71534
+ if (progress)
71535
+ return inProgressFailure(progress.operation, "create a branch");
71447
71536
  const explicitBase = input.base?.trim() || null;
71448
71537
  const resolved = vcsConfig("resolve", cwd, {
71449
71538
  track: input.track ?? null,
@@ -71548,53 +71637,156 @@ function lintCommitMessage(cwd, message) {
71548
71637
  correction: `use a ${detected} commit subject`
71549
71638
  };
71550
71639
  }
71640
+ function messageTrailers(cwd, message) {
71641
+ const run = git5(cwd, [...TRAILER_CONFIG, "interpret-trailers", "--parse", "--no-divider"], {
71642
+ input: `${message}
71643
+ `
71644
+ });
71645
+ return run.ok ? run.stdout.split(`
71646
+ `).filter((line) => line.trim()) : [];
71647
+ }
71648
+ function withTrailers(cwd, message, trailers) {
71649
+ if (!trailers.length)
71650
+ return message;
71651
+ const run = git5(cwd, [
71652
+ ...TRAILER_CONFIG,
71653
+ "interpret-trailers",
71654
+ "--no-divider",
71655
+ "--if-exists",
71656
+ "addIfDifferent",
71657
+ ...trailers.flatMap((trailer) => ["--trailer", trailer])
71658
+ ], { input: `${message}
71659
+ ` });
71660
+ return run.ok ? run.stdout.trim() : `${message}
71661
+
71662
+ ${trailers.join(`
71663
+ `)}`;
71664
+ }
71665
+ function branchesContaining(cwd, sha) {
71666
+ const run = git5(cwd, [
71667
+ "for-each-ref",
71668
+ "--format=%(refname)",
71669
+ "--contains",
71670
+ sha,
71671
+ "refs/heads",
71672
+ "refs/remotes"
71673
+ ]);
71674
+ const names = new Set;
71675
+ for (const ref of run.ok ? run.stdout.split(`
71676
+ `) : []) {
71677
+ if (ref.startsWith("refs/heads/"))
71678
+ names.add(ref.slice("refs/heads/".length));
71679
+ else if (ref.startsWith("refs/remotes/")) {
71680
+ const name = ref.split("/").slice(3).join("/");
71681
+ if (name && name !== "HEAD")
71682
+ names.add(name);
71683
+ }
71684
+ }
71685
+ return names;
71686
+ }
71687
+ function sharedBranchContaining(cwd, sha, branch) {
71688
+ const resolved = vcsConfig("resolve", cwd);
71689
+ const defaults = new Set(resolved.ok === false ? [] : [resolved.defaultTargetBranch, resolved.baseBranch].map((value) => String(value ?? "")).filter(Boolean));
71690
+ for (const name of branchesContaining(cwd, sha))
71691
+ if (name !== branch && (defaults.has(name) || isProtectedTarget(cwd, name)))
71692
+ return name;
71693
+ return null;
71694
+ }
71551
71695
  function gitCommit(cwd, input) {
71552
- const message = input.message.trim();
71553
- if (!message)
71554
- return failure3("invalid_input", "a commit message is required (-m <msg>)");
71696
+ const given = input.message?.trim() ?? "";
71697
+ if (!given && !input.amend)
71698
+ return failure3("invalid_input", "a commit message is required (-m <msg> or -F <file>)");
71555
71699
  const paths = input.paths ?? [];
71556
71700
  if (input.all && paths.length)
71557
71701
  return failure3("invalid_input", "pass --all or paths, not both");
71558
- const branch = currentBranch(cwd);
71702
+ const progress = operationInProgress(cwd);
71703
+ if (progress && !progress.commitAllowed)
71704
+ return inProgressFailure(progress.operation, "commit");
71705
+ const branch = progress?.branch ?? currentBranch(cwd);
71559
71706
  if (!branch)
71560
71707
  return failure3("invalid_input", "HEAD is detached; commit on a branch (workit git branch <name>)");
71561
71708
  if (isProtectedTarget(cwd, branch))
71562
71709
  return failure3("blocked", `protected_branch: ${branch} is protected by the workspace branch policy; workit never commits to it`, "workit git branch <feature/name> # then commit there");
71563
- const lint = lintCommitMessage(cwd, message);
71564
- if (!lint.ok)
71565
- return failure3("blocked", lint.error, lint.correction);
71710
+ const previous = input.amend ? headSha(cwd) : null;
71711
+ if (input.amend && !previous)
71712
+ return failure3("invalid_input", `nothing to amend: ${branch} has no commit yet`);
71713
+ if (previous) {
71714
+ const shared = sharedBranchContaining(cwd, previous, branch);
71715
+ if (shared)
71716
+ return failure3("blocked", `amend_shared_commit: ${previous.slice(0, 12)} is already on ${shared}, so it is not ${branch}'s own commit; workit never rewrites it`, "workit git commit -m '<msg>' -- <paths> # add a new commit instead");
71717
+ }
71718
+ let message = given;
71719
+ let carried = [];
71720
+ if (given) {
71721
+ const lint = lintCommitMessage(cwd, given);
71722
+ if (!lint.ok)
71723
+ return failure3("blocked", lint.error, lint.correction);
71724
+ }
71725
+ if (previous) {
71726
+ const kept = git5(cwd, ["log", "-1", "--format=%B", previous]);
71727
+ if (!kept.ok)
71728
+ return failure3("failed", `could not read the message of ${branch}: ${gitError(kept)}`);
71729
+ if (given)
71730
+ carried = messageTrailers(cwd, kept.stdout.trim());
71731
+ else
71732
+ message = kept.stdout.trim();
71733
+ }
71566
71734
  const session = input.actor.session;
71567
71735
  if (session && !SESSION_SAFE.test(session))
71568
71736
  return failure3("invalid_input", "WORKIT_SESSION_ID must be 1-128 characters of [A-Za-z0-9_.:@/+-]");
71569
- const args = ["commit", "--no-edit", "-m", message];
71570
- if (session)
71571
- args.push("--trailer", `Workit-Session: ${session}`);
71737
+ const changeRequired = !input.amend && !input.allowEmpty;
71738
+ const emptyAmend = previous !== null && git5(cwd, ["rev-parse", "--verify", "-q", `${previous}^{tree}`]).stdout.trim() === git5(cwd, ["rev-parse", "--verify", "-q", `${previous}~1^{tree}`]).stdout.trim();
71739
+ const args = ["commit", "--no-edit"];
71740
+ if (input.amend)
71741
+ args.push("--amend");
71742
+ if (input.allowEmpty || emptyAmend)
71743
+ args.push("--allow-empty");
71744
+ args.push("-m", withTrailers(cwd, message, [...carried, ...session ? [`Workit-Session: ${session}`] : []]));
71572
71745
  if (paths.length) {
71573
71746
  if (paths.some((value) => value.startsWith("-") || !value.trim()))
71574
71747
  return failure3("invalid_input", "paths must not be empty or start with -");
71575
- const added = git5(cwd, ["add", "-A", "--", ...paths]);
71576
- if (!added.ok)
71577
- return failure3("failed", `git add failed: ${gitError(added)}`);
71748
+ const tracked = new Set(nulList(git5(cwd, ["ls-files", "-z", "--", ...paths]).stdout));
71749
+ const addable = paths.filter((value) => existsSync18(path39.resolve(cwd, value)) || [...tracked].some((file) => file === value || file.startsWith(`${value}/`)));
71750
+ if (addable.length) {
71751
+ const added = git5(cwd, ["add", "-A", "--", ...addable]);
71752
+ if (!added.ok)
71753
+ return failure3("failed", `git add failed: ${gitError(added)}`);
71754
+ }
71755
+ if (changeRequired && git5(cwd, ["diff", "--cached", "--quiet", "HEAD", "--", ...paths]).ok)
71756
+ return nothingToCommit(`${paths.join(", ")} has no change to commit`);
71578
71757
  args.push("--", ...paths);
71579
71758
  } else if (input.all) {
71580
71759
  const added = git5(cwd, ["add", "-A"]);
71581
71760
  if (!added.ok)
71582
71761
  return failure3("failed", `git add failed: ${gitError(added)}`);
71583
- } else {
71762
+ if (changeRequired && git5(cwd, ["diff", "--cached", "--quiet"]).ok)
71763
+ return nothingToCommit("the working tree is clean");
71764
+ } else if (changeRequired) {
71584
71765
  const staged = git5(cwd, ["diff", "--cached", "--name-only", "-z"]);
71585
71766
  if (staged.ok && !staged.stdout.replaceAll("\x00", "")) {
71586
71767
  const dirt = dirtEntries(cwd);
71587
- return failure3(dirt.length ? "blocked" : "failed", dirt.length ? `nothing_staged: nothing is staged and workit never commits unrelated changes implicitly (${dirt.length} changed: ${dirt.slice(0, 5).map((entry) => entry.slice(3)).join(", ")}${dirt.length > 5 ? ", …" : ""})` : "nothing_to_commit: the working tree is clean", dirt.length ? `workit git commit -m '<msg>' -- <paths> # or --all to commit every change` : undefined);
71768
+ if (!dirt.length)
71769
+ return nothingToCommit("the working tree is clean");
71770
+ return failure3("blocked", `nothing_staged: nothing is staged and workit never commits unrelated changes implicitly (${dirt.length} changed: ${dirt.slice(0, 5).map((entry) => entry.slice(3)).join(", ")}${dirt.length > 5 ? ", …" : ""})`, `workit git commit -m '<msg>' -- <paths> # or --all to commit every change`);
71588
71771
  }
71589
71772
  }
71590
- const committed = git5(cwd, args, { timeoutMs: COMMIT_TIMEOUT_MS, env: input.env });
71773
+ const committed = git5(cwd, args, {
71774
+ timeoutMs: COMMIT_TIMEOUT_MS,
71775
+ env: input.env
71776
+ });
71591
71777
  if (!committed.ok)
71592
- return failure3("failed", `git commit failed: ${gitError(committed, 6)}`);
71778
+ return failure3("failed", `git commit failed (a hook or git refused it):
71779
+ ${gitOutput(committed)}`);
71593
71780
  const sha = headSha(cwd);
71594
71781
  if (!sha)
71595
71782
  return failure3("failed", "the commit did not resolve");
71596
- const files = nulList(git5(cwd, ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", "-z", sha]).stdout);
71783
+ const merge = git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok;
71784
+ const files = nulList(git5(cwd, merge ? ["diff", "--name-only", "-z", `${sha}^1`, sha] : ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", "-z", sha]).stdout);
71597
71785
  const leftDirty = dirtEntries(cwd).length;
71786
+ const notes = [];
71787
+ const upstream = `refs/remotes/${pushRemoteName(cwd, branch) ?? "origin"}/${branch}`;
71788
+ if (previous && git5(cwd, ["merge-base", "--is-ancestor", previous, upstream]).ok)
71789
+ notes.push(`the amended commit ${previous.slice(0, 12)} was already pushed (${upstream.slice("refs/remotes/".length)}); publish the rewrite with: workit git push --force-with-lease`);
71598
71790
  const listed = [];
71599
71791
  let size = 0;
71600
71792
  for (const file of files) {
@@ -71614,7 +71806,8 @@ function gitCommit(cwd, input) {
71614
71806
  subject: (message.split(`
71615
71807
  `, 1)[0] ?? "").slice(0, 200),
71616
71808
  files: listed,
71617
- fileCount: files.length
71809
+ fileCount: files.length,
71810
+ ...previous ? { amended: previous } : {}
71618
71811
  });
71619
71812
  return success2({
71620
71813
  sha,
@@ -71623,10 +71816,15 @@ function gitCommit(cwd, input) {
71623
71816
  files,
71624
71817
  leftDirty,
71625
71818
  session,
71819
+ amended: previous,
71820
+ notes,
71626
71821
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
71627
71822
  });
71628
71823
  }
71629
71824
  function pushPreflight(cwd, options = {}) {
71825
+ const progress = operationInProgress(cwd);
71826
+ if (progress)
71827
+ return inProgressFailure(progress.operation, "push");
71630
71828
  const branch = options.branch ?? currentBranch(cwd);
71631
71829
  if (!branch)
71632
71830
  return failure3("invalid_input", "HEAD is detached; push a branch");
@@ -71665,11 +71863,35 @@ function recordedRemoteTip(cwd, plan) {
71665
71863
  return;
71666
71864
  }
71667
71865
  function includesRemoteTip(cwd, branch, tip) {
71668
- if (git5(cwd, ["merge-base", "--is-ancestor", tip, `refs/heads/${branch}`]).ok)
71866
+ const ref = `refs/heads/${branch}`;
71867
+ if (git5(cwd, ["merge-base", "--is-ancestor", tip, ref]).ok)
71868
+ return true;
71869
+ const reflog = git5(cwd, ["reflog", "show", "--format=%H", ref, "--"]);
71870
+ const former = [
71871
+ ...new Set((reflog.ok ? reflog.stdout : "").split(`
71872
+ `).map((line) => line.trim()).filter(Boolean))
71873
+ ];
71874
+ if (former.includes(tip))
71669
71875
  return true;
71670
- const reflog = git5(cwd, ["reflog", "show", "--format=%H", `refs/heads/${branch}`, "--"]);
71671
- return reflog.ok && reflog.stdout.split(`
71672
- `).some((line) => line.trim() === tip);
71876
+ if (!former.length)
71877
+ return false;
71878
+ const outside = git5(cwd, ["rev-list", "-n", "1", "--stdin"], {
71879
+ input: `${tip}
71880
+ ${former.map((sha) => `^${sha}`).join(`
71881
+ `)}
71882
+ `
71883
+ });
71884
+ return outside.ok && !outside.stdout.trim();
71885
+ }
71886
+ function patchEquivalent(cwd, branch, tip) {
71887
+ const ref = `refs/heads/${branch}`;
71888
+ const merges = git5(cwd, ["rev-list", "--count", "--merges", `${ref}..${tip}`]);
71889
+ if (!merges.ok || merges.stdout.trim() !== "0")
71890
+ return false;
71891
+ const cherry = git5(cwd, ["cherry", ref, tip]);
71892
+ const lines = cherry.ok ? cherry.stdout.split(`
71893
+ `).filter(Boolean) : [];
71894
+ return lines.length > 0 && lines.every((line) => line.startsWith("-"));
71673
71895
  }
71674
71896
  function executePush(cwd, plan, input) {
71675
71897
  const before = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.branch}`);
@@ -71704,9 +71926,17 @@ ${run.stdout}`;
71704
71926
  return failure3("unavailable", `git push to ${plan.url} timed out`);
71705
71927
  if (/stale info/iu.test(text))
71706
71928
  return failure3("blocked", `lease_mismatch: ${plan.remote}/${plan.branch} moved while pushing`, `git fetch ${plan.remote} ${plan.branch} && git rebase ${plan.remote}/${plan.branch} # or merge it; then: workit git push`);
71929
+ const rejected = /non-fast-forward|fetch first|\[rejected\]/iu.test(text);
71930
+ const integrated = rejected && before.sha !== null && includesRemoteTip(cwd, plan.branch, before.sha);
71931
+ const equivalent = rejected && !integrated && before.sha !== null && patchEquivalent(cwd, plan.branch, before.sha);
71932
+ if (before.sha !== null && (integrated || equivalent)) {
71933
+ const lease = recordedRemoteTip(cwd, plan) === before.sha ? "workit git push --force-with-lease" : `workit git push --force-with-lease --expect ${before.sha}`;
71934
+ return failure3("failed", integrated ? `non_fast_forward: local ${plan.branch} was rewritten (amend or rebase) after ${plan.remote}/${plan.branch} (${before.sha.slice(0, 12)}) was pushed; that tip is already part of the local history, so do not fetch and rebase` : `non_fast_forward: every commit on ${plan.remote}/${plan.branch} (${before.sha.slice(0, 12)}) has a patch-equivalent commit in local ${plan.branch} (rebased elsewhere); fetching and rebasing would apply them twice`, integrated ? lease : `${lease} --overwrite-unintegrated # after checking ${plan.remote}/${plan.branch} holds nothing else`);
71935
+ }
71707
71936
  if (/non-fast-forward|fetch first|\[rejected\]/iu.test(text))
71708
71937
  return failure3("failed", `non_fast_forward: ${plan.remote}/${plan.branch} has commits this branch does not`, `git fetch ${plan.remote} && git rebase ${plan.remote}/${plan.branch} # or, if the rewrite is intended: workit git push --force-with-lease`);
71709
- return failure3("failed", `git push to ${plan.url} failed: ${gitError(run)}`);
71938
+ return failure3("failed", `git push to ${plan.url} failed:
71939
+ ${gitOutput(run)}`);
71710
71940
  }
71711
71941
  pushed = true;
71712
71942
  }
@@ -71728,7 +71958,8 @@ ${run.stdout}`;
71728
71958
  url: plan.url,
71729
71959
  previous: before.sha,
71730
71960
  pushed,
71731
- forced
71961
+ forced,
71962
+ ...input.identity ? { identity: input.identity } : {}
71732
71963
  });
71733
71964
  return success2({
71734
71965
  remote: plan.remote,
@@ -71765,10 +71996,14 @@ function deleteRemoteBranch(cwd, target, branch, expectedTip, options = {}) {
71765
71996
  return { ok: true };
71766
71997
  const text = `${run.stderr}
71767
71998
  ${run.stdout}`;
71768
- return /stale info|cannot lock ref|fetch first|non-fast-forward/iu.test(text) ? { ok: false, lease: true, error: "the remote branch tip changed before deletion" } : { ok: false, lease: false, error: gitError(run) };
71999
+ return /stale info|cannot lock ref|fetch first|non-fast-forward/iu.test(text) ? {
72000
+ ok: false,
72001
+ lease: true,
72002
+ error: "the remote branch tip changed before deletion"
72003
+ } : { ok: false, lease: false, error: gitError(run) };
71769
72004
  }
71770
72005
  var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}) => {
71771
- const result = spawnSync16("git", args, {
72006
+ const result = spawnSync17("git", args, {
71772
72007
  cwd,
71773
72008
  encoding: "utf8",
71774
72009
  env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
@@ -71776,7 +72011,7 @@ var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}
71776
72011
  timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
71777
72012
  killSignal: "SIGKILL",
71778
72013
  windowsHide: true,
71779
- stdio: ["ignore", "pipe", "pipe"]
72014
+ ...options.input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }
71780
72015
  });
71781
72016
  return {
71782
72017
  ok: result.status === 0,
@@ -71788,7 +72023,22 @@ var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}
71788
72023
  const lines = redactText(`${run.stderr}
71789
72024
  ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trim()).filter((value) => value && !value.startsWith("hint:"));
71790
72025
  return lines.slice(-max).join(" / ").slice(0, 400) || "git failed";
71791
- }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, SHA;
72026
+ }, OUTPUT_LINES = 200, OUTPUT_LINE_CHARS = 1000, gitOutput = (run) => {
72027
+ const lines = redactText(`${run.stderr}
72028
+ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((value) => value.trim() && !value.trimStart().startsWith("hint:")).map((value) => value.length > OUTPUT_LINE_CHARS ? `${value.slice(0, OUTPUT_LINE_CHARS)}…` : value);
72029
+ if (!lines.length)
72030
+ return "git failed";
72031
+ if (lines.length <= OUTPUT_LINES)
72032
+ return lines.join(`
72033
+ `);
72034
+ const half = OUTPUT_LINES / 2;
72035
+ return [
72036
+ ...lines.slice(0, half),
72037
+ `… ${lines.length - OUTPUT_LINES} lines omitted …`,
72038
+ ...lines.slice(-half)
72039
+ ].join(`
72040
+ `);
72041
+ }, 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;
71792
72042
  var init_ops = __esm(() => {
71793
72043
  init_branch();
71794
72044
  init_commit_flavors();
@@ -71800,15 +72050,16 @@ var init_ops = __esm(() => {
71800
72050
  init_rev();
71801
72051
  COMMIT_TIMEOUT_MS = 10 * 60000;
71802
72052
  SESSION_SAFE = /^[\w.:@/+-]{1,128}$/u;
72053
+ TRAILER_CONFIG = ["-c", "trailer.separators=:", "-c", "trailer.where=end"];
71803
72054
  SHA = /^[0-9a-f]{40,64}$/u;
71804
72055
  });
71805
72056
 
71806
72057
  // packages/workit-core/src/forge/reruns.ts
71807
72058
  import fs19 from "node:fs";
71808
- import path39 from "node:path";
72059
+ import path40 from "node:path";
71809
72060
  function rerunLogPath(cwd) {
71810
72061
  const common = gitCommonDir(cwd);
71811
- return common ? path39.join(common, "workit", "ci-reruns.jsonl") : null;
72062
+ return common ? path40.join(common, "workit", "ci-reruns.jsonl") : null;
71812
72063
  }
71813
72064
  function rerunCounts(cwd, key) {
71814
72065
  const counts = new Map;
@@ -71843,7 +72094,7 @@ function recordReruns(cwd, rows) {
71843
72094
  if (!file)
71844
72095
  return false;
71845
72096
  try {
71846
- fs19.mkdirSync(path39.dirname(file), { recursive: true });
72097
+ fs19.mkdirSync(path40.dirname(file), { recursive: true });
71847
72098
  fs19.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
71848
72099
  `).join(""));
71849
72100
  return true;
@@ -71856,7 +72107,7 @@ function withRerunLock(cwd, fn) {
71856
72107
  if (!file)
71857
72108
  return fn();
71858
72109
  const lock = `${file.slice(0, -".jsonl".length)}.lock`;
71859
- fs19.mkdirSync(path39.dirname(lock), { recursive: true });
72110
+ fs19.mkdirSync(path40.dirname(lock), { recursive: true });
71860
72111
  const until = Date.now() + LOCK_WAIT_MS2;
71861
72112
  for (;; ) {
71862
72113
  try {
@@ -72132,16 +72383,16 @@ function waitVerdict(doc, options) {
72132
72383
  const checks = doc.checks.state;
72133
72384
  if (checks === "failing")
72134
72385
  return { state: "failed", reason: "checks_failing" };
72135
- if (checks === "pending")
72136
- return { state: "waiting", reason: "checks_pending" };
72137
- if (checks === "passing")
72138
- return { state: "ready", reason: "checks_passing" };
72139
- if (doc.conflicts)
72386
+ if (doc.conflicts && checks !== "passing")
72140
72387
  return {
72141
72388
  state: "blocked",
72142
72389
  reason: "conflicts",
72143
72390
  unblock: `rebase onto ${doc.base} and push; CI does not run on a conflicting PR`
72144
72391
  };
72392
+ if (checks === "pending")
72393
+ return { state: "waiting", reason: "checks_pending" };
72394
+ if (checks === "passing")
72395
+ return { state: "ready", reason: "checks_passing" };
72145
72396
  const ciRequired = doc.checks.missingRequired === null || doc.mergeState === "ci_must_pass" || doc.mergeState === "ci_still_running";
72146
72397
  if (ciRequired)
72147
72398
  return { state: "waiting", reason: "no_checks_yet" };
@@ -72249,7 +72500,7 @@ var init_report = __esm(() => {
72249
72500
  });
72250
72501
 
72251
72502
  // packages/workit-core/src/forge/pr-ops.ts
72252
- import { spawnSync as spawnSync17 } from "node:child_process";
72503
+ import { spawnSync as spawnSync18 } from "node:child_process";
72253
72504
  function fillFromCommits(cwd, resolved, base) {
72254
72505
  const tracking = `refs/remotes/${resolved.baseRemote}/${base}`;
72255
72506
  if (!base.startsWith("-"))
@@ -72257,7 +72508,7 @@ function fillFromCommits(cwd, resolved, base) {
72257
72508
  const from = resolveRef(cwd, tracking) ?? resolveRef(cwd, `refs/heads/${base}`);
72258
72509
  if (!from)
72259
72510
  return failure3("not_found", `base ${base} is not available locally`, `git fetch ${resolved.baseRemote} ${base}`);
72260
- const log = spawnSync17("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
72511
+ const log = spawnSync18("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
72261
72512
  cwd,
72262
72513
  encoding: "utf8",
72263
72514
  timeout: GIT_TIMEOUTS.local,
@@ -72562,15 +72813,15 @@ var init_pr_ops = __esm(() => {
72562
72813
  });
72563
72814
 
72564
72815
  // packages/workit-core/src/forge/exec.ts
72565
- import { spawnSync as spawnSync18 } from "node:child_process";
72816
+ import { spawnSync as spawnSync19 } from "node:child_process";
72566
72817
  import fs20 from "node:fs";
72567
- import path40 from "node:path";
72818
+ import path41 from "node:path";
72568
72819
  function findExecutable(bin, env) {
72569
- const dirs = (env.PATH ?? env.Path ?? "").split(path40.delimiter).filter(Boolean);
72820
+ const dirs = (env.PATH ?? env.Path ?? "").split(path41.delimiter).filter(Boolean);
72570
72821
  const exts = process.platform === "win32" ? [".exe"] : [""];
72571
72822
  for (const dir of dirs)
72572
72823
  for (const ext of exts) {
72573
- const candidate = path40.join(dir, `${bin}${ext}`);
72824
+ const candidate = path41.join(dir, `${bin}${ext}`);
72574
72825
  try {
72575
72826
  const stat = fs20.statSync(candidate);
72576
72827
  if (stat.isFile())
@@ -72597,7 +72848,7 @@ function systemRunner(kind, apiHost, env = process.env) {
72597
72848
  if (!exe)
72598
72849
  return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
72599
72850
  const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
72600
- const result = spawnSync18(exe, [...args], {
72851
+ const result = spawnSync19(exe, [...args], {
72601
72852
  encoding: "utf8",
72602
72853
  env: { ...childEnv, ...token },
72603
72854
  maxBuffer: MAX_BUFFER2,
@@ -72616,17 +72867,45 @@ function systemRunner(kind, apiHost, env = process.env) {
72616
72867
  };
72617
72868
  };
72618
72869
  }
72870
+ function forgeReason(run) {
72871
+ const parts = [];
72872
+ try {
72873
+ const body = JSON.parse(run.stdout);
72874
+ const add = (value) => {
72875
+ if (typeof value === "string" && value.trim())
72876
+ parts.push(value.trim());
72877
+ else if (Array.isArray(value))
72878
+ value.forEach(add);
72879
+ else if (value && typeof value === "object") {
72880
+ const record = value;
72881
+ if (typeof record.message === "string")
72882
+ add(record.message);
72883
+ else if (!("code" in record))
72884
+ Object.values(record).forEach(add);
72885
+ }
72886
+ };
72887
+ add(body.message);
72888
+ add(body.error);
72889
+ add(body.errors);
72890
+ } catch {}
72891
+ const stderr = (run.stderr.split(/\r?\n/u).find((value) => value.trim()) ?? "").trim();
72892
+ const unique = [...new Set(parts)];
72893
+ const text = unique.length ? `${unique.join("; ")}${stderr ? ` (${stderr.replace(/^gh: |^glab: /u, "")})` : ""}` : stderr || (run.stdout.split(/\r?\n/u).find((value) => value.trim()) ?? "");
72894
+ return redactText(text).slice(0, 300);
72895
+ }
72619
72896
  function cliFailure(bin, apiHost, run, what, timeoutMs) {
72620
72897
  if (run.missing)
72621
72898
  return failure3("unavailable", `${bin} is not installed`, installHint(bin));
72622
72899
  if (run.timedOut)
72623
- return failure3("unavailable", `${bin} api ${what} timed out after ${timeoutMs} ms`);
72900
+ return failure3("unavailable", run.timeoutMs === 0 ? `${bin} api ${what} not attempted: the command's time budget ran out` : `${bin} api ${what} timed out after ${run.timeoutMs ?? timeoutMs} ms`, `check the network to ${apiHost}, then retry`);
72624
72901
  const stderr = redactText(run.stderr).trim();
72625
72902
  if (/auth login|not logged in|no token|authentication required|bad credentials|\b401\b|unauthorized/iu.test(stderr))
72626
72903
  return failure3("unavailable", `${bin} is not authenticated for ${apiHost}`, loginHint(bin, apiHost));
72627
72904
  if (/\b404\b|not found/iu.test(stderr))
72628
72905
  return failure3("not_found", `${what} was not found`);
72629
72906
  const first = stderr.split(/\r?\n/u).find((value) => value.trim()) ?? `exit ${run.status}`;
72907
+ if (isNetworkError(stderr))
72908
+ return failure3("unavailable", `${bin} api ${what} failed (network): ${first.slice(0, 200)}`, `check the network to ${apiHost}, then retry`);
72630
72909
  return failure3("failed", `${bin} api ${what} failed: ${first.slice(0, 200)}`);
72631
72910
  }
72632
72911
  function apiJson(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api, options = {}) {
@@ -72657,12 +72936,15 @@ function apiWrite(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.a
72657
72936
  if (run.status !== 0) {
72658
72937
  const text = redactText(`${run.stderr}
72659
72938
  ${run.stdout}`).trim();
72660
- const reason = (text.split(/\r?\n/u).find((value) => value.trim()) ?? "").slice(0, 200);
72939
+ const reason = forgeReason(run);
72661
72940
  if (!run.missing && !run.timedOut && /\b409\b|head branch was modified|sha does not match|sha.*mismatch/iu.test(text))
72662
72941
  return failure3("blocked", `head_moved: ${what} refused because the head changed (${reason})`, "workit pr status # re-check the new head, then verify it again");
72663
72942
  if (!run.missing && !run.timedOut && /\b(405|406|422)\b/u.test(text))
72664
72943
  return failure3("blocked", `${what} refused by the forge: ${reason}`);
72665
- return cliFailure(bin, apiHost, run, what, timeoutMs);
72944
+ const failed = cliFailure(bin, apiHost, run, what, timeoutMs);
72945
+ if (!failed.ok && failed.code === "unavailable" && !run.missing && !/ is not authenticated for /u.test(failed.error))
72946
+ return failure3("unavailable", failed.error, "workit pr status # the write may have gone through; check it first, then retry");
72947
+ return failed;
72666
72948
  }
72667
72949
  if (!run.stdout.trim())
72668
72950
  return { ok: true, data: {} };
@@ -72672,11 +72954,12 @@ ${run.stdout}`).trim();
72672
72954
  return failure3("failed", `${bin} api ${what} returned invalid JSON`);
72673
72955
  }
72674
72956
  }
72675
- var FORGE_TIMEOUTS, MAX_BUFFER2, installHint = (bin) => bin === "gh" ? "install the GitHub CLI (https://cli.github.com) and run: gh auth login" : "install glab (https://gitlab.com/gitlab-org/cli) and run: glab auth login", loginHint = (bin, apiHost) => `${bin} auth login --hostname ${apiHost}`;
72957
+ var FORGE_TIMEOUTS, MAX_BUFFER2, installHint = (bin) => bin === "gh" ? "install the GitHub CLI (https://cli.github.com) and run: gh auth login" : "install glab (https://gitlab.com/gitlab-org/cli) and run: glab auth login", loginHint = (bin, apiHost) => `${bin} auth login --hostname ${apiHost}`, NETWORK, isNetworkError = (text) => NETWORK.test(text);
72676
72958
  var init_exec = __esm(() => {
72677
72959
  init_redact();
72678
72960
  FORGE_TIMEOUTS = { api: 20000, log: 30000 };
72679
72961
  MAX_BUFFER2 = 64 * 1024 * 1024;
72962
+ NETWORK = /error connecting|could not resolve|no such host|connection (refused|reset|timed out)|network is unreachable|i\/o timeout|tls handshake|unexpected eof|\bHTTP 5\d\d\b|bad gateway|service unavailable|gateway time-?out/iu;
72680
72963
  });
72681
72964
 
72682
72965
  // packages/workit-core/src/forge/github.ts
@@ -73268,6 +73551,82 @@ var init_gitlab = __esm(() => {
73268
73551
 
73269
73552
  // packages/workit-core/src/forge/resolve.ts
73270
73553
  import fs21 from "node:fs";
73554
+ function memo4(scope, key, compute, now = Date.now) {
73555
+ let entries = memoScopes.get(scope.owner);
73556
+ if (!entries)
73557
+ memoScopes.set(scope.owner, entries = new Map);
73558
+ const full = `${scope.key} ${key}`;
73559
+ const hit = entries.get(full);
73560
+ if (hit && now() - hit.at < MEMO_TTL_MS)
73561
+ return hit.value;
73562
+ const { keep, value } = compute();
73563
+ if (keep)
73564
+ entries.set(full, { at: now(), value });
73565
+ return value;
73566
+ }
73567
+ function clampedRunner(base, limits, now, token) {
73568
+ return (bin, args, callOptions) => {
73569
+ let timeoutMs = callOptions.timeoutMs;
73570
+ if (limits.deadline !== null) {
73571
+ const remaining = limits.deadline - now();
73572
+ if (remaining <= 0)
73573
+ return {
73574
+ status: null,
73575
+ stdout: "",
73576
+ stderr: "",
73577
+ timedOut: true,
73578
+ missing: false,
73579
+ timeoutMs: 0
73580
+ };
73581
+ timeoutMs = Math.min(timeoutMs, remaining);
73582
+ }
73583
+ const run = base(bin, args, { ...callOptions, timeoutMs, ...token ? { token } : {} });
73584
+ return run.timedOut && run.timeoutMs === undefined ? { ...run, timeoutMs } : run;
73585
+ };
73586
+ }
73587
+ function resolveCredential(workspace, derived, run, scope) {
73588
+ const { kind, apiHost } = derived;
73589
+ const expectedAccount = workspace?.vcs?.account ?? null;
73590
+ const tokenFile = workspace?.vcs?.tokenFile;
73591
+ if (typeof tokenFile === "string" && tokenFile.trim()) {
73592
+ const workspaceHost = workspace?.vcs?.host;
73593
+ const explicit = typeof workspaceHost === "string" && workspaceHost.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "") === apiHost;
73594
+ if (derived.via === "host_name" && !explicit)
73595
+ 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`);
73596
+ let token = "";
73597
+ try {
73598
+ token = fs21.readFileSync(tokenFile.trim(), "utf8").trim();
73599
+ } catch {
73600
+ return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
73601
+ }
73602
+ if (!token)
73603
+ return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile is empty`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
73604
+ return success2({ token, credential: "workspace_token_file" });
73605
+ }
73606
+ if (!expectedAccount || kind !== "github")
73607
+ return success2({ token: undefined, credential: "cli_login" });
73608
+ return memo4(scope, `token ${apiHost} ${expectedAccount}`, () => {
73609
+ const issued = run("gh", ["auth", "token", "--hostname", apiHost, "--user", expectedAccount], {
73610
+ timeoutMs: FORGE_TIMEOUTS.api
73611
+ });
73612
+ const value = authTokenResult(issued, apiHost, expectedAccount, workspace?.name ?? "?");
73613
+ return { keep: value.ok, value };
73614
+ });
73615
+ }
73616
+ function authTokenResult(issued, apiHost, account, workspace) {
73617
+ if (issued.missing)
73618
+ return failure3("unavailable", "gh is not installed", "install the GitHub CLI (https://cli.github.com) and run: gh auth login");
73619
+ if (issued.timedOut)
73620
+ return failure3("unavailable", `gh auth token ${issued.timeoutMs === 0 ? "not attempted: the time budget ran out" : `timed out after ${issued.timeoutMs ?? FORGE_TIMEOUTS.api} ms`} (looking up the ${account} login on ${apiHost})`, "gh auth status # check gh and the network, then retry");
73621
+ const token = issued.status === 0 ? issued.stdout.trim() : "";
73622
+ if (token)
73623
+ return success2({ token, credential: "gh_account_token" });
73624
+ const stderr = redactText(issued.stderr).trim();
73625
+ const first = stderr.split(/\r?\n/u).find((line) => line.trim()) ?? "";
73626
+ if (issued.status !== 0 && first && !/no oauth token|not logged in|no account/iu.test(stderr))
73627
+ return failure3("unavailable", `gh auth token failed${isNetworkError(stderr) ? " (network)" : ""}: ${first.slice(0, 200)}`, "gh auth status # check gh and the network, then retry");
73628
+ return failure3("blocked", `identity_unavailable: gh has no login for ${account} on ${apiHost} (workspace ${workspace})`, `gh auth login --hostname ${apiHost} # sign in as ${account}; workit passes that account's token per call and never switches the active account`);
73629
+ }
73271
73630
  function resolveForge(cwd, options = {}) {
73272
73631
  let workspace;
73273
73632
  try {
@@ -73285,48 +73644,16 @@ function resolveForge(cwd, options = {}) {
73285
73644
  const derived = pushed.forge;
73286
73645
  const { kind, apiHost } = derived;
73287
73646
  const base = options.runner ?? systemRunner(kind, apiHost, options.env ?? process.env);
73647
+ const scope = memoScope(options.runner, options.env ?? process.env);
73288
73648
  const now = options.now ?? Date.now;
73289
73649
  const expectedAccount = workspace?.vcs?.account ?? null;
73290
- let token;
73291
- let credential = "cli_login";
73292
- const tokenFile = workspace?.vcs?.tokenFile;
73293
- if (typeof tokenFile === "string" && tokenFile.trim()) {
73294
- const workspaceHost = workspace?.vcs?.host;
73295
- const explicit = typeof workspaceHost === "string" && workspaceHost.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "") === apiHost;
73296
- if (derived.via === "host_name" && !explicit)
73297
- 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`);
73298
- try {
73299
- token = fs21.readFileSync(tokenFile.trim(), "utf8").trim();
73300
- } catch {
73301
- return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
73302
- }
73303
- if (!token)
73304
- return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile is empty`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
73305
- credential = "workspace_token_file";
73306
- } else if (expectedAccount && kind === "github") {
73307
- const issued = base("gh", ["auth", "token", "--hostname", apiHost, "--user", expectedAccount], {
73308
- timeoutMs: FORGE_TIMEOUTS.api
73309
- });
73310
- if (issued.missing)
73311
- return failure3("unavailable", "gh is not installed", "install the GitHub CLI (https://cli.github.com) and run: gh auth login");
73312
- token = issued.status === 0 ? issued.stdout.trim() : "";
73313
- if (!token)
73314
- return failure3("blocked", `identity_unavailable: gh has no login for ${expectedAccount} on ${apiHost} (workspace ${workspace?.name ?? "?"})`, `gh auth login --hostname ${apiHost} # sign in as ${expectedAccount}; workit passes that account's token per call and never switches the active account`);
73315
- credential = "gh_account_token";
73316
- }
73317
- const limits = { deadline: null };
73318
- const runner = (bin, args, callOptions) => {
73319
- let timeoutMs = callOptions.timeoutMs;
73320
- if (limits.deadline !== null) {
73321
- const remaining = limits.deadline - now();
73322
- if (remaining <= 0)
73323
- return { status: null, stdout: "", stderr: "", timedOut: true, missing: false };
73324
- timeoutMs = Math.min(timeoutMs, remaining);
73325
- }
73326
- return base(bin, args, { ...callOptions, timeoutMs, ...token ? { token } : {} });
73327
- };
73650
+ const limits = { deadline: options.deadline ?? null };
73651
+ const credential = resolveCredential(workspace, derived, clampedRunner(base, limits, now), scope);
73652
+ if (!credential.ok)
73653
+ return credential;
73654
+ const runner = clampedRunner(base, limits, now, credential.data.token);
73328
73655
  const headRepo = derived.repo;
73329
- const probe = createForge(derived, headRepo, runner);
73656
+ const probe = memoizedIdentity(createForge(derived, headRepo, runner), scope, credential.data);
73330
73657
  const info = probe.repoInfo(headRepo);
73331
73658
  if (!info.ok && info.code !== "not_found")
73332
73659
  return info;
@@ -73341,13 +73668,13 @@ function resolveForge(cwd, options = {}) {
73341
73668
  const baseRepo = upstream && upstream !== headRepo ? upstream : info.ok && info.data.parent ? info.data.parent : headRepo;
73342
73669
  const baseRemote = baseRepo === headRepo ? pushed.remote : remotes.find((name) => repoOfRemote(name) === baseRepo) ?? pushed.remote;
73343
73670
  return success2({
73344
- forge: baseRepo === headRepo ? probe : createForge(derived, baseRepo, runner),
73671
+ forge: baseRepo === headRepo ? probe : memoizedIdentity(createForge(derived, baseRepo, runner), scope, credential.data),
73345
73672
  remote: pushed.remote,
73346
73673
  url: pushed.url,
73347
73674
  derived,
73348
73675
  workspace: workspace?.name ?? null,
73349
73676
  expectedAccount,
73350
- credential,
73677
+ credential: credential.data.credential,
73351
73678
  headRepo,
73352
73679
  headProjectId,
73353
73680
  fork: baseRepo !== headRepo,
@@ -73360,14 +73687,74 @@ function checkIdentity(resolved) {
73360
73687
  const identity = forge.identity(expectedAccount);
73361
73688
  if (!identity.ok)
73362
73689
  return identity;
73363
- if (identity.data.matches === false) {
73364
- const bin = forge.kind === "github" ? "gh" : "glab";
73365
- const where = `workspace ${resolved.workspace ?? "?"}`;
73366
- return failure3("blocked", `identity_mismatch: the ${resolved.credential === "workspace_token_file" ? "vcs.tokenFile token" : `${bin} login`} is ${identity.data.login} but ${where} expects ${expectedAccount}`, resolved.credential === "workspace_token_file" ? `put a token for ${expectedAccount} in the ${where} vcs.tokenFile` : `${bin} auth login --hostname ${forge.apiHost} # sign in as ${expectedAccount}`);
73367
- }
73690
+ if (identity.data.matches === false)
73691
+ return mismatch(resolved, identity.data.login ?? "?", resolved.workspace);
73368
73692
  return identity;
73369
73693
  }
73370
- var configuredHosts = () => {
73694
+ function checkPushIdentity(cwd, options = {}) {
73695
+ let workspace;
73696
+ try {
73697
+ workspace = resolveRuntimeWorkspaceVcs(cwd);
73698
+ } catch (error) {
73699
+ return failure3("blocked", error instanceof Error ? error.message : String(error), "fix ~/.config/workit/workspaces.json (workit init → Workspaces)");
73700
+ }
73701
+ const account = workspace?.vcs?.account ?? null;
73702
+ const skipped = (forge, reason, unblock) => success2({
73703
+ status: "skipped",
73704
+ forge,
73705
+ account,
73706
+ login: null,
73707
+ reason,
73708
+ ...unblock ? { unblock } : {}
73709
+ });
73710
+ const pushed = pushForge(cwd, {
73711
+ branch: options.branch,
73712
+ hosts: configuredHosts(),
73713
+ sshConfig: options.sshConfig
73714
+ });
73715
+ if (!pushed.ok) {
73716
+ const host = /^unsupported_forge: .*?"([^"]+)"/u.exec(pushed.error)?.[1];
73717
+ const what = /could not resolve ssh alias/u.test(pushed.error) ? `could not resolve ssh alias ${host} to GitHub or GitLab` : pushed.error.startsWith("unsupported_forge: ssh host") ? `ssh host ${host} is not a GitHub/GitLab host or an alias workit can resolve` : `${host} is not a GitHub or GitLab host workit knows`;
73718
+ return skipped(null, host ? `unsupported_forge: ${what}; account checks skipped, pushed with git only (no PR or CI verbs)` : pushed.error, pushed.unblock);
73719
+ }
73720
+ const derived = pushed.forge;
73721
+ const conflict = forgeConflict(derived, workspace?.vcs?.provider);
73722
+ if (conflict)
73723
+ return skipped(derived.kind, conflict.error, conflict.unblock);
73724
+ if (!account)
73725
+ return success2({ status: "no_account", forge: derived.kind, account, login: null });
73726
+ const base = options.runner ?? systemRunner(derived.kind, derived.apiHost, options.env ?? process.env);
73727
+ const scope = memoScope(options.runner, options.env ?? process.env);
73728
+ const now = options.now ?? Date.now;
73729
+ const limits = { deadline: now() + (options.budgetMs ?? PUSH_IDENTITY_BUDGET_MS) };
73730
+ const credential = resolveCredential(workspace, derived, clampedRunner(base, limits, now), scope);
73731
+ if (!credential.ok)
73732
+ return unreachable(credential) ? skipped(derived.kind, credential.error, credential.unblock) : credential;
73733
+ const forge = memoizedIdentity(createForge(derived, derived.repo, clampedRunner(base, limits, now, credential.data.token)), scope, credential.data);
73734
+ const identity = forge.identity(account);
73735
+ if (!identity.ok)
73736
+ return unreachable(identity) ? skipped(derived.kind, identity.error, identity.unblock) : identity;
73737
+ if (identity.data.matches === false)
73738
+ return mismatch({ forge, credential: credential.data.credential, expectedAccount: account }, identity.data.login ?? "?", workspace?.name ?? null);
73739
+ if (identity.data.matches === null)
73740
+ return skipped(derived.kind, identity.data.note ?? "the credential cannot read /user");
73741
+ return success2({ status: "verified", forge: derived.kind, account, login: identity.data.login });
73742
+ }
73743
+ var MEMO_TTL_MS, SYSTEM_SCOPE, memoScopes, memoScope = (runner, env) => ({
73744
+ owner: runner ?? SYSTEM_SCOPE,
73745
+ key: [env.GH_CONFIG_DIR, env.GLAB_CONFIG_DIR, env.XDG_CONFIG_HOME, env.HOME].map((value) => value ?? "").join("|")
73746
+ }), memoizedIdentity = (forge, scope, credential) => {
73747
+ if (credential.credential === "cli_login")
73748
+ return forge;
73749
+ const identity = (expected) => forge.identity(expected);
73750
+ return {
73751
+ ...forge,
73752
+ identity: (expected) => memo4(scope, `identity ${forge.kind} ${forge.apiHost} ${credential.credential} ${credential.token ?? ""} ${expected ?? ""}`, () => {
73753
+ const value = identity(expected);
73754
+ return { keep: value.ok, value };
73755
+ })
73756
+ };
73757
+ }, configuredHosts = () => {
73371
73758
  const vcs = readVcsConfig();
73372
73759
  if (vcs.status !== "valid")
73373
73760
  return {};
@@ -73376,7 +73763,13 @@ var configuredHosts = () => {
73376
73763
  return typeof value === "string" && value.trim() ? [value.trim()] : [];
73377
73764
  };
73378
73765
  return { github: host("github"), gitlab: host("gitlab") };
73379
- }, createForge = (derived, repo, runner) => derived.kind === "github" ? createGitHubForge({ apiHost: derived.apiHost, repo, runner }) : createGitLabForge({ apiHost: derived.apiHost, repo, runner });
73766
+ }, createForge = (derived, repo, runner) => derived.kind === "github" ? createGitHubForge({ apiHost: derived.apiHost, repo, runner }) : createGitLabForge({ apiHost: derived.apiHost, repo, runner }), mismatch = (resolved, login, workspace) => {
73767
+ const { forge, expectedAccount } = resolved;
73768
+ const bin = forge.kind === "github" ? "gh" : "glab";
73769
+ const where = `workspace ${workspace ?? "?"}`;
73770
+ const fileFix = `put a token for ${expectedAccount} in the ${where} vcs.tokenFile`;
73771
+ return failure3("blocked", `identity_mismatch: the ${resolved.credential === "workspace_token_file" ? "vcs.tokenFile token" : `${bin} login`} is ${login} but ${where} expects ${expectedAccount}`, resolved.credential === "workspace_token_file" ? fileFix : forge.kind === "github" ? `gh auth login --hostname ${forge.apiHost} # sign in as ${expectedAccount}` : `${fileFix} (glab keeps one login per host, so workit never switches it)`);
73772
+ }, unreachable = (result) => result.code === "unavailable" && !/ is not authenticated for /u.test(result.error), PUSH_IDENTITY_BUDGET_MS = 5000;
73380
73773
  var init_resolve = __esm(() => {
73381
73774
  init_vcs_config();
73382
73775
  init_workspaces();
@@ -73384,6 +73777,10 @@ var init_resolve = __esm(() => {
73384
73777
  init_exec();
73385
73778
  init_github();
73386
73779
  init_gitlab();
73780
+ init_redact();
73781
+ MEMO_TTL_MS = 5 * 60000;
73782
+ SYSTEM_SCOPE = {};
73783
+ memoScopes = new WeakMap;
73387
73784
  });
73388
73785
 
73389
73786
  // packages/workit-core/src/duration.ts
@@ -73476,16 +73873,19 @@ function releaseTrunk(io, track, branch) {
73476
73873
  tracked: Boolean(resolved.releaseTrack)
73477
73874
  };
73478
73875
  }
73479
- function connect(io, branch) {
73876
+ function connect(io, branch, options = {}) {
73877
+ const verbDeadline = options.deadline ?? null;
73480
73878
  const resolved = resolveForge(io.cwd, {
73481
73879
  branch,
73482
73880
  env: io.env,
73483
73881
  runner: forgeDeps.runner,
73484
- now: forgeDeps.now
73882
+ now: forgeDeps.now,
73883
+ deadline: verbDeadline ?? forgeDeps.now() + CONNECT_BUDGET_MS
73485
73884
  });
73486
73885
  if (!resolved.ok)
73487
73886
  return resolved;
73488
73887
  const identity = checkIdentity(resolved.data);
73888
+ resolved.data.limits.deadline = verbDeadline;
73489
73889
  if (!identity.ok)
73490
73890
  return identity;
73491
73891
  return {
@@ -73540,7 +73940,12 @@ function renderStatus(doc) {
73540
73940
  lines.push(`next: ${doc.next}`);
73541
73941
  return lines;
73542
73942
  }
73543
- var forgeDeps, MAX_REASON = 500, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
73943
+ var forgeDeps, MAX_REASON = 500, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), CONNECT_BUDGET_MS = 15000, pushIdentity = (io, branch) => checkPushIdentity(io.cwd, {
73944
+ branch,
73945
+ env: io.env,
73946
+ runner: forgeDeps.runner,
73947
+ now: forgeDeps.now
73948
+ }), short = (sha) => sha ? sha.slice(0, 7) : "?";
73544
73949
  var init_forge_common = __esm(() => {
73545
73950
  init_resolve();
73546
73951
  init_vcs_config();
@@ -73559,8 +73964,8 @@ var exports_pr = {};
73559
73964
  __export(exports_pr, {
73560
73965
  run: () => run10
73561
73966
  });
73562
- import { readFileSync as readFileSync18 } from "node:fs";
73563
- import path41 from "node:path";
73967
+ import { readFileSync as readFileSync19 } from "node:fs";
73968
+ import path42 from "node:path";
73564
73969
  async function status(argv, io) {
73565
73970
  const flags = parseFlags(argv, { pr: "value", branch: "value", "log-lines": "value" });
73566
73971
  if (typeof flags === "string")
@@ -73631,7 +74036,7 @@ async function create4(argv, io) {
73631
74036
  let body = flags.values.body ?? null;
73632
74037
  if (flags.values["body-file"] !== undefined) {
73633
74038
  try {
73634
- body = readFileSync18(path41.resolve(io.cwd, flags.values["body-file"]), "utf8");
74039
+ body = readFileSync19(path42.resolve(io.cwd, flags.values["body-file"]), "utf8");
73635
74040
  } catch {
73636
74041
  return usage3(io, `cannot read --body-file ${flags.values["body-file"]}`, CREATE_USAGE);
73637
74042
  }
@@ -73790,16 +74195,17 @@ async function wait(argv, io) {
73790
74195
  const head = flags.values.head ?? null;
73791
74196
  if (head !== null && !/^[0-9a-f]{7,64}$/u.test(head))
73792
74197
  return usage3(io, "--head must be a commit sha (7-64 hex chars)", WAIT_USAGE);
73793
- const connected = connect(io, flags.values.branch);
74198
+ const started = forgeDeps.now();
74199
+ const deadline = started + timeoutMs;
74200
+ const connected = connect(io, flags.values.branch, {
74201
+ deadline: started + Math.max(timeoutMs, ONE_POLL_MS)
74202
+ });
73794
74203
  if (!connected.ok)
73795
74204
  return forgeFail(io, connected);
73796
74205
  const resolved = connected.data;
73797
74206
  const number = selectPr(io.cwd, resolved, { pr, branch: flags.values.branch ?? null });
73798
74207
  if (!number.ok)
73799
74208
  return forgeFail(io, number);
73800
- const started = forgeDeps.now();
73801
- const deadline = started + timeoutMs;
73802
- resolved.limits.deadline = started + Math.max(timeoutMs, ONE_POLL_MS);
73803
74209
  let polls = 0;
73804
74210
  let errors = 0;
73805
74211
  let status = null;
@@ -73976,6 +74382,8 @@ var exports_git = {};
73976
74382
  __export(exports_git, {
73977
74383
  run: () => run12
73978
74384
  });
74385
+ import { readFileSync as readFileSync20 } from "node:fs";
74386
+ import path43 from "node:path";
73979
74387
  async function branch(argv, io) {
73980
74388
  const flags = parseFlags(argv, {
73981
74389
  base: "value",
@@ -74012,7 +74420,15 @@ async function branch(argv, io) {
74012
74420
  ]);
74013
74421
  }
74014
74422
  function parseCommit(argv) {
74015
- const out = { messages: [], all: false, paths: [] };
74423
+ const out = {
74424
+ messages: [],
74425
+ files: [],
74426
+ all: false,
74427
+ amend: false,
74428
+ noEdit: false,
74429
+ allowEmpty: false,
74430
+ paths: []
74431
+ };
74016
74432
  for (let index = 0;index < argv.length; index += 1) {
74017
74433
  const arg = argv[index];
74018
74434
  if (arg === "--") {
@@ -74021,23 +74437,24 @@ function parseCommit(argv) {
74021
74437
  }
74022
74438
  if (arg === "--json")
74023
74439
  continue;
74024
- if (arg === "-a" || arg === "--all") {
74025
- out.all = true;
74026
- continue;
74027
- }
74028
- if (arg === "-m" || arg === "--message") {
74029
- const value = argv[++index];
74030
- if (value === undefined)
74031
- return `${arg} requires a message`;
74032
- out.messages.push(value);
74033
- continue;
74034
- }
74035
- if (arg.startsWith("--message=")) {
74036
- out.messages.push(arg.slice("--message=".length));
74440
+ const flag = COMMIT_SWITCHES[arg];
74441
+ if (flag) {
74442
+ out[flag] = true;
74037
74443
  continue;
74038
74444
  }
74039
- if (arg.startsWith("-m") && arg.length > 2) {
74040
- out.messages.push(arg.slice(2));
74445
+ const valued = [
74446
+ ["-m", "--message", out.messages],
74447
+ ["-F", "--file", out.files]
74448
+ ].find(([shortName, longName]) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`) || arg.startsWith(shortName) && arg.length > 2);
74449
+ if (valued) {
74450
+ const [shortName, longName, into] = valued;
74451
+ if (arg === shortName || arg === longName) {
74452
+ const value = argv[++index];
74453
+ if (value === undefined)
74454
+ return `${arg} requires a ${shortName === "-m" ? "message" : "file"}`;
74455
+ into.push(value);
74456
+ } else
74457
+ into.push(arg.startsWith(`${longName}=`) ? arg.slice(longName.length + 1) : arg.slice(2));
74041
74458
  continue;
74042
74459
  }
74043
74460
  if (arg.startsWith("-"))
@@ -74046,20 +74463,48 @@ function parseCommit(argv) {
74046
74463
  }
74047
74464
  return out;
74048
74465
  }
74466
+ function commitMessage(parsed, io) {
74467
+ if (parsed.messages.length && parsed.files.length)
74468
+ return { error: "pass -m or -F, not both" };
74469
+ if (parsed.files.length > 1)
74470
+ return { error: "-F takes one file" };
74471
+ const [file] = parsed.files;
74472
+ if (file === undefined)
74473
+ return parsed.messages.join(`
74474
+
74475
+ `);
74476
+ try {
74477
+ return file === "-" ? readFileSync20(0, "utf8") : readFileSync20(path43.resolve(io.cwd, file), "utf8");
74478
+ } catch (error) {
74479
+ return {
74480
+ error: `cannot read -F ${file}: ${error instanceof Error ? error.message : String(error)}`
74481
+ };
74482
+ }
74483
+ }
74049
74484
  async function commit(argv, io) {
74050
74485
  const parsed = parseCommit(argv);
74051
74486
  if (typeof parsed === "string")
74052
74487
  return usage3(io, parsed, COMMIT_USAGE);
74053
- if (!parsed.messages.length)
74054
- return usage3(io, "missing -m <msg>", COMMIT_USAGE);
74055
74488
  if (parsed.all && parsed.paths.length)
74056
74489
  return usage3(io, "pass --all or paths, not both", COMMIT_USAGE);
74490
+ if (parsed.noEdit && !parsed.amend)
74491
+ return usage3(io, "--no-edit only applies with --amend", COMMIT_USAGE);
74492
+ const given = parsed.messages.length + parsed.files.length > 0;
74493
+ if (parsed.noEdit && given)
74494
+ return usage3(io, "--no-edit keeps the message; drop -m/-F or --no-edit", COMMIT_USAGE);
74495
+ if (!given && !parsed.noEdit)
74496
+ return usage3(io, parsed.amend ? "--amend needs -m/-F or --no-edit" : "missing -m <msg> or -F <file>", COMMIT_USAGE);
74497
+ const message = given ? commitMessage(parsed, io) : null;
74498
+ if (message !== null && typeof message !== "string")
74499
+ return usage3(io, message.error, COMMIT_USAGE);
74500
+ if (message !== null && !message.trim())
74501
+ return usage3(io, "the commit message is empty", COMMIT_USAGE);
74057
74502
  const result = gitCommit(io.cwd, {
74058
- message: parsed.messages.join(`
74059
-
74060
- `),
74503
+ message,
74061
74504
  all: parsed.all,
74062
74505
  paths: parsed.paths,
74506
+ amend: parsed.amend,
74507
+ allowEmpty: parsed.allowEmpty,
74063
74508
  actor: actorFromEnv(io.env),
74064
74509
  env: io.env
74065
74510
  });
@@ -74068,8 +74513,9 @@ async function commit(argv, io) {
74068
74513
  await ensureImplicitTask(io);
74069
74514
  return emit(io, ok(result.data), (data) => [
74070
74515
  `${data.branch} ${short2(data.sha)} ${data.message.split(`
74071
- `, 1)[0]} (${data.files.length} file${data.files.length === 1 ? "" : "s"})`,
74516
+ `, 1)[0]} (${data.files.length ? `${data.files.length} file${data.files.length === 1 ? "" : "s"}` : "empty commit"})${data.amended ? `, amends ${short2(data.amended)}` : ""}`,
74072
74517
  ...data.leftDirty ? [`${data.leftDirty} change(s) left uncommitted`] : [],
74518
+ ...data.notes.map((note) => `note: ${note}`),
74073
74519
  ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : [],
74074
74520
  ...data.session ? [] : ["note: WORKIT_SESSION_ID is unset; this commit has no session author"]
74075
74521
  ]);
@@ -74096,24 +74542,37 @@ async function push(argv, io) {
74096
74542
  const plan = pushPreflight(io.cwd);
74097
74543
  if (!plan.ok)
74098
74544
  return forgeFail(io, plan);
74099
- if (!plan.data.local) {
74100
- const connected = connect(io, plan.data.branch);
74101
- if (!connected.ok)
74102
- return forgeFail(io, connected);
74103
- }
74104
- const grant = requireGrant(io.cwd, "push", { forge: !plan.data.local });
74545
+ const identity = plan.data.local ? null : pushIdentity(io, plan.data.branch);
74546
+ if (identity && !identity.ok)
74547
+ return forgeFail(io, identity);
74548
+ const checked = identity?.data ?? null;
74549
+ const grant = requireGrant(io.cwd, "push", { forge: checked?.forge != null });
74105
74550
  if (!grant.allowed)
74106
- return emit(io, fail("blocked", grant.error, { unblock: grant.unblock, data: { reason: grant.reason } }));
74551
+ return emit(io, fail("blocked", grant.error, {
74552
+ unblock: grant.unblock,
74553
+ data: { reason: grant.reason }
74554
+ }));
74107
74555
  const result = executePush(io.cwd, plan.data, {
74108
74556
  forceWithLease: flags.booleans.has("force-with-lease"),
74109
74557
  overwriteUnintegrated: flags.booleans.has("overwrite-unintegrated"),
74110
74558
  expect,
74111
74559
  setUpstream: flags.booleans.has("set-upstream"),
74112
- actor: actorFromEnv(io.env)
74560
+ actor: actorFromEnv(io.env),
74561
+ ...checked ? { identity: { ...checked } } : {}
74113
74562
  });
74114
74563
  if (!result.ok)
74115
- return forgeFail(io, result, { branch: plan.data.branch, sha: plan.data.sha });
74116
- return emit(io, ok(result.data), (data) => [
74564
+ return forgeFail(io, result, {
74565
+ branch: plan.data.branch,
74566
+ sha: plan.data.sha
74567
+ });
74568
+ const warnings = checked?.status === "skipped" ? [
74569
+ `identity check skipped: ${checked.reason}${checked.unblock ? ` (to restore it: ${checked.unblock})` : ""}`
74570
+ ] : [];
74571
+ for (const warning of warnings)
74572
+ io.stderr(`warning: ${warning}
74573
+ `);
74574
+ const envelope = ok({ ...result.data, identity: checked });
74575
+ return emit(io, warnings.length ? { ...envelope, warnings } : envelope, (data) => [
74117
74576
  data.pushed ? `pushed ${data.branch} ${short2(data.previous)} -> ${short2(data.sha)} to ${data.remote}${data.forced ? " (force-with-lease)" : ""}; remote tip verified` : `${data.remote}/${data.branch} is already at ${short2(data.sha)}; remote tip verified`,
74118
74577
  ...data.upstream ? [`upstream: ${data.remote}/${data.branch}`] : []
74119
74578
  ]);
@@ -74128,7 +74587,7 @@ async function run12(argv, io) {
74128
74587
  return push(rest, io);
74129
74588
  return usage3(io, sub && !sub.startsWith("-") ? `unknown git subcommand "${sub}"` : "missing git subcommand", USAGE5);
74130
74589
  }
74131
- 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 <msg> [--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)";
74590
+ 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;
74132
74591
  var init_git2 = __esm(() => {
74133
74592
  init_autonomy();
74134
74593
  init_ops();
@@ -74136,10 +74595,17 @@ var init_git2 = __esm(() => {
74136
74595
  init_implicit_task();
74137
74596
  init_output();
74138
74597
  init_forge_common();
74598
+ COMMIT_SWITCHES = {
74599
+ "-a": "all",
74600
+ "--all": "all",
74601
+ "--amend": "amend",
74602
+ "--no-edit": "noEdit",
74603
+ "--allow-empty": "allowEmpty"
74604
+ };
74139
74605
  });
74140
74606
 
74141
74607
  // packages/workit-core/src/forge/verify.ts
74142
- import { spawnSync as spawnSync19 } from "node:child_process";
74608
+ import { spawnSync as spawnSync20 } from "node:child_process";
74143
74609
  function finish(cwd, report, actor) {
74144
74610
  const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
74145
74611
  const out = { ...report, delivered };
@@ -74352,7 +74818,7 @@ var NPM_NAME, SEMVER, SAFE_ARG, systemNpm = (args) => {
74352
74818
  };
74353
74819
  const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
74354
74820
  const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
74355
- const result = process.platform === "win32" ? spawnSync19(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync19("npm", [...args], options);
74821
+ const result = process.platform === "win32" ? spawnSync20(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync20("npm", [...args], options);
74356
74822
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
74357
74823
  }, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
74358
74824
  try {
@@ -74459,10 +74925,10 @@ var init_verify_delivery = __esm(() => {
74459
74925
  });
74460
74926
 
74461
74927
  // packages/workit-core/src/stack.ts
74462
- import { spawnSync as spawnSync20 } from "node:child_process";
74928
+ import { spawnSync as spawnSync21 } from "node:child_process";
74463
74929
  import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
74464
74930
  import fs22 from "node:fs";
74465
- import path42 from "node:path";
74931
+ import path44 from "node:path";
74466
74932
  function worktreeOf(cwd, branch) {
74467
74933
  const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
74468
74934
  if (!list.ok)
@@ -74506,7 +74972,7 @@ function gitRestack(cwd, input) {
74506
74972
  if (!scratch)
74507
74973
  return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
74508
74974
  fs22.mkdirSync(scratch, { recursive: true });
74509
- dir = path42.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74975
+ dir = path44.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74510
74976
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
74511
74977
  if (!added.ok)
74512
74978
  return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
@@ -74547,7 +75013,7 @@ function pruneScratch(cwd) {
74547
75013
  if (!scratch || !fs22.existsSync(scratch))
74548
75014
  return;
74549
75015
  for (const name of fs22.readdirSync(scratch)) {
74550
- const dir = path42.join(scratch, name);
75016
+ const dir = path44.join(scratch, name);
74551
75017
  if (!rebaseInProgress(dir) && !trackedDirt(dir))
74552
75018
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
74553
75019
  }
@@ -74557,7 +75023,7 @@ function stacksDir(cwd) {
74557
75023
  const root = storeRoot(cwd);
74558
75024
  if (!root.ok)
74559
75025
  return stackFail(root.code, root.error, root.unblock);
74560
- return { ok: true, data: path42.join(root.value.root, "stacks") };
75026
+ return { ok: true, data: path44.join(root.value.root, "stacks") };
74561
75027
  }
74562
75028
  function parseStack(raw) {
74563
75029
  if (!isRecord7(raw) || !Array.isArray(raw.branches))
@@ -74609,7 +75075,7 @@ function readStack(cwd, name) {
74609
75075
  const dir = stacksDir(cwd);
74610
75076
  if (!dir.ok)
74611
75077
  return dir;
74612
- const file = path42.join(dir.data, fileFor(name));
75078
+ const file = path44.join(dir.data, fileFor(name));
74613
75079
  let text;
74614
75080
  try {
74615
75081
  text = fs22.readFileSync(file, "utf8");
@@ -74630,7 +75096,7 @@ function writeStack(cwd, stack, now = new Date) {
74630
75096
  const dir = stacksDir(cwd);
74631
75097
  if (!dir.ok)
74632
75098
  return dir;
74633
- const file = path42.join(dir.data, fileFor(stack.name));
75099
+ const file = path44.join(dir.data, fileFor(stack.name));
74634
75100
  const temp = `${file}.${process.pid}.tmp`;
74635
75101
  try {
74636
75102
  fs22.mkdirSync(dir.data, { recursive: true });
@@ -74658,7 +75124,7 @@ function listStacks(cwd) {
74658
75124
  for (const name of names) {
74659
75125
  let parsed = null;
74660
75126
  try {
74661
- parsed = parseStack(JSON.parse(fs22.readFileSync(path42.join(dir.data, name), "utf8")));
75127
+ parsed = parseStack(JSON.parse(fs22.readFileSync(path44.join(dir.data, name), "utf8")));
74662
75128
  } catch {
74663
75129
  parsed = null;
74664
75130
  }
@@ -74717,7 +75183,7 @@ function takeLock(lock) {
74717
75183
  host: localLockHost(),
74718
75184
  nonce: randomBytes3(8).toString("hex")
74719
75185
  };
74720
- fs22.writeFileSync(path42.join(lock, OWNER), JSON.stringify(owner));
75186
+ fs22.writeFileSync(path44.join(lock, OWNER), JSON.stringify(owner));
74721
75187
  return owner;
74722
75188
  }
74723
75189
  function reapLock(lock, observed) {
@@ -74754,7 +75220,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74754
75220
  return dir;
74755
75221
  if (!validName(name))
74756
75222
  return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
74757
- const lock = path42.join(dir.data, `${fileFor(name)}.lock`);
75223
+ const lock = path44.join(dir.data, `${fileFor(name)}.lock`);
74758
75224
  try {
74759
75225
  fs22.mkdirSync(dir.data, { recursive: true });
74760
75226
  } catch (error) {
@@ -74784,7 +75250,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74784
75250
  const touch = () => {
74785
75251
  try {
74786
75252
  const now = new Date;
74787
- fs22.utimesSync(path42.join(lock, OWNER), now, now);
75253
+ fs22.utimesSync(path44.join(lock, OWNER), now, now);
74788
75254
  } catch {}
74789
75255
  };
74790
75256
  const beat = setInterval(touch, HEARTBEAT_MS);
@@ -74972,7 +75438,7 @@ function commitKeys(cwd, range) {
74972
75438
  const patches = new Map;
74973
75439
  const diff = gitRun3(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
74974
75440
  if (diff.ok && diff.stdout.trim()) {
74975
- const ids = spawnSync20("git", ["patch-id", "--stable"], {
75441
+ const ids = spawnSync21("git", ["patch-id", "--stable"], {
74976
75442
  cwd,
74977
75443
  input: diff.stdout,
74978
75444
  encoding: "utf8",
@@ -75596,7 +76062,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75596
76062
  ...unblock ? { unblock } : {},
75597
76063
  ...data ? { data } : {}
75598
76064
  }), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
75599
- const run = spawnSync20("git", args, {
76065
+ const run = spawnSync21("git", args, {
75600
76066
  cwd,
75601
76067
  encoding: "utf8",
75602
76068
  timeout: timeoutMs,
@@ -75624,7 +76090,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75624
76090
  return !status.ok || status.stdout.trim() !== "";
75625
76091
  }, scratchDir = (cwd) => {
75626
76092
  const dir = stacksDir(cwd);
75627
- return dir.ok ? path42.join(dir.data, "worktrees") : null;
76093
+ return dir.ok ? path44.join(dir.data, "worktrees") : null;
75628
76094
  }, firstLine = (run) => (`${run.stderr}
75629
76095
  ${run.stdout}`.split(`
75630
76096
  `).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()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
@@ -75639,12 +76105,12 @@ ${run.stdout}`.split(`
75639
76105
  }, 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) => {
75640
76106
  let raw = null;
75641
76107
  try {
75642
- raw = fs22.readFileSync(path42.join(lock, OWNER), "utf8");
76108
+ raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
75643
76109
  } catch {
75644
76110
  raw = null;
75645
76111
  }
75646
76112
  let age = null;
75647
- for (const target of [path42.join(lock, OWNER), lock])
76113
+ for (const target of [path44.join(lock, OWNER), lock])
75648
76114
  try {
75649
76115
  age = Date.now() - fs22.statSync(target).mtimeMs;
75650
76116
  break;
@@ -75891,10 +76357,10 @@ var init_stack2 = __esm(() => {
75891
76357
  });
75892
76358
 
75893
76359
  // packages/workit-core/src/fanout.ts
75894
- import { spawnSync as spawnSync21 } from "node:child_process";
76360
+ import { spawnSync as spawnSync22 } from "node:child_process";
75895
76361
  import { createHash as createHash16 } from "node:crypto";
75896
76362
  import fs23 from "node:fs";
75897
- import path43 from "node:path";
76363
+ import path45 from "node:path";
75898
76364
  function expandBraces(pattern, limit = 64) {
75899
76365
  for (let index = 0;index < pattern.length; index += 1) {
75900
76366
  if (pattern[index] === "\\") {
@@ -76128,7 +76594,7 @@ function parsePlan(raw, defaults, name) {
76128
76594
  id,
76129
76595
  branch,
76130
76596
  base: optional("base") ?? "",
76131
- worktree: path43.resolve(defaults.root, optional("worktree") ?? path43.join(defaults.worktreeRoot, id)),
76597
+ worktree: path45.resolve(defaults.root, optional("worktree") ?? path45.join(defaults.worktreeRoot, id)),
76132
76598
  tier,
76133
76599
  dependsOn: [...new Set(dependsOn ?? [])],
76134
76600
  scope,
@@ -76357,11 +76823,11 @@ function defaultTrunk(cwd) {
76357
76823
  }
76358
76824
  function mainCheckout(cwd) {
76359
76825
  const common = gitCommonDir(cwd);
76360
- return common && path43.basename(common) === ".git" ? path43.dirname(common) : path43.resolve(cwd);
76826
+ return common && path45.basename(common) === ".git" ? path45.dirname(common) : path45.resolve(cwd);
76361
76827
  }
76362
76828
  function worktreeRoot(cwd) {
76363
76829
  const main = mainCheckout(cwd);
76364
- return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
76830
+ return path45.join(path45.dirname(main), `${path45.basename(main)}-wt`);
76365
76831
  }
76366
76832
  function sliceHash(slice) {
76367
76833
  const {
@@ -76379,7 +76845,7 @@ function fanoutsDir(cwd) {
76379
76845
  const root = storeRoot(cwd);
76380
76846
  if (!root.ok)
76381
76847
  return fanoutFail(root.code, root.error, root.unblock);
76382
- return { ok: true, data: path43.join(root.value.root, "fanouts") };
76848
+ return { ok: true, data: path45.join(root.value.root, "fanouts") };
76383
76849
  }
76384
76850
  function parseStored(raw) {
76385
76851
  if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
@@ -76445,7 +76911,7 @@ function readFanout(cwd, name) {
76445
76911
  const dir = fanoutsDir(cwd);
76446
76912
  if (!dir.ok)
76447
76913
  return dir;
76448
- const file = path43.join(dir.data, stackFileName(name));
76914
+ const file = path45.join(dir.data, stackFileName(name));
76449
76915
  let parsed = null;
76450
76916
  try {
76451
76917
  parsed = parseStored(JSON.parse(fs23.readFileSync(file, "utf8")));
@@ -76470,7 +76936,7 @@ function listFanouts(cwd) {
76470
76936
  for (const entry of names) {
76471
76937
  let parsed = null;
76472
76938
  try {
76473
- parsed = parseStored(JSON.parse(fs23.readFileSync(path43.join(dir.data, entry), "utf8")));
76939
+ parsed = parseStored(JSON.parse(fs23.readFileSync(path45.join(dir.data, entry), "utf8")));
76474
76940
  } catch {
76475
76941
  parsed = null;
76476
76942
  }
@@ -76500,7 +76966,7 @@ function writeFanout(cwd, plan) {
76500
76966
  const dir = fanoutsDir(cwd);
76501
76967
  if (!dir.ok)
76502
76968
  return dir;
76503
- const file = path43.join(dir.data, stackFileName(plan.name));
76969
+ const file = path45.join(dir.data, stackFileName(plan.name));
76504
76970
  const temp = `${file}.${process.pid}.tmp`;
76505
76971
  try {
76506
76972
  fs23.mkdirSync(dir.data, { recursive: true });
@@ -76664,7 +77130,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
76664
77130
  text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
76665
77131
  };
76666
77132
  }, gitRun4 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
76667
- const run = spawnSync21("git", args, {
77133
+ const run = spawnSync22("git", args, {
76668
77134
  cwd,
76669
77135
  encoding: "utf8",
76670
77136
  timeout: timeoutMs,
@@ -76704,7 +77170,7 @@ var init_fanout = __esm(() => {
76704
77170
  // packages/workit-core/src/fanout-worktree.ts
76705
77171
  import { randomBytes as randomBytes4 } from "node:crypto";
76706
77172
  import fs24 from "node:fs";
76707
- import path44 from "node:path";
77173
+ import path46 from "node:path";
76708
77174
  function rowSample(lines) {
76709
77175
  const out = [];
76710
77176
  let bytes = 0;
@@ -76764,7 +77230,7 @@ function adminDir(worktree) {
76764
77230
  const file = dir.stdout.trim();
76765
77231
  let token = null;
76766
77232
  try {
76767
- token = fs24.readFileSync(path44.join(file, TOKEN_FILE), "utf8").trim() || null;
77233
+ token = fs24.readFileSync(path46.join(file, TOKEN_FILE), "utf8").trim() || null;
76768
77234
  } catch {
76769
77235
  token = null;
76770
77236
  }
@@ -76772,7 +77238,7 @@ function adminDir(worktree) {
76772
77238
  const stat = worktreeDeps.stat(file);
76773
77239
  return {
76774
77240
  path: file,
76775
- id: path44.basename(file),
77241
+ id: path46.basename(file),
76776
77242
  ino: stat.ino.toString(),
76777
77243
  token,
76778
77244
  birthMs: stat.birthtimeNs > 0n ? Number(stat.birthtimeMs) : null,
@@ -76788,7 +77254,7 @@ function markAdminDir(worktree) {
76788
77254
  return null;
76789
77255
  const token = randomBytes4(16).toString("hex");
76790
77256
  try {
76791
- fs24.writeFileSync(path44.join(admin.path, TOKEN_FILE), `${token}
77257
+ fs24.writeFileSync(path46.join(admin.path, TOKEN_FILE), `${token}
76792
77258
  `);
76793
77259
  } catch {
76794
77260
  return null;
@@ -76803,7 +77269,7 @@ function excludeScratch(cwd) {
76803
77269
  const common = gitCommonDir(cwd);
76804
77270
  if (!common)
76805
77271
  return;
76806
- const file = path44.join(common, "info", "exclude");
77272
+ const file = path46.join(common, "info", "exclude");
76807
77273
  let text = "";
76808
77274
  try {
76809
77275
  text = fs24.readFileSync(file, "utf8");
@@ -76812,7 +77278,7 @@ function excludeScratch(cwd) {
76812
77278
  }
76813
77279
  if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
76814
77280
  return;
76815
- fs24.mkdirSync(path44.dirname(file), { recursive: true });
77281
+ fs24.mkdirSync(path46.dirname(file), { recursive: true });
76816
77282
  fs24.appendFileSync(file, `${text && !text.endsWith(`
76817
77283
  `) ? `
76818
77284
  ` : ""}# workit fanout: per-worker scratch dirs
@@ -76820,7 +77286,7 @@ ${EXCLUDE_LINE}
76820
77286
  `);
76821
77287
  }
76822
77288
  function ensureScratch(worktree) {
76823
- const scratch = path44.join(worktree, SCRATCH_DIR);
77289
+ const scratch = path46.join(worktree, SCRATCH_DIR);
76824
77290
  fs24.mkdirSync(scratch, { recursive: true });
76825
77291
  return scratch;
76826
77292
  }
@@ -77038,7 +77504,7 @@ function unpushedCount(cwd, branch) {
77038
77504
  return count.ok ? Number(count.stdout.trim()) : null;
77039
77505
  }
77040
77506
  function removeScratch(worktree) {
77041
- const scratch = path44.join(worktree, SCRATCH_DIR);
77507
+ const scratch = path46.join(worktree, SCRATCH_DIR);
77042
77508
  let stat;
77043
77509
  try {
77044
77510
  stat = fs24.lstatSync(scratch);
@@ -77054,16 +77520,16 @@ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000,
77054
77520
  try {
77055
77521
  return fs24.realpathSync.native(target);
77056
77522
  } catch {
77057
- return path44.resolve(target);
77523
+ return path46.resolve(target);
77058
77524
  }
77059
77525
  }, samePath = (a, b) => {
77060
77526
  const left = canonical2(a);
77061
77527
  const right = canonical2(b);
77062
77528
  return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
77063
77529
  }, inside2 = (child, parent) => {
77064
- const relative = path44.relative(canonical2(parent), canonical2(child));
77065
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
77066
- }, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
77530
+ const relative = path46.relative(canonical2(parent), canonical2(child));
77531
+ return relative === "" || !relative.startsWith("..") && !path46.isAbsolute(relative);
77532
+ }, slicePath = (cwd, slice) => path46.isAbsolute(slice.worktree) ? slice.worktree : path46.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(`
77067
77533
  `)[0] || "unknown error"}`);
77068
77534
  var init_fanout_worktree = __esm(() => {
77069
77535
  init_ops();
@@ -77184,7 +77650,7 @@ var init_fanout_brief = __esm(() => {
77184
77650
  });
77185
77651
 
77186
77652
  // packages/workit-core/src/fanout-landed.ts
77187
- import { spawnSync as spawnSync22 } from "node:child_process";
77653
+ import { spawnSync as spawnSync23 } from "node:child_process";
77188
77654
  function forgeBreaker(resolved) {
77189
77655
  if (!resolved)
77190
77656
  return { forge: null, down: () => null };
@@ -77275,7 +77741,7 @@ function trunkPatchIds(ctx, fork) {
77275
77741
  "--"
77276
77742
  ]);
77277
77743
  if (log.ok && log.stdout.trim()) {
77278
- const run = spawnSync22("git", ["patch-id", "--stable"], {
77744
+ const run = spawnSync23("git", ["patch-id", "--stable"], {
77279
77745
  cwd: ctx.cwd,
77280
77746
  input: log.stdout,
77281
77747
  encoding: "utf8",
@@ -77839,7 +78305,7 @@ __export(exports_fanout, {
77839
78305
  run: () => run15
77840
78306
  });
77841
78307
  import fs25 from "node:fs";
77842
- import path45 from "node:path";
78308
+ import path47 from "node:path";
77843
78309
  function forgeFor(io, offline) {
77844
78310
  if (offline)
77845
78311
  return { forge: null, error: "--offline" };
@@ -77900,7 +78366,7 @@ function plan2(argv, io) {
77900
78366
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
77901
78367
  if (flags.positionals.length !== 1)
77902
78368
  return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
77903
- const file = path45.resolve(io.cwd, flags.positionals[0]);
78369
+ const file = path47.resolve(io.cwd, flags.positionals[0]);
77904
78370
  let raw;
77905
78371
  try {
77906
78372
  raw = JSON.parse(fs25.readFileSync(file, "utf8"));
@@ -93332,7 +93798,7 @@ var init_ast = __esm(() => {
93332
93798
 
93333
93799
  // packages/workit-cli/src/test-audit/rules.ts
93334
93800
  import { builtinModules } from "node:module";
93335
- import path46 from "node:path";
93801
+ import path48 from "node:path";
93336
93802
  function testCallee(call) {
93337
93803
  let callee = call.callee;
93338
93804
  if (callee.type === "CallExpression")
@@ -93825,12 +94291,12 @@ function checkConstantsOnly(ctx, emit) {
93825
94291
  }
93826
94292
  function checkOverMocking(ctx, emit) {
93827
94293
  const { program } = ctx.parsed;
93828
- const unit = path46.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
94294
+ const unit = path48.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
93829
94295
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
93830
94296
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
93831
94297
  for (const call of mocked) {
93832
94298
  const spec = call.arguments[0].value;
93833
- const base = path46.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
94299
+ const base = path48.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
93834
94300
  if (base === unit)
93835
94301
  emit({
93836
94302
  rule: "over-mocking",
@@ -94218,11 +94684,11 @@ var init_rules = __esm(() => {
94218
94684
  });
94219
94685
 
94220
94686
  // packages/workit-cli/src/test-audit/audit.ts
94221
- import { spawnSync as spawnSync23 } from "node:child_process";
94222
- import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
94223
- import path47 from "node:path";
94687
+ import { spawnSync as spawnSync24 } from "node:child_process";
94688
+ import { existsSync as existsSync19, readdirSync as readdirSync8, readFileSync as readFileSync21, statSync as statSync7 } from "node:fs";
94689
+ import path49 from "node:path";
94224
94690
  function git6(cwd, args) {
94225
- const run = spawnSync23("git", ["-c", "core.quotepath=off", ...args], {
94691
+ const run = spawnSync24("git", ["-c", "core.quotepath=off", ...args], {
94226
94692
  cwd,
94227
94693
  encoding: "utf8",
94228
94694
  maxBuffer: 64 * 1024 * 1024
@@ -94233,7 +94699,7 @@ function walkTests(dir, out) {
94233
94699
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
94234
94700
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
94235
94701
  continue;
94236
- const full = path47.join(dir, entry.name);
94702
+ const full = path49.join(dir, entry.name);
94237
94703
  if (entry.isDirectory()) {
94238
94704
  if (!SKIP_DIRS.has(entry.name))
94239
94705
  walkTests(full, out);
@@ -94245,8 +94711,8 @@ function expandPaths(cwd, inputs) {
94245
94711
  const files = [];
94246
94712
  const missing = [];
94247
94713
  for (const input of inputs.length > 0 ? inputs : ["."]) {
94248
- const full = path47.resolve(cwd, input);
94249
- if (!existsSync18(full))
94714
+ const full = path49.resolve(cwd, input);
94715
+ if (!existsSync19(full))
94250
94716
  missing.push(input);
94251
94717
  else if (statSync7(full).isDirectory())
94252
94718
  walkTests(full, files);
@@ -94282,11 +94748,11 @@ function changedLines(cwd, base) {
94282
94748
  for (let offset = 0;offset < count; offset += 1)
94283
94749
  lines.add(start + offset);
94284
94750
  }
94285
- changed.set(path47.join(root, file), lines);
94751
+ changed.set(path49.join(root, file), lines);
94286
94752
  }
94287
94753
  const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
94288
94754
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
94289
- changed.set(path47.join(root, file), "all");
94755
+ changed.set(path49.join(root, file), "all");
94290
94756
  return changed;
94291
94757
  }
94292
94758
  function auditFiles(files, display) {
@@ -94299,7 +94765,7 @@ function auditFiles(files, display) {
94299
94765
  const shown = display(file);
94300
94766
  let parsed;
94301
94767
  try {
94302
- parsed = parseSource(readFileSync19(file, "utf8"), file);
94768
+ parsed = parseSource(readFileSync21(file, "utf8"), file);
94303
94769
  } catch (error) {
94304
94770
  parseErrors.push({
94305
94771
  file: shown,
@@ -94348,7 +94814,7 @@ function auditFiles(files, display) {
94348
94814
  durationMs: Math.round(performance.now() - started)
94349
94815
  };
94350
94816
  }
94351
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path47.sep).join("/"), SEVERITY_ORDER, ruleIds;
94817
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path49.sep).join("/"), SEVERITY_ORDER, ruleIds;
94352
94818
  var init_audit = __esm(() => {
94353
94819
  init_ast();
94354
94820
  init_rules();
@@ -94362,12 +94828,12 @@ var init_audit = __esm(() => {
94362
94828
  // packages/workit-cli/src/test-audit/mutate.ts
94363
94829
  import { spawn as spawn2 } from "node:child_process";
94364
94830
  import {
94365
- existsSync as existsSync19,
94831
+ existsSync as existsSync20,
94366
94832
  lstatSync as lstatSync7,
94367
94833
  mkdirSync as mkdirSync9,
94368
94834
  mkdtempSync as mkdtempSync4,
94369
94835
  readdirSync as readdirSync9,
94370
- readFileSync as readFileSync20,
94836
+ readFileSync as readFileSync22,
94371
94837
  readlinkSync as readlinkSync3,
94372
94838
  realpathSync as realpathSync9,
94373
94839
  renameSync as renameSync5,
@@ -94378,7 +94844,7 @@ import {
94378
94844
  copyFileSync as copyFileSync6
94379
94845
  } from "node:fs";
94380
94846
  import os14 from "node:os";
94381
- import path48 from "node:path";
94847
+ import path50 from "node:path";
94382
94848
  function mutantsFor(source, file, lines) {
94383
94849
  const parsed = parseSource(source, file);
94384
94850
  const out = [];
@@ -94451,7 +94917,7 @@ function relatedTests(source, testFiles) {
94451
94917
  for (const match of text.matchAll(SPECIFIER)) {
94452
94918
  const spec = match[1];
94453
94919
  if (spec.startsWith(".")) {
94454
- if (stripExt(path48.posix.join(path48.posix.dirname(file), spec)) === target)
94920
+ if (stripExt(path50.posix.join(path50.posix.dirname(file), spec)) === target)
94455
94921
  return true;
94456
94922
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
94457
94923
  return true;
@@ -94471,12 +94937,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
94471
94937
  for (const name of names) {
94472
94938
  if (!name.startsWith(TEMP_PREFIX))
94473
94939
  continue;
94474
- const dir = path48.join(tmp, name);
94940
+ const dir = path50.join(tmp, name);
94475
94941
  try {
94476
94942
  const stat = lstatSync7(dir);
94477
94943
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
94478
94944
  continue;
94479
- const pid = Number(readFileSync20(path48.join(dir, PID_FILE), "utf8"));
94945
+ const pid = Number(readFileSync22(path50.join(dir, PID_FILE), "utf8"));
94480
94946
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
94481
94947
  continue;
94482
94948
  } catch (error) {
@@ -94493,8 +94959,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
94493
94959
  function linkNodeModules(origin, target, repo, copy) {
94494
94960
  mkdirSync9(target, { recursive: true });
94495
94961
  for (const entry of readdirSync9(origin, { withFileTypes: true })) {
94496
- const from = path48.join(origin, entry.name);
94497
- const to = path48.join(target, entry.name);
94962
+ const from = path50.join(origin, entry.name);
94963
+ const to = path50.join(target, entry.name);
94498
94964
  if (entry.name.startsWith("@") && entry.isDirectory()) {
94499
94965
  linkNodeModules(from, to, repo, copy);
94500
94966
  continue;
@@ -94510,9 +94976,9 @@ function linkNodeModules(origin, target, repo, copy) {
94510
94976
  } catch {
94511
94977
  continue;
94512
94978
  }
94513
- const relative = path48.relative(repo, real);
94514
- if (!relative.startsWith("..") && !path48.isAbsolute(relative) && !relative.split(path48.sep).includes("node_modules")) {
94515
- symlinkSync(path48.join(copy, relative), to, "junction");
94979
+ const relative = path50.relative(repo, real);
94980
+ if (!relative.startsWith("..") && !path50.isAbsolute(relative) && !relative.split(path50.sep).includes("node_modules")) {
94981
+ symlinkSync(path50.join(copy, relative), to, "junction");
94516
94982
  continue;
94517
94983
  }
94518
94984
  }
@@ -94528,7 +94994,7 @@ function copySymlink(repo, tree, from, to) {
94528
94994
  }
94529
94995
  const realRepo = realpathSync9(repo);
94530
94996
  if (real && inside3(realRepo, real)) {
94531
- symlinkSync(path48.relative(path48.dirname(to), path48.join(tree, path48.relative(realRepo, real))), to);
94997
+ symlinkSync(path50.relative(path50.dirname(to), path50.join(tree, path50.relative(realRepo, real))), to);
94532
94998
  return;
94533
94999
  }
94534
95000
  if (real && statSync8(real).isFile())
@@ -94537,10 +95003,10 @@ function copySymlink(repo, tree, from, to) {
94537
95003
  symlinkSync(readlinkSync3(from), to);
94538
95004
  }
94539
95005
  function writeInside(copy, file, content) {
94540
- const target = path48.join(copy, file);
95006
+ const target = path50.join(copy, file);
94541
95007
  try {
94542
95008
  const stat = lstatSync7(target);
94543
- if (!stat.isFile() || !inside3(realpathSync9(copy), realpathSync9(path48.dirname(target))))
95009
+ if (!stat.isFile() || !inside3(realpathSync9(copy), realpathSync9(path50.dirname(target))))
94544
95010
  return false;
94545
95011
  } catch {
94546
95012
  return false;
@@ -94551,9 +95017,9 @@ function writeInside(copy, file, content) {
94551
95017
  return true;
94552
95018
  }
94553
95019
  function copyWorkingTree(repo) {
94554
- const dir = mkdtempSync4(path48.join(os14.tmpdir(), TEMP_PREFIX));
94555
- writeFileSync10(path48.join(dir, PID_FILE), String(process.pid));
94556
- const tree = path48.join(dir, "tree");
95020
+ const dir = mkdtempSync4(path50.join(os14.tmpdir(), TEMP_PREFIX));
95021
+ writeFileSync10(path50.join(dir, PID_FILE), String(process.pid));
95022
+ const tree = path50.join(dir, "tree");
94557
95023
  const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
94558
95024
  if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
94559
95025
  const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -94570,29 +95036,29 @@ function copyWorkingTree(repo) {
94570
95036
  for (const relative of listed) {
94571
95037
  if (!relative)
94572
95038
  continue;
94573
- const from = path48.join(repo, relative);
95039
+ const from = path50.join(repo, relative);
94574
95040
  let stat;
94575
95041
  try {
94576
95042
  stat = lstatSync7(from);
94577
95043
  } catch {
94578
95044
  continue;
94579
95045
  }
94580
- const to = path48.join(tree, relative);
94581
- mkdirSync9(path48.dirname(to), { recursive: true });
95046
+ const to = path50.join(tree, relative);
95047
+ mkdirSync9(path50.dirname(to), { recursive: true });
94582
95048
  if (stat.isSymbolicLink())
94583
95049
  copySymlink(repo, tree, from, to);
94584
95050
  else if (stat.isFile())
94585
95051
  copyFileSync6(from, to);
94586
- let parent = path48.dirname(relative);
95052
+ let parent = path50.dirname(relative);
94587
95053
  while (parent !== "." && !dirs.has(parent)) {
94588
95054
  dirs.add(parent);
94589
- parent = path48.dirname(parent);
95055
+ parent = path50.dirname(parent);
94590
95056
  }
94591
95057
  }
94592
95058
  for (const parent of dirs) {
94593
- const modules = path48.join(repo, parent, "node_modules");
94594
- if (existsSync19(modules) && !existsSync19(path48.join(tree, parent, "node_modules")))
94595
- linkNodeModules(modules, path48.join(tree, parent, "node_modules"), repo, tree);
95059
+ const modules = path50.join(repo, parent, "node_modules");
95060
+ if (existsSync20(modules) && !existsSync20(path50.join(tree, parent, "node_modules")))
95061
+ linkNodeModules(modules, path50.join(tree, parent, "node_modules"), repo, tree);
94596
95062
  }
94597
95063
  return { dir, tree };
94598
95064
  }
@@ -94631,20 +95097,20 @@ async function runMutation(options) {
94631
95097
  const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
94632
95098
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
94633
95099
  try {
94634
- return { file, text: readFileSync20(path48.join(repo, file), "utf8") };
95100
+ return { file, text: readFileSync22(path50.join(repo, file), "utf8") };
94635
95101
  } catch {
94636
95102
  return { file, text: "" };
94637
95103
  }
94638
95104
  });
94639
95105
  const candidates = [];
94640
95106
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
94641
- const relative = toPosix(path48.relative(repo, absolute));
95107
+ const relative = toPosix(path50.relative(repo, absolute));
94642
95108
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
94643
95109
  continue;
94644
- if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
95110
+ if (NOT_SOURCE.test(relative) || !existsSync20(absolute))
94645
95111
  continue;
94646
95112
  try {
94647
- candidates.push(...mutantsFor(readFileSync20(absolute, "utf8"), relative, lines));
95113
+ candidates.push(...mutantsFor(readFileSync22(absolute, "utf8"), relative, lines));
94648
95114
  } catch {}
94649
95115
  }
94650
95116
  const mutants = candidates.map((mutant, index) => ({ ...mutant, id: index + 1 }));
@@ -94652,9 +95118,9 @@ async function runMutation(options) {
94652
95118
  for (const mutant of mutants)
94653
95119
  if (!related.has(mutant.file))
94654
95120
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
94655
- const cwdRelative = path48.relative(repo, options.cwd);
95121
+ const cwdRelative = path50.relative(repo, options.cwd);
94656
95122
  const asArg = (file) => {
94657
- const relative = toPosix(path48.relative(cwdRelative, file));
95123
+ const relative = toPosix(path50.relative(cwdRelative, file));
94658
95124
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
94659
95125
  };
94660
95126
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -94689,7 +95155,7 @@ async function runMutation(options) {
94689
95155
  return finish2(options, 0, results, counts, started);
94690
95156
  removeStaleCopies();
94691
95157
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
94692
- const copyCwd = path48.join(copy, cwdRelative);
95158
+ const copyCwd = path50.join(copy, cwdRelative);
94693
95159
  let current = null;
94694
95160
  const restore = () => {
94695
95161
  if (current)
@@ -94724,7 +95190,7 @@ async function runMutation(options) {
94724
95190
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
94725
95191
  continue;
94726
95192
  }
94727
- const original = readFileSync20(path48.join(copy, mutant.file), "utf8");
95193
+ const original = readFileSync22(path50.join(copy, mutant.file), "utf8");
94728
95194
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
94729
95195
  current = { file: mutant.file, original };
94730
95196
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -94781,8 +95247,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
94781
95247
  return error.code === "EPERM";
94782
95248
  }
94783
95249
  }, inside3 = (root, file) => {
94784
- const relative = path48.relative(root, file);
94785
- return relative === "" || !relative.startsWith("..") && !path48.isAbsolute(relative);
95250
+ const relative = path50.relative(root, file);
95251
+ return relative === "" || !relative.startsWith("..") && !path50.isAbsolute(relative);
94786
95252
  }, active, killTree = (child) => {
94787
95253
  try {
94788
95254
  if (child.pid && process.platform !== "win32")
@@ -94830,8 +95296,8 @@ var exports_test_audit = {};
94830
95296
  __export(exports_test_audit, {
94831
95297
  run: () => run17
94832
95298
  });
94833
- import { existsSync as existsSync20 } from "node:fs";
94834
- import path49 from "node:path";
95299
+ import { existsSync as existsSync21 } from "node:fs";
95300
+ import path51 from "node:path";
94835
95301
  function parse7(argv) {
94836
95302
  const options = {
94837
95303
  paths: [],
@@ -94935,8 +95401,8 @@ async function run17(argv, io) {
94935
95401
  return emit(io, fail("invalid_input", `${options}
94936
95402
  usage: ${USAGE10}`));
94937
95403
  const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
94938
- const repo = top.ok ? path49.resolve(top.stdout.trim()) : null;
94939
- const display = (file) => toPosix(path49.relative(io.cwd, file) || file);
95404
+ const repo = top.ok ? path51.resolve(top.stdout.trim()) : null;
95405
+ const display = (file) => toPosix(path51.relative(io.cwd, file) || file);
94940
95406
  let base = null;
94941
95407
  let changed = null;
94942
95408
  if (options.diff || options.mutate) {
@@ -94953,7 +95419,7 @@ usage: ${USAGE10}`));
94953
95419
  }
94954
95420
  let files;
94955
95421
  if (options.diff && changed)
94956
- files = [...changed.keys()].filter((file) => TEST_FILE.test(toPosix(file)) && existsSync20(file)).toSorted();
95422
+ files = [...changed.keys()].filter((file) => TEST_FILE.test(toPosix(file)) && existsSync21(file)).toSorted();
94957
95423
  else {
94958
95424
  const expanded = expandPaths(io.cwd, options.paths);
94959
95425
  if (expanded.missing.length > 0)
@@ -95178,8 +95644,8 @@ var exports_youtrack = {};
95178
95644
  __export(exports_youtrack, {
95179
95645
  run: () => run19
95180
95646
  });
95181
- import { readFileSync as readFileSync21 } from "node:fs";
95182
- import path50 from "node:path";
95647
+ import { readFileSync as readFileSync23 } from "node:fs";
95648
+ import path52 from "node:path";
95183
95649
  async function run19(argv, io) {
95184
95650
  const [sub, ...rest] = argv;
95185
95651
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
@@ -95225,7 +95691,7 @@ async function run19(argv, io) {
95225
95691
  let markdown = flags.values.markdown ?? "";
95226
95692
  if (flags.values.file)
95227
95693
  try {
95228
- markdown = readFileSync21(path50.resolve(io.cwd, flags.values.file), "utf8");
95694
+ markdown = readFileSync23(path52.resolve(io.cwd, flags.values.file), "utf8");
95229
95695
  } catch (error) {
95230
95696
  return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
95231
95697
  }
@@ -95293,8 +95759,8 @@ var exports_changelog = {};
95293
95759
  __export(exports_changelog, {
95294
95760
  run: () => run20
95295
95761
  });
95296
- import { readFileSync as readFileSync22 } from "node:fs";
95297
- import path51 from "node:path";
95762
+ import { readFileSync as readFileSync24 } from "node:fs";
95763
+ import path53 from "node:path";
95298
95764
  async function run20(argv, io) {
95299
95765
  const [sub, ...rest] = argv;
95300
95766
  if (sub !== "apply")
@@ -95340,7 +95806,7 @@ async function run20(argv, io) {
95340
95806
  ]);
95341
95807
  }
95342
95808
  var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
95343
- const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path51.resolve(cwd, raw.slice(1)), "utf8") : raw;
95809
+ const text = raw === "-" ? readFileSync24(0, "utf8") : raw.startsWith("@") ? readFileSync24(path53.resolve(cwd, raw.slice(1)), "utf8") : raw;
95344
95810
  return JSON.parse(text);
95345
95811
  };
95346
95812
  var init_changelog2 = __esm(() => {
@@ -95436,7 +95902,7 @@ var init_handoff = __esm(() => {
95436
95902
 
95437
95903
  // packages/workit-cli/src/main.ts
95438
95904
  init_package();
95439
- import path52 from "node:path";
95905
+ import path54 from "node:path";
95440
95906
 
95441
95907
  // packages/workit-cli/src/help.ts
95442
95908
  var HELP_FLAGS = new Set(["--help", "-h"]);
@@ -95764,8 +96230,8 @@ var VERBS = [
95764
96230
  },
95765
96231
  {
95766
96232
  name: "commit",
95767
- usage: "workit git commit -m|--message <msg>… [-a|--all | [--] <paths…>]",
95768
- summary: "Commit with a linted Conventional Commit message and the session trailer"
96233
+ usage: "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>]",
96234
+ summary: "Commit (or --amend) with a linted Conventional Commit message and the session trailer"
95769
96235
  },
95770
96236
  {
95771
96237
  name: "push",
@@ -95776,6 +96242,7 @@ var VERBS = [
95776
96242
  examples: [
95777
96243
  "workit git branch --kind feature --slug help-everywhere",
95778
96244
  'workit git commit -m "fix(cli): answer help flags first" --all',
96245
+ "workit git commit --amend --no-edit -- src/forgotten.ts",
95779
96246
  "workit git push --set-upstream"
95780
96247
  ],
95781
96248
  load: () => Promise.resolve().then(() => (init_git2(), exports_git))
@@ -96056,7 +96523,7 @@ var defaultIo = (json) => ({
96056
96523
  });
96057
96524
  async function main(argv, overrides = {}, options = {}) {
96058
96525
  const parsed = parseGlobals(argv);
96059
- const cwd = path52.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
96526
+ const cwd = path54.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
96060
96527
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
96061
96528
  if (parsed.error)
96062
96529
  return emit(io, fail("invalid_input", parsed.error));
@@ -96114,9 +96581,21 @@ async function main(argv, overrides = {}, options = {}) {
96114
96581
  globalThis[Symbol.for("workit.migrationReporter")] = (report) => io.stderr(`workit: migrated ${report.tasks} task${report.tasks === 1 ? "" : "s"} from ${report.from} to ${report.to} (backup: ${report.backup})
96115
96582
  `);
96116
96583
  const verb = await entry.load();
96584
+ const runVerb = (target) => guarded(target, () => verb.run(ended, target));
96117
96585
  if (!io.json)
96118
- return verb.run(ended, io);
96119
- return runJsonPure(io, command, (pure) => verb.run(ended, pure));
96586
+ return runVerb(io);
96587
+ return runJsonPure(io, command, runVerb);
96588
+ }
96589
+ async function guarded(io, run) {
96590
+ try {
96591
+ return await run();
96592
+ } catch (error) {
96593
+ if (!(error instanceof Error) || error.name !== "WorkspaceConfigError")
96594
+ throw error;
96595
+ return emit(io, fail("blocked", error.message, {
96596
+ unblock: "fix ~/.config/workit/workspaces.json (workit init → Workspaces)"
96597
+ }));
96598
+ }
96120
96599
  }
96121
96600
  var CODE_FOR_EXIT = {
96122
96601
  1: "failed",