@brainervirus/workit-cursor 8.12.3 → 8.13.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.
@@ -8638,1557 +8638,1697 @@ var init_rev = __esm(() => {
8638
8638
  MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
8639
8639
  });
8640
8640
 
8641
- // packages/workit-core/src/core/boundary.ts
8642
- var EVENT, errorDetail = (err) => {
8643
- if (err instanceof Error) {
8644
- return { error_name: err.name, error: err.message };
8645
- }
8646
- return { error_name: "unknown", error: String(err) };
8647
- };
8648
- var init_boundary = __esm(() => {
8649
- EVENT = {
8650
- initialization: "initialization",
8651
- provenance: "provenance",
8652
- assets: "assets",
8653
- configurationSource: "configuration_source",
8654
- hooks: "hooks",
8655
- mcpConnection: "mcp_connection",
8656
- migration: "migration",
8657
- installSteps: "install_steps",
8658
- uncaughtFailure: "uncaught_failure",
8659
- toolsFailed: "tools_failed",
8660
- doctor: "doctor"
8661
- };
8662
- });
8663
-
8664
- // packages/workit-core/src/core/config.ts
8665
- import {
8666
- copyFileSync,
8667
- cpSync,
8668
- existsSync,
8669
- mkdirSync,
8670
- readFileSync,
8671
- readdirSync,
8672
- writeFileSync
8673
- } from "node:fs";
8674
- import os2 from "node:os";
8641
+ // packages/workit-core/src/store/paths.ts
8642
+ import { spawnSync as spawnSync2 } from "node:child_process";
8643
+ import { createHash as createHash3 } from "node:crypto";
8644
+ import fs2 from "node:fs";
8675
8645
  import path2 from "node:path";
8676
- var diagnosticLogger, PRESETS, mergePreset = (preset, input = {}, current = {
8677
- branchPolicy: { preset, allowed: [], protected: [] }
8678
- }) => {
8679
- const defs = PRESETS[preset];
8680
- return {
8681
- preset,
8682
- allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
8683
- protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
8684
- };
8685
- }, resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path2.join(process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config"), "workit"), migratedDir = null, migrationFailed = false, ensureConfigDir = (dir = resolveConfigDir()) => {
8686
- if (migratedDir === dir)
8687
- return dir;
8688
- if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
8689
- migratedDir = dir;
8690
- return dir;
8691
- }
8692
- const legacy = path2.join(process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config"), "workflow-toolkit");
8693
- if (!existsSync(legacy)) {
8694
- migratedDir = dir;
8695
- return dir;
8646
+ function fastGit(root) {
8647
+ if (DISCOVERY_ENV.some((name) => process.env[name]))
8648
+ return null;
8649
+ let dir = real(root);
8650
+ let device = null;
8651
+ for (;; ) {
8652
+ let stat;
8653
+ try {
8654
+ stat = fs2.statSync(dir);
8655
+ } catch {
8656
+ return null;
8657
+ }
8658
+ if (device !== null && stat.dev !== device)
8659
+ return { kind: "none" };
8660
+ device = stat.dev;
8661
+ const dotGit = path2.join(dir, ".git");
8662
+ let entry = null;
8663
+ try {
8664
+ entry = fs2.statSync(dotGit);
8665
+ } catch {}
8666
+ let gitDir = null;
8667
+ let top = null;
8668
+ if (entry) {
8669
+ top = dir;
8670
+ if (entry.isDirectory())
8671
+ gitDir = dotGit;
8672
+ else if (entry.isFile()) {
8673
+ let text = "";
8674
+ try {
8675
+ text = fs2.readFileSync(dotGit, "utf8");
8676
+ } catch {
8677
+ return null;
8678
+ }
8679
+ const match = /^gitdir: (.+)$/mu.exec(text);
8680
+ if (!match)
8681
+ return null;
8682
+ gitDir = path2.resolve(dir, match[1].trim());
8683
+ } else
8684
+ return null;
8685
+ if (!fs2.existsSync(path2.join(gitDir, "HEAD")))
8686
+ return null;
8687
+ } else if (isGitDir(dir))
8688
+ gitDir = dir;
8689
+ if (gitDir) {
8690
+ let common = gitDir;
8691
+ try {
8692
+ common = path2.resolve(gitDir, fs2.readFileSync(path2.join(gitDir, "commondir"), "utf8").trim());
8693
+ } catch {}
8694
+ if (!fs2.existsSync(path2.join(common, "objects")))
8695
+ return null;
8696
+ return {
8697
+ kind: "repo",
8698
+ common: real(common),
8699
+ gitDir: real(gitDir),
8700
+ top: top === null ? null : real(top),
8701
+ reftable: reftableRefs(common)
8702
+ };
8703
+ }
8704
+ const parent = path2.dirname(dir);
8705
+ if (parent === dir)
8706
+ return { kind: "none" };
8707
+ dir = parent;
8696
8708
  }
8697
- if (!migrationFailed && existsSync(dir)) {
8698
- migratedDir = dir;
8699
- return dir;
8709
+ }
8710
+ function resolveStore(root) {
8711
+ root = path2.resolve(root);
8712
+ let directory = false;
8713
+ try {
8714
+ directory = fs2.statSync(root).isDirectory();
8715
+ } catch {}
8716
+ if (!directory)
8717
+ return { dir: path2.join(root, ".workit"), shared: false, top: null };
8718
+ const fast = fastGit(root);
8719
+ if (fast?.kind === "none")
8720
+ return { dir: path2.join(root, ".workit"), shared: false, top: null };
8721
+ if (fast?.kind === "repo")
8722
+ return {
8723
+ dir: path2.join(fast.common, "workit"),
8724
+ shared: true,
8725
+ top: fast.top,
8726
+ git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
8727
+ };
8728
+ const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
8729
+ const common = run.stdout.trim();
8730
+ if (run.ok && common) {
8731
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
8732
+ return {
8733
+ dir: path2.join(real(path2.resolve(root, common)), "workit"),
8734
+ shared: true,
8735
+ top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
8736
+ };
8700
8737
  }
8701
- migrationFailed = false;
8702
- mkdirSync(dir, { recursive: true });
8703
- diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
8704
- for (const entry of readdirSync(legacy, { withFileTypes: true })) {
8705
- const src = path2.join(legacy, entry.name);
8706
- const dest = path2.join(dir, entry.name);
8707
- if (existsSync(dest))
8708
- continue;
8738
+ if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
8739
+ return { dir: path2.join(root, ".workit"), shared: false, top: null };
8740
+ return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
8741
+ `)[0] || "no output"}); fix the repository (git status should work here)`);
8742
+ }
8743
+ function resolveTaskKey(root, location) {
8744
+ if (!location.shared)
8745
+ return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
8746
+ const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
8747
+ if (location.git && !location.git.reftable) {
8748
+ let head = "";
8709
8749
  try {
8710
- if (entry.isDirectory())
8711
- cpSync(src, dest, { recursive: true });
8712
- else if (entry.isFile())
8713
- copyFileSync(src, dest);
8714
- } catch (err) {
8715
- migrationFailed = true;
8716
- diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
8717
- console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
8750
+ head = fs2.readFileSync(path2.join(location.git.gitDir, "HEAD"), "utf8").trim();
8751
+ } catch {}
8752
+ const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
8753
+ if (ref && ref[1] !== ".invalid")
8754
+ return branchKey(ref[1]);
8755
+ if (/^[0-9a-f]{40,64}$/u.test(head)) {
8756
+ const rebasing = rebasingBranch(location.git.gitDir);
8757
+ if (rebasing)
8758
+ return branchKey(rebasing);
8759
+ return {
8760
+ key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
8761
+ kind: "detached",
8762
+ branch: null
8763
+ };
8718
8764
  }
8719
8765
  }
8720
- if (!migrationFailed)
8721
- migratedDir = dir;
8722
- return dir;
8723
- }, configDir = () => ensureConfigDir(), LOCALE_RE, DEFAULTS, unchosenBranchPolicy, readSafe = (p) => {
8766
+ const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
8767
+ const name = branch.ok ? branch.stdout.trim() : "";
8768
+ if (name && name !== ".invalid")
8769
+ return branchKey(name);
8770
+ const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
8771
+ if (gitDir.ok && gitDir.stdout.trim()) {
8772
+ const rebasing = rebasingBranch(gitDir.stdout.trim());
8773
+ if (rebasing)
8774
+ return branchKey(rebasing);
8775
+ }
8776
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
8777
+ const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
8778
+ return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
8779
+ }
8780
+ function checkoutRootOf(root, location) {
8781
+ const resolved = path2.resolve(root);
8782
+ let canonical = resolved;
8724
8783
  try {
8725
- return readFileSync(p, "utf8");
8784
+ canonical = fs2.realpathSync(resolved);
8726
8785
  } catch {
8727
- return null;
8786
+ return resolved;
8728
8787
  }
8729
- }, isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value), parseConfigResult = (raw, file) => {
8730
- if (raw === null)
8731
- return { status: "missing", path: file };
8732
- let parsed;
8788
+ const found = location ?? resolveStore(canonical);
8789
+ if (found instanceof Error || !found.shared || !found.top)
8790
+ return canonical;
8791
+ if (real(canonical) === found.top)
8792
+ return canonical;
8733
8793
  try {
8734
- parsed = JSON.parse(raw);
8794
+ return fs2.realpathSync(found.top);
8735
8795
  } catch {
8736
- return { status: "malformed", path: file, error: `${file} is not valid JSON` };
8737
- }
8738
- if (!isConfigObject(parsed)) {
8739
- return { status: "malformed", path: file, error: `${file} is not a JSON object` };
8796
+ return canonical;
8740
8797
  }
8741
- const input = parsed;
8742
- const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
8743
- const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
8744
- const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
8745
- const config = {
8746
- locale,
8747
- localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
8748
- branchPolicy: mergePreset(preset, {
8749
- allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
8750
- protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
8751
- }, DEFAULTS),
8752
- commitPolicy: {
8753
- preset: commitPreset,
8754
- ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
8798
+ }
8799
+ function branchRenames(location) {
8800
+ if (!location.shared)
8801
+ return [];
8802
+ const files = [];
8803
+ const walk = (dir) => {
8804
+ let entries;
8805
+ try {
8806
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
8807
+ } catch {
8808
+ return;
8755
8809
  }
8810
+ for (const entry of entries)
8811
+ if (entry.isDirectory())
8812
+ walk(path2.join(dir, entry.name));
8813
+ else
8814
+ files.push(path2.join(dir, entry.name));
8756
8815
  };
8757
- if (!Object.hasOwn(PRESETS, input.branchPolicy?.preset))
8758
- unchosenBranchPolicy.add(config);
8759
- return { status: "valid", path: file, config };
8760
- }, readConfigTyped = (dir) => {
8761
- const file = path2.join(dir ?? configDir(), "config.json");
8762
- return parseConfigResult(readSafe(file), file);
8763
- }, readConfig = () => {
8764
- const result = readConfigTyped();
8765
- if (result.status === "malformed")
8766
- throw new Error(result.error);
8767
- return result.config ?? DEFAULTS;
8768
- }, vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path2.join(configDir(), "vcs.json"), branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"), resolveBranchPolicy = (config, workspace, repoPreset) => {
8769
- const wp = workspace?.branchPolicy ?? {};
8770
- const workspacePreset = Object.hasOwn(PRESETS, wp.preset);
8771
- const detected = !workspacePreset && unchosenBranchPolicy.has(config) ? repoPreset?.() ?? null : null;
8772
- const preset = workspacePreset ? wp.preset : detected ?? config.branchPolicy?.preset ?? "gitflow";
8773
- const merged = mergePreset(preset, {
8774
- allowed: wp.allowed,
8775
- protectedNames: wp.protected
8776
- }, config);
8777
- const allowed = merged.allowed.map(branchGlobPattern);
8778
- return {
8779
- preset,
8780
- allowed,
8781
- protected: new Set([...merged.protected, ...detected ? PRESETS.gitflow.protected : []].map((p) => p.toLowerCase())),
8782
- integration: wp.integration === "merge" ? "merge" : "pr",
8783
- defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop",
8784
- detected: detected !== null
8785
- };
8786
- }, COMMIT_PRESETS, resolveCommitPolicy = (config, workspace) => {
8787
- const wp = workspace?.commitPolicy ?? {};
8788
- if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
8789
- return {
8790
- preset: wp.preset,
8791
- ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
8792
- };
8816
+ walk(path2.join(path2.dirname(location.dir), "logs", "refs", "heads"));
8817
+ if (location.git)
8818
+ files.push(path2.join(location.git.gitDir, "logs", "HEAD"));
8819
+ const seen = new Set;
8820
+ const renames = [];
8821
+ for (const file of files) {
8822
+ let text = "";
8823
+ try {
8824
+ text = fs2.readFileSync(file, "utf8");
8825
+ } catch {
8826
+ continue;
8827
+ }
8828
+ for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
8829
+ const rename = { from: match[2], to: match[3], at: Number(match[1]) };
8830
+ const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
8831
+ if (seen.has(id))
8832
+ continue;
8833
+ seen.add(id);
8834
+ renames.push(rename);
8835
+ }
8793
8836
  }
8794
- return config.commitPolicy ?? { preset: "conventional" };
8795
- };
8796
- var init_config = __esm(() => {
8797
- init_boundary();
8798
- PRESETS = {
8799
- gitflow: {
8800
- allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
8801
- protected: ["main", "develop", "master", "prod", "production"]
8802
- },
8803
- "github-flow": { allowed: ["*"], protected: ["main"] },
8804
- "trunk-based": { allowed: ["*"], protected: ["main"] },
8805
- custom: { allowed: [], protected: [] }
8806
- };
8807
- LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
8808
- DEFAULTS = {
8809
- locale: "en",
8810
- localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
8811
- branchPolicy: {
8812
- preset: "gitflow",
8813
- allowed: [...PRESETS.gitflow.allowed],
8814
- protected: [...PRESETS.gitflow.protected]
8815
- },
8816
- commitPolicy: { preset: "conventional" }
8837
+ return renames.toSorted((left, right) => left.at - right.at);
8838
+ }
8839
+ function followRenames(key, boundAt, renames) {
8840
+ let current = key;
8841
+ let since = Math.floor(Date.parse(boundAt) / 1000);
8842
+ for (const rename of renames)
8843
+ if (rename.from === current && rename.at >= since) {
8844
+ current = rename.to;
8845
+ since = rename.at;
8846
+ }
8847
+ return current;
8848
+ }
8849
+ var NOT_A_REPO, gitRun = (cwd, args) => {
8850
+ const run = spawnSync2("git", args, {
8851
+ cwd,
8852
+ encoding: "utf8",
8853
+ timeout: 20000,
8854
+ killSignal: "SIGKILL",
8855
+ windowsHide: true,
8856
+ env: {
8857
+ ...process.env,
8858
+ GIT_TERMINAL_PROMPT: "0",
8859
+ GIT_OPTIONAL_LOCKS: "0",
8860
+ LC_ALL: "C",
8861
+ LANGUAGE: "C"
8862
+ }
8863
+ });
8864
+ const error = run.error;
8865
+ return {
8866
+ ok: run.status === 0,
8867
+ stdout: run.stdout ?? "",
8868
+ stderr: error ? String(error.message) : run.stderr ?? "",
8869
+ missing: error?.code === "ENOENT"
8817
8870
  };
8818
- unchosenBranchPolicy = new WeakSet([DEFAULTS]);
8819
- COMMIT_PRESETS = [
8820
- "conventional",
8821
- "gitmoji",
8822
- "ticket-prefix",
8823
- "freeform",
8824
- "custom",
8825
- "auto"
8826
- ];
8827
- });
8828
-
8829
- // packages/workit-core/src/core/branch-policy.ts
8830
- import { spawnSync as spawnSync2 } from "node:child_process";
8831
- var PREFIXES, branchNames = (workspaceRoot) => {
8832
- const r = spawnSync2("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"], { cwd: workspaceRoot, encoding: "utf8" });
8833
- if (r.status !== 0)
8834
- return new Set;
8835
- return new Set((r.stdout ?? "").split(`
8836
- `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
8837
- }, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
8838
- const names = branchNames(workspaceRoot);
8839
- const develop = names.has("develop");
8840
- const main = names.has("main");
8841
- const master = names.has("master");
8842
- const root = main ? "main" : master ? "master" : null;
8843
- if (develop) {
8844
- return {
8845
- preset: "gitflow",
8846
- developBranch: "develop",
8847
- integration: "merge",
8848
- protected: root ? [root, "develop"] : ["develop"],
8849
- allowed: [...PRESETS.gitflow.allowed],
8850
- prefixes: PREFIXES
8851
- };
8871
+ }, shortHash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
8872
+ try {
8873
+ return fs2.realpathSync.native(file);
8874
+ } catch {
8875
+ return file;
8852
8876
  }
8853
- if (root === "main") {
8854
- return {
8855
- preset: "github-flow",
8856
- developBranch: null,
8857
- integration: "pr",
8858
- protected: [root],
8859
- allowed: ["*"],
8860
- prefixes: PREFIXES
8861
- };
8877
+ }, DISCOVERY_ENV, isGitDir = (dir) => {
8878
+ try {
8879
+ return fs2.statSync(path2.join(dir, "HEAD")).isFile() && fs2.statSync(path2.join(dir, "objects")).isDirectory() && fs2.statSync(path2.join(dir, "refs")).isDirectory();
8880
+ } catch {
8881
+ return false;
8862
8882
  }
8863
- if (root === "master") {
8864
- return {
8865
- preset: "trunk-based",
8866
- developBranch: null,
8867
- integration: "pr",
8868
- protected: [root],
8869
- allowed: ["*"],
8870
- prefixes: PREFIXES
8871
- };
8883
+ }, reftableRefs = (common) => {
8884
+ try {
8885
+ return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs2.readFileSync(path2.join(common, "config"), "utf8"));
8886
+ } catch {
8887
+ return false;
8872
8888
  }
8873
- return {
8874
- preset: "gitflow",
8875
- developBranch: null,
8876
- integration: "merge",
8877
- protected: [],
8878
- allowed: [],
8879
- prefixes: PREFIXES
8880
- };
8881
- }, repoBranchPreset = (workspaceRoot) => {
8882
- const detected = detectBranchPolicy(workspaceRoot);
8883
- return detected.developBranch || detected.allowed.length ? detected.preset : null;
8889
+ }, rebasingBranch = (gitDir) => {
8890
+ for (const state of ["rebase-merge", "rebase-apply"]) {
8891
+ try {
8892
+ const ref = fs2.readFileSync(path2.join(gitDir, state, "head-name"), "utf8").trim();
8893
+ const match = /^refs\/heads\/(.+)$/u.exec(ref);
8894
+ if (match)
8895
+ return match[1];
8896
+ } catch {}
8897
+ }
8898
+ return null;
8899
+ }, checkoutSlug = (root) => {
8900
+ let canonical = real(root);
8901
+ if (process.platform === "win32")
8902
+ canonical = canonical.toLowerCase();
8903
+ const base = path2.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
8904
+ return `${base}-${shortHash(canonical)}`;
8884
8905
  };
8885
- var init_branch_policy = __esm(() => {
8886
- init_config();
8887
- PREFIXES = {
8888
- feature: "feature/*",
8889
- bugfix: "bugfix/*",
8890
- release: "release/*",
8891
- hotfix: "hotfix/*"
8892
- };
8906
+ var init_paths = __esm(() => {
8907
+ NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
8908
+ DISCOVERY_ENV = [
8909
+ "GIT_DIR",
8910
+ "GIT_COMMON_DIR",
8911
+ "GIT_WORK_TREE",
8912
+ "GIT_CEILING_DIRECTORIES",
8913
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM"
8914
+ ];
8893
8915
  });
8894
8916
 
8895
- // packages/workit-core/src/core/workspaces.ts
8917
+ // packages/workit-core/src/core/store-lock.ts
8896
8918
  import { spawnSync as spawnSync3 } from "node:child_process";
8897
- import { createHash as createHash3 } from "node:crypto";
8898
- import { readFileSync as readFileSync2, realpathSync } from "node:fs";
8899
- import path3 from "node:path";
8900
- function workspaceDefaultTarget(cwd, globalDefault) {
8901
- const ws = resolveRuntimeWorkspaceVcs(cwd);
8902
- const wp = ws?.branchPolicy ?? {};
8903
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
8904
- const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
8905
- const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
8906
- return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
8907
- }
8908
- var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
8909
- if (!isConfigObject(parsed)) {
8910
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
8919
+ import fs3 from "node:fs";
8920
+ import { hostname } from "node:os";
8921
+ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, IN_PROCESS_LOCK_TIMEOUT_MS = 250, defaultLockTimeoutMs, defaultLockTimeout = () => defaultLockTimeoutMs, hostAwareLockTimeout = (cliMs) => defaultLockTimeoutMs > IN_PROCESS_LOCK_TIMEOUT_MS ? cliMs : IN_PROCESS_LOCK_TIMEOUT_MS, parseMetadataLock = (raw) => {
8922
+ let value;
8923
+ try {
8924
+ value = JSON.parse(raw);
8925
+ } catch {
8926
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
8911
8927
  }
8912
- const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
8913
- if (!validated.success) {
8914
- const detail = validated.error.issues.map((issue) => {
8915
- const field = String(issue.path[issue.path.length - 1] ?? "");
8916
- const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
8917
- return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
8918
- }).join("; ");
8919
- return {
8920
- status: "invalid",
8921
- path: file,
8922
- entries: [],
8923
- error: `${file} has invalid workspace configuration: ${detail}`
8924
- };
8928
+ const parsed = metadataLockSchema.safeParse(value);
8929
+ if (!parsed.success)
8930
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
8931
+ return parsed.data;
8932
+ }, parseMetadataLockOrNull = (raw) => {
8933
+ try {
8934
+ return parseMetadataLock(raw);
8935
+ } catch {
8936
+ return null;
8925
8937
  }
8926
- for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
8927
- const glob = validateWorkspaceGlob(workspace.glob);
8928
- if (!glob.ok) {
8929
- return {
8930
- status: "invalid",
8931
- path: file,
8932
- entries: [],
8933
- error: `${file} workspaces.${index}.glob: ${glob.error}`
8934
- };
8938
+ }, parseProcStatStart = (stat) => {
8939
+ const close = stat.lastIndexOf(")");
8940
+ if (close < 0)
8941
+ return null;
8942
+ return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
8943
+ }, processStartOf = (pid) => {
8944
+ if (process.platform === "linux") {
8945
+ try {
8946
+ return parseProcStatStart(fs3.readFileSync(`/proc/${pid}/stat`, "utf8"));
8947
+ } catch {
8948
+ return null;
8935
8949
  }
8936
8950
  }
8937
- return {
8938
- status: "valid",
8939
- path: file,
8940
- entries: validated.data.workspaces ?? [],
8941
- document: parsed
8942
- };
8943
- }, readWorkspacesResult = (dir = configDir()) => {
8944
- const file = path3.join(dir, "workspaces.json");
8945
- let raw;
8946
- try {
8947
- raw = readFileSync2(file, "utf8");
8948
- } catch (error) {
8949
- if (error.code === "ENOENT")
8950
- return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
8951
- return {
8952
- status: "malformed",
8953
- path: file,
8954
- entries: [],
8955
- error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
8956
- };
8951
+ if (process.platform === "darwin" || process.platform === "freebsd") {
8952
+ try {
8953
+ const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
8954
+ encoding: "utf8",
8955
+ timeout: 1000
8956
+ });
8957
+ const value = run.status === 0 ? run.stdout.trim() : "";
8958
+ return value || null;
8959
+ } catch {
8960
+ return null;
8961
+ }
8957
8962
  }
8958
- let parsed;
8963
+ return null;
8964
+ }, readTrimmed = (file) => {
8959
8965
  try {
8960
- parsed = JSON.parse(raw);
8966
+ return fs3.readFileSync(file, "utf8").trim() || null;
8961
8967
  } catch {
8962
- return {
8963
- status: "malformed",
8964
- path: file,
8965
- entries: [],
8966
- revision: workspacesRevision(raw),
8967
- error: `${file} is not valid JSON`
8968
- };
8968
+ return null;
8969
8969
  }
8970
- return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
8971
- }, globToRegExp = (glob) => {
8972
- let out = "";
8973
- for (let i = 0;i < glob.length; i++) {
8974
- const c = glob[i];
8975
- if (c === "*") {
8976
- if (glob[i + 1] === "*") {
8977
- if (glob[i + 2] === "/") {
8978
- if (out === "")
8979
- out += "/?";
8980
- out += "(?:[^/]+/)*";
8981
- i += 2;
8982
- } else {
8983
- if (i + 2 >= glob.length) {
8984
- if (out.endsWith("/")) {
8985
- out = out.slice(0, -1);
8986
- out += "(?:/.*)?";
8987
- } else {
8988
- out += ".*";
8989
- }
8990
- } else {
8991
- out += ".*";
8992
- }
8993
- i++;
8994
- }
8995
- } else {
8996
- out += "[^/]*";
8997
- }
8998
- } else {
8999
- out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
9000
- }
8970
+ }, cachedLockHost = null, localLockHost = () => {
8971
+ if (cachedLockHost !== null)
8972
+ return cachedLockHost;
8973
+ let pidns = null;
8974
+ try {
8975
+ pidns = /\[(\d+)\]/.exec(fs3.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
8976
+ } catch {}
8977
+ const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
8978
+ cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
8979
+ return cachedLockHost;
8980
+ }, pidAlive = (pid) => {
8981
+ if (!Number.isSafeInteger(pid) || pid <= 0)
8982
+ return false;
8983
+ try {
8984
+ process.kill(pid, 0);
8985
+ return true;
8986
+ } catch (error) {
8987
+ return error.code === "EPERM";
9001
8988
  }
9002
- return new RegExp(`^${out}$`);
9003
- }, matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/")), UNSUPPORTED_GLOB, UNSUPPORTED_EXTGLOB, validateWorkspaceGlob = (glob) => {
9004
- const trimmed = glob.trim();
9005
- if (!trimmed)
9006
- return { ok: false, error: "workspace pattern is required" };
9007
- const m = UNSUPPORTED_GLOB.exec(trimmed);
9008
- if (m) {
9009
- return {
9010
- ok: false,
9011
- error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
9012
- };
8989
+ }, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
8990
+ const lock = metadataLockSchema.safeParse(payload);
8991
+ if (!lock.success)
8992
+ return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
8993
+ const { pid, processStart, host } = lock.data;
8994
+ const [lockName, lockSpace] = host.split("#");
8995
+ const [localName, localSpace] = localHost.split("#");
8996
+ if (lockSpace && localSpace && lockName === localName) {
8997
+ const [lockNs, lockBoot] = lockSpace.split(":");
8998
+ const [localNs, localBoot] = localSpace.split(":");
8999
+ if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
9000
+ return { state: "stale", reason: "lock was taken before this machine rebooted" };
9013
9001
  }
9014
- const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
9015
- if (ext) {
9016
- const token = ext[0];
9017
- const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
9002
+ if (host !== localHost)
9003
+ return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
9004
+ if (!pidAlive(pid))
9005
+ return { state: "stale", reason: `pid ${pid} is not running` };
9006
+ const currentStart = processStartOf(pid);
9007
+ if (processStart !== null && currentStart !== null && processStart !== currentStart)
9008
+ return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
9009
+ if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
9018
9010
  return {
9019
- ok: false,
9020
- error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
9011
+ state: "stale",
9012
+ reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
9021
9013
  };
9022
- }
9023
- return { ok: true };
9024
- }, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
9025
- const cached = mainCheckouts.get(cwd);
9026
- if (cached !== undefined)
9027
- return cached;
9028
- const env = { ...process.env };
9029
- for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
9030
- delete env[name];
9031
- const git = (args) => {
9032
- const run = spawnSync3("git", args, {
9033
- cwd,
9034
- env,
9035
- encoding: "utf8",
9036
- timeout: 5000,
9037
- windowsHide: true
9038
- });
9039
- return run.status === 0 ? run.stdout : null;
9040
- };
9041
- let main = null;
9042
- const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
9043
- const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
9044
- if (gitDir && common && path3.resolve(gitDir) !== path3.resolve(common)) {
9045
- const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
9046
- main = listed?.trim() || (path3.basename(common) === ".git" ? path3.dirname(common) : common);
9047
- }
9048
- mainCheckouts.set(cwd, main);
9049
- return main;
9050
- }, matchingEntries = (cwd, entries) => {
9051
- const match = (where) => {
9052
- const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
9053
- return entries.filter((entry) => {
9054
- const glob = entry.glob.replaceAll("\\", "/");
9055
- const canonical = canonicalGlob(glob);
9056
- return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
9057
- });
9058
- };
9059
- const direct = match(cwd);
9060
- if (direct.length)
9061
- return direct;
9062
- const main = mainCheckoutOf(cwd);
9063
- return main ? match(main) : direct;
9064
- }, realpathOf = (p) => {
9065
- const real = realpathSync.native ?? realpathSync;
9014
+ return { state: "live", reason: `held by running pid ${pid}` };
9015
+ }, ageOf = (file, nowMs) => {
9066
9016
  try {
9067
- return real(p);
9017
+ return nowMs - fs3.lstatSync(file).mtimeMs;
9068
9018
  } catch {
9069
- return p;
9070
- }
9071
- }, canonicalGlob = (glob) => {
9072
- const m = /[*?[\]{]/.exec(glob);
9073
- const prefix = m ? glob.slice(0, m.index) : glob;
9074
- const rest = m ? glob.slice(m.index) : "";
9075
- if (!prefix)
9076
- return glob;
9077
- const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
9078
- return real + (prefix.endsWith("/") ? "/" : "") + rest;
9079
- }, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
9080
- if (workspaceName !== undefined) {
9081
- const named = matches.filter((entry) => entry.name === workspaceName);
9082
- if (named.length === 1)
9083
- return named[0];
9084
- if (named.length > 1)
9085
- throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
9086
- if (matches.length > 0) {
9087
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
9088
- }
9089
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
9090
- }
9091
- if (matches.length < 2)
9092
- return matches[0];
9093
- const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
9094
- const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
9095
- if (mostSpecific.length > 1) {
9096
- throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
9097
- }
9098
- return mostSpecific[0];
9099
- }, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
9100
- const matches = matchingEntries(cwd, entries);
9101
- return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
9102
- }, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
9103
- const result = readWorkspacesResult(dir);
9104
- if (result.status === "malformed" || result.status === "invalid")
9105
- throw new WorkspaceConfigError(result.error);
9106
- return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
9107
- }, runtimeWorkspacePolicySchema = (kind) => {
9108
- const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
9109
- const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
9110
- provider: _enum(["gitlab", "github"]),
9111
- defaultTargetBranch: nonBlank.optional(),
9112
- account: nonBlank.optional()
9113
- }).passthrough();
9114
- const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
9115
- const entrySchema = object({
9116
- name: nonBlank,
9117
- glob: nonBlank,
9118
- [key]: policySchema.optional(),
9119
- defaultProfile: nonBlank.optional(),
9120
- profiles: record(nonBlank, profileSchema).optional()
9121
- }).passthrough();
9122
- return entrySchema;
9123
- }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
9124
- const file = path3.join(dir, "workspaces.json");
9125
- let raw;
9126
- try {
9127
- raw = readFileSync2(file, "utf8");
9128
- } catch (error) {
9129
- if (error.code === "ENOENT")
9130
- return null;
9131
- throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
9019
+ return null;
9132
9020
  }
9133
- let parsed;
9021
+ }, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
9022
+ const guard = `${lockPath}.reclaim`;
9023
+ const age = ageOf(guard, nowMs);
9024
+ if (age === null || age <= RECLAIM_GUARD_TTL_MS)
9025
+ return false;
9134
9026
  try {
9135
- parsed = JSON.parse(raw);
9027
+ fs3.rmdirSync(guard);
9028
+ return true;
9136
9029
  } catch {
9137
- throw new WorkspaceConfigError(`${file} is not valid JSON`);
9138
- }
9139
- const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
9140
- if (!validated.success) {
9141
- const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
9142
- throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
9143
- }
9144
- const entries = validated.data.workspaces;
9145
- for (const entry of entries ?? []) {
9146
- const glob = validateWorkspaceGlob(entry.glob);
9147
- if (!glob.ok)
9148
- throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
9030
+ return false;
9149
9031
  }
9150
- const matches = matchingEntries(cwd, entries ?? []);
9151
- const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
9152
- const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
9153
- return selected ? { file, selected, matches } : null;
9154
- }, resolveRuntimeReleaseTracks = (cwd) => {
9155
- const matched = matchRuntimeWorkspace(cwd);
9156
- return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
9157
- }, workspaceTrailers = (cwd, dir) => {
9158
- let matched;
9159
- try {
9160
- matched = matchRuntimeWorkspace(cwd, dir);
9161
- } catch {
9162
- return { enabled: true, workspace: null, from: null };
9032
+ };
9033
+ var init_store_lock = __esm(() => {
9034
+ init_zod();
9035
+ init_task_contract();
9036
+ init_paths();
9037
+ metadataLockSchema = object({
9038
+ pid: number2().int().nonnegative().safe(),
9039
+ processStart: string2().nullable(),
9040
+ host: string2().min(1),
9041
+ nonce: string2().min(1),
9042
+ externalAction: literal(true).optional()
9043
+ }).strict();
9044
+ FOREIGN_LOCK_TTL_MS = 10 * 60000;
9045
+ defaultLockTimeoutMs = IN_PROCESS_LOCK_TIMEOUT_MS;
9046
+ TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
9047
+ });
9048
+
9049
+ // packages/workit-core/src/core/boundary.ts
9050
+ var EVENT, errorDetail = (err) => {
9051
+ if (err instanceof Error) {
9052
+ return { error_name: err.name, error: err.message };
9163
9053
  }
9164
- if (!matched)
9165
- return { enabled: true, workspace: null, from: null };
9166
- const { selected, matches } = matched;
9167
- const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9168
- const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
9054
+ return { error_name: "unknown", error: String(err) };
9055
+ };
9056
+ var init_boundary = __esm(() => {
9057
+ EVENT = {
9058
+ initialization: "initialization",
9059
+ provenance: "provenance",
9060
+ assets: "assets",
9061
+ configurationSource: "configuration_source",
9062
+ hooks: "hooks",
9063
+ mcpConnection: "mcp_connection",
9064
+ migration: "migration",
9065
+ installSteps: "install_steps",
9066
+ uncaughtFailure: "uncaught_failure",
9067
+ toolsFailed: "tools_failed",
9068
+ doctor: "doctor"
9069
+ };
9070
+ });
9071
+
9072
+ // packages/workit-core/src/core/config.ts
9073
+ import {
9074
+ copyFileSync,
9075
+ cpSync,
9076
+ existsSync,
9077
+ mkdirSync,
9078
+ readFileSync,
9079
+ readdirSync,
9080
+ writeFileSync
9081
+ } from "node:fs";
9082
+ import os2 from "node:os";
9083
+ import path3 from "node:path";
9084
+ var diagnosticLogger, PRESETS, mergePreset = (preset, input = {}, current = {
9085
+ branchPolicy: { preset, allowed: [], protected: [] }
9086
+ }) => {
9087
+ const defs = PRESETS[preset];
9169
9088
  return {
9170
- enabled: source?.trailers !== false,
9171
- workspace: selected.name,
9172
- from: source?.name ?? null
9089
+ preset,
9090
+ allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
9091
+ protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
9173
9092
  };
9174
- }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9175
- const matched = matchRuntimeWorkspace(cwd);
9176
- if (!matched)
9093
+ }, resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path3.join(process.env.XDG_CONFIG_HOME || path3.join(os2.homedir(), ".config"), "workit"), migratedDir = null, migrationFailed = false, ensureConfigDir = (dir = resolveConfigDir()) => {
9094
+ if (migratedDir === dir)
9095
+ return dir;
9096
+ if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
9097
+ migratedDir = dir;
9098
+ return dir;
9099
+ }
9100
+ const legacy = path3.join(process.env.XDG_CONFIG_HOME || path3.join(os2.homedir(), ".config"), "workflow-toolkit");
9101
+ if (!existsSync(legacy)) {
9102
+ migratedDir = dir;
9103
+ return dir;
9104
+ }
9105
+ if (!migrationFailed && existsSync(dir)) {
9106
+ migratedDir = dir;
9107
+ return dir;
9108
+ }
9109
+ migrationFailed = false;
9110
+ mkdirSync(dir, { recursive: true });
9111
+ diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
9112
+ for (const entry of readdirSync(legacy, { withFileTypes: true })) {
9113
+ const src = path3.join(legacy, entry.name);
9114
+ const dest = path3.join(dir, entry.name);
9115
+ if (existsSync(dest))
9116
+ continue;
9117
+ try {
9118
+ if (entry.isDirectory())
9119
+ cpSync(src, dest, { recursive: true });
9120
+ else if (entry.isFile())
9121
+ copyFileSync(src, dest);
9122
+ } catch (err) {
9123
+ migrationFailed = true;
9124
+ diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
9125
+ console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
9126
+ }
9127
+ }
9128
+ if (!migrationFailed)
9129
+ migratedDir = dir;
9130
+ return dir;
9131
+ }, configDir = () => ensureConfigDir(), LOCALE_RE, DEFAULTS, unchosenBranchPolicy, readSafe = (p) => {
9132
+ try {
9133
+ return readFileSync(p, "utf8");
9134
+ } catch {
9177
9135
  return null;
9178
- const { file, selected } = matched;
9179
- const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
9180
- if (!policy.success) {
9181
- const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
9182
- throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
9183
9136
  }
9184
- return policy.data;
9185
- }, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
9186
- const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
9187
- if (!candidate)
9188
- return { policy: undefined, source: "user-default" };
9189
- const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
9190
- const profileName = requestedProfile?.trim() || candidate.defaultProfile;
9191
- if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
9192
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
9193
- const profile = profileName ? candidate.profiles?.[profileName] : undefined;
9194
- if (profileName && !profile)
9195
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
9196
- const profilePolicy = profile?.[key];
9197
- const workspacePolicy = candidate[key];
9137
+ }, isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value), parseConfigResult = (raw, file) => {
9138
+ if (raw === null)
9139
+ return { status: "missing", path: file };
9140
+ let parsed;
9141
+ try {
9142
+ parsed = JSON.parse(raw);
9143
+ } catch {
9144
+ return { status: "malformed", path: file, error: `${file} is not valid JSON` };
9145
+ }
9146
+ if (!isConfigObject(parsed)) {
9147
+ return { status: "malformed", path: file, error: `${file} is not a JSON object` };
9148
+ }
9149
+ const input = parsed;
9150
+ const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
9151
+ const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
9152
+ const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
9153
+ const config = {
9154
+ locale,
9155
+ localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
9156
+ branchPolicy: mergePreset(preset, {
9157
+ allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
9158
+ protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
9159
+ }, DEFAULTS),
9160
+ commitPolicy: {
9161
+ preset: commitPreset,
9162
+ ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
9163
+ }
9164
+ };
9165
+ if (!Object.hasOwn(PRESETS, input.branchPolicy?.preset))
9166
+ unchosenBranchPolicy.add(config);
9167
+ return { status: "valid", path: file, config };
9168
+ }, readConfigTyped = (dir) => {
9169
+ const file = path3.join(dir ?? configDir(), "config.json");
9170
+ return parseConfigResult(readSafe(file), file);
9171
+ }, readConfig = () => {
9172
+ const result = readConfigTyped();
9173
+ if (result.status === "malformed")
9174
+ throw new Error(result.error);
9175
+ return result.config ?? DEFAULTS;
9176
+ }, vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path3.join(configDir(), "vcs.json"), branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"), resolveBranchPolicy = (config, workspace, repoPreset) => {
9177
+ const wp = workspace?.branchPolicy ?? {};
9178
+ const workspacePreset = Object.hasOwn(PRESETS, wp.preset);
9179
+ const detected = !workspacePreset && unchosenBranchPolicy.has(config) ? repoPreset?.() ?? null : null;
9180
+ const preset = workspacePreset ? wp.preset : detected ?? config.branchPolicy?.preset ?? "gitflow";
9181
+ const merged = mergePreset(preset, {
9182
+ allowed: wp.allowed,
9183
+ protectedNames: wp.protected
9184
+ }, config);
9185
+ const allowed = merged.allowed.map(branchGlobPattern);
9198
9186
  return {
9199
- policy: profilePolicy ?? workspacePolicy,
9200
- source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
9187
+ preset,
9188
+ allowed,
9189
+ protected: new Set([...merged.protected, ...detected ? PRESETS.gitflow.protected : []].map((p) => p.toLowerCase())),
9190
+ integration: wp.integration === "merge" ? "merge" : "pr",
9191
+ defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop",
9192
+ detected: detected !== null
9201
9193
  };
9194
+ }, COMMIT_PRESETS, resolveCommitPolicy = (config, workspace) => {
9195
+ const wp = workspace?.commitPolicy ?? {};
9196
+ if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
9197
+ return {
9198
+ preset: wp.preset,
9199
+ ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
9200
+ };
9201
+ }
9202
+ return config.commitPolicy ?? { preset: "conventional" };
9202
9203
  };
9203
- var init_workspaces = __esm(() => {
9204
- init_zod();
9205
- init_config();
9206
- init_branch_policy();
9207
- nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
9208
- branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
9209
- commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
9210
- workspaceBranchPolicySchema = object({
9211
- preset: branchPreset,
9212
- developBranch: nonBlank.optional(),
9213
- prefixes: object({
9214
- feature: nonBlank,
9215
- bugfix: nonBlank,
9216
- release: nonBlank,
9217
- hotfix: nonBlank
9218
- }).passthrough().optional(),
9219
- allowed: array(nonBlank).optional(),
9220
- protected: array(nonBlank).optional(),
9221
- integration: _enum(["pr", "merge"]).optional()
9222
- }).strict();
9223
- workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
9224
- workspaceProfileSchema = object({
9225
- branchPolicy: workspaceBranchPolicySchema.optional(),
9226
- commitPolicy: workspaceCommitPolicySchema.optional()
9227
- }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
9228
- message: "a profile must define branchPolicy or commitPolicy"
9229
- });
9230
- versionSourceSchema = discriminatedUnion("kind", [
9231
- object({
9232
- kind: literal("package-json"),
9233
- path: nonBlank,
9234
- field: nonBlank
9235
- }).strict(),
9236
- object({ kind: literal("git-tag") }).strict(),
9237
- object({ kind: literal("manual") }).strict()
9238
- ]);
9239
- releaseTrackSchema = object({
9240
- strategy: branchPreset.optional(),
9241
- productionBranch: nonBlank,
9242
- integrationBranch: nonBlank,
9243
- naming: object({
9244
- feature: nonBlank.optional(),
9245
- release: nonBlank.optional(),
9246
- hotfix: nonBlank.optional()
9247
- }).passthrough().optional(),
9248
- baseBranch: nonBlank.optional(),
9249
- mergeBackBranches: array(nonBlank).optional(),
9250
- pullRequestTarget: nonBlank.optional(),
9251
- tagNamespace: string2().optional(),
9252
- versionSource: versionSourceSchema.optional(),
9253
- requiredChecks: array(nonBlank).optional(),
9254
- critical: array(nonBlank).optional()
9255
- }).passthrough().superRefine((track, context) => {
9256
- for (const [field, values] of [
9257
- ["mergeBackBranches", track.mergeBackBranches ?? []],
9258
- ["requiredChecks", track.requiredChecks ?? []]
9259
- ]) {
9260
- if (new Set(values).size !== values.length) {
9261
- context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
9262
- }
9263
- }
9264
- if (track.versionSource?.kind === "package-json" && (path3.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
9265
- context.addIssue({
9266
- code: "custom",
9267
- path: ["versionSource", "path"],
9268
- message: "must stay inside the repository"
9269
- });
9270
- }
9271
- });
9272
- workspaceConfigSchema = object({
9273
- name: nonBlank,
9274
- glob: nonBlank,
9275
- vcs: object({
9276
- provider: _enum(["gitlab", "github"]),
9277
- defaultTargetBranch: nonBlank.optional(),
9278
- tokenFile: nonBlank.optional(),
9279
- account: nonBlank.optional()
9280
- }).passthrough().optional(),
9281
- autoApprove: union([boolean2(), array(nonBlank)]).optional(),
9282
- defaultEndpoint: unknown().optional(),
9283
- verification: _enum(["self", "independent"]).optional(),
9284
- trailers: boolean2().optional(),
9285
- autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9286
- kind,
9287
- union([boolean2(), literal("verified")]).optional()
9288
- ]))).passthrough().optional(),
9289
- youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
9290
- issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
9291
- branchPolicy: workspaceBranchPolicySchema.optional(),
9292
- commitPolicy: workspaceCommitPolicySchema.optional(),
9293
- defaultProfile: nonBlank.optional(),
9294
- profiles: record(nonBlank, workspaceProfileSchema).optional(),
9295
- releaseTracks: record(nonBlank, releaseTrackSchema).optional()
9296
- }).passthrough().superRefine((workspace, context) => {
9297
- if (workspace.issues && workspace.vcs?.provider !== "github") {
9298
- context.addIssue({
9299
- code: "custom",
9300
- path: ["issues"],
9301
- message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
9302
- });
9303
- }
9304
- if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
9305
- context.addIssue({
9306
- code: "custom",
9307
- path: ["defaultProfile"],
9308
- message: `does not name a configured profile`
9309
- });
9310
- }
9311
- });
9312
- workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
9313
- const names = new Set;
9314
- for (const [index, workspace] of (file.workspaces ?? []).entries()) {
9315
- if (names.has(workspace.name)) {
9316
- context.addIssue({
9317
- code: "custom",
9318
- path: ["workspaces", index, "name"],
9319
- message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
9320
- });
9321
- }
9322
- names.add(workspace.name);
9323
- }
9324
- });
9325
- UNSUPPORTED_GLOB = /[?[\]{}]/;
9326
- UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
9327
- WorkspaceConfigError = class WorkspaceConfigError extends Error {
9328
- name = "WorkspaceConfigError";
9204
+ var init_config = __esm(() => {
9205
+ init_boundary();
9206
+ PRESETS = {
9207
+ gitflow: {
9208
+ allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
9209
+ protected: ["main", "develop", "master", "prod", "production"]
9210
+ },
9211
+ "github-flow": { allowed: ["*"], protected: ["main"] },
9212
+ "trunk-based": { allowed: ["*"], protected: ["main"] },
9213
+ custom: { allowed: [], protected: [] }
9329
9214
  };
9330
- mainCheckouts = new Map;
9331
- runtimeWorkspaceIndexSchema = object({
9332
- workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
9333
- }).passthrough();
9215
+ LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
9216
+ DEFAULTS = {
9217
+ locale: "en",
9218
+ localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
9219
+ branchPolicy: {
9220
+ preset: "gitflow",
9221
+ allowed: [...PRESETS.gitflow.allowed],
9222
+ protected: [...PRESETS.gitflow.protected]
9223
+ },
9224
+ commitPolicy: { preset: "conventional" }
9225
+ };
9226
+ unchosenBranchPolicy = new WeakSet([DEFAULTS]);
9227
+ COMMIT_PRESETS = [
9228
+ "conventional",
9229
+ "gitmoji",
9230
+ "ticket-prefix",
9231
+ "freeform",
9232
+ "custom",
9233
+ "auto"
9234
+ ];
9334
9235
  });
9335
9236
 
9336
- // packages/workit-core/src/core/release-tracks.ts
9337
- import { createHash as createHash4 } from "node:crypto";
9237
+ // packages/workit-core/src/core/branch-policy.ts
9338
9238
  import { spawnSync as spawnSync4 } from "node:child_process";
9339
- function readReleaseTracks(raw) {
9340
- const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
9341
- if (raw === undefined || raw === null)
9342
- return out;
9343
- if (!isRecord(raw)) {
9344
- out.issues.push("releaseTracks is not an object; ignored");
9345
- return out;
9346
- }
9347
- const fatal = [];
9348
- const protectedNames = new Set;
9349
- for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
9350
- const label = `release track ${JSON.stringify(name)}`;
9351
- if (!isRecord(entry)) {
9352
- out.issues.push(`${label} is not an object; ignored`);
9353
- continue;
9354
- }
9355
- for (const branch of namedBranches(entry))
9356
- protectedNames.add(branch);
9357
- if (!name.trim()) {
9358
- out.issues.push("a release track with a blank name was ignored");
9359
- continue;
9360
- }
9361
- let critical = [];
9362
- if (Array.isArray(entry.critical))
9363
- critical = entry.critical.filter((item) => typeof item === "string");
9364
- else if (entry.critical !== undefined)
9365
- out.issues.push(`${label} critical: must be a list of field names; ignored`);
9366
- const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
9367
- const problem = (field, message) => {
9368
- if (isCritical(field))
9369
- fatal.push(`${label} ${field}: ${message}`);
9370
- else
9371
- out.issues.push(`${label} ${field}: ${message}`);
9239
+ var PREFIXES, branchNames = (workspaceRoot) => {
9240
+ const r = spawnSync4("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"], { cwd: workspaceRoot, encoding: "utf8" });
9241
+ if (r.status !== 0)
9242
+ return new Set;
9243
+ return new Set((r.stdout ?? "").split(`
9244
+ `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
9245
+ }, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
9246
+ const names = branchNames(workspaceRoot);
9247
+ const develop = names.has("develop");
9248
+ const main = names.has("main");
9249
+ const master = names.has("master");
9250
+ const root = main ? "main" : master ? "master" : null;
9251
+ if (develop) {
9252
+ return {
9253
+ preset: "gitflow",
9254
+ developBranch: "develop",
9255
+ integration: "merge",
9256
+ protected: root ? [root, "develop"] : ["develop"],
9257
+ allowed: [...PRESETS.gitflow.allowed],
9258
+ prefixes: PREFIXES
9372
9259
  };
9373
- for (const key of Object.keys(entry))
9374
- if (!KNOWN_FIELDS.has(key))
9375
- problem(key, "unknown field, ignored");
9376
- const productionBranch = branchName(entry.productionBranch);
9377
- const integrationBranch = branchName(entry.integrationBranch);
9378
- if (!productionBranch)
9379
- problem("productionBranch", "missing or not a branch name");
9380
- if (!integrationBranch)
9381
- problem("integrationBranch", "missing or not a branch name");
9382
- const optionalBranch = (field) => {
9383
- if (entry[field] === undefined)
9384
- return null;
9385
- const value = branchName(entry[field]);
9386
- if (!value)
9387
- problem(field, "not a branch name; using the integration branch");
9388
- return value;
9260
+ }
9261
+ if (root === "main") {
9262
+ return {
9263
+ preset: "github-flow",
9264
+ developBranch: null,
9265
+ integration: "pr",
9266
+ protected: [root],
9267
+ allowed: ["*"],
9268
+ prefixes: PREFIXES
9389
9269
  };
9390
- const baseBranch = optionalBranch("baseBranch");
9391
- const pullRequestTarget = optionalBranch("pullRequestTarget");
9392
- let mergeBackBranches = [];
9393
- if (entry.mergeBackBranches !== undefined) {
9394
- const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
9395
- if (list.some((item) => item === null))
9396
- problem("mergeBackBranches", "must be a list of branch names; ignored");
9397
- else
9398
- mergeBackBranches = [...new Set(list)];
9399
- }
9400
- const naming = {};
9401
- if (entry.naming !== undefined) {
9402
- if (!isRecord(entry.naming))
9403
- problem("naming", "not an object; ignored");
9404
- else
9405
- for (const kind of ["feature", "release", "hotfix"]) {
9406
- const template = entry.naming[kind];
9407
- if (template === undefined)
9408
- continue;
9409
- if (typeof template !== "string" || !template.trim())
9410
- problem(`naming.${kind}`, "not a template string; ignored");
9411
- else if (!literalOf(template))
9412
- problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
9413
- else
9414
- naming[kind] = template.trim();
9415
- }
9416
- }
9417
- if (!productionBranch || !integrationBranch) {
9418
- out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
9419
- continue;
9420
- }
9421
- out.tracks.push({
9422
- name,
9423
- productionBranch,
9424
- integrationBranch,
9425
- baseBranch: baseBranch ?? integrationBranch,
9426
- pullRequestTarget: pullRequestTarget ?? integrationBranch,
9427
- mergeBackBranches,
9428
- naming
9429
- });
9430
9270
  }
9431
- out.protectedBranches = [...protectedNames];
9432
- if (fatal.length)
9433
- out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
9434
- return out;
9435
- }
9436
- function resolveReleaseTrack(input) {
9437
- const tracks = [...input.tracks];
9438
- const issues = [...input.issues ?? []];
9439
- const requested = input.requested?.trim();
9440
- if (tracks.length === 0) {
9441
- if (requested)
9442
- return {
9443
- status: "invalid",
9444
- error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
9445
- issues
9446
- };
9447
- return { status: "none", issues };
9271
+ if (root === "master") {
9272
+ return {
9273
+ preset: "trunk-based",
9274
+ developBranch: null,
9275
+ integration: "pr",
9276
+ protected: [root],
9277
+ allowed: ["*"],
9278
+ prefixes: PREFIXES
9279
+ };
9448
9280
  }
9449
- const names = tracks.map((track) => track.name).join(", ");
9450
- let envProblem = null;
9451
- const done = (track, source, detail, warnings = [], ambiguous = null) => ({
9452
- status: "resolved",
9453
- track: track && track.name !== IMPLICIT ? track : null,
9454
- source,
9455
- detail,
9456
- warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
9457
- tracks,
9458
- blocking: envProblem ?? ambiguous
9459
- });
9460
- if (requested) {
9461
- const named = tracks.find((track) => track.name === requested);
9462
- if (!named)
9281
+ return {
9282
+ preset: "gitflow",
9283
+ developBranch: null,
9284
+ integration: "merge",
9285
+ protected: [],
9286
+ allowed: [],
9287
+ prefixes: PREFIXES
9288
+ };
9289
+ }, repoBranchPreset = (workspaceRoot) => {
9290
+ const detected = detectBranchPolicy(workspaceRoot);
9291
+ return detected.developBranch || detected.allowed.length ? detected.preset : null;
9292
+ }, releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
9293
+ if (typeof policyPrefix === "string")
9294
+ return policyPrefix.trim();
9295
+ const r = spawnSync4("git", ["config", "--get", "gitflow.prefix.versiontag"], {
9296
+ cwd: workspaceRoot,
9297
+ encoding: "utf8"
9298
+ });
9299
+ return r.status === 0 ? (r.stdout ?? "").trim() : "v";
9300
+ };
9301
+ var init_branch_policy = __esm(() => {
9302
+ init_config();
9303
+ PREFIXES = {
9304
+ feature: "feature/*",
9305
+ bugfix: "bugfix/*",
9306
+ release: "release/*",
9307
+ hotfix: "hotfix/*"
9308
+ };
9309
+ });
9310
+
9311
+ // packages/workit-core/src/core/workspaces.ts
9312
+ import { spawnSync as spawnSync5 } from "node:child_process";
9313
+ import { createHash as createHash4 } from "node:crypto";
9314
+ import { readFileSync as readFileSync2, realpathSync } from "node:fs";
9315
+ import path4 from "node:path";
9316
+ function workspaceDefaultTarget(cwd, globalDefault) {
9317
+ const ws = resolveRuntimeWorkspaceVcs(cwd);
9318
+ const wp = ws?.branchPolicy ?? {};
9319
+ const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
9320
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
9321
+ const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
9322
+ return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
9323
+ }
9324
+ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash4("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
9325
+ if (!isConfigObject(parsed)) {
9326
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
9327
+ }
9328
+ const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
9329
+ if (!validated.success) {
9330
+ const detail = validated.error.issues.map((issue) => {
9331
+ const field = String(issue.path[issue.path.length - 1] ?? "");
9332
+ const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
9333
+ return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
9334
+ }).join("; ");
9335
+ return {
9336
+ status: "invalid",
9337
+ path: file,
9338
+ entries: [],
9339
+ error: `${file} has invalid workspace configuration: ${detail}`
9340
+ };
9341
+ }
9342
+ for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
9343
+ const glob = validateWorkspaceGlob(workspace.glob);
9344
+ if (!glob.ok) {
9463
9345
  return {
9464
9346
  status: "invalid",
9465
- error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
9466
- issues
9347
+ path: file,
9348
+ entries: [],
9349
+ error: `${file} workspaces.${index}.glob: ${glob.error}`
9467
9350
  };
9468
- return done(named, "flag", `track ${named.name} was requested explicitly`);
9351
+ }
9469
9352
  }
9470
- const owned = (branch) => branch !== null && tracks.some((track) => [
9471
- track.integrationBranch,
9472
- track.productionBranch,
9473
- track.baseBranch,
9474
- track.pullRequestTarget
9475
- ].includes(branch));
9476
- const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
9477
- name: IMPLICIT,
9478
- productionBranch: input.defaultBranch,
9479
- integrationBranch: input.defaultBranch,
9480
- baseBranch: input.defaultBranch,
9481
- pullRequestTarget: input.defaultBranch,
9482
- mergeBackBranches: [],
9483
- naming: {}
9484
- } : null;
9485
- const candidates = implicit ? [...tracks, implicit] : tracks;
9486
- const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
9487
- const fallback = (why) => {
9488
- const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
9489
- const track = byDefault.length === 1 ? byDefault[0] : null;
9490
- const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
9491
- const ambiguous = candidates.length >= 2 ? `can't tell which release line ${input.branch ?? "HEAD"} is on (${why}); pass --track <name> (${names}) or --base <branch>` : null;
9492
- return done(track, "default", `${why}; using the workspace ${target}`, [
9493
- `release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
9494
- ], ambiguous);
9353
+ return {
9354
+ status: "valid",
9355
+ path: file,
9356
+ entries: validated.data.workspaces ?? [],
9357
+ document: parsed
9495
9358
  };
9496
- const branch = input.branch;
9497
- if (branch) {
9498
- const owners = ownedBy(candidates, branch);
9499
- if (owners.length === 1)
9500
- return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
9501
- if (owners.length > 1)
9502
- return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
9359
+ }, readWorkspacesResult = (dir = configDir()) => {
9360
+ const file = path4.join(dir, "workspaces.json");
9361
+ let raw;
9362
+ try {
9363
+ raw = readFileSync2(file, "utf8");
9364
+ } catch (error) {
9365
+ if (error.code === "ENOENT")
9366
+ return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
9367
+ return {
9368
+ status: "malformed",
9369
+ path: file,
9370
+ entries: [],
9371
+ error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
9372
+ };
9503
9373
  }
9504
- const env = input.envRequested?.trim();
9505
- if (env) {
9506
- const named = tracks.find((track) => track.name === env);
9507
- if (named)
9508
- return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
9509
- envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
9374
+ let parsed;
9375
+ try {
9376
+ parsed = JSON.parse(raw);
9377
+ } catch {
9378
+ return {
9379
+ status: "malformed",
9380
+ path: file,
9381
+ entries: [],
9382
+ revision: workspacesRevision(raw),
9383
+ error: `${file} is not valid JSON`
9384
+ };
9510
9385
  }
9511
- if (!branch)
9512
- return fallback("HEAD is detached");
9513
- if (tracks.length === 1 && !implicit)
9514
- return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
9515
- let remotes = null;
9516
- const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
9517
- const ownerOf = (ref) => {
9518
- const owner = ownedBy(candidates, short(ref));
9519
- return owner.length === 1 ? owner[0] : null;
9520
- };
9521
- const followRecorded = (start) => {
9522
- let cursor = start;
9523
- const seen = new Set([start]);
9524
- for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
9525
- const recorded = input.probe.recordedBase(cursor);
9526
- if (!recorded)
9527
- return null;
9528
- const base = short(recorded);
9529
- const owner = ownerOf(base);
9530
- if (owner)
9531
- return owner;
9532
- if (ownedBy(candidates, base).length > 1 || seen.has(base))
9533
- return null;
9534
- seen.add(base);
9535
- cursor = base;
9386
+ return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
9387
+ }, globToRegExp = (glob) => {
9388
+ let out = "";
9389
+ for (let i = 0;i < glob.length; i++) {
9390
+ const c = glob[i];
9391
+ if (c === "*") {
9392
+ if (glob[i + 1] === "*") {
9393
+ if (glob[i + 2] === "/") {
9394
+ if (out === "")
9395
+ out += "/?";
9396
+ out += "(?:[^/]+/)*";
9397
+ i += 2;
9398
+ } else {
9399
+ if (i + 2 >= glob.length) {
9400
+ if (out.endsWith("/")) {
9401
+ out = out.slice(0, -1);
9402
+ out += "(?:/.*)?";
9403
+ } else {
9404
+ out += ".*";
9405
+ }
9406
+ } else {
9407
+ out += ".*";
9408
+ }
9409
+ i++;
9410
+ }
9411
+ } else {
9412
+ out += "[^/]*";
9413
+ }
9414
+ } else {
9415
+ out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
9536
9416
  }
9537
- return null;
9538
- };
9539
- const recordedOwner = followRecorded(branch);
9540
- if (recordedOwner)
9541
- return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
9542
- for (const source of ["reflog", "checkout"]) {
9543
- const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
9544
- if (!ref)
9545
- continue;
9546
- const owner = ownerOf(ref) ?? followRecorded(short(ref));
9547
- if (owner)
9548
- return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
9549
9417
  }
9550
- const forks = [];
9551
- for (const track of candidates)
9552
- for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
9553
- const sha = input.probe.mergeBase(branch, ref);
9554
- if (sha)
9555
- forks.push({ track, ref, sha });
9556
- }
9557
- if (forks.length === 0)
9558
- return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
9559
- const ancestry = new Map;
9560
- const descends = (older, newer) => {
9561
- if (older === newer)
9562
- return false;
9563
- const key = `${older}>${newer}`;
9564
- if (!ancestry.has(key))
9565
- ancestry.set(key, input.probe.isAncestor(older, newer));
9566
- return ancestry.get(key);
9567
- };
9568
- const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
9569
- const winningTracks = [...new Set(winners.map((fork) => fork.track))];
9570
- if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
9571
- const win = winners[0];
9572
- return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
9418
+ return new RegExp(`^${out}$`);
9419
+ }, matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/")), UNSUPPORTED_GLOB, UNSUPPORTED_EXTGLOB, validateWorkspaceGlob = (glob) => {
9420
+ const trimmed = glob.trim();
9421
+ if (!trimmed)
9422
+ return { ok: false, error: "workspace pattern is required" };
9423
+ const m = UNSUPPORTED_GLOB.exec(trimmed);
9424
+ if (m) {
9425
+ return {
9426
+ ok: false,
9427
+ error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
9428
+ };
9573
9429
  }
9574
- if (winningTracks.length === 1)
9575
- return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
9576
- const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
9577
- return fallback(`${branch} has the same or unrelated fork points with ${[...new Set(newest.map((fork) => `${fork.ref} (${fork.track.name === IMPLICIT ? "workspace default" : fork.track.name})`))].join(" and ")}`);
9578
- }
9579
- function gitTrackProbe(cwd) {
9580
- let remote;
9581
- let remotes;
9582
- const tip = (branch) => {
9583
- remote ??= pushRemoteName(cwd) ?? "origin";
9584
- return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
9585
- };
9586
- return {
9587
- remotes: () => remotes ??= remoteNames(cwd),
9588
- recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
9589
- createdFrom: (branch) => {
9590
- const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
9591
- const oldest = log?.split(`
9592
- `).findLast(Boolean) ?? "";
9593
- const match = /^branch: Created from (\S+)$/u.exec(oldest);
9594
- return match && match[1] !== "HEAD" ? match[1] : null;
9595
- },
9596
- checkedOutFrom: (branch) => {
9597
- const own = reflog(cwd, `refs/heads/${branch}`);
9598
- const created = own.at(-1);
9599
- if (!created || created.subject !== "branch: Created from HEAD")
9600
- return null;
9601
- const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
9602
- const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
9603
- return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
9604
- },
9605
- mergeBase: (branch, trackBranch) => {
9606
- const base = tip(trackBranch);
9607
- return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
9608
- },
9609
- isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
9610
- };
9611
- }
9612
- function workspaceReleaseTracks(cwd) {
9613
- const raw = resolveRuntimeReleaseTracks(cwd);
9614
- return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
9615
- }
9616
- function resolveTrackFor(cwd, options) {
9617
- const read = workspaceReleaseTracks(cwd);
9618
- if (read.error)
9619
- return { status: "invalid", error: read.error, issues: read.issues };
9620
- if (read.tracks.length === 0 && !options.track?.trim())
9621
- return { status: "none", issues: read.issues };
9622
- const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
9623
- const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
9624
- const input = {
9625
- tracks: read.tracks,
9626
- requested: options.track ?? null,
9627
- envRequested,
9628
- branch,
9629
- defaultBranch: options.defaultBranch,
9630
- issues: read.issues
9631
- };
9632
- let key = null;
9633
- if (!options.probe) {
9634
- const refs = git2(cwd, [
9635
- "for-each-ref",
9636
- "--format=%(refname) %(objectname)",
9637
- "refs/heads",
9638
- "refs/remotes"
9639
- ]) ?? "";
9640
- const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
9641
- const own = branch ? [
9642
- git2(cwd, [
9643
- "log",
9644
- "-g",
9645
- "-n",
9646
- "1",
9647
- "--date=unix",
9648
- "--format=%H %gd %gs",
9649
- `refs/heads/${branch}`,
9650
- "--"
9651
- ]) ?? "",
9652
- git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
9653
- ] : [];
9654
- key = createHash4("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
9655
- const hit = memo2.get(key);
9656
- if (hit)
9657
- return hit;
9658
- }
9659
- const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
9660
- if (key) {
9661
- if (memo2.size >= MEMO_LIMIT)
9662
- memo2.delete(memo2.keys().next().value);
9663
- memo2.set(key, resolution);
9430
+ const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
9431
+ if (ext) {
9432
+ const token = ext[0];
9433
+ const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
9434
+ return {
9435
+ ok: false,
9436
+ error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
9437
+ };
9664
9438
  }
9665
- return resolution;
9666
- }
9667
- var ReleaseTrackError, KNOWN_FIELDS, BRANCH_FIELDS, PLACEHOLDER, HAS_PLACEHOLDER, isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), branchName = (value) => typeof value === "string" && value.trim() && !value.trim().startsWith("-") ? value.trim() : null, namedBranches = (entry) => [
9668
- ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
9669
- ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
9670
- ].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
9671
- ...new Set(tracks.flatMap((track) => [
9672
- track.productionBranch,
9673
- track.integrationBranch,
9674
- track.baseBranch,
9675
- track.pullRequestTarget,
9676
- ...track.mergeBackBranches
9677
- ]))
9678
- ], namingGlob = (template) => HAS_PLACEHOLDER.test(template) ? template.replace(PLACEHOLDER, "*") : `${template.replace(/\/?\*?$/u, "")}/*`, IMPLICIT = "", ownedBy = (tracks, branch) => tracks.filter((track) => [track.integrationBranch, track.productionBranch, track.baseBranch].includes(branch)), shortRef = (ref, remotes) => {
9679
- const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
9680
- const remote = remotes.find((name) => bare.startsWith(`${name}/`));
9681
- return remote ? bare.slice(remote.length + 1) : bare;
9682
- }, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
9683
- const run = spawnSync4("git", args, {
9684
- cwd,
9685
- encoding: "utf8",
9686
- timeout: 20000,
9687
- windowsHide: true,
9688
- stdio: ["ignore", "pipe", "ignore"]
9689
- });
9690
- return run.status === 0 ? (run.stdout ?? "").trim() : null;
9691
- }, recordedBaseKey = (branch) => `branch.${branch}.workitBase`, escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), reflog = (cwd, ref) => (git2(cwd, ["log", "-g", "--date=unix", "--format=%H%x09%gd%x09%gs", ref, "--"]) ?? "").split(`
9692
- `).map((line) => {
9693
- const [sha = "", selector = "", ...subject] = line.split("\t");
9694
- return {
9695
- sha,
9696
- time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
9697
- subject: subject.join("\t")
9698
- };
9699
- }).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
9700
- var init_release_tracks = __esm(() => {
9701
- init_rev();
9702
- init_config();
9703
- init_workspaces();
9704
- ReleaseTrackError = class ReleaseTrackError extends Error {
9439
+ return { ok: true };
9440
+ }, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
9441
+ const cached = mainCheckouts.get(cwd);
9442
+ if (cached !== undefined)
9443
+ return cached;
9444
+ const env = { ...process.env };
9445
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
9446
+ delete env[name];
9447
+ const git = (args) => {
9448
+ const run = spawnSync5("git", args, {
9449
+ cwd,
9450
+ env,
9451
+ encoding: "utf8",
9452
+ timeout: 5000,
9453
+ windowsHide: true
9454
+ });
9455
+ return run.status === 0 ? run.stdout : null;
9705
9456
  };
9706
- KNOWN_FIELDS = new Set([
9707
- "strategy",
9708
- "productionBranch",
9709
- "integrationBranch",
9710
- "naming",
9711
- "baseBranch",
9712
- "mergeBackBranches",
9713
- "pullRequestTarget",
9714
- "tagNamespace",
9715
- "versionSource",
9716
- "requiredChecks",
9717
- "critical"
9718
- ]);
9719
- BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
9720
- PLACEHOLDER = /\{[^}]*\}/gu;
9721
- HAS_PLACEHOLDER = /\{[^}]*\}/u;
9722
- memo2 = new Map;
9723
- });
9724
-
9725
- // packages/workit-core/src/core/vcs-config.ts
9726
- import fs2 from "node:fs";
9727
- import path4 from "node:path";
9728
- import { spawnSync as spawnSync5 } from "node:child_process";
9729
- function vcsConfig(mode, cwd, options = {}) {
9730
- const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
9731
- const wsVcs = ws?.vcs ?? {};
9732
- const wsYt = ws?.youtrack ?? {};
9733
- const wsIssues = ws?.issues ?? {};
9734
- const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
9735
- const root = vcsCwd(cwd);
9736
- const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
9737
- const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
9738
- const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
9739
- const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
9740
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
9741
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
9742
- let issuesProvider = null;
9743
- let linkOnPr = null;
9744
- if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
9745
- issuesProvider = "github";
9746
- linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
9457
+ let main = null;
9458
+ const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
9459
+ const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
9460
+ if (gitDir && common && path4.resolve(gitDir) !== path4.resolve(common)) {
9461
+ const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
9462
+ main = listed?.trim() || (path4.basename(common) === ".git" ? path4.dirname(common) : common);
9747
9463
  }
9748
- if (mode === "resolve") {
9749
- if (cfgStatus === "malformed")
9750
- return { ok: false, error: cfgError, configPath: cfgPath };
9751
- const trackResolution = resolveTrackFor(root, {
9752
- track: options.track,
9753
- ...options.branch !== undefined ? { branch: options.branch } : {},
9754
- defaultBranch: workspaceDefault
9464
+ mainCheckouts.set(cwd, main);
9465
+ return main;
9466
+ }, matchingEntries = (cwd, entries) => {
9467
+ const match = (where) => {
9468
+ const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
9469
+ return entries.filter((entry) => {
9470
+ const glob = entry.glob.replaceAll("\\", "/");
9471
+ const canonical = canonicalGlob(glob);
9472
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
9755
9473
  });
9756
- if (trackResolution.status === "invalid")
9757
- return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
9758
- const releaseTrack = trackSummary(trackResolution);
9759
- const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
9760
- const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
9761
- return {
9762
- ok: true,
9763
- workspace_name: ws?.name ?? null,
9764
- provider,
9765
- defaultTargetBranch: defaultTarget,
9766
- workspaceDefaultTargetBranch: workspaceDefault,
9767
- baseBranch,
9768
- releaseTrack,
9769
- link_issues: linkIssues,
9770
- youtrack_base_url: youtrackBaseUrl,
9771
- issues_provider: issuesProvider,
9772
- link_on_pr: linkOnPr
9773
- };
9774
- }
9775
- if (cfgStatus === "malformed") {
9776
- return { ok: false, error: cfgError, configPath: cfgPath };
9474
+ };
9475
+ const direct = match(cwd);
9476
+ if (direct.length)
9477
+ return direct;
9478
+ const main = mainCheckoutOf(cwd);
9479
+ return main ? match(main) : direct;
9480
+ }, realpathOf = (p) => {
9481
+ const real = realpathSync.native ?? realpathSync;
9482
+ try {
9483
+ return real(p);
9484
+ } catch {
9485
+ return p;
9777
9486
  }
9778
- if (cfgStatus === "missing" && provider === null)
9779
- return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
9780
- if (provider === null) {
9781
- return {
9782
- ok: false,
9783
- error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
9784
- configPath: cfgPath
9785
- };
9487
+ }, canonicalGlob = (glob) => {
9488
+ const m = /[*?[\]{]/.exec(glob);
9489
+ const prefix = m ? glob.slice(0, m.index) : glob;
9490
+ const rest = m ? glob.slice(m.index) : "";
9491
+ if (!prefix)
9492
+ return glob;
9493
+ const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
9494
+ return real + (prefix.endsWith("/") ? "/" : "") + rest;
9495
+ }, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
9496
+ if (workspaceName !== undefined) {
9497
+ const named = matches.filter((entry) => entry.name === workspaceName);
9498
+ if (named.length === 1)
9499
+ return named[0];
9500
+ if (named.length > 1)
9501
+ throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
9502
+ if (matches.length > 0) {
9503
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
9504
+ }
9505
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
9786
9506
  }
9787
- const prov = cfg[provider] ?? {};
9788
- const out = {
9789
- ok: true,
9790
- configPath: path4.resolve(cfgPath),
9791
- provider,
9792
- defaultTargetBranch: workspaceDefault,
9793
- pr: cfg.pr ?? {},
9794
- workspace_name: ws?.name ?? null,
9795
- link_issues: linkIssues,
9796
- youtrack_base_url: youtrackBaseUrl,
9797
- issues_provider: issuesProvider,
9798
- link_on_pr: linkOnPr
9799
- };
9800
- if (provider === "gitlab") {
9801
- out.gitlab = {
9802
- host: prov.host ?? "gitlab.com",
9803
- apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
9804
- };
9805
- } else if (provider === "github") {
9806
- out.github = { host: prov.host ?? "github.com" };
9507
+ if (matches.length < 2)
9508
+ return matches[0];
9509
+ const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
9510
+ const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
9511
+ if (mostSpecific.length > 1) {
9512
+ throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
9807
9513
  }
9808
- return out;
9809
- }
9810
- var workspacesPath = () => path4.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
9811
- const r = spawnSync5("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
9812
- if (r.status !== 0)
9813
- return null;
9814
- const url = (r.stdout ?? "").trim();
9815
- if (/github\.com[:/]/.test(url))
9816
- return "github";
9817
- if (/gitlab\.com[:/]/.test(url))
9818
- return "gitlab";
9819
- return null;
9820
- }, readVcsConfig = () => {
9821
- const file = vcsConfigPath();
9514
+ return mostSpecific[0];
9515
+ }, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
9516
+ const matches = matchingEntries(cwd, entries);
9517
+ return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
9518
+ }, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
9519
+ const result = readWorkspacesResult(dir);
9520
+ if (result.status === "malformed" || result.status === "invalid")
9521
+ throw new WorkspaceConfigError(result.error);
9522
+ return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
9523
+ }, runtimeWorkspacePolicySchema = (kind) => {
9524
+ const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
9525
+ const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
9526
+ provider: _enum(["gitlab", "github"]),
9527
+ defaultTargetBranch: nonBlank.optional(),
9528
+ account: nonBlank.optional()
9529
+ }).passthrough();
9530
+ const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
9531
+ const entrySchema = object({
9532
+ name: nonBlank,
9533
+ glob: nonBlank,
9534
+ [key]: policySchema.optional(),
9535
+ defaultProfile: nonBlank.optional(),
9536
+ profiles: record(nonBlank, profileSchema).optional()
9537
+ }).passthrough();
9538
+ return entrySchema;
9539
+ }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
9540
+ const file = path4.join(dir, "workspaces.json");
9822
9541
  let raw;
9823
9542
  try {
9824
- raw = fs2.readFileSync(file, "utf8");
9825
- } catch {
9826
- return { status: "missing", path: file, config: {} };
9543
+ raw = readFileSync2(file, "utf8");
9544
+ } catch (error) {
9545
+ if (error.code === "ENOENT")
9546
+ return null;
9547
+ throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
9827
9548
  }
9828
9549
  let parsed;
9829
9550
  try {
9830
9551
  parsed = JSON.parse(raw);
9831
9552
  } catch {
9832
- return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
9553
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
9833
9554
  }
9834
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9835
- return { status: "valid", path: file, config: parsed };
9555
+ const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
9556
+ if (!validated.success) {
9557
+ const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
9558
+ throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
9836
9559
  }
9837
- return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
9838
- }, trackSummary = (resolution) => resolution.status === "resolved" ? {
9839
- name: resolution.track?.name ?? null,
9840
- source: resolution.source,
9841
- detail: resolution.detail,
9842
- warnings: resolution.warnings,
9843
- track: resolution.track,
9844
- tracks: resolution.tracks,
9845
- blocking: resolution.blocking
9846
- } : null;
9847
- var init_vcs_config = __esm(() => {
9848
- init_config();
9849
- init_workspaces();
9850
- init_release_tracks();
9851
- });
9852
-
9853
- // packages/workit-core/src/core/logger.ts
9854
- import os3 from "node:os";
9855
- import path5 from "node:path";
9856
- var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
9857
- const override = process.env.WORKFLOW_TOOLKIT_STATE;
9858
- if (override)
9859
- return override;
9860
- const home = os3.homedir();
9861
- if (process.env.XDG_STATE_HOME)
9862
- return path5.join(process.env.XDG_STATE_HOME, "workit");
9863
- switch (os3.platform()) {
9864
- case "darwin":
9865
- return path5.join(home, "Library", "Application Support", "workit");
9866
- case "win32":
9867
- return path5.join(process.env.LOCALAPPDATA ?? path5.join(home, "AppData", "Local"), "workit");
9868
- default:
9869
- return path5.join(process.env.HOME ?? home, ".local", "state", "workit");
9560
+ const entries = validated.data.workspaces;
9561
+ for (const entry of entries ?? []) {
9562
+ const glob = validateWorkspaceGlob(entry.glob);
9563
+ if (!glob.ok)
9564
+ throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
9870
9565
  }
9871
- }, homePattern;
9872
- var init_logger = __esm(() => {
9873
- SENSITIVE_WORDS = new Set([
9874
- "token",
9875
- "secret",
9876
- "password",
9877
- "passwd",
9878
- "authorization",
9879
- "credential",
9880
- "credentials",
9881
- "cookie",
9882
- "api",
9883
- "apikey",
9884
- "apitoken",
9885
- "accesstoken",
9886
- "clientsecret",
9887
- "refreshtoken",
9888
- "key",
9889
- "bearer",
9890
- "prompt",
9891
- "message",
9892
- "messages",
9893
- "content",
9894
- "body",
9895
- "args",
9896
- "argument",
9897
- "result",
9898
- "results",
9899
- "output",
9900
- "issue",
9901
- "description",
9902
- "summary",
9903
- "payload",
9904
- "script",
9905
- "command",
9906
- "text",
9907
- "query"
9566
+ const matches = matchingEntries(cwd, entries ?? []);
9567
+ const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
9568
+ const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
9569
+ return selected ? { file, selected, matches } : null;
9570
+ }, resolveRuntimeReleaseTracks = (cwd) => {
9571
+ const matched = matchRuntimeWorkspace(cwd);
9572
+ return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
9573
+ }, workspaceTrailers = (cwd, dir) => {
9574
+ let matched;
9575
+ try {
9576
+ matched = matchRuntimeWorkspace(cwd, dir);
9577
+ } catch {
9578
+ return { enabled: true, workspace: null, from: null };
9579
+ }
9580
+ if (!matched)
9581
+ return { enabled: true, workspace: null, from: null };
9582
+ const { selected, matches } = matched;
9583
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9584
+ const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
9585
+ return {
9586
+ enabled: source?.trailers !== false,
9587
+ workspace: selected.name,
9588
+ from: source?.name ?? null
9589
+ };
9590
+ }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9591
+ const matched = matchRuntimeWorkspace(cwd);
9592
+ if (!matched)
9593
+ return null;
9594
+ const { file, selected } = matched;
9595
+ const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
9596
+ if (!policy.success) {
9597
+ const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
9598
+ throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
9599
+ }
9600
+ return policy.data;
9601
+ }, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
9602
+ const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
9603
+ if (!candidate)
9604
+ return { policy: undefined, source: "user-default" };
9605
+ const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
9606
+ const profileName = requestedProfile?.trim() || candidate.defaultProfile;
9607
+ if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
9608
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
9609
+ const profile = profileName ? candidate.profiles?.[profileName] : undefined;
9610
+ if (profileName && !profile)
9611
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
9612
+ const profilePolicy = profile?.[key];
9613
+ const workspacePolicy = candidate[key];
9614
+ return {
9615
+ policy: profilePolicy ?? workspacePolicy,
9616
+ source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
9617
+ };
9618
+ };
9619
+ var init_workspaces = __esm(() => {
9620
+ init_zod();
9621
+ init_config();
9622
+ init_branch_policy();
9623
+ nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
9624
+ branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
9625
+ commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
9626
+ workspaceBranchPolicySchema = object({
9627
+ preset: branchPreset,
9628
+ developBranch: nonBlank.optional(),
9629
+ prefixes: object({
9630
+ feature: nonBlank,
9631
+ bugfix: nonBlank,
9632
+ release: nonBlank,
9633
+ hotfix: nonBlank
9634
+ }).passthrough().optional(),
9635
+ allowed: array(nonBlank).optional(),
9636
+ protected: array(nonBlank).optional(),
9637
+ integration: _enum(["pr", "merge"]).optional()
9638
+ }).strict();
9639
+ workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
9640
+ workspaceProfileSchema = object({
9641
+ branchPolicy: workspaceBranchPolicySchema.optional(),
9642
+ commitPolicy: workspaceCommitPolicySchema.optional()
9643
+ }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
9644
+ message: "a profile must define branchPolicy or commitPolicy"
9645
+ });
9646
+ versionSourceSchema = discriminatedUnion("kind", [
9647
+ object({
9648
+ kind: literal("package-json"),
9649
+ path: nonBlank,
9650
+ field: nonBlank
9651
+ }).strict(),
9652
+ object({ kind: literal("git-tag") }).strict(),
9653
+ object({ kind: literal("manual") }).strict()
9908
9654
  ]);
9909
- STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
9910
- homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
9655
+ releaseTrackSchema = object({
9656
+ strategy: branchPreset.optional(),
9657
+ productionBranch: nonBlank,
9658
+ integrationBranch: nonBlank,
9659
+ naming: object({
9660
+ feature: nonBlank.optional(),
9661
+ release: nonBlank.optional(),
9662
+ hotfix: nonBlank.optional()
9663
+ }).passthrough().optional(),
9664
+ baseBranch: nonBlank.optional(),
9665
+ mergeBackBranches: array(nonBlank).optional(),
9666
+ pullRequestTarget: nonBlank.optional(),
9667
+ tagNamespace: string2().optional(),
9668
+ versionSource: versionSourceSchema.optional(),
9669
+ requiredChecks: array(nonBlank).optional(),
9670
+ critical: array(nonBlank).optional()
9671
+ }).passthrough().superRefine((track, context) => {
9672
+ for (const [field, values] of [
9673
+ ["mergeBackBranches", track.mergeBackBranches ?? []],
9674
+ ["requiredChecks", track.requiredChecks ?? []]
9675
+ ]) {
9676
+ if (new Set(values).size !== values.length) {
9677
+ context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
9678
+ }
9679
+ }
9680
+ if (track.versionSource?.kind === "package-json" && (path4.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
9681
+ context.addIssue({
9682
+ code: "custom",
9683
+ path: ["versionSource", "path"],
9684
+ message: "must stay inside the repository"
9685
+ });
9686
+ }
9687
+ });
9688
+ workspaceConfigSchema = object({
9689
+ name: nonBlank,
9690
+ glob: nonBlank,
9691
+ vcs: object({
9692
+ provider: _enum(["gitlab", "github"]),
9693
+ defaultTargetBranch: nonBlank.optional(),
9694
+ tokenFile: nonBlank.optional(),
9695
+ account: nonBlank.optional()
9696
+ }).passthrough().optional(),
9697
+ autoApprove: union([boolean2(), array(nonBlank)]).optional(),
9698
+ defaultEndpoint: unknown().optional(),
9699
+ verification: _enum(["self", "independent"]).optional(),
9700
+ trailers: boolean2().optional(),
9701
+ autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9702
+ kind,
9703
+ union([boolean2(), literal("verified")]).optional()
9704
+ ]))).passthrough().optional(),
9705
+ youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
9706
+ issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
9707
+ branchPolicy: workspaceBranchPolicySchema.optional(),
9708
+ commitPolicy: workspaceCommitPolicySchema.optional(),
9709
+ defaultProfile: nonBlank.optional(),
9710
+ profiles: record(nonBlank, workspaceProfileSchema).optional(),
9711
+ releaseTracks: record(nonBlank, releaseTrackSchema).optional()
9712
+ }).passthrough().superRefine((workspace, context) => {
9713
+ if (workspace.issues && workspace.vcs?.provider !== "github") {
9714
+ context.addIssue({
9715
+ code: "custom",
9716
+ path: ["issues"],
9717
+ message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
9718
+ });
9719
+ }
9720
+ if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
9721
+ context.addIssue({
9722
+ code: "custom",
9723
+ path: ["defaultProfile"],
9724
+ message: `does not name a configured profile`
9725
+ });
9726
+ }
9727
+ });
9728
+ workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
9729
+ const names = new Set;
9730
+ for (const [index, workspace] of (file.workspaces ?? []).entries()) {
9731
+ if (names.has(workspace.name)) {
9732
+ context.addIssue({
9733
+ code: "custom",
9734
+ path: ["workspaces", index, "name"],
9735
+ message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
9736
+ });
9737
+ }
9738
+ names.add(workspace.name);
9739
+ }
9740
+ });
9741
+ UNSUPPORTED_GLOB = /[?[\]{}]/;
9742
+ UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
9743
+ WorkspaceConfigError = class WorkspaceConfigError extends Error {
9744
+ name = "WorkspaceConfigError";
9745
+ };
9746
+ mainCheckouts = new Map;
9747
+ runtimeWorkspaceIndexSchema = object({
9748
+ workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
9749
+ }).passthrough();
9911
9750
  });
9912
9751
 
9913
- // packages/workit-core/src/store/paths.ts
9914
- import { spawnSync as spawnSync6 } from "node:child_process";
9752
+ // packages/workit-core/src/core/release-tracks.ts
9915
9753
  import { createHash as createHash5 } from "node:crypto";
9916
- import fs3 from "node:fs";
9917
- import path6 from "node:path";
9918
- function fastGit(root) {
9919
- if (DISCOVERY_ENV.some((name) => process.env[name]))
9920
- return null;
9921
- let dir = real(root);
9922
- let device = null;
9923
- for (;; ) {
9924
- let stat;
9925
- try {
9926
- stat = fs3.statSync(dir);
9927
- } catch {
9928
- return null;
9754
+ import { spawnSync as spawnSync6 } from "node:child_process";
9755
+ function readReleaseTracks(raw) {
9756
+ const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
9757
+ if (raw === undefined || raw === null)
9758
+ return out;
9759
+ if (!isRecord(raw)) {
9760
+ out.issues.push("releaseTracks is not an object; ignored");
9761
+ return out;
9762
+ }
9763
+ const fatal = [];
9764
+ const protectedNames = new Set;
9765
+ for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
9766
+ const label = `release track ${JSON.stringify(name)}`;
9767
+ if (!isRecord(entry)) {
9768
+ out.issues.push(`${label} is not an object; ignored`);
9769
+ continue;
9929
9770
  }
9930
- if (device !== null && stat.dev !== device)
9931
- return { kind: "none" };
9932
- device = stat.dev;
9933
- const dotGit = path6.join(dir, ".git");
9934
- let entry = null;
9935
- try {
9936
- entry = fs3.statSync(dotGit);
9937
- } catch {}
9938
- let gitDir = null;
9939
- let top = null;
9940
- if (entry) {
9941
- top = dir;
9942
- if (entry.isDirectory())
9943
- gitDir = dotGit;
9944
- else if (entry.isFile()) {
9945
- let text = "";
9946
- try {
9947
- text = fs3.readFileSync(dotGit, "utf8");
9948
- } catch {
9949
- return null;
9950
- }
9951
- const match = /^gitdir: (.+)$/mu.exec(text);
9952
- if (!match)
9953
- return null;
9954
- gitDir = path6.resolve(dir, match[1].trim());
9955
- } else
9956
- return null;
9957
- if (!fs3.existsSync(path6.join(gitDir, "HEAD")))
9958
- return null;
9959
- } else if (isGitDir(dir))
9960
- gitDir = dir;
9961
- if (gitDir) {
9962
- let common = gitDir;
9963
- try {
9964
- common = path6.resolve(gitDir, fs3.readFileSync(path6.join(gitDir, "commondir"), "utf8").trim());
9965
- } catch {}
9966
- if (!fs3.existsSync(path6.join(common, "objects")))
9967
- return null;
9968
- return {
9969
- kind: "repo",
9970
- common: real(common),
9971
- gitDir: real(gitDir),
9972
- top: top === null ? null : real(top),
9973
- reftable: reftableRefs(common)
9974
- };
9771
+ for (const branch of namedBranches(entry))
9772
+ protectedNames.add(branch);
9773
+ if (!name.trim()) {
9774
+ out.issues.push("a release track with a blank name was ignored");
9775
+ continue;
9975
9776
  }
9976
- const parent = path6.dirname(dir);
9977
- if (parent === dir)
9978
- return { kind: "none" };
9979
- dir = parent;
9980
- }
9981
- }
9982
- function resolveStore(root) {
9983
- root = path6.resolve(root);
9984
- let directory = false;
9985
- try {
9986
- directory = fs3.statSync(root).isDirectory();
9987
- } catch {}
9988
- if (!directory)
9989
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
9990
- const fast = fastGit(root);
9991
- if (fast?.kind === "none")
9992
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
9993
- if (fast?.kind === "repo")
9994
- return {
9995
- dir: path6.join(fast.common, "workit"),
9996
- shared: true,
9997
- top: fast.top,
9998
- git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
9777
+ let critical = [];
9778
+ if (Array.isArray(entry.critical))
9779
+ critical = entry.critical.filter((item) => typeof item === "string");
9780
+ else if (entry.critical !== undefined)
9781
+ out.issues.push(`${label} critical: must be a list of field names; ignored`);
9782
+ const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
9783
+ const problem = (field, message) => {
9784
+ if (isCritical(field))
9785
+ fatal.push(`${label} ${field}: ${message}`);
9786
+ else
9787
+ out.issues.push(`${label} ${field}: ${message}`);
9999
9788
  };
10000
- const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
10001
- const common = run.stdout.trim();
10002
- if (run.ok && common) {
10003
- const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10004
- return {
10005
- dir: path6.join(real(path6.resolve(root, common)), "workit"),
10006
- shared: true,
10007
- top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
9789
+ for (const key of Object.keys(entry))
9790
+ if (!KNOWN_FIELDS.has(key))
9791
+ problem(key, "unknown field, ignored");
9792
+ const productionBranch = branchName(entry.productionBranch);
9793
+ const integrationBranch = branchName(entry.integrationBranch);
9794
+ if (!productionBranch)
9795
+ problem("productionBranch", "missing or not a branch name");
9796
+ if (!integrationBranch)
9797
+ problem("integrationBranch", "missing or not a branch name");
9798
+ const optionalBranch = (field) => {
9799
+ if (entry[field] === undefined)
9800
+ return null;
9801
+ const value = branchName(entry[field]);
9802
+ if (!value)
9803
+ problem(field, "not a branch name; using the integration branch");
9804
+ return value;
10008
9805
  };
9806
+ const baseBranch = optionalBranch("baseBranch");
9807
+ const pullRequestTarget = optionalBranch("pullRequestTarget");
9808
+ let mergeBackBranches = [];
9809
+ if (entry.mergeBackBranches !== undefined) {
9810
+ const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
9811
+ if (list.some((item) => item === null))
9812
+ problem("mergeBackBranches", "must be a list of branch names; ignored");
9813
+ else
9814
+ mergeBackBranches = [...new Set(list)];
9815
+ }
9816
+ const naming = {};
9817
+ if (entry.naming !== undefined) {
9818
+ if (!isRecord(entry.naming))
9819
+ problem("naming", "not an object; ignored");
9820
+ else
9821
+ for (const kind of ["feature", "release", "hotfix"]) {
9822
+ const template = entry.naming[kind];
9823
+ if (template === undefined)
9824
+ continue;
9825
+ if (typeof template !== "string" || !template.trim())
9826
+ problem(`naming.${kind}`, "not a template string; ignored");
9827
+ else if (!literalOf(template))
9828
+ problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
9829
+ else
9830
+ naming[kind] = template.trim();
9831
+ }
9832
+ }
9833
+ if (!productionBranch || !integrationBranch) {
9834
+ out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
9835
+ continue;
9836
+ }
9837
+ out.tracks.push({
9838
+ name,
9839
+ productionBranch,
9840
+ integrationBranch,
9841
+ baseBranch: baseBranch ?? integrationBranch,
9842
+ pullRequestTarget: pullRequestTarget ?? integrationBranch,
9843
+ mergeBackBranches,
9844
+ naming
9845
+ });
10009
9846
  }
10010
- if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
10011
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
10012
- return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
10013
- `)[0] || "no output"}); fix the repository (git status should work here)`);
9847
+ out.protectedBranches = [...protectedNames];
9848
+ if (fatal.length)
9849
+ out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
9850
+ return out;
10014
9851
  }
10015
- function resolveTaskKey(root, location) {
10016
- if (!location.shared)
10017
- return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
10018
- const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
10019
- if (location.git && !location.git.reftable) {
10020
- let head = "";
10021
- try {
10022
- head = fs3.readFileSync(path6.join(location.git.gitDir, "HEAD"), "utf8").trim();
10023
- } catch {}
10024
- const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
10025
- if (ref && ref[1] !== ".invalid")
10026
- return branchKey(ref[1]);
10027
- if (/^[0-9a-f]{40,64}$/u.test(head)) {
10028
- const rebasing = rebasingBranch(location.git.gitDir);
10029
- if (rebasing)
10030
- return branchKey(rebasing);
9852
+ function resolveReleaseTrack(input) {
9853
+ const tracks = [...input.tracks];
9854
+ const issues = [...input.issues ?? []];
9855
+ const requested = input.requested?.trim();
9856
+ if (tracks.length === 0) {
9857
+ if (requested)
10031
9858
  return {
10032
- key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
10033
- kind: "detached",
10034
- branch: null
9859
+ status: "invalid",
9860
+ error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
9861
+ issues
10035
9862
  };
10036
- }
9863
+ return { status: "none", issues };
10037
9864
  }
10038
- const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
10039
- const name = branch.ok ? branch.stdout.trim() : "";
10040
- if (name && name !== ".invalid")
10041
- return branchKey(name);
10042
- const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
10043
- if (gitDir.ok && gitDir.stdout.trim()) {
10044
- const rebasing = rebasingBranch(gitDir.stdout.trim());
10045
- if (rebasing)
10046
- return branchKey(rebasing);
9865
+ const names = tracks.map((track) => track.name).join(", ");
9866
+ let envProblem = null;
9867
+ const done = (track, source, detail, warnings = [], ambiguous = null) => ({
9868
+ status: "resolved",
9869
+ track: track && track.name !== IMPLICIT ? track : null,
9870
+ source,
9871
+ detail,
9872
+ warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
9873
+ tracks,
9874
+ blocking: envProblem ?? ambiguous
9875
+ });
9876
+ if (requested) {
9877
+ const named = tracks.find((track) => track.name === requested);
9878
+ if (!named)
9879
+ return {
9880
+ status: "invalid",
9881
+ error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
9882
+ issues
9883
+ };
9884
+ return done(named, "flag", `track ${named.name} was requested explicitly`);
10047
9885
  }
10048
- const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10049
- const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
10050
- return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
10051
- }
10052
- function checkoutRootOf(root, location) {
10053
- const resolved = path6.resolve(root);
10054
- let canonical = resolved;
10055
- try {
10056
- canonical = fs3.realpathSync(resolved);
10057
- } catch {
10058
- return resolved;
9886
+ const owned = (branch) => branch !== null && tracks.some((track) => [
9887
+ track.integrationBranch,
9888
+ track.productionBranch,
9889
+ track.baseBranch,
9890
+ track.pullRequestTarget
9891
+ ].includes(branch));
9892
+ const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
9893
+ name: IMPLICIT,
9894
+ productionBranch: input.defaultBranch,
9895
+ integrationBranch: input.defaultBranch,
9896
+ baseBranch: input.defaultBranch,
9897
+ pullRequestTarget: input.defaultBranch,
9898
+ mergeBackBranches: [],
9899
+ naming: {}
9900
+ } : null;
9901
+ const candidates = implicit ? [...tracks, implicit] : tracks;
9902
+ const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
9903
+ const fallback = (why) => {
9904
+ const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
9905
+ const track = byDefault.length === 1 ? byDefault[0] : null;
9906
+ const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
9907
+ const ambiguous = candidates.length >= 2 ? `can't tell which release line ${input.branch ?? "HEAD"} is on (${why}); pass --track <name> (${names}) or --base <branch>` : null;
9908
+ return done(track, "default", `${why}; using the workspace ${target}`, [
9909
+ `release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
9910
+ ], ambiguous);
9911
+ };
9912
+ const branch = input.branch;
9913
+ if (branch) {
9914
+ const owners = ownedBy(candidates, branch);
9915
+ if (owners.length === 1)
9916
+ return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
9917
+ if (owners.length > 1)
9918
+ return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
10059
9919
  }
10060
- const found = location ?? resolveStore(canonical);
10061
- if (found instanceof Error || !found.shared || !found.top)
10062
- return canonical;
10063
- if (real(canonical) === found.top)
10064
- return canonical;
10065
- try {
10066
- return fs3.realpathSync(found.top);
10067
- } catch {
10068
- return canonical;
9920
+ const env = input.envRequested?.trim();
9921
+ if (env) {
9922
+ const named = tracks.find((track) => track.name === env);
9923
+ if (named)
9924
+ return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
9925
+ envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
10069
9926
  }
10070
- }
10071
- function branchRenames(location) {
10072
- if (!location.shared)
10073
- return [];
10074
- const files = [];
10075
- const walk = (dir) => {
10076
- let entries;
10077
- try {
10078
- entries = fs3.readdirSync(dir, { withFileTypes: true });
10079
- } catch {
10080
- return;
9927
+ if (!branch)
9928
+ return fallback("HEAD is detached");
9929
+ if (tracks.length === 1 && !implicit)
9930
+ return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
9931
+ let remotes = null;
9932
+ const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
9933
+ const ownerOf = (ref) => {
9934
+ const owner = ownedBy(candidates, short(ref));
9935
+ return owner.length === 1 ? owner[0] : null;
9936
+ };
9937
+ const followRecorded = (start) => {
9938
+ let cursor = start;
9939
+ const seen = new Set([start]);
9940
+ for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
9941
+ const recorded = input.probe.recordedBase(cursor);
9942
+ if (!recorded)
9943
+ return null;
9944
+ const base = short(recorded);
9945
+ const owner = ownerOf(base);
9946
+ if (owner)
9947
+ return owner;
9948
+ if (ownedBy(candidates, base).length > 1 || seen.has(base))
9949
+ return null;
9950
+ seen.add(base);
9951
+ cursor = base;
10081
9952
  }
10082
- for (const entry of entries)
10083
- if (entry.isDirectory())
10084
- walk(path6.join(dir, entry.name));
10085
- else
10086
- files.push(path6.join(dir, entry.name));
9953
+ return null;
9954
+ };
9955
+ const recordedOwner = followRecorded(branch);
9956
+ if (recordedOwner)
9957
+ return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
9958
+ for (const source of ["reflog", "checkout"]) {
9959
+ const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
9960
+ if (!ref)
9961
+ continue;
9962
+ const owner = ownerOf(ref) ?? followRecorded(short(ref));
9963
+ if (owner)
9964
+ return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
9965
+ }
9966
+ const forks = [];
9967
+ for (const track of candidates)
9968
+ for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
9969
+ const sha = input.probe.mergeBase(branch, ref);
9970
+ if (sha)
9971
+ forks.push({ track, ref, sha });
9972
+ }
9973
+ if (forks.length === 0)
9974
+ return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
9975
+ const ancestry = new Map;
9976
+ const descends = (older, newer) => {
9977
+ if (older === newer)
9978
+ return false;
9979
+ const key = `${older}>${newer}`;
9980
+ if (!ancestry.has(key))
9981
+ ancestry.set(key, input.probe.isAncestor(older, newer));
9982
+ return ancestry.get(key);
9983
+ };
9984
+ const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
9985
+ const winningTracks = [...new Set(winners.map((fork) => fork.track))];
9986
+ if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
9987
+ const win = winners[0];
9988
+ return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
9989
+ }
9990
+ if (winningTracks.length === 1)
9991
+ return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
9992
+ const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
9993
+ return fallback(`${branch} has the same or unrelated fork points with ${[...new Set(newest.map((fork) => `${fork.ref} (${fork.track.name === IMPLICIT ? "workspace default" : fork.track.name})`))].join(" and ")}`);
9994
+ }
9995
+ function gitTrackProbe(cwd) {
9996
+ let remote;
9997
+ let remotes;
9998
+ const tip = (branch) => {
9999
+ remote ??= pushRemoteName(cwd) ?? "origin";
10000
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
10001
+ };
10002
+ return {
10003
+ remotes: () => remotes ??= remoteNames(cwd),
10004
+ recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
10005
+ createdFrom: (branch) => {
10006
+ const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
10007
+ const oldest = log?.split(`
10008
+ `).findLast(Boolean) ?? "";
10009
+ const match = /^branch: Created from (\S+)$/u.exec(oldest);
10010
+ return match && match[1] !== "HEAD" ? match[1] : null;
10011
+ },
10012
+ checkedOutFrom: (branch) => {
10013
+ const own = reflog(cwd, `refs/heads/${branch}`);
10014
+ const created = own.at(-1);
10015
+ if (!created || created.subject !== "branch: Created from HEAD")
10016
+ return null;
10017
+ const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
10018
+ const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
10019
+ return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
10020
+ },
10021
+ mergeBase: (branch, trackBranch) => {
10022
+ const base = tip(trackBranch);
10023
+ return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
10024
+ },
10025
+ isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
10026
+ };
10027
+ }
10028
+ function workspaceReleaseTracks(cwd) {
10029
+ const raw = resolveRuntimeReleaseTracks(cwd);
10030
+ return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
10031
+ }
10032
+ function resolveTrackFor(cwd, options) {
10033
+ const read = workspaceReleaseTracks(cwd);
10034
+ if (read.error)
10035
+ return { status: "invalid", error: read.error, issues: read.issues };
10036
+ if (read.tracks.length === 0 && !options.track?.trim())
10037
+ return { status: "none", issues: read.issues };
10038
+ const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
10039
+ const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
10040
+ const input = {
10041
+ tracks: read.tracks,
10042
+ requested: options.track ?? null,
10043
+ envRequested,
10044
+ branch,
10045
+ defaultBranch: options.defaultBranch,
10046
+ issues: read.issues
10087
10047
  };
10088
- walk(path6.join(path6.dirname(location.dir), "logs", "refs", "heads"));
10089
- if (location.git)
10090
- files.push(path6.join(location.git.gitDir, "logs", "HEAD"));
10091
- const seen = new Set;
10092
- const renames = [];
10093
- for (const file of files) {
10094
- let text = "";
10095
- try {
10096
- text = fs3.readFileSync(file, "utf8");
10097
- } catch {
10098
- continue;
10099
- }
10100
- for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
10101
- const rename = { from: match[2], to: match[3], at: Number(match[1]) };
10102
- const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
10103
- if (seen.has(id))
10104
- continue;
10105
- seen.add(id);
10106
- renames.push(rename);
10107
- }
10048
+ let key = null;
10049
+ if (!options.probe) {
10050
+ const refs = git2(cwd, [
10051
+ "for-each-ref",
10052
+ "--format=%(refname) %(objectname)",
10053
+ "refs/heads",
10054
+ "refs/remotes"
10055
+ ]) ?? "";
10056
+ const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
10057
+ const own = branch ? [
10058
+ git2(cwd, [
10059
+ "log",
10060
+ "-g",
10061
+ "-n",
10062
+ "1",
10063
+ "--date=unix",
10064
+ "--format=%H %gd %gs",
10065
+ `refs/heads/${branch}`,
10066
+ "--"
10067
+ ]) ?? "",
10068
+ git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
10069
+ ] : [];
10070
+ key = createHash5("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
10071
+ const hit = memo2.get(key);
10072
+ if (hit)
10073
+ return hit;
10108
10074
  }
10109
- return renames.toSorted((left, right) => left.at - right.at);
10110
- }
10111
- function followRenames(key, boundAt, renames) {
10112
- let current = key;
10113
- let since = Math.floor(Date.parse(boundAt) / 1000);
10114
- for (const rename of renames)
10115
- if (rename.from === current && rename.at >= since) {
10116
- current = rename.to;
10117
- since = rename.at;
10118
- }
10119
- return current;
10075
+ const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
10076
+ if (key) {
10077
+ if (memo2.size >= MEMO_LIMIT)
10078
+ memo2.delete(memo2.keys().next().value);
10079
+ memo2.set(key, resolution);
10080
+ }
10081
+ return resolution;
10120
10082
  }
10121
- var NOT_A_REPO, gitRun = (cwd, args) => {
10083
+ var ReleaseTrackError, KNOWN_FIELDS, BRANCH_FIELDS, PLACEHOLDER, HAS_PLACEHOLDER, isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), branchName = (value) => typeof value === "string" && value.trim() && !value.trim().startsWith("-") ? value.trim() : null, namedBranches = (entry) => [
10084
+ ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
10085
+ ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
10086
+ ].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
10087
+ ...new Set(tracks.flatMap((track) => [
10088
+ track.productionBranch,
10089
+ track.integrationBranch,
10090
+ track.baseBranch,
10091
+ track.pullRequestTarget,
10092
+ ...track.mergeBackBranches
10093
+ ]))
10094
+ ], namingGlob = (template) => HAS_PLACEHOLDER.test(template) ? template.replace(PLACEHOLDER, "*") : `${template.replace(/\/?\*?$/u, "")}/*`, IMPLICIT = "", ownedBy = (tracks, branch) => tracks.filter((track) => [track.integrationBranch, track.productionBranch, track.baseBranch].includes(branch)), shortRef = (ref, remotes) => {
10095
+ const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
10096
+ const remote = remotes.find((name) => bare.startsWith(`${name}/`));
10097
+ return remote ? bare.slice(remote.length + 1) : bare;
10098
+ }, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
10122
10099
  const run = spawnSync6("git", args, {
10123
10100
  cwd,
10124
10101
  encoding: "utf8",
10125
10102
  timeout: 20000,
10126
- killSignal: "SIGKILL",
10127
10103
  windowsHide: true,
10128
- env: {
10129
- ...process.env,
10130
- GIT_TERMINAL_PROMPT: "0",
10131
- GIT_OPTIONAL_LOCKS: "0",
10132
- LC_ALL: "C",
10133
- LANGUAGE: "C"
10134
- }
10104
+ stdio: ["ignore", "pipe", "ignore"]
10135
10105
  });
10136
- const error = run.error;
10106
+ return run.status === 0 ? (run.stdout ?? "").trim() : null;
10107
+ }, recordedBaseKey = (branch) => `branch.${branch}.workitBase`, escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), reflog = (cwd, ref) => (git2(cwd, ["log", "-g", "--date=unix", "--format=%H%x09%gd%x09%gs", ref, "--"]) ?? "").split(`
10108
+ `).map((line) => {
10109
+ const [sha = "", selector = "", ...subject] = line.split("\t");
10137
10110
  return {
10138
- ok: run.status === 0,
10139
- stdout: run.stdout ?? "",
10140
- stderr: error ? String(error.message) : run.stderr ?? "",
10141
- missing: error?.code === "ENOENT"
10111
+ sha,
10112
+ time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
10113
+ subject: subject.join("\t")
10142
10114
  };
10143
- }, shortHash = (value) => createHash5("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
10144
- try {
10145
- return fs3.realpathSync.native(file);
10146
- } catch {
10147
- return file;
10115
+ }).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
10116
+ var init_release_tracks = __esm(() => {
10117
+ init_rev();
10118
+ init_config();
10119
+ init_workspaces();
10120
+ ReleaseTrackError = class ReleaseTrackError extends Error {
10121
+ };
10122
+ KNOWN_FIELDS = new Set([
10123
+ "strategy",
10124
+ "productionBranch",
10125
+ "integrationBranch",
10126
+ "naming",
10127
+ "baseBranch",
10128
+ "mergeBackBranches",
10129
+ "pullRequestTarget",
10130
+ "tagNamespace",
10131
+ "versionSource",
10132
+ "requiredChecks",
10133
+ "critical"
10134
+ ]);
10135
+ BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
10136
+ PLACEHOLDER = /\{[^}]*\}/gu;
10137
+ HAS_PLACEHOLDER = /\{[^}]*\}/u;
10138
+ memo2 = new Map;
10139
+ });
10140
+
10141
+ // packages/workit-core/src/core/vcs-config.ts
10142
+ import fs4 from "node:fs";
10143
+ import path5 from "node:path";
10144
+ import { spawnSync as spawnSync7 } from "node:child_process";
10145
+ function vcsConfig(mode, cwd, options = {}) {
10146
+ const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
10147
+ const wsVcs = ws?.vcs ?? {};
10148
+ const wsYt = ws?.youtrack ?? {};
10149
+ const wsIssues = ws?.issues ?? {};
10150
+ const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
10151
+ const root = vcsCwd(cwd);
10152
+ const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
10153
+ const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
10154
+ const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
10155
+ const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
10156
+ const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
10157
+ const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
10158
+ let issuesProvider = null;
10159
+ let linkOnPr = null;
10160
+ if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
10161
+ issuesProvider = "github";
10162
+ linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
10148
10163
  }
10149
- }, DISCOVERY_ENV, isGitDir = (dir) => {
10164
+ if (mode === "resolve") {
10165
+ if (cfgStatus === "malformed")
10166
+ return { ok: false, error: cfgError, configPath: cfgPath };
10167
+ const trackResolution = resolveTrackFor(root, {
10168
+ track: options.track,
10169
+ ...options.branch !== undefined ? { branch: options.branch } : {},
10170
+ defaultBranch: workspaceDefault
10171
+ });
10172
+ if (trackResolution.status === "invalid")
10173
+ return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
10174
+ const releaseTrack = trackSummary(trackResolution);
10175
+ const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
10176
+ const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
10177
+ return {
10178
+ ok: true,
10179
+ workspace_name: ws?.name ?? null,
10180
+ provider,
10181
+ defaultTargetBranch: defaultTarget,
10182
+ workspaceDefaultTargetBranch: workspaceDefault,
10183
+ baseBranch,
10184
+ releaseTrack,
10185
+ link_issues: linkIssues,
10186
+ youtrack_base_url: youtrackBaseUrl,
10187
+ issues_provider: issuesProvider,
10188
+ link_on_pr: linkOnPr
10189
+ };
10190
+ }
10191
+ if (cfgStatus === "malformed") {
10192
+ return { ok: false, error: cfgError, configPath: cfgPath };
10193
+ }
10194
+ if (cfgStatus === "missing" && provider === null)
10195
+ return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
10196
+ if (provider === null) {
10197
+ return {
10198
+ ok: false,
10199
+ error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
10200
+ configPath: cfgPath
10201
+ };
10202
+ }
10203
+ const prov = cfg[provider] ?? {};
10204
+ const out = {
10205
+ ok: true,
10206
+ configPath: path5.resolve(cfgPath),
10207
+ provider,
10208
+ defaultTargetBranch: workspaceDefault,
10209
+ pr: cfg.pr ?? {},
10210
+ workspace_name: ws?.name ?? null,
10211
+ link_issues: linkIssues,
10212
+ youtrack_base_url: youtrackBaseUrl,
10213
+ issues_provider: issuesProvider,
10214
+ link_on_pr: linkOnPr
10215
+ };
10216
+ if (provider === "gitlab") {
10217
+ out.gitlab = {
10218
+ host: prov.host ?? "gitlab.com",
10219
+ apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
10220
+ };
10221
+ } else if (provider === "github") {
10222
+ out.github = { host: prov.host ?? "github.com" };
10223
+ }
10224
+ return out;
10225
+ }
10226
+ var workspacesPath = () => path5.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
10227
+ const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
10228
+ if (r.status !== 0)
10229
+ return null;
10230
+ const url = (r.stdout ?? "").trim();
10231
+ if (/github\.com[:/]/.test(url))
10232
+ return "github";
10233
+ if (/gitlab\.com[:/]/.test(url))
10234
+ return "gitlab";
10235
+ return null;
10236
+ }, readVcsConfig = () => {
10237
+ const file = vcsConfigPath();
10238
+ let raw;
10150
10239
  try {
10151
- return fs3.statSync(path6.join(dir, "HEAD")).isFile() && fs3.statSync(path6.join(dir, "objects")).isDirectory() && fs3.statSync(path6.join(dir, "refs")).isDirectory();
10240
+ raw = fs4.readFileSync(file, "utf8");
10152
10241
  } catch {
10153
- return false;
10242
+ return { status: "missing", path: file, config: {} };
10154
10243
  }
10155
- }, reftableRefs = (common) => {
10244
+ let parsed;
10156
10245
  try {
10157
- return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs3.readFileSync(path6.join(common, "config"), "utf8"));
10246
+ parsed = JSON.parse(raw);
10158
10247
  } catch {
10159
- return false;
10248
+ return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
10160
10249
  }
10161
- }, rebasingBranch = (gitDir) => {
10162
- for (const state of ["rebase-merge", "rebase-apply"]) {
10163
- try {
10164
- const ref = fs3.readFileSync(path6.join(gitDir, state, "head-name"), "utf8").trim();
10165
- const match = /^refs\/heads\/(.+)$/u.exec(ref);
10166
- if (match)
10167
- return match[1];
10168
- } catch {}
10250
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10251
+ return { status: "valid", path: file, config: parsed };
10252
+ }
10253
+ return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
10254
+ }, trackSummary = (resolution) => resolution.status === "resolved" ? {
10255
+ name: resolution.track?.name ?? null,
10256
+ source: resolution.source,
10257
+ detail: resolution.detail,
10258
+ warnings: resolution.warnings,
10259
+ track: resolution.track,
10260
+ tracks: resolution.tracks,
10261
+ blocking: resolution.blocking
10262
+ } : null;
10263
+ var init_vcs_config = __esm(() => {
10264
+ init_config();
10265
+ init_workspaces();
10266
+ init_release_tracks();
10267
+ });
10268
+
10269
+ // packages/workit-core/src/core/logger.ts
10270
+ import os3 from "node:os";
10271
+ import path6 from "node:path";
10272
+ var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
10273
+ const override = process.env.WORKFLOW_TOOLKIT_STATE;
10274
+ if (override)
10275
+ return override;
10276
+ const home = os3.homedir();
10277
+ if (process.env.XDG_STATE_HOME)
10278
+ return path6.join(process.env.XDG_STATE_HOME, "workit");
10279
+ switch (os3.platform()) {
10280
+ case "darwin":
10281
+ return path6.join(home, "Library", "Application Support", "workit");
10282
+ case "win32":
10283
+ return path6.join(process.env.LOCALAPPDATA ?? path6.join(home, "AppData", "Local"), "workit");
10284
+ default:
10285
+ return path6.join(process.env.HOME ?? home, ".local", "state", "workit");
10169
10286
  }
10170
- return null;
10171
- }, checkoutSlug = (root) => {
10172
- let canonical = real(root);
10173
- if (process.platform === "win32")
10174
- canonical = canonical.toLowerCase();
10175
- const base = path6.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
10176
- return `${base}-${shortHash(canonical)}`;
10177
- };
10178
- var init_paths = __esm(() => {
10179
- NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
10180
- DISCOVERY_ENV = [
10181
- "GIT_DIR",
10182
- "GIT_COMMON_DIR",
10183
- "GIT_WORK_TREE",
10184
- "GIT_CEILING_DIRECTORIES",
10185
- "GIT_DISCOVERY_ACROSS_FILESYSTEM"
10186
- ];
10287
+ }, homePattern;
10288
+ var init_logger = __esm(() => {
10289
+ SENSITIVE_WORDS = new Set([
10290
+ "token",
10291
+ "secret",
10292
+ "password",
10293
+ "passwd",
10294
+ "authorization",
10295
+ "credential",
10296
+ "credentials",
10297
+ "cookie",
10298
+ "api",
10299
+ "apikey",
10300
+ "apitoken",
10301
+ "accesstoken",
10302
+ "clientsecret",
10303
+ "refreshtoken",
10304
+ "key",
10305
+ "bearer",
10306
+ "prompt",
10307
+ "message",
10308
+ "messages",
10309
+ "content",
10310
+ "body",
10311
+ "args",
10312
+ "argument",
10313
+ "result",
10314
+ "results",
10315
+ "output",
10316
+ "issue",
10317
+ "description",
10318
+ "summary",
10319
+ "payload",
10320
+ "script",
10321
+ "command",
10322
+ "text",
10323
+ "query"
10324
+ ]);
10325
+ STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
10326
+ homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
10187
10327
  });
10188
10328
 
10189
10329
  // packages/workit-core/src/host-session.ts
10190
- import { createHash as createHash6 } from "node:crypto";
10191
- import fs4 from "node:fs";
10330
+ import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
10331
+ import fs5 from "node:fs";
10192
10332
  import path7 from "node:path";
10193
10333
  function hostSessionFromEnv(env) {
10194
10334
  const value = (key) => env[key]?.trim() || null;
@@ -10217,7 +10357,7 @@ function migrateRepoIssued(cwd) {
10217
10357
  if (location instanceof Error)
10218
10358
  return;
10219
10359
  old = path7.join(location.dir, "hooks", "issued");
10220
- if (!fs4.existsSync(old))
10360
+ if (!fs5.existsSync(old))
10221
10361
  return;
10222
10362
  } catch {
10223
10363
  return;
@@ -10225,13 +10365,18 @@ function migrateRepoIssued(cwd) {
10225
10365
  for (const lead of safeList(old)) {
10226
10366
  try {
10227
10367
  const from = path7.join(old, lead);
10228
- if (!fs4.statSync(from).isDirectory())
10368
+ if (!fs5.statSync(from).isDirectory())
10229
10369
  continue;
10230
10370
  const to = path7.join(userIssuedRoot(), lead);
10231
- fs4.mkdirSync(to, { recursive: true, mode: 448 });
10232
- for (const id of fs4.readdirSync(from)) {
10233
- fs4.copyFileSync(path7.join(from, id), path7.join(to, id));
10234
- fs4.rmSync(path7.join(from, id), { force: true });
10371
+ fs5.mkdirSync(to, { recursive: true, mode: 448 });
10372
+ for (const id of fs5.readdirSync(from)) {
10373
+ try {
10374
+ fs5.copyFileSync(path7.join(from, id), path7.join(to, id), fs5.constants.COPYFILE_EXCL);
10375
+ } catch (error) {
10376
+ if (error.code !== "EEXIST")
10377
+ throw error;
10378
+ }
10379
+ fs5.rmSync(path7.join(from, id), { force: true });
10235
10380
  }
10236
10381
  removeIfEmpty(from);
10237
10382
  } catch {}
@@ -10239,30 +10384,76 @@ function migrateRepoIssued(cwd) {
10239
10384
  removeIfEmpty(old);
10240
10385
  }
10241
10386
  function recordIssuedSession(cwd, lead, issued) {
10387
+ const token = randomBytes(18).toString("base64url");
10242
10388
  try {
10243
10389
  migrateRepoIssued(cwd);
10244
- const dir = path7.join(userIssuedRoot(), hash(lead));
10245
- fs4.mkdirSync(dir, { recursive: true, mode: 448 });
10246
10390
  const own = hash(issued);
10247
- fs4.writeFileSync(path7.join(dir, own), ISSUED_SESSIONS_MARKER);
10248
- const entries = fs4.readdirSync(dir);
10391
+ const tokens = path7.join(userTokenRoot(), hash(lead));
10392
+ fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
10393
+ fs5.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
10394
+ `, { mode: 384, flag: "wx" });
10395
+ const dir = path7.join(userIssuedRoot(), hash(lead));
10396
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
10397
+ fs5.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
10398
+ `, { mode: 384 });
10399
+ pruneIssued(dir, own, tokens);
10400
+ return token;
10401
+ } catch {
10402
+ return null;
10403
+ }
10404
+ }
10405
+ function pruneIssued(dir, own, tokens) {
10406
+ try {
10407
+ const entries = fs5.readdirSync(dir);
10249
10408
  if (entries.length <= ISSUED_PER_LEAD)
10250
10409
  return;
10251
- const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs4.statSync(path7.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
10252
- for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
10253
- fs4.rmSync(path7.join(dir, name), { force: true });
10410
+ const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs5.statSync(path7.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
10411
+ for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
10412
+ fs5.rmSync(path7.join(dir, name), { force: true });
10413
+ fs5.rmSync(path7.join(tokens, name), { force: true });
10414
+ }
10254
10415
  } catch {}
10255
10416
  }
10256
- var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null, ISSUED_PER_LEAD = 64, ISSUED_SESSIONS_MARKER = "workit-issued-sessions/2", hash = (value) => createHash6("sha256").update(value).digest("hex").slice(0, 32), userIssuedRoot = () => path7.join(resolveStateDir(), "issued"), safeList = (dir) => {
10417
+ function issuedBinding(cwd, claimed) {
10418
+ const record = issuedRecord(cwd, claimed);
10419
+ const { stored, leadBound } = issuedToken(claimed);
10420
+ if (stored)
10421
+ return "token";
10422
+ if (record === null)
10423
+ return null;
10424
+ return leadBound ? "token" : "legacy";
10425
+ }
10426
+ var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null, ISSUED_PER_LEAD = 64, ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3", hash = (value) => createHash6("sha256").update(value).digest("hex").slice(0, 32), userIssuedRoot = () => path7.join(resolveStateDir(), "issued"), userTokenRoot = () => path7.join(resolveStateDir(), "issued-tokens"), safeList = (dir) => {
10257
10427
  try {
10258
- return fs4.readdirSync(dir);
10428
+ return fs5.readdirSync(dir);
10259
10429
  } catch {
10260
10430
  return [];
10261
10431
  }
10262
10432
  }, removeIfEmpty = (dir) => {
10263
10433
  try {
10264
- fs4.rmdirSync(dir);
10434
+ fs5.rmdirSync(dir);
10265
10435
  } catch {}
10436
+ }, tokenHash = (token) => createHash6("sha256").update(token).digest("hex"), issuedRecord = (cwd, claimed) => {
10437
+ const cut = claimed.lastIndexOf(":");
10438
+ if (cut <= 0)
10439
+ return null;
10440
+ migrateRepoIssued(cwd);
10441
+ try {
10442
+ return fs5.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
10443
+ } catch {
10444
+ return null;
10445
+ }
10446
+ }, TOKEN_HASH, issuedToken = (claimed) => {
10447
+ const cut = claimed.lastIndexOf(":");
10448
+ if (cut <= 0)
10449
+ return { stored: null, leadBound: false };
10450
+ const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
10451
+ try {
10452
+ const text = fs5.readFileSync(path7.join(dir, hash(claimed)), "utf8");
10453
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
10454
+ } catch {
10455
+ return { stored: null, leadBound: safeList(dir).length > 0 };
10456
+ }
10266
10457
  }, OWN_CHILD_SHELL;
10267
10458
  var init_host_session = __esm(() => {
10268
10459
  init_logger();
@@ -10275,13 +10466,14 @@ var init_host_session = __esm(() => {
10275
10466
  (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
10276
10467
  ]
10277
10468
  ];
10469
+ TOKEN_HASH = /^([0-9a-f]{64})$/mu;
10278
10470
  OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
10279
10471
  });
10280
10472
 
10281
10473
  // packages/workit-core/src/ledger.ts
10282
- import { spawnSync as spawnSync7 } from "node:child_process";
10283
- import { createHash as createHash7, randomBytes } from "node:crypto";
10284
- import fs5 from "node:fs";
10474
+ import { spawnSync as spawnSync8 } from "node:child_process";
10475
+ import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
10476
+ import fs6 from "node:fs";
10285
10477
  import path8 from "node:path";
10286
10478
  function resolveCommit(cwd, ref) {
10287
10479
  if (!safeRef(ref))
@@ -10357,7 +10549,7 @@ function ledgerPath(cwd) {
10357
10549
  const root = storeRoot(cwd);
10358
10550
  return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
10359
10551
  }
10360
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10552
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
10361
10553
  const lock = `${file}.lock`;
10362
10554
  const deadline = Date.now() + waitMs;
10363
10555
  let token = null;
@@ -10381,13 +10573,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10381
10573
  }
10382
10574
  function appendLine(file, build, options) {
10383
10575
  const write = () => {
10384
- const fd = fs5.openSync(file, "a+");
10576
+ const fd = fs6.openSync(file, "a+", 384);
10385
10577
  try {
10386
- const size = fs5.fstatSync(fd).size;
10578
+ if (process.platform !== "win32")
10579
+ tighten(fd);
10580
+ const size = fs6.fstatSync(fd).size;
10387
10581
  let fence = "";
10388
10582
  if (size > 0) {
10389
10583
  const last = Buffer.alloc(1);
10390
- fs5.readSync(fd, last, 0, 1, size - 1);
10584
+ fs6.readSync(fd, last, 0, 1, size - 1);
10391
10585
  if (last[0] !== 10)
10392
10586
  fence = `
10393
10587
  `;
@@ -10396,16 +10590,16 @@ function appendLine(file, build, options) {
10396
10590
  if (!line.ok)
10397
10591
  return line;
10398
10592
  const bytes = Buffer.from(fence + line.value);
10399
- const written = fs5.writeSync(fd, bytes);
10593
+ const written = fs6.writeSync(fd, bytes);
10400
10594
  if (written !== bytes.length)
10401
10595
  return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
10402
10596
  return { ok: true, value: undefined };
10403
10597
  } finally {
10404
- fs5.closeSync(fd);
10598
+ fs6.closeSync(fd);
10405
10599
  }
10406
10600
  };
10407
10601
  try {
10408
- fs5.mkdirSync(path8.dirname(file), { recursive: true });
10602
+ fs6.mkdirSync(path8.dirname(file), { recursive: true });
10409
10603
  if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
10410
10604
  return write();
10411
10605
  const locked = withLedgerLock(file, write);
@@ -10459,8 +10653,10 @@ function readLedger(cwd, options = {}) {
10459
10653
  if (!file.ok)
10460
10654
  return file;
10461
10655
  let text = "";
10656
+ let stat = null;
10462
10657
  try {
10463
- text = fs5.readFileSync(file.value, "utf8");
10658
+ stat = fs6.statSync(file.value);
10659
+ text = fs6.readFileSync(file.value, "utf8");
10464
10660
  } catch (error) {
10465
10661
  if (error.code === "ENOENT")
10466
10662
  return {
@@ -10479,6 +10675,7 @@ function readLedger(cwd, options = {}) {
10479
10675
  const integrity = { chained: 0, legacy: 0, unverified: [] };
10480
10676
  const earlier = new Set([null]);
10481
10677
  let chainStarted = false;
10678
+ const ids = new Set;
10482
10679
  for (const raw of text.split(`
10483
10680
  `)) {
10484
10681
  const trimmed = raw.trim();
@@ -10512,6 +10709,10 @@ function readLedger(cwd, options = {}) {
10512
10709
  reason = "unsigned";
10513
10710
  else
10514
10711
  rowIntegrity = "legacy";
10712
+ if (options.integrity && !reason && ids.has(parsed.id))
10713
+ reason = "duplicate_id";
10714
+ if (options.integrity)
10715
+ ids.add(parsed.id);
10515
10716
  if (reason)
10516
10717
  rowIntegrity = "unverified";
10517
10718
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -10535,7 +10736,8 @@ function readLedger(cwd, options = {}) {
10535
10736
  seq: rows.length + 1,
10536
10737
  superseded: false,
10537
10738
  supersedeIgnored: false,
10538
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
10739
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
10740
+ ...reason ? { integrityReason: reason } : {}
10539
10741
  };
10540
10742
  rows.push(row);
10541
10743
  if (!options.integrity)
@@ -10563,18 +10765,44 @@ function readLedger(cwd, options = {}) {
10563
10765
  const byId = new Map;
10564
10766
  for (const row of rows) {
10565
10767
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
10566
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10768
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10567
10769
  target.superseded = true;
10568
10770
  else if (row.supersedes)
10569
10771
  row.supersedeIgnored = true;
10570
10772
  if (!byId.has(row.id))
10571
10773
  byId.set(row.id, row);
10572
10774
  }
10775
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
10776
+ tamperCache.set(file.value, {
10777
+ size: stat.size,
10778
+ stamp: fileStamp(stat),
10779
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
10780
+ });
10573
10781
  return {
10574
10782
  ok: true,
10575
10783
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
10576
10784
  };
10577
10785
  }
10786
+ function ledgerTampering(cwd) {
10787
+ const file = ledgerPath(cwd);
10788
+ if (!file.ok)
10789
+ return [];
10790
+ try {
10791
+ const stat = fs6.statSync(file.value);
10792
+ const cached = tamperCache.get(file.value);
10793
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
10794
+ return cached.breaks;
10795
+ } catch {
10796
+ return [];
10797
+ }
10798
+ const read = readLedger(cwd, { integrity: true });
10799
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
10800
+ }
10801
+ function tamperedUnblock(cwd) {
10802
+ const file = ledgerPath(cwd);
10803
+ const where = file.ok ? file.value : "the ledger file";
10804
+ return `workit ledger verify-integrity # lists the edited or missing rows; if the edit is understood, move ${where} aside (mv ${where} ${where}.tampered-<date>) and record the verdicts again`;
10805
+ }
10578
10806
  function verdictBasis(verdict, current) {
10579
10807
  if (verdict.head && verdict.head === current.head)
10580
10808
  return "fresh";
@@ -10641,15 +10869,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
10641
10869
  sessions.add(session);
10642
10870
  return sessions;
10643
10871
  }
10644
- function effectiveOf(rows, authors, sameCode, isCurrent) {
10872
+ function sessionTrust(cwd, row) {
10873
+ const session = row.actor.session;
10874
+ if (!session || row.self === true)
10875
+ return null;
10876
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
10877
+ const host = row.actor.host;
10878
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
10879
+ if (!issuedOnly)
10880
+ return null;
10881
+ const binding = issuedBinding(cwd, session);
10882
+ if (binding === null)
10883
+ return "unissued_session";
10884
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
10885
+ }
10886
+ function trustReasons(cwd, row) {
10887
+ if (row.integrity === "unverified")
10888
+ return [row.integrityReason ?? "unsigned"];
10889
+ if (row.integrity !== "verified")
10890
+ return ["unsigned"];
10891
+ const session = sessionTrust(cwd, row);
10892
+ return session ? [session] : [];
10893
+ }
10894
+ function withIntegrity(cwd, rows) {
10895
+ if (rows.every((row) => row.integrity !== undefined))
10896
+ return rows;
10897
+ const read = readLedger(cwd, { integrity: true });
10898
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
10899
+ return rows.map((row) => {
10900
+ if (row.integrity !== undefined)
10901
+ return row;
10902
+ const fresh = bySeq.get(row.seq);
10903
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
10904
+ });
10905
+ }
10906
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
10645
10907
  let effective = null;
10646
10908
  for (const row of rows) {
10647
10909
  if (!effective) {
10648
10910
  effective = row;
10649
10911
  continue;
10650
10912
  }
10651
- const rowIndependent = independenceReasons(row, authors).length === 0;
10652
- const effIndependent = independenceReasons(effective, authors).length === 0;
10913
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
10914
+ continue;
10915
+ const rowIndependent = reasonsOf(row).length === 0;
10916
+ const effIndependent = reasonsOf(effective).length === 0;
10653
10917
  if (effIndependent && !rowIndependent && isCurrent(effective))
10654
10918
  continue;
10655
10919
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -10663,7 +10927,7 @@ function restackCarries(rows, branch, from, to) {
10663
10927
  return false;
10664
10928
  const edges = new Map;
10665
10929
  for (const row of rows)
10666
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
10930
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
10667
10931
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
10668
10932
  const seen = new Set([from]);
10669
10933
  const queue = [from];
@@ -10679,7 +10943,8 @@ function restackCarries(rows, branch, from, to) {
10679
10943
  }
10680
10944
  return false;
10681
10945
  }
10682
- function checkVerdicts(cwd, branch, rows) {
10946
+ function checkVerdicts(cwd, branch, ledgerRows) {
10947
+ const rows = withIntegrity(cwd, ledgerRows);
10683
10948
  const head = branchHead(cwd, branch);
10684
10949
  const fallbackBase = defaultBase(cwd);
10685
10950
  const scope = branchScope(cwd, branch);
@@ -10709,21 +10974,47 @@ function checkVerdicts(cwd, branch, rows) {
10709
10974
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
10710
10975
  };
10711
10976
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
10977
+ const failureAuthorCache = new Map;
10978
+ const failureAuthors = (row) => {
10979
+ let set = failureAuthorCache.get(row.seq);
10980
+ if (!set) {
10981
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
10982
+ failureAuthorCache.set(row.seq, set);
10983
+ }
10984
+ return set;
10985
+ };
10986
+ const reasonCache = new Map;
10987
+ const reasonsOf = (row) => {
10988
+ let reasons = reasonCache.get(row);
10989
+ if (!reasons) {
10990
+ const failing = FAILING.has(String(row.result));
10991
+ reasons = [
10992
+ ...trustReasons(cwd, row),
10993
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
10994
+ ];
10995
+ reasonCache.set(row, reasons);
10996
+ }
10997
+ return reasons;
10998
+ };
10999
+ const retired = new Set;
11000
+ for (const row of rows)
11001
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
11002
+ retired.add(row.supersedes);
10712
11003
  const byKind = new Map;
10713
11004
  for (const row of rows)
10714
- if (isLiveVerdict(row) && scope(row)) {
11005
+ if (isLiveVerdict(row, retired) && scope(row)) {
10715
11006
  const kind = typeof row.kind === "string" ? row.kind : "review";
10716
11007
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
10717
11008
  }
10718
11009
  const verdicts = [];
10719
11010
  const warnings = [];
10720
11011
  for (const [kind, list] of byKind) {
10721
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
11012
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
10722
11013
  if (!row)
10723
11014
  continue;
10724
11015
  const basis = basisOf(row);
10725
11016
  const current = basis !== "stale";
10726
- const independence = independenceReasons(row, authors);
11017
+ const independence = reasonsOf(row);
10727
11018
  const reasons = [
10728
11019
  ...current ? [] : ["stale"],
10729
11020
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -10740,23 +11031,31 @@ function checkVerdicts(cwd, branch, rows) {
10740
11031
  });
10741
11032
  if (reasons.length === 0) {
10742
11033
  for (const later of list)
10743
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
11034
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
10744
11035
  warnings.push(`the author's own (self) ${String(later.result)} ${kind} verdict ${later.id} by ${later.actor.session ?? "no session"} on the current code is newer than the accepted pass ${row.id}: read it before merging`);
10745
11036
  }
10746
11037
  }
10747
11038
  for (const row of rows)
10748
11039
  if (row.integrity === "unverified" && scope(row))
10749
11040
  warnings.push(`ledger row ${row.id} (${row.type}) on ${row.branch ?? branch} is unverified: the workit CLI did not write it or it changed afterwards (workit ledger verify-integrity)`);
11041
+ const tampered = ledgerTampering(cwd);
11042
+ if (tampered.length) {
11043
+ warnings.push(tamperedWarning(cwd, tampered));
11044
+ for (const entry of verdicts) {
11045
+ entry.accepted = false;
11046
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
11047
+ }
11048
+ }
10750
11049
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
10751
11050
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
10752
11051
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
10753
11052
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
10754
- const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
11053
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
10755
11054
  accepted: false,
10756
11055
  verdict: failing?.verdict ?? null,
10757
11056
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
10758
11057
  };
10759
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
11058
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
10760
11059
  return {
10761
11060
  branch,
10762
11061
  head,
@@ -10777,7 +11076,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10777
11076
  error,
10778
11077
  ...unblock ? { unblock } : {}
10779
11078
  }), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
10780
- const run = spawnSync7("git", args, {
11079
+ const run = spawnSync8("git", args, {
10781
11080
  cwd,
10782
11081
  encoding: "utf8",
10783
11082
  timeout: timeoutMs,
@@ -10811,24 +11110,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10811
11110
  } catch {
10812
11111
  return null;
10813
11112
  }
10814
- }, newId2 = () => `${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
11113
+ }, newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
10815
11114
  if (process.platform !== "linux")
10816
11115
  return false;
10817
11116
  try {
10818
- return isNetworkFsType(fs5.statfsSync(dir).type);
11117
+ return isNetworkFsType(fs6.statfsSync(dir).type);
10819
11118
  } catch {
10820
11119
  return false;
10821
11120
  }
10822
11121
  }, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
10823
11122
  try {
10824
- return fs5.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
11123
+ return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
10825
11124
  } catch {
10826
11125
  return null;
10827
11126
  }
10828
11127
  }, lockAgeMs = (lock) => {
10829
11128
  for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
10830
11129
  try {
10831
- return Date.now() - fs5.statSync(target).mtimeMs;
11130
+ return Date.now() - fs6.statSync(target).mtimeMs;
10832
11131
  } catch {}
10833
11132
  }
10834
11133
  return null;
@@ -10836,7 +11135,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10836
11135
  ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10837
11136
  for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
10838
11137
  const buffer = Buffer.alloc(span);
10839
- fs5.readSync(fd, buffer, 0, span, size - span);
11138
+ fs6.readSync(fd, buffer, 0, span, size - span);
10840
11139
  const end = buffer.lastIndexOf(10);
10841
11140
  if (end < 0) {
10842
11141
  if (span === size)
@@ -10864,6 +11163,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10864
11163
  return text || null;
10865
11164
  }
10866
11165
  return null;
11166
+ }, tighten = (fd) => {
11167
+ try {
11168
+ if ((fs6.fstatSync(fd).mode & 63) !== 0)
11169
+ fs6.fchmodSync(fd, 384);
11170
+ } catch {}
10867
11171
  }, sealed = (row, id, at, previous) => {
10868
11172
  const { prevHash: _prev, rowHash: _row, ...fields } = row;
10869
11173
  const prevHash = previous === null ? null : sha2562(previous);
@@ -10878,7 +11182,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10878
11182
  session: str(actor.session),
10879
11183
  agentId: str(actor.agentId)
10880
11184
  };
10881
- }, isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded, RENAMED, renameAwareBranch = (scopeOf, branch) => {
11185
+ }, TAMPER_REASONS, tamperCache, fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`, tamperedWarning = (cwd, breaks) => `the ledger was edited, or a row was removed or moved (${breaks.slice(0, 3).map((row) => `${row.reason} at row ${row.seq}`).join(", ")}${breaks.length > 3 ? ", …" : ""}): no verdict is accepted until it is repaired. ${tamperedUnblock(cwd)}`, isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id), RENAMED, renameAwareBranch = (scopeOf, branch) => {
10882
11186
  return (target) => {
10883
11187
  const own = typeof branch === "string" ? branch : null;
10884
11188
  const theirs = typeof target.branch === "string" ? target.branch : null;
@@ -10895,7 +11199,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10895
11199
  if (derivesFrom(author, session) || derivesFrom(session, author))
10896
11200
  return true;
10897
11201
  return false;
10898
- }, independenceReasons = (row, authors) => {
11202
+ }, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
10899
11203
  const reasons = [];
10900
11204
  if (!row.actor.session)
10901
11205
  reasons.push("no_session");
@@ -10906,6 +11210,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10906
11210
  return reasons;
10907
11211
  }, OBSERVED_ONLY;
10908
11212
  var init_ledger = __esm(() => {
11213
+ init_store_lock();
10909
11214
  init_vcs_config();
10910
11215
  init_rev();
10911
11216
  init_host_session();
@@ -10929,56 +11234,73 @@ var init_ledger = __esm(() => {
10929
11234
  ledgerLock = {
10930
11235
  acquire(lock) {
10931
11236
  try {
10932
- fs5.mkdirSync(lock);
11237
+ fs6.mkdirSync(lock);
10933
11238
  } catch (error) {
10934
11239
  if (error.code === "EEXIST")
10935
11240
  return null;
10936
11241
  throw error;
10937
11242
  }
10938
- const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
10939
- fs5.writeFileSync(path8.join(lock, LOCK_OWNER), token);
11243
+ const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
11244
+ fs6.writeFileSync(path8.join(lock, LOCK_OWNER), token);
10940
11245
  return token;
10941
11246
  },
10942
11247
  release(lock, token) {
10943
11248
  if (lockToken(lock) !== token)
10944
11249
  return false;
10945
- const tomb = `${lock}.release-${randomBytes(6).toString("hex")}`;
11250
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
10946
11251
  try {
10947
- fs5.renameSync(lock, tomb);
11252
+ fs6.renameSync(lock, tomb);
10948
11253
  } catch {
10949
11254
  return false;
10950
11255
  }
10951
11256
  if (lockToken(tomb) !== token) {
10952
11257
  try {
10953
- fs5.renameSync(tomb, lock);
11258
+ fs6.renameSync(tomb, lock);
10954
11259
  } catch {}
10955
11260
  return false;
10956
11261
  }
10957
- fs5.rmSync(tomb, { recursive: true, force: true });
11262
+ fs6.rmSync(tomb, { recursive: true, force: true });
10958
11263
  return true;
10959
11264
  },
10960
11265
  token: lockToken,
10961
11266
  reap(lock, observed) {
10962
- const tomb = `${lock}.reap-${randomBytes(6).toString("hex")}`;
11267
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
10963
11268
  try {
10964
- fs5.renameSync(lock, tomb);
11269
+ fs6.renameSync(lock, tomb);
10965
11270
  } catch {
10966
11271
  return false;
10967
11272
  }
10968
11273
  if (lockToken(tomb) === observed) {
10969
- fs5.rmSync(tomb, { recursive: true, force: true });
11274
+ fs6.rmSync(tomb, { recursive: true, force: true });
10970
11275
  return true;
10971
11276
  }
10972
11277
  try {
10973
- fs5.renameSync(tomb, lock);
11278
+ fs6.renameSync(tomb, lock);
10974
11279
  } catch {
10975
- fs5.rmSync(tomb, { recursive: true, force: true });
11280
+ fs6.rmSync(tomb, { recursive: true, force: true });
10976
11281
  }
10977
11282
  return false;
10978
11283
  }
10979
11284
  };
10980
11285
  TAIL_BYTES = 64 * 1024;
11286
+ TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
11287
+ tamperCache = new Map;
10981
11288
  RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
11289
+ TRUST_REASONS = new Set([
11290
+ "unsigned",
11291
+ "hash_mismatch",
11292
+ "broken_link",
11293
+ "duplicate_id",
11294
+ "unissued_session",
11295
+ "unbound_session"
11296
+ ]);
11297
+ SHELL_VOUCHED_HOSTS = new Set([
11298
+ "cli",
11299
+ "opencode",
11300
+ "pi",
11301
+ "codex_cli",
11302
+ "codex_desktop"
11303
+ ]);
10982
11304
  OBSERVED_ONLY = new Set([
10983
11305
  "check",
10984
11306
  "merge.unverified",
@@ -11928,7 +12250,7 @@ function normalizeMaxBytes(value, options = {}) {
11928
12250
  }
11929
12251
 
11930
12252
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
11931
- import fs6 from "node:fs";
12253
+ import fs7 from "node:fs";
11932
12254
  function createScratchBuffer(maxBytes) {
11933
12255
  const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
11934
12256
  return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
@@ -11951,7 +12273,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
11951
12273
  let total = 0;
11952
12274
  while (true) {
11953
12275
  const length = nextReadLength(total, maxBytes, scratch.length);
11954
- const bytesRead = fs6.readSync(fd, scratch, 0, length, null);
12276
+ const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
11955
12277
  if (bytesRead === 0) {
11956
12278
  return Buffer.concat(chunks, total);
11957
12279
  }
@@ -11965,9 +12287,9 @@ var init_bounded_read = __esm(() => {
11965
12287
  });
11966
12288
 
11967
12289
  // node_modules/@openclaw/fs-safe/dist/read-open-flags.js
11968
- import fs7 from "node:fs";
12290
+ import fs8 from "node:fs";
11969
12291
  function resolveReadOpenFlags(options) {
11970
- const constants = options?.constants ?? fs7.constants;
12292
+ const constants = options?.constants ?? fs8.constants;
11971
12293
  const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
11972
12294
  const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
11973
12295
  return constants.O_RDONLY | noFollow | nonBlocking;
@@ -11975,7 +12297,7 @@ function resolveReadOpenFlags(options) {
11975
12297
  var init_read_open_flags = () => {};
11976
12298
 
11977
12299
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
11978
- import { randomBytes as randomBytes2 } from "node:crypto";
12300
+ import { randomBytes as randomBytes3 } from "node:crypto";
11979
12301
  import fsSync from "node:fs";
11980
12302
  import path11 from "node:path";
11981
12303
  function parseSidecarLockSnapshot(snapshot, parser) {
@@ -11983,7 +12305,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
11983
12305
  }
11984
12306
  function createSidecarLockOwnershipToken() {
11985
12307
  let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
11986
- for (const byte of randomBytes2(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
12308
+ for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
11987
12309
  for (let bit = 7;bit >= 0; bit -= 1) {
11988
12310
  token += byte & 1 << bit ? "\t" : " ";
11989
12311
  }
@@ -12180,7 +12502,7 @@ function sleepSync(ms) {
12180
12502
  }
12181
12503
 
12182
12504
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
12183
- import fs8 from "node:fs";
12505
+ import fs9 from "node:fs";
12184
12506
  import path12 from "node:path";
12185
12507
  function getSyncHeldLocks() {
12186
12508
  const globalWithState = globalThis;
@@ -12198,7 +12520,7 @@ function releaseAllSyncHeldLocks() {
12198
12520
  }
12199
12521
  try {
12200
12522
  if (held.fd !== undefined)
12201
- fs8.closeSync(held.fd);
12523
+ fs9.closeSync(held.fd);
12202
12524
  } catch {}
12203
12525
  try {
12204
12526
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
@@ -12233,7 +12555,7 @@ function releaseSyncHeldLock(held) {
12233
12555
  if (held.fd !== undefined) {
12234
12556
  const fd = held.fd;
12235
12557
  held.fd = undefined;
12236
- fs8.closeSync(fd);
12558
+ fs9.closeSync(fd);
12237
12559
  }
12238
12560
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
12239
12561
  heldLocks.delete(held.normalizedTargetPath);
@@ -12256,11 +12578,11 @@ function createSyncHeldLockHandle(held) {
12256
12578
  };
12257
12579
  }
12258
12580
  function canonicalLockParentSync(parent) {
12259
- return process.platform === "win32" ? fs8.realpathSync.native(parent) : fs8.realpathSync(parent);
12581
+ return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
12260
12582
  }
12261
12583
  function normalizeTargetPath(targetPath) {
12262
12584
  const resolved = path12.resolve(targetPath);
12263
- fs8.mkdirSync(path12.dirname(resolved), { recursive: true });
12585
+ fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
12264
12586
  try {
12265
12587
  return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
12266
12588
  } catch {
@@ -12288,7 +12610,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
12288
12610
  }
12289
12611
  function reclaimGuardExists(reclaimGuardPath) {
12290
12612
  try {
12291
- fs8.lstatSync(reclaimGuardPath);
12613
+ fs9.lstatSync(reclaimGuardPath);
12292
12614
  return true;
12293
12615
  } catch (error) {
12294
12616
  if (error.code === "ENOENT")
@@ -12352,19 +12674,19 @@ function acquireFileLockSync(targetPath, options) {
12352
12674
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
12353
12675
  let lockFileCreateDenied = false;
12354
12676
  try {
12355
- const noFollow = process.platform !== "win32" && typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
12677
+ const noFollow = process.platform !== "win32" && typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
12356
12678
  try {
12357
- fd = fs8.openSync(lockPath, fs8.constants.O_WRONLY | fs8.constants.O_CREAT | fs8.constants.O_EXCL | noFollow, 384);
12679
+ fd = fs9.openSync(lockPath, fs9.constants.O_WRONLY | fs9.constants.O_CREAT | fs9.constants.O_EXCL | noFollow, 384);
12358
12680
  } catch (error) {
12359
12681
  lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
12360
12682
  throw error;
12361
12683
  }
12362
- fs8.writeFileSync(fd, raw, "utf8");
12363
- fs8.fsyncSync(fd);
12684
+ fs9.writeFileSync(fd, raw, "utf8");
12685
+ fs9.fsyncSync(fd);
12364
12686
  const snapshot = {
12365
12687
  raw,
12366
12688
  payload,
12367
- stat: fs8.fstatSync(fd),
12689
+ stat: fs9.fstatSync(fd),
12368
12690
  ownershipToken
12369
12691
  };
12370
12692
  const createdHeld = {
@@ -12399,8 +12721,8 @@ function acquireFileLockSync(targetPath, options) {
12399
12721
  return returnedHandle;
12400
12722
  } catch (error) {
12401
12723
  if (fd !== undefined) {
12402
- const failed = { payload: null, stat: fs8.fstatSync(fd) };
12403
- fs8.closeSync(fd);
12724
+ const failed = { payload: null, stat: fs9.fstatSync(fd) };
12725
+ fs9.closeSync(fd);
12404
12726
  fd = undefined;
12405
12727
  removeSidecarLockIfUnchangedSync(lockPath, failed);
12406
12728
  }
@@ -12449,7 +12771,7 @@ function acquireFileLockSync(targetPath, options) {
12449
12771
  })) {
12450
12772
  let ownsReclaimGuard = false;
12451
12773
  try {
12452
- fs8.mkdirSync(reclaimGuardPath);
12774
+ fs9.mkdirSync(reclaimGuardPath);
12453
12775
  ownsReclaimGuard = true;
12454
12776
  if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
12455
12777
  continue;
@@ -12462,7 +12784,7 @@ function acquireFileLockSync(targetPath, options) {
12462
12784
  } finally {
12463
12785
  if (ownsReclaimGuard) {
12464
12786
  try {
12465
- fs8.rmdirSync(reclaimGuardPath);
12787
+ fs9.rmdirSync(reclaimGuardPath);
12466
12788
  } catch {}
12467
12789
  }
12468
12790
  }
@@ -12508,137 +12830,6 @@ var packageRoot = () => {
12508
12830
  };
12509
12831
  var init_package_root = () => {};
12510
12832
 
12511
- // packages/workit-core/src/core/store-lock.ts
12512
- import { spawnSync as spawnSync8 } from "node:child_process";
12513
- import fs9 from "node:fs";
12514
- import { hostname } from "node:os";
12515
- var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
12516
- let value;
12517
- try {
12518
- value = JSON.parse(raw);
12519
- } catch {
12520
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
12521
- }
12522
- const parsed = metadataLockSchema.safeParse(value);
12523
- if (!parsed.success)
12524
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
12525
- return parsed.data;
12526
- }, parseMetadataLockOrNull = (raw) => {
12527
- try {
12528
- return parseMetadataLock(raw);
12529
- } catch {
12530
- return null;
12531
- }
12532
- }, parseProcStatStart = (stat) => {
12533
- const close = stat.lastIndexOf(")");
12534
- if (close < 0)
12535
- return null;
12536
- return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
12537
- }, processStartOf = (pid) => {
12538
- if (process.platform === "linux") {
12539
- try {
12540
- return parseProcStatStart(fs9.readFileSync(`/proc/${pid}/stat`, "utf8"));
12541
- } catch {
12542
- return null;
12543
- }
12544
- }
12545
- if (process.platform === "darwin" || process.platform === "freebsd") {
12546
- try {
12547
- const run = spawnSync8("ps", ["-o", "lstart=", "-p", String(pid)], {
12548
- encoding: "utf8",
12549
- timeout: 1000
12550
- });
12551
- const value = run.status === 0 ? run.stdout.trim() : "";
12552
- return value || null;
12553
- } catch {
12554
- return null;
12555
- }
12556
- }
12557
- return null;
12558
- }, readTrimmed = (file) => {
12559
- try {
12560
- return fs9.readFileSync(file, "utf8").trim() || null;
12561
- } catch {
12562
- return null;
12563
- }
12564
- }, cachedLockHost = null, localLockHost = () => {
12565
- if (cachedLockHost !== null)
12566
- return cachedLockHost;
12567
- let pidns = null;
12568
- try {
12569
- pidns = /\[(\d+)\]/.exec(fs9.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
12570
- } catch {}
12571
- const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
12572
- cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
12573
- return cachedLockHost;
12574
- }, pidAlive = (pid) => {
12575
- if (!Number.isSafeInteger(pid) || pid <= 0)
12576
- return false;
12577
- try {
12578
- process.kill(pid, 0);
12579
- return true;
12580
- } catch (error) {
12581
- return error.code === "EPERM";
12582
- }
12583
- }, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
12584
- const lock = metadataLockSchema.safeParse(payload);
12585
- if (!lock.success)
12586
- return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
12587
- const { pid, processStart, host } = lock.data;
12588
- const [lockName, lockSpace] = host.split("#");
12589
- const [localName, localSpace] = localHost.split("#");
12590
- if (lockSpace && localSpace && lockName === localName) {
12591
- const [lockNs, lockBoot] = lockSpace.split(":");
12592
- const [localNs, localBoot] = localSpace.split(":");
12593
- if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
12594
- return { state: "stale", reason: "lock was taken before this machine rebooted" };
12595
- }
12596
- if (host !== localHost)
12597
- return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
12598
- if (!pidAlive(pid))
12599
- return { state: "stale", reason: `pid ${pid} is not running` };
12600
- const currentStart = processStartOf(pid);
12601
- if (processStart !== null && currentStart !== null && processStart !== currentStart)
12602
- return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
12603
- if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
12604
- return {
12605
- state: "stale",
12606
- reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
12607
- };
12608
- return { state: "live", reason: `held by running pid ${pid}` };
12609
- }, ageOf = (file, nowMs) => {
12610
- try {
12611
- return nowMs - fs9.lstatSync(file).mtimeMs;
12612
- } catch {
12613
- return null;
12614
- }
12615
- }, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
12616
- const guard = `${lockPath}.reclaim`;
12617
- const age = ageOf(guard, nowMs);
12618
- if (age === null || age <= RECLAIM_GUARD_TTL_MS)
12619
- return false;
12620
- try {
12621
- fs9.rmdirSync(guard);
12622
- return true;
12623
- } catch {
12624
- return false;
12625
- }
12626
- };
12627
- var init_store_lock = __esm(() => {
12628
- init_zod();
12629
- init_task_contract();
12630
- init_paths();
12631
- metadataLockSchema = object({
12632
- pid: number2().int().nonnegative().safe(),
12633
- processStart: string2().nullable(),
12634
- host: string2().min(1),
12635
- nonce: string2().min(1),
12636
- externalAction: literal(true).optional()
12637
- }).strict();
12638
- FOREIGN_LOCK_TTL_MS = 10 * 60000;
12639
- TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
12640
- });
12641
-
12642
12833
  // packages/workit-core/src/store/event-log.ts
12643
12834
  import { randomUUID as randomUUID2 } from "node:crypto";
12644
12835
  import fs10 from "node:fs";
@@ -15650,11 +15841,11 @@ what and whether, the CLI does how and records what it observed.
15650
15841
  Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
15651
15842
  switch paths to evade a denial, never claim enforcement a host cannot provide,
15652
15843
  and never fabricate an approval or verdict. A question answer is not
15653
- host permission, and imported decisions or handoffs never grant authority.
15844
+ host permission, and imported decisions or handoffs grant no authority.
15654
15845
 
15655
15846
  Autonomy contract:
15656
15847
  - Continue to the requested endpoint (commit, push, PR, green CI, verified,
15657
- merged) without asking "continue?", running checks and repairs on the way.
15848
+ merged) without asking "continue?", running checks and repairs.
15658
15849
  Stop only for a new consequential choice, a host denial, a conflicting edit
15659
15850
  or an unresolved blocker; report open gaps and uncertain workers.
15660
15851
  - No endpoint named: stop at a local commit on a policy-compliant branch and
@@ -15664,7 +15855,8 @@ Autonomy contract:
15664
15855
  never merging; \`merged\` also lands it with \`workit pr merge\`.
15665
15856
  Merge and release need a workspace grant only the user raises from their
15666
15857
  own terminal (a blocked verb names how); never edit workspaces.json or route
15667
- around a grant_required.
15858
+ around grant_required. Release: \`workit release start <v>\`, commits,
15859
+ \`finish\`; \`publish\` if asked.
15668
15860
  - Ask only for a product or preference choice, or for authority you lack, with
15669
15861
  your recommended answer. Facts are yours: read, run or prototype.
15670
15862
  - Label claims measured (run and seen this session), inferred, or guess.
@@ -15679,22 +15871,22 @@ Autonomy contract:
15679
15871
  reconcile every requested item and observe it (\`workit verify-delivery\`).
15680
15872
 
15681
15873
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
15682
- hand-running git, gh or glab (\`workit git|pr|ci|check\`; \`workit help\` lists
15683
- the rest; lacking \`workit\`, \`npx -y @brainervirus/workit-cli\`).
15874
+ hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
15875
+ else \`npx -y @brainervirus/workit-cli\`).
15684
15876
  Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
15685
15877
  an amend.
15686
15878
  The task families (task, policy, evidence, finding, decision, worker, state)
15687
15879
  are optional continuity for tracked work; each branch has one implicit task,
15688
- and a solo edit needs no task. A decision
15689
- records a user choice; it never authorizes an effect. Judge work once with
15880
+ and a solo edit needs no task. A decision records a user choice; it never
15881
+ authorizes an effect. Judge work once with
15690
15882
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
15691
- Across repositories, bind each item to its checkout, branch and endpoint,
15692
- resolve competing targets before a mutation, and reconcile an uncertain
15693
- external effect before retrying it.
15883
+ Across repositories, bind items to checkout, branch and endpoint, resolve
15884
+ competing targets before a mutation, and reconcile an uncertain
15885
+ external effect before a retry.
15694
15886
 
15695
15887
  Mid-task: never silently drop or resume an objective (workit-continue).
15696
15888
 
15697
- Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
15889
+ Skills: load the skill, never act from memory (aliases /wk-<name>).
15698
15890
  - brainstorm, plan, spec, grill, should we: workit-shape
15699
15891
  - implement, build, add a feature: workit-implement
15700
15892
  - review, blast radius: workit-review
@@ -19285,7 +19477,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
19285
19477
  return `workit: raw ${quote(action.via === "api" ? `${action.forge} api` : action.forge === "gh" ? "gh pr merge" : "glab mr merge")}${action.via === "api" ? " merge" : ""} bypasses the merge grant and the verdict gate. Run ${quote("workit pr merge")} instead (it checks both; ${quote("workit help pr")}).`;
19286
19478
  const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
19287
19479
  if (dir && branch && plainPush && releaseBranch(dir, branch))
19288
- return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release v<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
19480
+ return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release ${releaseTagPrefixOf(dir)}<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
19289
19481
  return `workit: raw ${quote("git push")}${action.kind === "push" && action.blindForce ? " --force" : ""} onto ${branch}, a protected branch or the default target, bypasses workit's protected-branch check. Push your feature branch with ${quote("workit git push")} (${quote("workit git push --force-with-lease")} to rewrite it) and land it with ${quote("workit pr merge")}.`;
19290
19482
  }, refFacts = (repo, remote) => {
19291
19483
  const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
@@ -19365,15 +19557,16 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
19365
19557
  return bare;
19366
19558
  }
19367
19559
  return null;
19368
- }, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts) => {
19560
+ }, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts, dir) => {
19369
19561
  const tags = [
19370
19562
  ...action.tags ?? [],
19371
19563
  ...action.targets.filter((name) => name !== null && isTag(facts, name))
19372
19564
  ];
19373
19565
  if (!tags.length && !action.allTags)
19374
19566
  return null;
19375
- const named = tags.length ? tags.join(" --release ") : "v<version>";
19376
- return `workit: raw ${quote(`git push${action.allTags ? " --tags" : ""}`)} of ${tags.length ? `tag ${tags.join(", ")}` : "tags"} bypasses the release grant. Push a release tag with ${quote(`workit git push --release ${named}`)} (v<version> tags whose commit the production branch already has; a tag never moves), under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}).`;
19567
+ const shape = `${releaseTagPrefixOf(dir)}<version>`;
19568
+ const named = tags.length ? tags.join(" --release ") : shape;
19569
+ return `workit: raw ${quote(`git push${action.allTags ? " --tags" : ""}`)} of ${tags.length ? `tag ${tags.join(", ")}` : "tags"} bypasses the release grant. Push a release tag with ${quote(`workit git push --release ${named}`)} (${shape} tags whose commit the production branch already has; a tag never moves), under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}).`;
19377
19570
  }, remoteHeads = (repo, remote) => {
19378
19571
  const listed = spawnSync11("git", ["ls-remote", "--heads", remote], {
19379
19572
  cwd: repo.dir,
@@ -19435,7 +19628,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
19435
19628
  if (guarded)
19436
19629
  return refusal(action, guarded, repo.dir);
19437
19630
  }
19438
- return tagRefusal(pushed, facts) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
19631
+ return tagRefusal(pushed, facts, repo.dir) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
19439
19632
  }, actionsIn = (cwd, command, dialect, cache = new Map) => {
19440
19633
  const out = [];
19441
19634
  const repos = new Map;
@@ -19539,6 +19732,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
19539
19732
  };
19540
19733
  var init_raw_git = __esm(() => {
19541
19734
  init_branch();
19735
+ init_branch_policy();
19542
19736
  init_workspaces();
19543
19737
  init_ledger();
19544
19738
  init_paths();
@@ -20045,10 +20239,10 @@ function stopObligation(input, descriptor) {
20045
20239
  const branch = currentBranch(input.cwd);
20046
20240
  if (!branch || guardedBranch(input.cwd, branch))
20047
20241
  return null;
20048
- const ledger = readLedger(input.cwd);
20242
+ const ledger = readLedger(input.cwd, { integrity: true });
20049
20243
  if (!ledger.ok)
20050
20244
  return null;
20051
- const all = ledger.value.rows;
20245
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
20052
20246
  const rows = all.filter((row) => row.branch === branch);
20053
20247
  const mine = rows.filter((row) => sessionOf(row) === session);
20054
20248
  const commits = new Set(mine.filter((row) => row.type === "commit.recorded").map((row) => typeof row.sha === "string" ? row.sha : row.head).filter((sha) => typeof sha === "string"));
@@ -20576,9 +20770,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
20576
20770
  if (role !== "judge" || !event.agentId)
20577
20771
  return readOnly;
20578
20772
  const own = subagentSession(session, event.agentId);
20579
- if (session)
20580
- recordIssuedSession(input.cwd || process.cwd(), session, own);
20581
- return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> --session ${own} ...\` so the ledger tells it apart from the author.`;
20773
+ const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
20774
+ const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
20775
+ return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> ${flags} ...\` so the ledger tells it apart from the author.${token ? " The token is this subagent's alone: never write it into a report, a file or a commit. --token needs a workit CLI at least as new as this plugin; if the CLI answers that --token is an unknown option, update the workit CLI (workit upgrade --cli) and record again." : ""}`;
20582
20776
  }, failureDecision = (descriptor, event, error) => {
20583
20777
  if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
20584
20778
  return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;