@brainervirus/workit-cursor 8.13.0 → 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,1565 +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;
8884
- }, releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
8885
- if (typeof policyPrefix === "string")
8886
- return policyPrefix.trim();
8887
- const r = spawnSync2("git", ["config", "--get", "gitflow.prefix.versiontag"], {
8888
- cwd: workspaceRoot,
8889
- encoding: "utf8"
8890
- });
8891
- return r.status === 0 ? (r.stdout ?? "").trim() : "v";
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)}`;
8892
8905
  };
8893
- var init_branch_policy = __esm(() => {
8894
- init_config();
8895
- PREFIXES = {
8896
- feature: "feature/*",
8897
- bugfix: "bugfix/*",
8898
- release: "release/*",
8899
- hotfix: "hotfix/*"
8900
- };
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
+ ];
8901
8915
  });
8902
8916
 
8903
- // packages/workit-core/src/core/workspaces.ts
8917
+ // packages/workit-core/src/core/store-lock.ts
8904
8918
  import { spawnSync as spawnSync3 } from "node:child_process";
8905
- import { createHash as createHash3 } from "node:crypto";
8906
- import { readFileSync as readFileSync2, realpathSync } from "node:fs";
8907
- import path3 from "node:path";
8908
- function workspaceDefaultTarget(cwd, globalDefault) {
8909
- const ws = resolveRuntimeWorkspaceVcs(cwd);
8910
- const wp = ws?.branchPolicy ?? {};
8911
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
8912
- const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
8913
- const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
8914
- return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
8915
- }
8916
- var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
8917
- if (!isConfigObject(parsed)) {
8918
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
8919
- }
8920
- const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
8921
- if (!validated.success) {
8922
- const detail = validated.error.issues.map((issue) => {
8923
- const field = String(issue.path[issue.path.length - 1] ?? "");
8924
- const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
8925
- return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
8926
- }).join("; ");
8927
- return {
8928
- status: "invalid",
8929
- path: file,
8930
- entries: [],
8931
- error: `${file} has invalid workspace configuration: ${detail}`
8932
- };
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" });
8933
8927
  }
8934
- for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
8935
- const glob = validateWorkspaceGlob(workspace.glob);
8936
- if (!glob.ok) {
8937
- return {
8938
- status: "invalid",
8939
- path: file,
8940
- entries: [],
8941
- error: `${file} workspaces.${index}.glob: ${glob.error}`
8942
- };
8943
- }
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;
8944
8937
  }
8945
- return {
8946
- status: "valid",
8947
- path: file,
8948
- entries: validated.data.workspaces ?? [],
8949
- document: parsed
8950
- };
8951
- }, readWorkspacesResult = (dir = configDir()) => {
8952
- const file = path3.join(dir, "workspaces.json");
8953
- let raw;
8954
- try {
8955
- raw = readFileSync2(file, "utf8");
8956
- } catch (error) {
8957
- if (error.code === "ENOENT")
8958
- return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
8959
- return {
8960
- status: "malformed",
8961
- path: file,
8962
- entries: [],
8963
- error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
8964
- };
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;
8949
+ }
8965
8950
  }
8966
- let parsed;
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
+ }
8962
+ }
8963
+ return null;
8964
+ }, readTrimmed = (file) => {
8967
8965
  try {
8968
- parsed = JSON.parse(raw);
8966
+ return fs3.readFileSync(file, "utf8").trim() || null;
8969
8967
  } catch {
8970
- return {
8971
- status: "malformed",
8972
- path: file,
8973
- entries: [],
8974
- revision: workspacesRevision(raw),
8975
- error: `${file} is not valid JSON`
8976
- };
8968
+ return null;
8977
8969
  }
8978
- return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
8979
- }, globToRegExp = (glob) => {
8980
- let out = "";
8981
- for (let i = 0;i < glob.length; i++) {
8982
- const c = glob[i];
8983
- if (c === "*") {
8984
- if (glob[i + 1] === "*") {
8985
- if (glob[i + 2] === "/") {
8986
- if (out === "")
8987
- out += "/?";
8988
- out += "(?:[^/]+/)*";
8989
- i += 2;
8990
- } else {
8991
- if (i + 2 >= glob.length) {
8992
- if (out.endsWith("/")) {
8993
- out = out.slice(0, -1);
8994
- out += "(?:/.*)?";
8995
- } else {
8996
- out += ".*";
8997
- }
8998
- } else {
8999
- out += ".*";
9000
- }
9001
- i++;
9002
- }
9003
- } else {
9004
- out += "[^/]*";
9005
- }
9006
- } else {
9007
- out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
9008
- }
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";
9009
8988
  }
9010
- return new RegExp(`^${out}$`);
9011
- }, matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/")), UNSUPPORTED_GLOB, UNSUPPORTED_EXTGLOB, validateWorkspaceGlob = (glob) => {
9012
- const trimmed = glob.trim();
9013
- if (!trimmed)
9014
- return { ok: false, error: "workspace pattern is required" };
9015
- const m = UNSUPPORTED_GLOB.exec(trimmed);
9016
- if (m) {
9017
- return {
9018
- ok: false,
9019
- error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
9020
- };
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" };
9021
9001
  }
9022
- const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
9023
- if (ext) {
9024
- const token = ext[0];
9025
- 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)
9026
9010
  return {
9027
- ok: false,
9028
- 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)`
9029
9013
  };
9030
- }
9031
- return { ok: true };
9032
- }, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
9033
- const cached = mainCheckouts.get(cwd);
9034
- if (cached !== undefined)
9035
- return cached;
9036
- const env = { ...process.env };
9037
- for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
9038
- delete env[name];
9039
- const git = (args) => {
9040
- const run = spawnSync3("git", args, {
9041
- cwd,
9042
- env,
9043
- encoding: "utf8",
9044
- timeout: 5000,
9045
- windowsHide: true
9046
- });
9047
- return run.status === 0 ? run.stdout : null;
9048
- };
9049
- let main = null;
9050
- const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
9051
- const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
9052
- if (gitDir && common && path3.resolve(gitDir) !== path3.resolve(common)) {
9053
- const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
9054
- main = listed?.trim() || (path3.basename(common) === ".git" ? path3.dirname(common) : common);
9055
- }
9056
- mainCheckouts.set(cwd, main);
9057
- return main;
9058
- }, matchingEntries = (cwd, entries) => {
9059
- const match = (where) => {
9060
- const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
9061
- return entries.filter((entry) => {
9062
- const glob = entry.glob.replaceAll("\\", "/");
9063
- const canonical = canonicalGlob(glob);
9064
- return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
9065
- });
9066
- };
9067
- const direct = match(cwd);
9068
- if (direct.length)
9069
- return direct;
9070
- const main = mainCheckoutOf(cwd);
9071
- return main ? match(main) : direct;
9072
- }, realpathOf = (p) => {
9073
- const real = realpathSync.native ?? realpathSync;
9014
+ return { state: "live", reason: `held by running pid ${pid}` };
9015
+ }, ageOf = (file, nowMs) => {
9074
9016
  try {
9075
- return real(p);
9017
+ return nowMs - fs3.lstatSync(file).mtimeMs;
9076
9018
  } catch {
9077
- return p;
9078
- }
9079
- }, canonicalGlob = (glob) => {
9080
- const m = /[*?[\]{]/.exec(glob);
9081
- const prefix = m ? glob.slice(0, m.index) : glob;
9082
- const rest = m ? glob.slice(m.index) : "";
9083
- if (!prefix)
9084
- return glob;
9085
- const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
9086
- return real + (prefix.endsWith("/") ? "/" : "") + rest;
9087
- }, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
9088
- if (workspaceName !== undefined) {
9089
- const named = matches.filter((entry) => entry.name === workspaceName);
9090
- if (named.length === 1)
9091
- return named[0];
9092
- if (named.length > 1)
9093
- throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
9094
- if (matches.length > 0) {
9095
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
9096
- }
9097
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
9098
- }
9099
- if (matches.length < 2)
9100
- return matches[0];
9101
- const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
9102
- const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
9103
- if (mostSpecific.length > 1) {
9104
- throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
9105
- }
9106
- return mostSpecific[0];
9107
- }, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
9108
- const matches = matchingEntries(cwd, entries);
9109
- return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
9110
- }, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
9111
- const result = readWorkspacesResult(dir);
9112
- if (result.status === "malformed" || result.status === "invalid")
9113
- throw new WorkspaceConfigError(result.error);
9114
- return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
9115
- }, runtimeWorkspacePolicySchema = (kind) => {
9116
- const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
9117
- const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
9118
- provider: _enum(["gitlab", "github"]),
9119
- defaultTargetBranch: nonBlank.optional(),
9120
- account: nonBlank.optional()
9121
- }).passthrough();
9122
- const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
9123
- const entrySchema = object({
9124
- name: nonBlank,
9125
- glob: nonBlank,
9126
- [key]: policySchema.optional(),
9127
- defaultProfile: nonBlank.optional(),
9128
- profiles: record(nonBlank, profileSchema).optional()
9129
- }).passthrough();
9130
- return entrySchema;
9131
- }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
9132
- const file = path3.join(dir, "workspaces.json");
9133
- let raw;
9134
- try {
9135
- raw = readFileSync2(file, "utf8");
9136
- } catch (error) {
9137
- if (error.code === "ENOENT")
9138
- return null;
9139
- throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
9019
+ return null;
9140
9020
  }
9141
- 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;
9142
9026
  try {
9143
- parsed = JSON.parse(raw);
9027
+ fs3.rmdirSync(guard);
9028
+ return true;
9144
9029
  } catch {
9145
- throw new WorkspaceConfigError(`${file} is not valid JSON`);
9030
+ return false;
9146
9031
  }
9147
- const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
9148
- if (!validated.success) {
9149
- const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
9150
- throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
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 };
9151
9053
  }
9152
- const entries = validated.data.workspaces;
9153
- for (const entry of entries ?? []) {
9154
- const glob = validateWorkspaceGlob(entry.glob);
9155
- if (!glob.ok)
9156
- throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
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];
9088
+ return {
9089
+ preset,
9090
+ allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
9091
+ protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
9092
+ };
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;
9157
9099
  }
9158
- const matches = matchingEntries(cwd, entries ?? []);
9159
- const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
9160
- const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
9161
- return selected ? { file, selected, matches } : null;
9162
- }, resolveRuntimeReleaseTracks = (cwd) => {
9163
- const matched = matchRuntimeWorkspace(cwd);
9164
- return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
9165
- }, workspaceTrailers = (cwd, dir) => {
9166
- let matched;
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) => {
9167
9132
  try {
9168
- matched = matchRuntimeWorkspace(cwd, dir);
9133
+ return readFileSync(p, "utf8");
9169
9134
  } catch {
9170
- return { enabled: true, workspace: null, from: null };
9171
- }
9172
- if (!matched)
9173
- return { enabled: true, workspace: null, from: null };
9174
- const { selected, matches } = matched;
9175
- const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9176
- const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
9177
- return {
9178
- enabled: source?.trailers !== false,
9179
- workspace: selected.name,
9180
- from: source?.name ?? null
9181
- };
9182
- }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9183
- const matched = matchRuntimeWorkspace(cwd);
9184
- if (!matched)
9185
9135
  return null;
9186
- const { file, selected } = matched;
9187
- const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
9188
- if (!policy.success) {
9189
- const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
9190
- throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
9191
9136
  }
9192
- return policy.data;
9193
- }, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
9194
- const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
9195
- if (!candidate)
9196
- return { policy: undefined, source: "user-default" };
9197
- const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
9198
- const profileName = requestedProfile?.trim() || candidate.defaultProfile;
9199
- if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
9200
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
9201
- const profile = profileName ? candidate.profiles?.[profileName] : undefined;
9202
- if (profileName && !profile)
9203
- throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
9204
- const profilePolicy = profile?.[key];
9205
- 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);
9206
9186
  return {
9207
- policy: profilePolicy ?? workspacePolicy,
9208
- 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
9209
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" };
9210
9203
  };
9211
- var init_workspaces = __esm(() => {
9212
- init_zod();
9213
- init_config();
9214
- init_branch_policy();
9215
- nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
9216
- branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
9217
- commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
9218
- workspaceBranchPolicySchema = object({
9219
- preset: branchPreset,
9220
- developBranch: nonBlank.optional(),
9221
- prefixes: object({
9222
- feature: nonBlank,
9223
- bugfix: nonBlank,
9224
- release: nonBlank,
9225
- hotfix: nonBlank
9226
- }).passthrough().optional(),
9227
- allowed: array(nonBlank).optional(),
9228
- protected: array(nonBlank).optional(),
9229
- integration: _enum(["pr", "merge"]).optional()
9230
- }).strict();
9231
- workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
9232
- workspaceProfileSchema = object({
9233
- branchPolicy: workspaceBranchPolicySchema.optional(),
9234
- commitPolicy: workspaceCommitPolicySchema.optional()
9235
- }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
9236
- message: "a profile must define branchPolicy or commitPolicy"
9237
- });
9238
- versionSourceSchema = discriminatedUnion("kind", [
9239
- object({
9240
- kind: literal("package-json"),
9241
- path: nonBlank,
9242
- field: nonBlank
9243
- }).strict(),
9244
- object({ kind: literal("git-tag") }).strict(),
9245
- object({ kind: literal("manual") }).strict()
9246
- ]);
9247
- releaseTrackSchema = object({
9248
- strategy: branchPreset.optional(),
9249
- productionBranch: nonBlank,
9250
- integrationBranch: nonBlank,
9251
- naming: object({
9252
- feature: nonBlank.optional(),
9253
- release: nonBlank.optional(),
9254
- hotfix: nonBlank.optional()
9255
- }).passthrough().optional(),
9256
- baseBranch: nonBlank.optional(),
9257
- mergeBackBranches: array(nonBlank).optional(),
9258
- pullRequestTarget: nonBlank.optional(),
9259
- tagNamespace: string2().optional(),
9260
- versionSource: versionSourceSchema.optional(),
9261
- requiredChecks: array(nonBlank).optional(),
9262
- critical: array(nonBlank).optional()
9263
- }).passthrough().superRefine((track, context) => {
9264
- for (const [field, values] of [
9265
- ["mergeBackBranches", track.mergeBackBranches ?? []],
9266
- ["requiredChecks", track.requiredChecks ?? []]
9267
- ]) {
9268
- if (new Set(values).size !== values.length) {
9269
- context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
9270
- }
9271
- }
9272
- 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(".."))) {
9273
- context.addIssue({
9274
- code: "custom",
9275
- path: ["versionSource", "path"],
9276
- message: "must stay inside the repository"
9277
- });
9278
- }
9279
- });
9280
- workspaceConfigSchema = object({
9281
- name: nonBlank,
9282
- glob: nonBlank,
9283
- vcs: object({
9284
- provider: _enum(["gitlab", "github"]),
9285
- defaultTargetBranch: nonBlank.optional(),
9286
- tokenFile: nonBlank.optional(),
9287
- account: nonBlank.optional()
9288
- }).passthrough().optional(),
9289
- autoApprove: union([boolean2(), array(nonBlank)]).optional(),
9290
- defaultEndpoint: unknown().optional(),
9291
- verification: _enum(["self", "independent"]).optional(),
9292
- trailers: boolean2().optional(),
9293
- autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9294
- kind,
9295
- union([boolean2(), literal("verified")]).optional()
9296
- ]))).passthrough().optional(),
9297
- youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
9298
- issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
9299
- branchPolicy: workspaceBranchPolicySchema.optional(),
9300
- commitPolicy: workspaceCommitPolicySchema.optional(),
9301
- defaultProfile: nonBlank.optional(),
9302
- profiles: record(nonBlank, workspaceProfileSchema).optional(),
9303
- releaseTracks: record(nonBlank, releaseTrackSchema).optional()
9304
- }).passthrough().superRefine((workspace, context) => {
9305
- if (workspace.issues && workspace.vcs?.provider !== "github") {
9306
- context.addIssue({
9307
- code: "custom",
9308
- path: ["issues"],
9309
- message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
9310
- });
9311
- }
9312
- if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
9313
- context.addIssue({
9314
- code: "custom",
9315
- path: ["defaultProfile"],
9316
- message: `does not name a configured profile`
9317
- });
9318
- }
9319
- });
9320
- workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
9321
- const names = new Set;
9322
- for (const [index, workspace] of (file.workspaces ?? []).entries()) {
9323
- if (names.has(workspace.name)) {
9324
- context.addIssue({
9325
- code: "custom",
9326
- path: ["workspaces", index, "name"],
9327
- message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
9328
- });
9329
- }
9330
- names.add(workspace.name);
9331
- }
9332
- });
9333
- UNSUPPORTED_GLOB = /[?[\]{}]/;
9334
- UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
9335
- WorkspaceConfigError = class WorkspaceConfigError extends Error {
9336
- 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: [] }
9337
9214
  };
9338
- mainCheckouts = new Map;
9339
- runtimeWorkspaceIndexSchema = object({
9340
- workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
9341
- }).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
+ ];
9342
9235
  });
9343
9236
 
9344
- // packages/workit-core/src/core/release-tracks.ts
9345
- import { createHash as createHash4 } from "node:crypto";
9237
+ // packages/workit-core/src/core/branch-policy.ts
9346
9238
  import { spawnSync as spawnSync4 } from "node:child_process";
9347
- function readReleaseTracks(raw) {
9348
- const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
9349
- if (raw === undefined || raw === null)
9350
- return out;
9351
- if (!isRecord(raw)) {
9352
- out.issues.push("releaseTracks is not an object; ignored");
9353
- return out;
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
9259
+ };
9354
9260
  }
9355
- const fatal = [];
9356
- const protectedNames = new Set;
9357
- for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
9358
- const label = `release track ${JSON.stringify(name)}`;
9359
- if (!isRecord(entry)) {
9360
- out.issues.push(`${label} is not an object; ignored`);
9361
- continue;
9362
- }
9363
- for (const branch of namedBranches(entry))
9364
- protectedNames.add(branch);
9365
- if (!name.trim()) {
9366
- out.issues.push("a release track with a blank name was ignored");
9367
- continue;
9368
- }
9369
- let critical = [];
9370
- if (Array.isArray(entry.critical))
9371
- critical = entry.critical.filter((item) => typeof item === "string");
9372
- else if (entry.critical !== undefined)
9373
- out.issues.push(`${label} critical: must be a list of field names; ignored`);
9374
- const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
9375
- const problem = (field, message) => {
9376
- if (isCritical(field))
9377
- fatal.push(`${label} ${field}: ${message}`);
9378
- else
9379
- out.issues.push(`${label} ${field}: ${message}`);
9261
+ if (root === "main") {
9262
+ return {
9263
+ preset: "github-flow",
9264
+ developBranch: null,
9265
+ integration: "pr",
9266
+ protected: [root],
9267
+ allowed: ["*"],
9268
+ prefixes: PREFIXES
9380
9269
  };
9381
- for (const key of Object.keys(entry))
9382
- if (!KNOWN_FIELDS.has(key))
9383
- problem(key, "unknown field, ignored");
9384
- const productionBranch = branchName(entry.productionBranch);
9385
- const integrationBranch = branchName(entry.integrationBranch);
9386
- if (!productionBranch)
9387
- problem("productionBranch", "missing or not a branch name");
9388
- if (!integrationBranch)
9389
- problem("integrationBranch", "missing or not a branch name");
9390
- const optionalBranch = (field) => {
9391
- if (entry[field] === undefined)
9392
- return null;
9393
- const value = branchName(entry[field]);
9394
- if (!value)
9395
- problem(field, "not a branch name; using the integration branch");
9396
- return value;
9270
+ }
9271
+ if (root === "master") {
9272
+ return {
9273
+ preset: "trunk-based",
9274
+ developBranch: null,
9275
+ integration: "pr",
9276
+ protected: [root],
9277
+ allowed: ["*"],
9278
+ prefixes: PREFIXES
9397
9279
  };
9398
- const baseBranch = optionalBranch("baseBranch");
9399
- const pullRequestTarget = optionalBranch("pullRequestTarget");
9400
- let mergeBackBranches = [];
9401
- if (entry.mergeBackBranches !== undefined) {
9402
- const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
9403
- if (list.some((item) => item === null))
9404
- problem("mergeBackBranches", "must be a list of branch names; ignored");
9405
- else
9406
- mergeBackBranches = [...new Set(list)];
9407
- }
9408
- const naming = {};
9409
- if (entry.naming !== undefined) {
9410
- if (!isRecord(entry.naming))
9411
- problem("naming", "not an object; ignored");
9412
- else
9413
- for (const kind of ["feature", "release", "hotfix"]) {
9414
- const template = entry.naming[kind];
9415
- if (template === undefined)
9416
- continue;
9417
- if (typeof template !== "string" || !template.trim())
9418
- problem(`naming.${kind}`, "not a template string; ignored");
9419
- else if (!literalOf(template))
9420
- problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
9421
- else
9422
- naming[kind] = template.trim();
9423
- }
9424
- }
9425
- if (!productionBranch || !integrationBranch) {
9426
- out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
9427
- continue;
9428
- }
9429
- out.tracks.push({
9430
- name,
9431
- productionBranch,
9432
- integrationBranch,
9433
- baseBranch: baseBranch ?? integrationBranch,
9434
- pullRequestTarget: pullRequestTarget ?? integrationBranch,
9435
- mergeBackBranches,
9436
- naming
9437
- });
9438
9280
  }
9439
- out.protectedBranches = [...protectedNames];
9440
- if (fatal.length)
9441
- out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
9442
- return out;
9443
- }
9444
- function resolveReleaseTrack(input) {
9445
- const tracks = [...input.tracks];
9446
- const issues = [...input.issues ?? []];
9447
- const requested = input.requested?.trim();
9448
- if (tracks.length === 0) {
9449
- if (requested)
9450
- return {
9451
- status: "invalid",
9452
- error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
9453
- issues
9454
- };
9455
- return { status: "none", issues };
9456
- }
9457
- const names = tracks.map((track) => track.name).join(", ");
9458
- let envProblem = null;
9459
- const done = (track, source, detail, warnings = [], ambiguous = null) => ({
9460
- status: "resolved",
9461
- track: track && track.name !== IMPLICIT ? track : null,
9462
- source,
9463
- detail,
9464
- warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
9465
- tracks,
9466
- blocking: envProblem ?? ambiguous
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"
9467
9298
  });
9468
- if (requested) {
9469
- const named = tracks.find((track) => track.name === requested);
9470
- if (!named)
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) {
9471
9345
  return {
9472
9346
  status: "invalid",
9473
- error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
9474
- issues
9347
+ path: file,
9348
+ entries: [],
9349
+ error: `${file} workspaces.${index}.glob: ${glob.error}`
9475
9350
  };
9476
- return done(named, "flag", `track ${named.name} was requested explicitly`);
9351
+ }
9477
9352
  }
9478
- const owned = (branch) => branch !== null && tracks.some((track) => [
9479
- track.integrationBranch,
9480
- track.productionBranch,
9481
- track.baseBranch,
9482
- track.pullRequestTarget
9483
- ].includes(branch));
9484
- const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
9485
- name: IMPLICIT,
9486
- productionBranch: input.defaultBranch,
9487
- integrationBranch: input.defaultBranch,
9488
- baseBranch: input.defaultBranch,
9489
- pullRequestTarget: input.defaultBranch,
9490
- mergeBackBranches: [],
9491
- naming: {}
9492
- } : null;
9493
- const candidates = implicit ? [...tracks, implicit] : tracks;
9494
- const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
9495
- const fallback = (why) => {
9496
- const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
9497
- const track = byDefault.length === 1 ? byDefault[0] : null;
9498
- const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
9499
- 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;
9500
- return done(track, "default", `${why}; using the workspace ${target}`, [
9501
- `release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
9502
- ], ambiguous);
9353
+ return {
9354
+ status: "valid",
9355
+ path: file,
9356
+ entries: validated.data.workspaces ?? [],
9357
+ document: parsed
9503
9358
  };
9504
- const branch = input.branch;
9505
- if (branch) {
9506
- const owners = ownedBy(candidates, branch);
9507
- if (owners.length === 1)
9508
- return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
9509
- if (owners.length > 1)
9510
- 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
+ };
9511
9373
  }
9512
- const env = input.envRequested?.trim();
9513
- if (env) {
9514
- const named = tracks.find((track) => track.name === env);
9515
- if (named)
9516
- return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
9517
- 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
+ };
9518
9385
  }
9519
- if (!branch)
9520
- return fallback("HEAD is detached");
9521
- if (tracks.length === 1 && !implicit)
9522
- return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
9523
- let remotes = null;
9524
- const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
9525
- const ownerOf = (ref) => {
9526
- const owner = ownedBy(candidates, short(ref));
9527
- return owner.length === 1 ? owner[0] : null;
9528
- };
9529
- const followRecorded = (start) => {
9530
- let cursor = start;
9531
- const seen = new Set([start]);
9532
- for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
9533
- const recorded = input.probe.recordedBase(cursor);
9534
- if (!recorded)
9535
- return null;
9536
- const base = short(recorded);
9537
- const owner = ownerOf(base);
9538
- if (owner)
9539
- return owner;
9540
- if (ownedBy(candidates, base).length > 1 || seen.has(base))
9541
- return null;
9542
- seen.add(base);
9543
- 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, "\\$&");
9544
9416
  }
9545
- return null;
9546
- };
9547
- const recordedOwner = followRecorded(branch);
9548
- if (recordedOwner)
9549
- return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
9550
- for (const source of ["reflog", "checkout"]) {
9551
- const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
9552
- if (!ref)
9553
- continue;
9554
- const owner = ownerOf(ref) ?? followRecorded(short(ref));
9555
- if (owner)
9556
- return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
9557
9417
  }
9558
- const forks = [];
9559
- for (const track of candidates)
9560
- for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
9561
- const sha = input.probe.mergeBase(branch, ref);
9562
- if (sha)
9563
- forks.push({ track, ref, sha });
9564
- }
9565
- if (forks.length === 0)
9566
- return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
9567
- const ancestry = new Map;
9568
- const descends = (older, newer) => {
9569
- if (older === newer)
9570
- return false;
9571
- const key = `${older}>${newer}`;
9572
- if (!ancestry.has(key))
9573
- ancestry.set(key, input.probe.isAncestor(older, newer));
9574
- return ancestry.get(key);
9575
- };
9576
- const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
9577
- const winningTracks = [...new Set(winners.map((fork) => fork.track))];
9578
- if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
9579
- const win = winners[0];
9580
- 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
+ };
9581
9429
  }
9582
- if (winningTracks.length === 1)
9583
- return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
9584
- const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
9585
- 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 ")}`);
9586
- }
9587
- function gitTrackProbe(cwd) {
9588
- let remote;
9589
- let remotes;
9590
- const tip = (branch) => {
9591
- remote ??= pushRemoteName(cwd) ?? "origin";
9592
- return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
9593
- };
9594
- return {
9595
- remotes: () => remotes ??= remoteNames(cwd),
9596
- recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
9597
- createdFrom: (branch) => {
9598
- const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
9599
- const oldest = log?.split(`
9600
- `).findLast(Boolean) ?? "";
9601
- const match = /^branch: Created from (\S+)$/u.exec(oldest);
9602
- return match && match[1] !== "HEAD" ? match[1] : null;
9603
- },
9604
- checkedOutFrom: (branch) => {
9605
- const own = reflog(cwd, `refs/heads/${branch}`);
9606
- const created = own.at(-1);
9607
- if (!created || created.subject !== "branch: Created from HEAD")
9608
- return null;
9609
- const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
9610
- const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
9611
- return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
9612
- },
9613
- mergeBase: (branch, trackBranch) => {
9614
- const base = tip(trackBranch);
9615
- return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
9616
- },
9617
- isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
9618
- };
9619
- }
9620
- function workspaceReleaseTracks(cwd) {
9621
- const raw = resolveRuntimeReleaseTracks(cwd);
9622
- return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
9623
- }
9624
- function resolveTrackFor(cwd, options) {
9625
- const read = workspaceReleaseTracks(cwd);
9626
- if (read.error)
9627
- return { status: "invalid", error: read.error, issues: read.issues };
9628
- if (read.tracks.length === 0 && !options.track?.trim())
9629
- return { status: "none", issues: read.issues };
9630
- const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
9631
- const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
9632
- const input = {
9633
- tracks: read.tracks,
9634
- requested: options.track ?? null,
9635
- envRequested,
9636
- branch,
9637
- defaultBranch: options.defaultBranch,
9638
- issues: read.issues
9639
- };
9640
- let key = null;
9641
- if (!options.probe) {
9642
- const refs = git2(cwd, [
9643
- "for-each-ref",
9644
- "--format=%(refname) %(objectname)",
9645
- "refs/heads",
9646
- "refs/remotes"
9647
- ]) ?? "";
9648
- const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
9649
- const own = branch ? [
9650
- git2(cwd, [
9651
- "log",
9652
- "-g",
9653
- "-n",
9654
- "1",
9655
- "--date=unix",
9656
- "--format=%H %gd %gs",
9657
- `refs/heads/${branch}`,
9658
- "--"
9659
- ]) ?? "",
9660
- git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
9661
- ] : [];
9662
- key = createHash4("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
9663
- const hit = memo2.get(key);
9664
- if (hit)
9665
- return hit;
9666
- }
9667
- const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
9668
- if (key) {
9669
- if (memo2.size >= MEMO_LIMIT)
9670
- memo2.delete(memo2.keys().next().value);
9671
- 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
+ };
9672
9438
  }
9673
- return resolution;
9674
- }
9675
- 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) => [
9676
- ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
9677
- ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
9678
- ].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
9679
- ...new Set(tracks.flatMap((track) => [
9680
- track.productionBranch,
9681
- track.integrationBranch,
9682
- track.baseBranch,
9683
- track.pullRequestTarget,
9684
- ...track.mergeBackBranches
9685
- ]))
9686
- ], 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) => {
9687
- const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
9688
- const remote = remotes.find((name) => bare.startsWith(`${name}/`));
9689
- return remote ? bare.slice(remote.length + 1) : bare;
9690
- }, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
9691
- const run = spawnSync4("git", args, {
9692
- cwd,
9693
- encoding: "utf8",
9694
- timeout: 20000,
9695
- windowsHide: true,
9696
- stdio: ["ignore", "pipe", "ignore"]
9697
- });
9698
- return run.status === 0 ? (run.stdout ?? "").trim() : null;
9699
- }, 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(`
9700
- `).map((line) => {
9701
- const [sha = "", selector = "", ...subject] = line.split("\t");
9702
- return {
9703
- sha,
9704
- time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
9705
- subject: subject.join("\t")
9706
- };
9707
- }).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
9708
- var init_release_tracks = __esm(() => {
9709
- init_rev();
9710
- init_config();
9711
- init_workspaces();
9712
- 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;
9713
9456
  };
9714
- KNOWN_FIELDS = new Set([
9715
- "strategy",
9716
- "productionBranch",
9717
- "integrationBranch",
9718
- "naming",
9719
- "baseBranch",
9720
- "mergeBackBranches",
9721
- "pullRequestTarget",
9722
- "tagNamespace",
9723
- "versionSource",
9724
- "requiredChecks",
9725
- "critical"
9726
- ]);
9727
- BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
9728
- PLACEHOLDER = /\{[^}]*\}/gu;
9729
- HAS_PLACEHOLDER = /\{[^}]*\}/u;
9730
- memo2 = new Map;
9731
- });
9732
-
9733
- // packages/workit-core/src/core/vcs-config.ts
9734
- import fs2 from "node:fs";
9735
- import path4 from "node:path";
9736
- import { spawnSync as spawnSync5 } from "node:child_process";
9737
- function vcsConfig(mode, cwd, options = {}) {
9738
- const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
9739
- const wsVcs = ws?.vcs ?? {};
9740
- const wsYt = ws?.youtrack ?? {};
9741
- const wsIssues = ws?.issues ?? {};
9742
- const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
9743
- const root = vcsCwd(cwd);
9744
- const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
9745
- const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
9746
- const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
9747
- const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
9748
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
9749
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
9750
- let issuesProvider = null;
9751
- let linkOnPr = null;
9752
- if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
9753
- issuesProvider = "github";
9754
- 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);
9755
9463
  }
9756
- if (mode === "resolve") {
9757
- if (cfgStatus === "malformed")
9758
- return { ok: false, error: cfgError, configPath: cfgPath };
9759
- const trackResolution = resolveTrackFor(root, {
9760
- track: options.track,
9761
- ...options.branch !== undefined ? { branch: options.branch } : {},
9762
- 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));
9763
9473
  });
9764
- if (trackResolution.status === "invalid")
9765
- return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
9766
- const releaseTrack = trackSummary(trackResolution);
9767
- const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
9768
- const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
9769
- return {
9770
- ok: true,
9771
- workspace_name: ws?.name ?? null,
9772
- provider,
9773
- defaultTargetBranch: defaultTarget,
9774
- workspaceDefaultTargetBranch: workspaceDefault,
9775
- baseBranch,
9776
- releaseTrack,
9777
- link_issues: linkIssues,
9778
- youtrack_base_url: youtrackBaseUrl,
9779
- issues_provider: issuesProvider,
9780
- link_on_pr: linkOnPr
9781
- };
9782
- }
9783
- if (cfgStatus === "malformed") {
9784
- 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;
9785
9486
  }
9786
- if (cfgStatus === "missing" && provider === null)
9787
- return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
9788
- if (provider === null) {
9789
- return {
9790
- ok: false,
9791
- error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
9792
- configPath: cfgPath
9793
- };
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}`);
9794
9506
  }
9795
- const prov = cfg[provider] ?? {};
9796
- const out = {
9797
- ok: true,
9798
- configPath: path4.resolve(cfgPath),
9799
- provider,
9800
- defaultTargetBranch: workspaceDefault,
9801
- pr: cfg.pr ?? {},
9802
- workspace_name: ws?.name ?? null,
9803
- link_issues: linkIssues,
9804
- youtrack_base_url: youtrackBaseUrl,
9805
- issues_provider: issuesProvider,
9806
- link_on_pr: linkOnPr
9807
- };
9808
- if (provider === "gitlab") {
9809
- out.gitlab = {
9810
- host: prov.host ?? "gitlab.com",
9811
- apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
9812
- };
9813
- } else if (provider === "github") {
9814
- 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(", ")}`);
9815
9513
  }
9816
- return out;
9817
- }
9818
- var workspacesPath = () => path4.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
9819
- const r = spawnSync5("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
9820
- if (r.status !== 0)
9821
- return null;
9822
- const url = (r.stdout ?? "").trim();
9823
- if (/github\.com[:/]/.test(url))
9824
- return "github";
9825
- if (/gitlab\.com[:/]/.test(url))
9826
- return "gitlab";
9827
- return null;
9828
- }, readVcsConfig = () => {
9829
- 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");
9830
9541
  let raw;
9831
9542
  try {
9832
- raw = fs2.readFileSync(file, "utf8");
9833
- } catch {
9834
- 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 });
9835
9548
  }
9836
9549
  let parsed;
9837
9550
  try {
9838
9551
  parsed = JSON.parse(raw);
9839
9552
  } catch {
9840
- return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
9553
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
9841
9554
  }
9842
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9843
- 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}`);
9844
9559
  }
9845
- return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
9846
- }, trackSummary = (resolution) => resolution.status === "resolved" ? {
9847
- name: resolution.track?.name ?? null,
9848
- source: resolution.source,
9849
- detail: resolution.detail,
9850
- warnings: resolution.warnings,
9851
- track: resolution.track,
9852
- tracks: resolution.tracks,
9853
- blocking: resolution.blocking
9854
- } : null;
9855
- var init_vcs_config = __esm(() => {
9856
- init_config();
9857
- init_workspaces();
9858
- init_release_tracks();
9859
- });
9860
-
9861
- // packages/workit-core/src/core/logger.ts
9862
- import os3 from "node:os";
9863
- import path5 from "node:path";
9864
- var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
9865
- const override = process.env.WORKFLOW_TOOLKIT_STATE;
9866
- if (override)
9867
- return override;
9868
- const home = os3.homedir();
9869
- if (process.env.XDG_STATE_HOME)
9870
- return path5.join(process.env.XDG_STATE_HOME, "workit");
9871
- switch (os3.platform()) {
9872
- case "darwin":
9873
- return path5.join(home, "Library", "Application Support", "workit");
9874
- case "win32":
9875
- return path5.join(process.env.LOCALAPPDATA ?? path5.join(home, "AppData", "Local"), "workit");
9876
- default:
9877
- 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}`);
9878
9565
  }
9879
- }, homePattern;
9880
- var init_logger = __esm(() => {
9881
- SENSITIVE_WORDS = new Set([
9882
- "token",
9883
- "secret",
9884
- "password",
9885
- "passwd",
9886
- "authorization",
9887
- "credential",
9888
- "credentials",
9889
- "cookie",
9890
- "api",
9891
- "apikey",
9892
- "apitoken",
9893
- "accesstoken",
9894
- "clientsecret",
9895
- "refreshtoken",
9896
- "key",
9897
- "bearer",
9898
- "prompt",
9899
- "message",
9900
- "messages",
9901
- "content",
9902
- "body",
9903
- "args",
9904
- "argument",
9905
- "result",
9906
- "results",
9907
- "output",
9908
- "issue",
9909
- "description",
9910
- "summary",
9911
- "payload",
9912
- "script",
9913
- "command",
9914
- "text",
9915
- "query"
9916
- ]);
9917
- STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
9918
- homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
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()
9654
+ ]);
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();
9919
9750
  });
9920
9751
 
9921
- // packages/workit-core/src/store/paths.ts
9922
- import { spawnSync as spawnSync6 } from "node:child_process";
9752
+ // packages/workit-core/src/core/release-tracks.ts
9923
9753
  import { createHash as createHash5 } from "node:crypto";
9924
- import fs3 from "node:fs";
9925
- import path6 from "node:path";
9926
- function fastGit(root) {
9927
- if (DISCOVERY_ENV.some((name) => process.env[name]))
9928
- return null;
9929
- let dir = real(root);
9930
- let device = null;
9931
- for (;; ) {
9932
- let stat;
9933
- try {
9934
- stat = fs3.statSync(dir);
9935
- } catch {
9936
- 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;
9937
9770
  }
9938
- if (device !== null && stat.dev !== device)
9939
- return { kind: "none" };
9940
- device = stat.dev;
9941
- const dotGit = path6.join(dir, ".git");
9942
- let entry = null;
9943
- try {
9944
- entry = fs3.statSync(dotGit);
9945
- } catch {}
9946
- let gitDir = null;
9947
- let top = null;
9948
- if (entry) {
9949
- top = dir;
9950
- if (entry.isDirectory())
9951
- gitDir = dotGit;
9952
- else if (entry.isFile()) {
9953
- let text = "";
9954
- try {
9955
- text = fs3.readFileSync(dotGit, "utf8");
9956
- } catch {
9957
- return null;
9958
- }
9959
- const match = /^gitdir: (.+)$/mu.exec(text);
9960
- if (!match)
9961
- return null;
9962
- gitDir = path6.resolve(dir, match[1].trim());
9963
- } else
9964
- return null;
9965
- if (!fs3.existsSync(path6.join(gitDir, "HEAD")))
9966
- return null;
9967
- } else if (isGitDir(dir))
9968
- gitDir = dir;
9969
- if (gitDir) {
9970
- let common = gitDir;
9971
- try {
9972
- common = path6.resolve(gitDir, fs3.readFileSync(path6.join(gitDir, "commondir"), "utf8").trim());
9973
- } catch {}
9974
- if (!fs3.existsSync(path6.join(common, "objects")))
9975
- return null;
9976
- return {
9977
- kind: "repo",
9978
- common: real(common),
9979
- gitDir: real(gitDir),
9980
- top: top === null ? null : real(top),
9981
- reftable: reftableRefs(common)
9982
- };
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;
9983
9776
  }
9984
- const parent = path6.dirname(dir);
9985
- if (parent === dir)
9986
- return { kind: "none" };
9987
- dir = parent;
9988
- }
9989
- }
9990
- function resolveStore(root) {
9991
- root = path6.resolve(root);
9992
- let directory = false;
9993
- try {
9994
- directory = fs3.statSync(root).isDirectory();
9995
- } catch {}
9996
- if (!directory)
9997
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
9998
- const fast = fastGit(root);
9999
- if (fast?.kind === "none")
10000
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
10001
- if (fast?.kind === "repo")
10002
- return {
10003
- dir: path6.join(fast.common, "workit"),
10004
- shared: true,
10005
- top: fast.top,
10006
- 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}`);
10007
9788
  };
10008
- const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
10009
- const common = run.stdout.trim();
10010
- if (run.ok && common) {
10011
- const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10012
- return {
10013
- dir: path6.join(real(path6.resolve(root, common)), "workit"),
10014
- shared: true,
10015
- 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;
10016
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
+ });
10017
9846
  }
10018
- if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
10019
- return { dir: path6.join(root, ".workit"), shared: false, top: null };
10020
- return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
10021
- `)[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;
10022
9851
  }
10023
- function resolveTaskKey(root, location) {
10024
- if (!location.shared)
10025
- return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
10026
- const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
10027
- if (location.git && !location.git.reftable) {
10028
- let head = "";
10029
- try {
10030
- head = fs3.readFileSync(path6.join(location.git.gitDir, "HEAD"), "utf8").trim();
10031
- } catch {}
10032
- const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
10033
- if (ref && ref[1] !== ".invalid")
10034
- return branchKey(ref[1]);
10035
- if (/^[0-9a-f]{40,64}$/u.test(head)) {
10036
- const rebasing = rebasingBranch(location.git.gitDir);
10037
- if (rebasing)
10038
- 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)
10039
9858
  return {
10040
- key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
10041
- kind: "detached",
10042
- branch: null
9859
+ status: "invalid",
9860
+ error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
9861
+ issues
10043
9862
  };
10044
- }
9863
+ return { status: "none", issues };
10045
9864
  }
10046
- const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
10047
- const name = branch.ok ? branch.stdout.trim() : "";
10048
- if (name && name !== ".invalid")
10049
- return branchKey(name);
10050
- const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
10051
- if (gitDir.ok && gitDir.stdout.trim()) {
10052
- const rebasing = rebasingBranch(gitDir.stdout.trim());
10053
- if (rebasing)
10054
- 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`);
10055
9885
  }
10056
- const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10057
- const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
10058
- return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
10059
- }
10060
- function checkoutRootOf(root, location) {
10061
- const resolved = path6.resolve(root);
10062
- let canonical = resolved;
10063
- try {
10064
- canonical = fs3.realpathSync(resolved);
10065
- } catch {
10066
- 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(", ")})`);
10067
9919
  }
10068
- const found = location ?? resolveStore(canonical);
10069
- if (found instanceof Error || !found.shared || !found.top)
10070
- return canonical;
10071
- if (real(canonical) === found.top)
10072
- return canonical;
10073
- try {
10074
- return fs3.realpathSync(found.top);
10075
- } catch {
10076
- 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`;
10077
9926
  }
10078
- }
10079
- function branchRenames(location) {
10080
- if (!location.shared)
10081
- return [];
10082
- const files = [];
10083
- const walk = (dir) => {
10084
- let entries;
10085
- try {
10086
- entries = fs3.readdirSync(dir, { withFileTypes: true });
10087
- } catch {
10088
- 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;
10089
9952
  }
10090
- for (const entry of entries)
10091
- if (entry.isDirectory())
10092
- walk(path6.join(dir, entry.name));
10093
- else
10094
- 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
10095
10047
  };
10096
- walk(path6.join(path6.dirname(location.dir), "logs", "refs", "heads"));
10097
- if (location.git)
10098
- files.push(path6.join(location.git.gitDir, "logs", "HEAD"));
10099
- const seen = new Set;
10100
- const renames = [];
10101
- for (const file of files) {
10102
- let text = "";
10103
- try {
10104
- text = fs3.readFileSync(file, "utf8");
10105
- } catch {
10106
- continue;
10107
- }
10108
- for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
10109
- const rename = { from: match[2], to: match[3], at: Number(match[1]) };
10110
- const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
10111
- if (seen.has(id))
10112
- continue;
10113
- seen.add(id);
10114
- renames.push(rename);
10115
- }
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;
10116
10074
  }
10117
- return renames.toSorted((left, right) => left.at - right.at);
10118
- }
10119
- function followRenames(key, boundAt, renames) {
10120
- let current = key;
10121
- let since = Math.floor(Date.parse(boundAt) / 1000);
10122
- for (const rename of renames)
10123
- if (rename.from === current && rename.at >= since) {
10124
- current = rename.to;
10125
- since = rename.at;
10126
- }
10127
- 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;
10128
10082
  }
10129
- 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) => {
10130
10099
  const run = spawnSync6("git", args, {
10131
10100
  cwd,
10132
10101
  encoding: "utf8",
10133
10102
  timeout: 20000,
10134
- killSignal: "SIGKILL",
10135
10103
  windowsHide: true,
10136
- env: {
10137
- ...process.env,
10138
- GIT_TERMINAL_PROMPT: "0",
10139
- GIT_OPTIONAL_LOCKS: "0",
10140
- LC_ALL: "C",
10141
- LANGUAGE: "C"
10142
- }
10104
+ stdio: ["ignore", "pipe", "ignore"]
10143
10105
  });
10144
- 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");
10145
10110
  return {
10146
- ok: run.status === 0,
10147
- stdout: run.stdout ?? "",
10148
- stderr: error ? String(error.message) : run.stderr ?? "",
10149
- missing: error?.code === "ENOENT"
10111
+ sha,
10112
+ time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
10113
+ subject: subject.join("\t")
10150
10114
  };
10151
- }, shortHash = (value) => createHash5("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
10152
- try {
10153
- return fs3.realpathSync.native(file);
10154
- } catch {
10155
- 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;
10156
10163
  }
10157
- }, 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;
10158
10239
  try {
10159
- 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");
10160
10241
  } catch {
10161
- return false;
10242
+ return { status: "missing", path: file, config: {} };
10162
10243
  }
10163
- }, reftableRefs = (common) => {
10244
+ let parsed;
10164
10245
  try {
10165
- return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs3.readFileSync(path6.join(common, "config"), "utf8"));
10246
+ parsed = JSON.parse(raw);
10166
10247
  } catch {
10167
- return false;
10248
+ return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
10168
10249
  }
10169
- }, rebasingBranch = (gitDir) => {
10170
- for (const state of ["rebase-merge", "rebase-apply"]) {
10171
- try {
10172
- const ref = fs3.readFileSync(path6.join(gitDir, state, "head-name"), "utf8").trim();
10173
- const match = /^refs\/heads\/(.+)$/u.exec(ref);
10174
- if (match)
10175
- return match[1];
10176
- } 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");
10177
10286
  }
10178
- return null;
10179
- }, checkoutSlug = (root) => {
10180
- let canonical = real(root);
10181
- if (process.platform === "win32")
10182
- canonical = canonical.toLowerCase();
10183
- const base = path6.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
10184
- return `${base}-${shortHash(canonical)}`;
10185
- };
10186
- var init_paths = __esm(() => {
10187
- NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
10188
- DISCOVERY_ENV = [
10189
- "GIT_DIR",
10190
- "GIT_COMMON_DIR",
10191
- "GIT_WORK_TREE",
10192
- "GIT_CEILING_DIRECTORIES",
10193
- "GIT_DISCOVERY_ACROSS_FILESYSTEM"
10194
- ];
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");
10195
10327
  });
10196
10328
 
10197
10329
  // packages/workit-core/src/host-session.ts
10198
- import { createHash as createHash6 } from "node:crypto";
10199
- import fs4 from "node:fs";
10330
+ import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
10331
+ import fs5 from "node:fs";
10200
10332
  import path7 from "node:path";
10201
10333
  function hostSessionFromEnv(env) {
10202
10334
  const value = (key) => env[key]?.trim() || null;
@@ -10225,7 +10357,7 @@ function migrateRepoIssued(cwd) {
10225
10357
  if (location instanceof Error)
10226
10358
  return;
10227
10359
  old = path7.join(location.dir, "hooks", "issued");
10228
- if (!fs4.existsSync(old))
10360
+ if (!fs5.existsSync(old))
10229
10361
  return;
10230
10362
  } catch {
10231
10363
  return;
@@ -10233,13 +10365,18 @@ function migrateRepoIssued(cwd) {
10233
10365
  for (const lead of safeList(old)) {
10234
10366
  try {
10235
10367
  const from = path7.join(old, lead);
10236
- if (!fs4.statSync(from).isDirectory())
10368
+ if (!fs5.statSync(from).isDirectory())
10237
10369
  continue;
10238
10370
  const to = path7.join(userIssuedRoot(), lead);
10239
- fs4.mkdirSync(to, { recursive: true, mode: 448 });
10240
- for (const id of fs4.readdirSync(from)) {
10241
- fs4.copyFileSync(path7.join(from, id), path7.join(to, id));
10242
- 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 });
10243
10380
  }
10244
10381
  removeIfEmpty(from);
10245
10382
  } catch {}
@@ -10247,30 +10384,76 @@ function migrateRepoIssued(cwd) {
10247
10384
  removeIfEmpty(old);
10248
10385
  }
10249
10386
  function recordIssuedSession(cwd, lead, issued) {
10387
+ const token = randomBytes(18).toString("base64url");
10250
10388
  try {
10251
10389
  migrateRepoIssued(cwd);
10252
- const dir = path7.join(userIssuedRoot(), hash(lead));
10253
- fs4.mkdirSync(dir, { recursive: true, mode: 448 });
10254
10390
  const own = hash(issued);
10255
- fs4.writeFileSync(path7.join(dir, own), ISSUED_SESSIONS_MARKER);
10256
- 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);
10257
10408
  if (entries.length <= ISSUED_PER_LEAD)
10258
10409
  return;
10259
- 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);
10260
- for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
10261
- 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
+ }
10262
10415
  } catch {}
10263
10416
  }
10264
- 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) => {
10265
10427
  try {
10266
- return fs4.readdirSync(dir);
10428
+ return fs5.readdirSync(dir);
10267
10429
  } catch {
10268
10430
  return [];
10269
10431
  }
10270
10432
  }, removeIfEmpty = (dir) => {
10271
10433
  try {
10272
- fs4.rmdirSync(dir);
10434
+ fs5.rmdirSync(dir);
10273
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
+ }
10274
10457
  }, OWN_CHILD_SHELL;
10275
10458
  var init_host_session = __esm(() => {
10276
10459
  init_logger();
@@ -10283,13 +10466,14 @@ var init_host_session = __esm(() => {
10283
10466
  (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
10284
10467
  ]
10285
10468
  ];
10469
+ TOKEN_HASH = /^([0-9a-f]{64})$/mu;
10286
10470
  OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
10287
10471
  });
10288
10472
 
10289
10473
  // packages/workit-core/src/ledger.ts
10290
- import { spawnSync as spawnSync7 } from "node:child_process";
10291
- import { createHash as createHash7, randomBytes } from "node:crypto";
10292
- 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";
10293
10477
  import path8 from "node:path";
10294
10478
  function resolveCommit(cwd, ref) {
10295
10479
  if (!safeRef(ref))
@@ -10365,7 +10549,7 @@ function ledgerPath(cwd) {
10365
10549
  const root = storeRoot(cwd);
10366
10550
  return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
10367
10551
  }
10368
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10552
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
10369
10553
  const lock = `${file}.lock`;
10370
10554
  const deadline = Date.now() + waitMs;
10371
10555
  let token = null;
@@ -10389,13 +10573,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10389
10573
  }
10390
10574
  function appendLine(file, build, options) {
10391
10575
  const write = () => {
10392
- const fd = fs5.openSync(file, "a+");
10576
+ const fd = fs6.openSync(file, "a+", 384);
10393
10577
  try {
10394
- const size = fs5.fstatSync(fd).size;
10578
+ if (process.platform !== "win32")
10579
+ tighten(fd);
10580
+ const size = fs6.fstatSync(fd).size;
10395
10581
  let fence = "";
10396
10582
  if (size > 0) {
10397
10583
  const last = Buffer.alloc(1);
10398
- fs5.readSync(fd, last, 0, 1, size - 1);
10584
+ fs6.readSync(fd, last, 0, 1, size - 1);
10399
10585
  if (last[0] !== 10)
10400
10586
  fence = `
10401
10587
  `;
@@ -10404,16 +10590,16 @@ function appendLine(file, build, options) {
10404
10590
  if (!line.ok)
10405
10591
  return line;
10406
10592
  const bytes = Buffer.from(fence + line.value);
10407
- const written = fs5.writeSync(fd, bytes);
10593
+ const written = fs6.writeSync(fd, bytes);
10408
10594
  if (written !== bytes.length)
10409
10595
  return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
10410
10596
  return { ok: true, value: undefined };
10411
10597
  } finally {
10412
- fs5.closeSync(fd);
10598
+ fs6.closeSync(fd);
10413
10599
  }
10414
10600
  };
10415
10601
  try {
10416
- fs5.mkdirSync(path8.dirname(file), { recursive: true });
10602
+ fs6.mkdirSync(path8.dirname(file), { recursive: true });
10417
10603
  if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
10418
10604
  return write();
10419
10605
  const locked = withLedgerLock(file, write);
@@ -10467,8 +10653,10 @@ function readLedger(cwd, options = {}) {
10467
10653
  if (!file.ok)
10468
10654
  return file;
10469
10655
  let text = "";
10656
+ let stat = null;
10470
10657
  try {
10471
- text = fs5.readFileSync(file.value, "utf8");
10658
+ stat = fs6.statSync(file.value);
10659
+ text = fs6.readFileSync(file.value, "utf8");
10472
10660
  } catch (error) {
10473
10661
  if (error.code === "ENOENT")
10474
10662
  return {
@@ -10487,6 +10675,7 @@ function readLedger(cwd, options = {}) {
10487
10675
  const integrity = { chained: 0, legacy: 0, unverified: [] };
10488
10676
  const earlier = new Set([null]);
10489
10677
  let chainStarted = false;
10678
+ const ids = new Set;
10490
10679
  for (const raw of text.split(`
10491
10680
  `)) {
10492
10681
  const trimmed = raw.trim();
@@ -10520,6 +10709,10 @@ function readLedger(cwd, options = {}) {
10520
10709
  reason = "unsigned";
10521
10710
  else
10522
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);
10523
10716
  if (reason)
10524
10717
  rowIntegrity = "unverified";
10525
10718
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -10543,7 +10736,8 @@ function readLedger(cwd, options = {}) {
10543
10736
  seq: rows.length + 1,
10544
10737
  superseded: false,
10545
10738
  supersedeIgnored: false,
10546
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
10739
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
10740
+ ...reason ? { integrityReason: reason } : {}
10547
10741
  };
10548
10742
  rows.push(row);
10549
10743
  if (!options.integrity)
@@ -10571,18 +10765,44 @@ function readLedger(cwd, options = {}) {
10571
10765
  const byId = new Map;
10572
10766
  for (const row of rows) {
10573
10767
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
10574
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10768
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10575
10769
  target.superseded = true;
10576
10770
  else if (row.supersedes)
10577
10771
  row.supersedeIgnored = true;
10578
10772
  if (!byId.has(row.id))
10579
10773
  byId.set(row.id, row);
10580
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
+ });
10581
10781
  return {
10582
10782
  ok: true,
10583
10783
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
10584
10784
  };
10585
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
+ }
10586
10806
  function verdictBasis(verdict, current) {
10587
10807
  if (verdict.head && verdict.head === current.head)
10588
10808
  return "fresh";
@@ -10649,15 +10869,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
10649
10869
  sessions.add(session);
10650
10870
  return sessions;
10651
10871
  }
10652
- 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) {
10653
10907
  let effective = null;
10654
10908
  for (const row of rows) {
10655
10909
  if (!effective) {
10656
10910
  effective = row;
10657
10911
  continue;
10658
10912
  }
10659
- const rowIndependent = independenceReasons(row, authors).length === 0;
10660
- 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;
10661
10917
  if (effIndependent && !rowIndependent && isCurrent(effective))
10662
10918
  continue;
10663
10919
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -10671,7 +10927,7 @@ function restackCarries(rows, branch, from, to) {
10671
10927
  return false;
10672
10928
  const edges = new Map;
10673
10929
  for (const row of rows)
10674
- 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)
10675
10931
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
10676
10932
  const seen = new Set([from]);
10677
10933
  const queue = [from];
@@ -10687,7 +10943,8 @@ function restackCarries(rows, branch, from, to) {
10687
10943
  }
10688
10944
  return false;
10689
10945
  }
10690
- function checkVerdicts(cwd, branch, rows) {
10946
+ function checkVerdicts(cwd, branch, ledgerRows) {
10947
+ const rows = withIntegrity(cwd, ledgerRows);
10691
10948
  const head = branchHead(cwd, branch);
10692
10949
  const fallbackBase = defaultBase(cwd);
10693
10950
  const scope = branchScope(cwd, branch);
@@ -10717,21 +10974,47 @@ function checkVerdicts(cwd, branch, rows) {
10717
10974
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
10718
10975
  };
10719
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);
10720
11003
  const byKind = new Map;
10721
11004
  for (const row of rows)
10722
- if (isLiveVerdict(row) && scope(row)) {
11005
+ if (isLiveVerdict(row, retired) && scope(row)) {
10723
11006
  const kind = typeof row.kind === "string" ? row.kind : "review";
10724
11007
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
10725
11008
  }
10726
11009
  const verdicts = [];
10727
11010
  const warnings = [];
10728
11011
  for (const [kind, list] of byKind) {
10729
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
11012
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
10730
11013
  if (!row)
10731
11014
  continue;
10732
11015
  const basis = basisOf(row);
10733
11016
  const current = basis !== "stale";
10734
- const independence = independenceReasons(row, authors);
11017
+ const independence = reasonsOf(row);
10735
11018
  const reasons = [
10736
11019
  ...current ? [] : ["stale"],
10737
11020
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -10748,23 +11031,31 @@ function checkVerdicts(cwd, branch, rows) {
10748
11031
  });
10749
11032
  if (reasons.length === 0) {
10750
11033
  for (const later of list)
10751
- 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")
10752
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`);
10753
11036
  }
10754
11037
  }
10755
11038
  for (const row of rows)
10756
11039
  if (row.integrity === "unverified" && scope(row))
10757
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
+ }
10758
11049
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
10759
11050
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
10760
11051
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
10761
11052
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
10762
- 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: [] } : {
10763
11054
  accepted: false,
10764
11055
  verdict: failing?.verdict ?? null,
10765
11056
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
10766
11057
  };
10767
- 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)));
10768
11059
  return {
10769
11060
  branch,
10770
11061
  head,
@@ -10785,7 +11076,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10785
11076
  error,
10786
11077
  ...unblock ? { unblock } : {}
10787
11078
  }), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
10788
- const run = spawnSync7("git", args, {
11079
+ const run = spawnSync8("git", args, {
10789
11080
  cwd,
10790
11081
  encoding: "utf8",
10791
11082
  timeout: timeoutMs,
@@ -10819,24 +11110,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10819
11110
  } catch {
10820
11111
  return null;
10821
11112
  }
10822
- }, 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) => {
10823
11114
  if (process.platform !== "linux")
10824
11115
  return false;
10825
11116
  try {
10826
- return isNetworkFsType(fs5.statfsSync(dir).type);
11117
+ return isNetworkFsType(fs6.statfsSync(dir).type);
10827
11118
  } catch {
10828
11119
  return false;
10829
11120
  }
10830
11121
  }, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
10831
11122
  try {
10832
- return fs5.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
11123
+ return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
10833
11124
  } catch {
10834
11125
  return null;
10835
11126
  }
10836
11127
  }, lockAgeMs = (lock) => {
10837
11128
  for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
10838
11129
  try {
10839
- return Date.now() - fs5.statSync(target).mtimeMs;
11130
+ return Date.now() - fs6.statSync(target).mtimeMs;
10840
11131
  } catch {}
10841
11132
  }
10842
11133
  return null;
@@ -10844,7 +11135,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10844
11135
  ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10845
11136
  for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
10846
11137
  const buffer = Buffer.alloc(span);
10847
- fs5.readSync(fd, buffer, 0, span, size - span);
11138
+ fs6.readSync(fd, buffer, 0, span, size - span);
10848
11139
  const end = buffer.lastIndexOf(10);
10849
11140
  if (end < 0) {
10850
11141
  if (span === size)
@@ -10872,6 +11163,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10872
11163
  return text || null;
10873
11164
  }
10874
11165
  return null;
11166
+ }, tighten = (fd) => {
11167
+ try {
11168
+ if ((fs6.fstatSync(fd).mode & 63) !== 0)
11169
+ fs6.fchmodSync(fd, 384);
11170
+ } catch {}
10875
11171
  }, sealed = (row, id, at, previous) => {
10876
11172
  const { prevHash: _prev, rowHash: _row, ...fields } = row;
10877
11173
  const prevHash = previous === null ? null : sha2562(previous);
@@ -10886,7 +11182,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10886
11182
  session: str(actor.session),
10887
11183
  agentId: str(actor.agentId)
10888
11184
  };
10889
- }, 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) => {
10890
11186
  return (target) => {
10891
11187
  const own = typeof branch === "string" ? branch : null;
10892
11188
  const theirs = typeof target.branch === "string" ? target.branch : null;
@@ -10903,7 +11199,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10903
11199
  if (derivesFrom(author, session) || derivesFrom(session, author))
10904
11200
  return true;
10905
11201
  return false;
10906
- }, independenceReasons = (row, authors) => {
11202
+ }, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
10907
11203
  const reasons = [];
10908
11204
  if (!row.actor.session)
10909
11205
  reasons.push("no_session");
@@ -10914,6 +11210,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10914
11210
  return reasons;
10915
11211
  }, OBSERVED_ONLY;
10916
11212
  var init_ledger = __esm(() => {
11213
+ init_store_lock();
10917
11214
  init_vcs_config();
10918
11215
  init_rev();
10919
11216
  init_host_session();
@@ -10937,56 +11234,73 @@ var init_ledger = __esm(() => {
10937
11234
  ledgerLock = {
10938
11235
  acquire(lock) {
10939
11236
  try {
10940
- fs5.mkdirSync(lock);
11237
+ fs6.mkdirSync(lock);
10941
11238
  } catch (error) {
10942
11239
  if (error.code === "EEXIST")
10943
11240
  return null;
10944
11241
  throw error;
10945
11242
  }
10946
- const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
10947
- 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);
10948
11245
  return token;
10949
11246
  },
10950
11247
  release(lock, token) {
10951
11248
  if (lockToken(lock) !== token)
10952
11249
  return false;
10953
- const tomb = `${lock}.release-${randomBytes(6).toString("hex")}`;
11250
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
10954
11251
  try {
10955
- fs5.renameSync(lock, tomb);
11252
+ fs6.renameSync(lock, tomb);
10956
11253
  } catch {
10957
11254
  return false;
10958
11255
  }
10959
11256
  if (lockToken(tomb) !== token) {
10960
11257
  try {
10961
- fs5.renameSync(tomb, lock);
11258
+ fs6.renameSync(tomb, lock);
10962
11259
  } catch {}
10963
11260
  return false;
10964
11261
  }
10965
- fs5.rmSync(tomb, { recursive: true, force: true });
11262
+ fs6.rmSync(tomb, { recursive: true, force: true });
10966
11263
  return true;
10967
11264
  },
10968
11265
  token: lockToken,
10969
11266
  reap(lock, observed) {
10970
- const tomb = `${lock}.reap-${randomBytes(6).toString("hex")}`;
11267
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
10971
11268
  try {
10972
- fs5.renameSync(lock, tomb);
11269
+ fs6.renameSync(lock, tomb);
10973
11270
  } catch {
10974
11271
  return false;
10975
11272
  }
10976
11273
  if (lockToken(tomb) === observed) {
10977
- fs5.rmSync(tomb, { recursive: true, force: true });
11274
+ fs6.rmSync(tomb, { recursive: true, force: true });
10978
11275
  return true;
10979
11276
  }
10980
11277
  try {
10981
- fs5.renameSync(tomb, lock);
11278
+ fs6.renameSync(tomb, lock);
10982
11279
  } catch {
10983
- fs5.rmSync(tomb, { recursive: true, force: true });
11280
+ fs6.rmSync(tomb, { recursive: true, force: true });
10984
11281
  }
10985
11282
  return false;
10986
11283
  }
10987
11284
  };
10988
11285
  TAIL_BYTES = 64 * 1024;
11286
+ TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
11287
+ tamperCache = new Map;
10989
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
+ ]);
10990
11304
  OBSERVED_ONLY = new Set([
10991
11305
  "check",
10992
11306
  "merge.unverified",
@@ -11936,7 +12250,7 @@ function normalizeMaxBytes(value, options = {}) {
11936
12250
  }
11937
12251
 
11938
12252
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
11939
- import fs6 from "node:fs";
12253
+ import fs7 from "node:fs";
11940
12254
  function createScratchBuffer(maxBytes) {
11941
12255
  const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
11942
12256
  return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
@@ -11959,7 +12273,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
11959
12273
  let total = 0;
11960
12274
  while (true) {
11961
12275
  const length = nextReadLength(total, maxBytes, scratch.length);
11962
- const bytesRead = fs6.readSync(fd, scratch, 0, length, null);
12276
+ const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
11963
12277
  if (bytesRead === 0) {
11964
12278
  return Buffer.concat(chunks, total);
11965
12279
  }
@@ -11973,9 +12287,9 @@ var init_bounded_read = __esm(() => {
11973
12287
  });
11974
12288
 
11975
12289
  // node_modules/@openclaw/fs-safe/dist/read-open-flags.js
11976
- import fs7 from "node:fs";
12290
+ import fs8 from "node:fs";
11977
12291
  function resolveReadOpenFlags(options) {
11978
- const constants = options?.constants ?? fs7.constants;
12292
+ const constants = options?.constants ?? fs8.constants;
11979
12293
  const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
11980
12294
  const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
11981
12295
  return constants.O_RDONLY | noFollow | nonBlocking;
@@ -11983,7 +12297,7 @@ function resolveReadOpenFlags(options) {
11983
12297
  var init_read_open_flags = () => {};
11984
12298
 
11985
12299
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
11986
- import { randomBytes as randomBytes2 } from "node:crypto";
12300
+ import { randomBytes as randomBytes3 } from "node:crypto";
11987
12301
  import fsSync from "node:fs";
11988
12302
  import path11 from "node:path";
11989
12303
  function parseSidecarLockSnapshot(snapshot, parser) {
@@ -11991,7 +12305,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
11991
12305
  }
11992
12306
  function createSidecarLockOwnershipToken() {
11993
12307
  let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
11994
- for (const byte of randomBytes2(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
12308
+ for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
11995
12309
  for (let bit = 7;bit >= 0; bit -= 1) {
11996
12310
  token += byte & 1 << bit ? "\t" : " ";
11997
12311
  }
@@ -12188,7 +12502,7 @@ function sleepSync(ms) {
12188
12502
  }
12189
12503
 
12190
12504
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
12191
- import fs8 from "node:fs";
12505
+ import fs9 from "node:fs";
12192
12506
  import path12 from "node:path";
12193
12507
  function getSyncHeldLocks() {
12194
12508
  const globalWithState = globalThis;
@@ -12206,7 +12520,7 @@ function releaseAllSyncHeldLocks() {
12206
12520
  }
12207
12521
  try {
12208
12522
  if (held.fd !== undefined)
12209
- fs8.closeSync(held.fd);
12523
+ fs9.closeSync(held.fd);
12210
12524
  } catch {}
12211
12525
  try {
12212
12526
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
@@ -12241,7 +12555,7 @@ function releaseSyncHeldLock(held) {
12241
12555
  if (held.fd !== undefined) {
12242
12556
  const fd = held.fd;
12243
12557
  held.fd = undefined;
12244
- fs8.closeSync(fd);
12558
+ fs9.closeSync(fd);
12245
12559
  }
12246
12560
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
12247
12561
  heldLocks.delete(held.normalizedTargetPath);
@@ -12264,11 +12578,11 @@ function createSyncHeldLockHandle(held) {
12264
12578
  };
12265
12579
  }
12266
12580
  function canonicalLockParentSync(parent) {
12267
- return process.platform === "win32" ? fs8.realpathSync.native(parent) : fs8.realpathSync(parent);
12581
+ return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
12268
12582
  }
12269
12583
  function normalizeTargetPath(targetPath) {
12270
12584
  const resolved = path12.resolve(targetPath);
12271
- fs8.mkdirSync(path12.dirname(resolved), { recursive: true });
12585
+ fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
12272
12586
  try {
12273
12587
  return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
12274
12588
  } catch {
@@ -12296,7 +12610,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
12296
12610
  }
12297
12611
  function reclaimGuardExists(reclaimGuardPath) {
12298
12612
  try {
12299
- fs8.lstatSync(reclaimGuardPath);
12613
+ fs9.lstatSync(reclaimGuardPath);
12300
12614
  return true;
12301
12615
  } catch (error) {
12302
12616
  if (error.code === "ENOENT")
@@ -12360,19 +12674,19 @@ function acquireFileLockSync(targetPath, options) {
12360
12674
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
12361
12675
  let lockFileCreateDenied = false;
12362
12676
  try {
12363
- 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;
12364
12678
  try {
12365
- 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);
12366
12680
  } catch (error) {
12367
12681
  lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
12368
12682
  throw error;
12369
12683
  }
12370
- fs8.writeFileSync(fd, raw, "utf8");
12371
- fs8.fsyncSync(fd);
12684
+ fs9.writeFileSync(fd, raw, "utf8");
12685
+ fs9.fsyncSync(fd);
12372
12686
  const snapshot = {
12373
12687
  raw,
12374
12688
  payload,
12375
- stat: fs8.fstatSync(fd),
12689
+ stat: fs9.fstatSync(fd),
12376
12690
  ownershipToken
12377
12691
  };
12378
12692
  const createdHeld = {
@@ -12407,8 +12721,8 @@ function acquireFileLockSync(targetPath, options) {
12407
12721
  return returnedHandle;
12408
12722
  } catch (error) {
12409
12723
  if (fd !== undefined) {
12410
- const failed = { payload: null, stat: fs8.fstatSync(fd) };
12411
- fs8.closeSync(fd);
12724
+ const failed = { payload: null, stat: fs9.fstatSync(fd) };
12725
+ fs9.closeSync(fd);
12412
12726
  fd = undefined;
12413
12727
  removeSidecarLockIfUnchangedSync(lockPath, failed);
12414
12728
  }
@@ -12457,7 +12771,7 @@ function acquireFileLockSync(targetPath, options) {
12457
12771
  })) {
12458
12772
  let ownsReclaimGuard = false;
12459
12773
  try {
12460
- fs8.mkdirSync(reclaimGuardPath);
12774
+ fs9.mkdirSync(reclaimGuardPath);
12461
12775
  ownsReclaimGuard = true;
12462
12776
  if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
12463
12777
  continue;
@@ -12470,7 +12784,7 @@ function acquireFileLockSync(targetPath, options) {
12470
12784
  } finally {
12471
12785
  if (ownsReclaimGuard) {
12472
12786
  try {
12473
- fs8.rmdirSync(reclaimGuardPath);
12787
+ fs9.rmdirSync(reclaimGuardPath);
12474
12788
  } catch {}
12475
12789
  }
12476
12790
  }
@@ -12516,137 +12830,6 @@ var packageRoot = () => {
12516
12830
  };
12517
12831
  var init_package_root = () => {};
12518
12832
 
12519
- // packages/workit-core/src/core/store-lock.ts
12520
- import { spawnSync as spawnSync8 } from "node:child_process";
12521
- import fs9 from "node:fs";
12522
- import { hostname } from "node:os";
12523
- var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
12524
- let value;
12525
- try {
12526
- value = JSON.parse(raw);
12527
- } catch {
12528
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
12529
- }
12530
- const parsed = metadataLockSchema.safeParse(value);
12531
- if (!parsed.success)
12532
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
12533
- return parsed.data;
12534
- }, parseMetadataLockOrNull = (raw) => {
12535
- try {
12536
- return parseMetadataLock(raw);
12537
- } catch {
12538
- return null;
12539
- }
12540
- }, parseProcStatStart = (stat) => {
12541
- const close = stat.lastIndexOf(")");
12542
- if (close < 0)
12543
- return null;
12544
- return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
12545
- }, processStartOf = (pid) => {
12546
- if (process.platform === "linux") {
12547
- try {
12548
- return parseProcStatStart(fs9.readFileSync(`/proc/${pid}/stat`, "utf8"));
12549
- } catch {
12550
- return null;
12551
- }
12552
- }
12553
- if (process.platform === "darwin" || process.platform === "freebsd") {
12554
- try {
12555
- const run = spawnSync8("ps", ["-o", "lstart=", "-p", String(pid)], {
12556
- encoding: "utf8",
12557
- timeout: 1000
12558
- });
12559
- const value = run.status === 0 ? run.stdout.trim() : "";
12560
- return value || null;
12561
- } catch {
12562
- return null;
12563
- }
12564
- }
12565
- return null;
12566
- }, readTrimmed = (file) => {
12567
- try {
12568
- return fs9.readFileSync(file, "utf8").trim() || null;
12569
- } catch {
12570
- return null;
12571
- }
12572
- }, cachedLockHost = null, localLockHost = () => {
12573
- if (cachedLockHost !== null)
12574
- return cachedLockHost;
12575
- let pidns = null;
12576
- try {
12577
- pidns = /\[(\d+)\]/.exec(fs9.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
12578
- } catch {}
12579
- const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
12580
- cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
12581
- return cachedLockHost;
12582
- }, pidAlive = (pid) => {
12583
- if (!Number.isSafeInteger(pid) || pid <= 0)
12584
- return false;
12585
- try {
12586
- process.kill(pid, 0);
12587
- return true;
12588
- } catch (error) {
12589
- return error.code === "EPERM";
12590
- }
12591
- }, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
12592
- const lock = metadataLockSchema.safeParse(payload);
12593
- if (!lock.success)
12594
- return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
12595
- const { pid, processStart, host } = lock.data;
12596
- const [lockName, lockSpace] = host.split("#");
12597
- const [localName, localSpace] = localHost.split("#");
12598
- if (lockSpace && localSpace && lockName === localName) {
12599
- const [lockNs, lockBoot] = lockSpace.split(":");
12600
- const [localNs, localBoot] = localSpace.split(":");
12601
- if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
12602
- return { state: "stale", reason: "lock was taken before this machine rebooted" };
12603
- }
12604
- if (host !== localHost)
12605
- 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` };
12606
- if (!pidAlive(pid))
12607
- return { state: "stale", reason: `pid ${pid} is not running` };
12608
- const currentStart = processStartOf(pid);
12609
- if (processStart !== null && currentStart !== null && processStart !== currentStart)
12610
- return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
12611
- if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
12612
- return {
12613
- state: "stale",
12614
- reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
12615
- };
12616
- return { state: "live", reason: `held by running pid ${pid}` };
12617
- }, ageOf = (file, nowMs) => {
12618
- try {
12619
- return nowMs - fs9.lstatSync(file).mtimeMs;
12620
- } catch {
12621
- return null;
12622
- }
12623
- }, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
12624
- const guard = `${lockPath}.reclaim`;
12625
- const age = ageOf(guard, nowMs);
12626
- if (age === null || age <= RECLAIM_GUARD_TTL_MS)
12627
- return false;
12628
- try {
12629
- fs9.rmdirSync(guard);
12630
- return true;
12631
- } catch {
12632
- return false;
12633
- }
12634
- };
12635
- var init_store_lock = __esm(() => {
12636
- init_zod();
12637
- init_task_contract();
12638
- init_paths();
12639
- metadataLockSchema = object({
12640
- pid: number2().int().nonnegative().safe(),
12641
- processStart: string2().nullable(),
12642
- host: string2().min(1),
12643
- nonce: string2().min(1),
12644
- externalAction: literal(true).optional()
12645
- }).strict();
12646
- FOREIGN_LOCK_TTL_MS = 10 * 60000;
12647
- TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
12648
- });
12649
-
12650
12833
  // packages/workit-core/src/store/event-log.ts
12651
12834
  import { randomUUID as randomUUID2 } from "node:crypto";
12652
12835
  import fs10 from "node:fs";
@@ -20056,10 +20239,10 @@ function stopObligation(input, descriptor) {
20056
20239
  const branch = currentBranch(input.cwd);
20057
20240
  if (!branch || guardedBranch(input.cwd, branch))
20058
20241
  return null;
20059
- const ledger = readLedger(input.cwd);
20242
+ const ledger = readLedger(input.cwd, { integrity: true });
20060
20243
  if (!ledger.ok)
20061
20244
  return null;
20062
- const all = ledger.value.rows;
20245
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
20063
20246
  const rows = all.filter((row) => row.branch === branch);
20064
20247
  const mine = rows.filter((row) => sessionOf(row) === session);
20065
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"));
@@ -20587,9 +20770,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
20587
20770
  if (role !== "judge" || !event.agentId)
20588
20771
  return readOnly;
20589
20772
  const own = subagentSession(session, event.agentId);
20590
- if (session)
20591
- recordIssuedSession(input.cwd || process.cwd(), session, own);
20592
- 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." : ""}`;
20593
20776
  }, failureDecision = (descriptor, event, error) => {
20594
20777
  if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
20595
20778
  return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;