@brainervirus/workit-cursor 8.13.0 → 8.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8638,1565 +8638,1712 @@ 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;
9354
- }
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}`);
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
9380
9259
  };
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;
9260
+ }
9261
+ if (root === "main") {
9262
+ return {
9263
+ preset: "github-flow",
9264
+ developBranch: null,
9265
+ integration: "pr",
9266
+ protected: [root],
9267
+ allowed: ["*"],
9268
+ prefixes: PREFIXES
9397
9269
  };
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
9270
  }
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 };
9271
+ if (root === "master") {
9272
+ return {
9273
+ preset: "trunk-based",
9274
+ developBranch: null,
9275
+ integration: "pr",
9276
+ protected: [root],
9277
+ allowed: ["*"],
9278
+ prefixes: PREFIXES
9279
+ };
9456
9280
  }
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
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
+ };
9438
+ }
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;
9618
9456
  };
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
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);
9463
+ }
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));
9473
+ });
9639
9474
  };
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;
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;
9666
9486
  }
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);
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}`);
9672
9506
  }
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 {
9713
- };
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;
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(", ")}`);
9755
9513
  }
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
9763
- });
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
- };
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");
9541
+ let raw;
9542
+ try {
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 });
9782
9548
  }
9783
- if (cfgStatus === "malformed") {
9784
- return { ok: false, error: cfgError, configPath: cfgPath };
9549
+ let parsed;
9550
+ try {
9551
+ parsed = JSON.parse(raw);
9552
+ } catch {
9553
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
9785
9554
  }
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
- };
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}`);
9794
9559
  }
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
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}`);
9565
+ }
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
+ }, inheritedSetting = (cwd, dir, has) => {
9574
+ let matched;
9575
+ try {
9576
+ matched = matchRuntimeWorkspace(cwd, dir);
9577
+ } catch {
9578
+ return null;
9579
+ }
9580
+ if (!matched)
9581
+ return null;
9582
+ const { selected, matches } = matched;
9583
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9584
+ return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
9585
+ }, workspaceTrailers = (cwd, dir) => {
9586
+ const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
9587
+ return {
9588
+ enabled: found?.source?.trailers !== false,
9589
+ workspace: found?.workspace ?? null,
9590
+ from: found?.source?.name ?? null
9591
+ };
9592
+ }, workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false, TIER_KEYS, TIER_HOSTS, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9593
+ const matched = matchRuntimeWorkspace(cwd);
9594
+ if (!matched)
9595
+ return null;
9596
+ const { file, selected } = matched;
9597
+ const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
9598
+ if (!policy.success) {
9599
+ const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
9600
+ throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
9601
+ }
9602
+ return policy.data;
9603
+ }, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
9604
+ const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
9605
+ if (!candidate)
9606
+ return { policy: undefined, source: "user-default" };
9607
+ const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
9608
+ const profileName = requestedProfile?.trim() || candidate.defaultProfile;
9609
+ if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
9610
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
9611
+ const profile = profileName ? candidate.profiles?.[profileName] : undefined;
9612
+ if (profileName && !profile)
9613
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
9614
+ const profilePolicy = profile?.[key];
9615
+ const workspacePolicy = candidate[key];
9616
+ return {
9617
+ policy: profilePolicy ?? workspacePolicy,
9618
+ source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
9619
+ };
9620
+ };
9621
+ var init_workspaces = __esm(() => {
9622
+ init_zod();
9623
+ init_config();
9624
+ init_branch_policy();
9625
+ nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
9626
+ branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
9627
+ commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
9628
+ workspaceBranchPolicySchema = object({
9629
+ preset: branchPreset,
9630
+ developBranch: nonBlank.optional(),
9631
+ prefixes: object({
9632
+ feature: nonBlank,
9633
+ bugfix: nonBlank,
9634
+ release: nonBlank,
9635
+ hotfix: nonBlank
9636
+ }).passthrough().optional(),
9637
+ allowed: array(nonBlank).optional(),
9638
+ protected: array(nonBlank).optional(),
9639
+ integration: _enum(["pr", "merge"]).optional()
9640
+ }).strict();
9641
+ workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
9642
+ workspaceProfileSchema = object({
9643
+ branchPolicy: workspaceBranchPolicySchema.optional(),
9644
+ commitPolicy: workspaceCommitPolicySchema.optional()
9645
+ }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
9646
+ message: "a profile must define branchPolicy or commitPolicy"
9647
+ });
9648
+ versionSourceSchema = discriminatedUnion("kind", [
9649
+ object({
9650
+ kind: literal("package-json"),
9651
+ path: nonBlank,
9652
+ field: nonBlank
9653
+ }).strict(),
9654
+ object({ kind: literal("git-tag") }).strict(),
9655
+ object({ kind: literal("manual") }).strict()
9656
+ ]);
9657
+ releaseTrackSchema = object({
9658
+ strategy: branchPreset.optional(),
9659
+ productionBranch: nonBlank,
9660
+ integrationBranch: nonBlank,
9661
+ naming: object({
9662
+ feature: nonBlank.optional(),
9663
+ release: nonBlank.optional(),
9664
+ hotfix: nonBlank.optional()
9665
+ }).passthrough().optional(),
9666
+ baseBranch: nonBlank.optional(),
9667
+ mergeBackBranches: array(nonBlank).optional(),
9668
+ pullRequestTarget: nonBlank.optional(),
9669
+ tagNamespace: string2().optional(),
9670
+ versionSource: versionSourceSchema.optional(),
9671
+ requiredChecks: array(nonBlank).optional(),
9672
+ critical: array(nonBlank).optional()
9673
+ }).passthrough().superRefine((track, context) => {
9674
+ for (const [field, values] of [
9675
+ ["mergeBackBranches", track.mergeBackBranches ?? []],
9676
+ ["requiredChecks", track.requiredChecks ?? []]
9677
+ ]) {
9678
+ if (new Set(values).size !== values.length) {
9679
+ context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
9680
+ }
9681
+ }
9682
+ 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(".."))) {
9683
+ context.addIssue({
9684
+ code: "custom",
9685
+ path: ["versionSource", "path"],
9686
+ message: "must stay inside the repository"
9687
+ });
9688
+ }
9689
+ });
9690
+ workspaceConfigSchema = object({
9691
+ name: nonBlank,
9692
+ glob: nonBlank,
9693
+ vcs: object({
9694
+ provider: _enum(["gitlab", "github"]),
9695
+ defaultTargetBranch: nonBlank.optional(),
9696
+ tokenFile: nonBlank.optional(),
9697
+ account: nonBlank.optional()
9698
+ }).passthrough().optional(),
9699
+ autoApprove: union([boolean2(), array(nonBlank)]).optional(),
9700
+ defaultEndpoint: unknown().optional(),
9701
+ verification: _enum(["self", "independent"]).optional(),
9702
+ trailers: boolean2().optional(),
9703
+ liveSessionGuard: boolean2().optional(),
9704
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
9705
+ autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9706
+ kind,
9707
+ union([boolean2(), literal("verified")]).optional()
9708
+ ]))).passthrough().optional(),
9709
+ youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
9710
+ issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
9711
+ branchPolicy: workspaceBranchPolicySchema.optional(),
9712
+ commitPolicy: workspaceCommitPolicySchema.optional(),
9713
+ defaultProfile: nonBlank.optional(),
9714
+ profiles: record(nonBlank, workspaceProfileSchema).optional(),
9715
+ releaseTracks: record(nonBlank, releaseTrackSchema).optional()
9716
+ }).passthrough().superRefine((workspace, context) => {
9717
+ if (workspace.issues && workspace.vcs?.provider !== "github") {
9718
+ context.addIssue({
9719
+ code: "custom",
9720
+ path: ["issues"],
9721
+ message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
9722
+ });
9723
+ }
9724
+ if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
9725
+ context.addIssue({
9726
+ code: "custom",
9727
+ path: ["defaultProfile"],
9728
+ message: `does not name a configured profile`
9729
+ });
9730
+ }
9731
+ });
9732
+ workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
9733
+ const names = new Set;
9734
+ for (const [index, workspace] of (file.workspaces ?? []).entries()) {
9735
+ if (names.has(workspace.name)) {
9736
+ context.addIssue({
9737
+ code: "custom",
9738
+ path: ["workspaces", index, "name"],
9739
+ message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
9740
+ });
9741
+ }
9742
+ names.add(workspace.name);
9743
+ }
9744
+ });
9745
+ UNSUPPORTED_GLOB = /[?[\]{}]/;
9746
+ UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
9747
+ WorkspaceConfigError = class WorkspaceConfigError extends Error {
9748
+ name = "WorkspaceConfigError";
9807
9749
  };
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" };
9815
- }
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();
9830
- let raw;
9831
- try {
9832
- raw = fs2.readFileSync(file, "utf8");
9833
- } catch {
9834
- return { status: "missing", path: file, config: {} };
9835
- }
9836
- let parsed;
9837
- try {
9838
- parsed = JSON.parse(raw);
9839
- } catch {
9840
- return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
9841
- }
9842
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9843
- return { status: "valid", path: file, config: parsed };
9844
- }
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");
9878
- }
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"
9750
+ mainCheckouts = new Map;
9751
+ runtimeWorkspaceIndexSchema = object({
9752
+ workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
9753
+ }).passthrough();
9754
+ TIER_KEYS = new Set(["mundane", "standard", "hard"]);
9755
+ TIER_HOSTS = new Set([
9756
+ "claude_code",
9757
+ "opencode",
9758
+ "codex",
9759
+ "codex_cli",
9760
+ "codex_desktop",
9761
+ "cursor",
9762
+ "pi",
9763
+ "default"
9916
9764
  ]);
9917
- STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
9918
- homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
9919
9765
  });
9920
9766
 
9921
- // packages/workit-core/src/store/paths.ts
9922
- import { spawnSync as spawnSync6 } from "node:child_process";
9767
+ // packages/workit-core/src/core/release-tracks.ts
9923
9768
  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;
9769
+ import { spawnSync as spawnSync6 } from "node:child_process";
9770
+ function readReleaseTracks(raw) {
9771
+ const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
9772
+ if (raw === undefined || raw === null)
9773
+ return out;
9774
+ if (!isRecord(raw)) {
9775
+ out.issues.push("releaseTracks is not an object; ignored");
9776
+ return out;
9777
+ }
9778
+ const fatal = [];
9779
+ const protectedNames = new Set;
9780
+ for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
9781
+ const label = `release track ${JSON.stringify(name)}`;
9782
+ if (!isRecord(entry)) {
9783
+ out.issues.push(`${label} is not an object; ignored`);
9784
+ continue;
9937
9785
  }
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")))
9786
+ for (const branch of namedBranches(entry))
9787
+ protectedNames.add(branch);
9788
+ if (!name.trim()) {
9789
+ out.issues.push("a release track with a blank name was ignored");
9790
+ continue;
9791
+ }
9792
+ let critical = [];
9793
+ if (Array.isArray(entry.critical))
9794
+ critical = entry.critical.filter((item) => typeof item === "string");
9795
+ else if (entry.critical !== undefined)
9796
+ out.issues.push(`${label} critical: must be a list of field names; ignored`);
9797
+ const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
9798
+ const problem = (field, message) => {
9799
+ if (isCritical(field))
9800
+ fatal.push(`${label} ${field}: ${message}`);
9801
+ else
9802
+ out.issues.push(`${label} ${field}: ${message}`);
9803
+ };
9804
+ for (const key of Object.keys(entry))
9805
+ if (!KNOWN_FIELDS.has(key))
9806
+ problem(key, "unknown field, ignored");
9807
+ const productionBranch = branchName(entry.productionBranch);
9808
+ const integrationBranch = branchName(entry.integrationBranch);
9809
+ if (!productionBranch)
9810
+ problem("productionBranch", "missing or not a branch name");
9811
+ if (!integrationBranch)
9812
+ problem("integrationBranch", "missing or not a branch name");
9813
+ const optionalBranch = (field) => {
9814
+ if (entry[field] === undefined)
9975
9815
  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
- };
9816
+ const value = branchName(entry[field]);
9817
+ if (!value)
9818
+ problem(field, "not a branch name; using the integration branch");
9819
+ return value;
9820
+ };
9821
+ const baseBranch = optionalBranch("baseBranch");
9822
+ const pullRequestTarget = optionalBranch("pullRequestTarget");
9823
+ let mergeBackBranches = [];
9824
+ if (entry.mergeBackBranches !== undefined) {
9825
+ const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
9826
+ if (list.some((item) => item === null))
9827
+ problem("mergeBackBranches", "must be a list of branch names; ignored");
9828
+ else
9829
+ mergeBackBranches = [...new Set(list)];
9830
+ }
9831
+ const naming = {};
9832
+ if (entry.naming !== undefined) {
9833
+ if (!isRecord(entry.naming))
9834
+ problem("naming", "not an object; ignored");
9835
+ else
9836
+ for (const kind of ["feature", "release", "hotfix"]) {
9837
+ const template = entry.naming[kind];
9838
+ if (template === undefined)
9839
+ continue;
9840
+ if (typeof template !== "string" || !template.trim())
9841
+ problem(`naming.${kind}`, "not a template string; ignored");
9842
+ else if (!literalOf(template))
9843
+ problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
9844
+ else
9845
+ naming[kind] = template.trim();
9846
+ }
9983
9847
  }
9984
- const parent = path6.dirname(dir);
9985
- if (parent === dir)
9986
- return { kind: "none" };
9987
- dir = parent;
9848
+ if (!productionBranch || !integrationBranch) {
9849
+ out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
9850
+ continue;
9851
+ }
9852
+ out.tracks.push({
9853
+ name,
9854
+ productionBranch,
9855
+ integrationBranch,
9856
+ baseBranch: baseBranch ?? integrationBranch,
9857
+ pullRequestTarget: pullRequestTarget ?? integrationBranch,
9858
+ mergeBackBranches,
9859
+ naming
9860
+ });
9988
9861
  }
9862
+ out.protectedBranches = [...protectedNames];
9863
+ if (fatal.length)
9864
+ out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
9865
+ return out;
9989
9866
  }
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 }
10007
- };
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
10016
- };
9867
+ function resolveReleaseTrack(input) {
9868
+ const tracks = [...input.tracks];
9869
+ const issues = [...input.issues ?? []];
9870
+ const requested = input.requested?.trim();
9871
+ if (tracks.length === 0) {
9872
+ if (requested)
9873
+ return {
9874
+ status: "invalid",
9875
+ error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
9876
+ issues
9877
+ };
9878
+ return { status: "none", issues };
10017
9879
  }
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)`);
10022
- }
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);
9880
+ const names = tracks.map((track) => track.name).join(", ");
9881
+ let envProblem = null;
9882
+ const done = (track, source, detail, warnings = [], ambiguous = null) => ({
9883
+ status: "resolved",
9884
+ track: track && track.name !== IMPLICIT ? track : null,
9885
+ source,
9886
+ detail,
9887
+ warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
9888
+ tracks,
9889
+ blocking: envProblem ?? ambiguous
9890
+ });
9891
+ if (requested) {
9892
+ const named = tracks.find((track) => track.name === requested);
9893
+ if (!named)
10039
9894
  return {
10040
- key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
10041
- kind: "detached",
10042
- branch: null
9895
+ status: "invalid",
9896
+ error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
9897
+ issues
10043
9898
  };
10044
- }
9899
+ return done(named, "flag", `track ${named.name} was requested explicitly`);
10045
9900
  }
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);
9901
+ const owned = (branch) => branch !== null && tracks.some((track) => [
9902
+ track.integrationBranch,
9903
+ track.productionBranch,
9904
+ track.baseBranch,
9905
+ track.pullRequestTarget
9906
+ ].includes(branch));
9907
+ const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
9908
+ name: IMPLICIT,
9909
+ productionBranch: input.defaultBranch,
9910
+ integrationBranch: input.defaultBranch,
9911
+ baseBranch: input.defaultBranch,
9912
+ pullRequestTarget: input.defaultBranch,
9913
+ mergeBackBranches: [],
9914
+ naming: {}
9915
+ } : null;
9916
+ const candidates = implicit ? [...tracks, implicit] : tracks;
9917
+ const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
9918
+ const fallback = (why) => {
9919
+ const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
9920
+ const track = byDefault.length === 1 ? byDefault[0] : null;
9921
+ const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
9922
+ 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;
9923
+ return done(track, "default", `${why}; using the workspace ${target}`, [
9924
+ `release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
9925
+ ], ambiguous);
9926
+ };
9927
+ const branch = input.branch;
9928
+ if (branch) {
9929
+ const owners = ownedBy(candidates, branch);
9930
+ if (owners.length === 1)
9931
+ return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
9932
+ if (owners.length > 1)
9933
+ return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
10055
9934
  }
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;
9935
+ const env = input.envRequested?.trim();
9936
+ if (env) {
9937
+ const named = tracks.find((track) => track.name === env);
9938
+ if (named)
9939
+ return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
9940
+ envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
10067
9941
  }
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;
9942
+ if (!branch)
9943
+ return fallback("HEAD is detached");
9944
+ if (tracks.length === 1 && !implicit)
9945
+ return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
9946
+ let remotes = null;
9947
+ const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
9948
+ const ownerOf = (ref) => {
9949
+ const owner = ownedBy(candidates, short(ref));
9950
+ return owner.length === 1 ? owner[0] : null;
9951
+ };
9952
+ const followRecorded = (start) => {
9953
+ let cursor = start;
9954
+ const seen = new Set([start]);
9955
+ for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
9956
+ const recorded = input.probe.recordedBase(cursor);
9957
+ if (!recorded)
9958
+ return null;
9959
+ const base = short(recorded);
9960
+ const owner = ownerOf(base);
9961
+ if (owner)
9962
+ return owner;
9963
+ if (ownedBy(candidates, base).length > 1 || seen.has(base))
9964
+ return null;
9965
+ seen.add(base);
9966
+ cursor = base;
9967
+ }
9968
+ return null;
9969
+ };
9970
+ const recordedOwner = followRecorded(branch);
9971
+ if (recordedOwner)
9972
+ return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
9973
+ for (const source of ["reflog", "checkout"]) {
9974
+ const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
9975
+ if (!ref)
9976
+ continue;
9977
+ const owner = ownerOf(ref) ?? followRecorded(short(ref));
9978
+ if (owner)
9979
+ return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
9980
+ }
9981
+ const forks = [];
9982
+ for (const track of candidates)
9983
+ for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
9984
+ const sha = input.probe.mergeBase(branch, ref);
9985
+ if (sha)
9986
+ forks.push({ track, ref, sha });
9987
+ }
9988
+ if (forks.length === 0)
9989
+ return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
9990
+ const ancestry = new Map;
9991
+ const descends = (older, newer) => {
9992
+ if (older === newer)
9993
+ return false;
9994
+ const key = `${older}>${newer}`;
9995
+ if (!ancestry.has(key))
9996
+ ancestry.set(key, input.probe.isAncestor(older, newer));
9997
+ return ancestry.get(key);
9998
+ };
9999
+ const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
10000
+ const winningTracks = [...new Set(winners.map((fork) => fork.track))];
10001
+ if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
10002
+ const win = winners[0];
10003
+ return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
10077
10004
  }
10005
+ if (winningTracks.length === 1)
10006
+ return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
10007
+ const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
10008
+ 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 ")}`);
10009
+ }
10010
+ function gitTrackProbe(cwd) {
10011
+ let remote;
10012
+ let remotes;
10013
+ const tip = (branch) => {
10014
+ remote ??= pushRemoteName(cwd) ?? "origin";
10015
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
10016
+ };
10017
+ return {
10018
+ remotes: () => remotes ??= remoteNames(cwd),
10019
+ recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
10020
+ createdFrom: (branch) => {
10021
+ const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
10022
+ const oldest = log?.split(`
10023
+ `).findLast(Boolean) ?? "";
10024
+ const match = /^branch: Created from (\S+)$/u.exec(oldest);
10025
+ return match && match[1] !== "HEAD" ? match[1] : null;
10026
+ },
10027
+ checkedOutFrom: (branch) => {
10028
+ const own = reflog(cwd, `refs/heads/${branch}`);
10029
+ const created = own.at(-1);
10030
+ if (!created || created.subject !== "branch: Created from HEAD")
10031
+ return null;
10032
+ const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
10033
+ const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
10034
+ return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
10035
+ },
10036
+ mergeBase: (branch, trackBranch) => {
10037
+ const base = tip(trackBranch);
10038
+ return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
10039
+ },
10040
+ isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
10041
+ };
10078
10042
  }
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;
10089
- }
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));
10043
+ function workspaceReleaseTracks(cwd) {
10044
+ const raw = resolveRuntimeReleaseTracks(cwd);
10045
+ return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
10046
+ }
10047
+ function resolveTrackFor(cwd, options) {
10048
+ const read = workspaceReleaseTracks(cwd);
10049
+ if (read.error)
10050
+ return { status: "invalid", error: read.error, issues: read.issues };
10051
+ if (read.tracks.length === 0 && !options.track?.trim())
10052
+ return { status: "none", issues: read.issues };
10053
+ const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
10054
+ const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
10055
+ const input = {
10056
+ tracks: read.tracks,
10057
+ requested: options.track ?? null,
10058
+ envRequested,
10059
+ branch,
10060
+ defaultBranch: options.defaultBranch,
10061
+ issues: read.issues
10095
10062
  };
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
- }
10063
+ let key = null;
10064
+ if (!options.probe) {
10065
+ const refs = git2(cwd, [
10066
+ "for-each-ref",
10067
+ "--format=%(refname) %(objectname)",
10068
+ "refs/heads",
10069
+ "refs/remotes"
10070
+ ]) ?? "";
10071
+ const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
10072
+ const own = branch ? [
10073
+ git2(cwd, [
10074
+ "log",
10075
+ "-g",
10076
+ "-n",
10077
+ "1",
10078
+ "--date=unix",
10079
+ "--format=%H %gd %gs",
10080
+ `refs/heads/${branch}`,
10081
+ "--"
10082
+ ]) ?? "",
10083
+ git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
10084
+ ] : [];
10085
+ key = createHash5("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
10086
+ const hit = memo2.get(key);
10087
+ if (hit)
10088
+ return hit;
10116
10089
  }
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;
10090
+ const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
10091
+ if (key) {
10092
+ if (memo2.size >= MEMO_LIMIT)
10093
+ memo2.delete(memo2.keys().next().value);
10094
+ memo2.set(key, resolution);
10095
+ }
10096
+ return resolution;
10128
10097
  }
10129
- var NOT_A_REPO, gitRun = (cwd, args) => {
10098
+ 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) => [
10099
+ ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
10100
+ ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
10101
+ ].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
10102
+ ...new Set(tracks.flatMap((track) => [
10103
+ track.productionBranch,
10104
+ track.integrationBranch,
10105
+ track.baseBranch,
10106
+ track.pullRequestTarget,
10107
+ ...track.mergeBackBranches
10108
+ ]))
10109
+ ], 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) => {
10110
+ const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
10111
+ const remote = remotes.find((name) => bare.startsWith(`${name}/`));
10112
+ return remote ? bare.slice(remote.length + 1) : bare;
10113
+ }, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
10130
10114
  const run = spawnSync6("git", args, {
10131
10115
  cwd,
10132
10116
  encoding: "utf8",
10133
10117
  timeout: 20000,
10134
- killSignal: "SIGKILL",
10135
10118
  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
- }
10119
+ stdio: ["ignore", "pipe", "ignore"]
10143
10120
  });
10144
- const error = run.error;
10121
+ return run.status === 0 ? (run.stdout ?? "").trim() : null;
10122
+ }, 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(`
10123
+ `).map((line) => {
10124
+ const [sha = "", selector = "", ...subject] = line.split("\t");
10145
10125
  return {
10146
- ok: run.status === 0,
10147
- stdout: run.stdout ?? "",
10148
- stderr: error ? String(error.message) : run.stderr ?? "",
10149
- missing: error?.code === "ENOENT"
10126
+ sha,
10127
+ time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
10128
+ subject: subject.join("\t")
10129
+ };
10130
+ }).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
10131
+ var init_release_tracks = __esm(() => {
10132
+ init_rev();
10133
+ init_config();
10134
+ init_workspaces();
10135
+ ReleaseTrackError = class ReleaseTrackError extends Error {
10136
+ };
10137
+ KNOWN_FIELDS = new Set([
10138
+ "strategy",
10139
+ "productionBranch",
10140
+ "integrationBranch",
10141
+ "naming",
10142
+ "baseBranch",
10143
+ "mergeBackBranches",
10144
+ "pullRequestTarget",
10145
+ "tagNamespace",
10146
+ "versionSource",
10147
+ "requiredChecks",
10148
+ "critical"
10149
+ ]);
10150
+ BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
10151
+ PLACEHOLDER = /\{[^}]*\}/gu;
10152
+ HAS_PLACEHOLDER = /\{[^}]*\}/u;
10153
+ memo2 = new Map;
10154
+ });
10155
+
10156
+ // packages/workit-core/src/core/vcs-config.ts
10157
+ import fs4 from "node:fs";
10158
+ import path5 from "node:path";
10159
+ import { spawnSync as spawnSync7 } from "node:child_process";
10160
+ function vcsConfig(mode, cwd, options = {}) {
10161
+ const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
10162
+ const wsVcs = ws?.vcs ?? {};
10163
+ const wsYt = ws?.youtrack ?? {};
10164
+ const wsIssues = ws?.issues ?? {};
10165
+ const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
10166
+ const root = vcsCwd(cwd);
10167
+ const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
10168
+ const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
10169
+ const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
10170
+ const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
10171
+ const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
10172
+ const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
10173
+ let issuesProvider = null;
10174
+ let linkOnPr = null;
10175
+ if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
10176
+ issuesProvider = "github";
10177
+ linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
10178
+ }
10179
+ if (mode === "resolve") {
10180
+ if (cfgStatus === "malformed")
10181
+ return { ok: false, error: cfgError, configPath: cfgPath };
10182
+ const trackResolution = resolveTrackFor(root, {
10183
+ track: options.track,
10184
+ ...options.branch !== undefined ? { branch: options.branch } : {},
10185
+ defaultBranch: workspaceDefault
10186
+ });
10187
+ if (trackResolution.status === "invalid")
10188
+ return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
10189
+ const releaseTrack = trackSummary(trackResolution);
10190
+ const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
10191
+ const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
10192
+ return {
10193
+ ok: true,
10194
+ workspace_name: ws?.name ?? null,
10195
+ provider,
10196
+ defaultTargetBranch: defaultTarget,
10197
+ workspaceDefaultTargetBranch: workspaceDefault,
10198
+ baseBranch,
10199
+ releaseTrack,
10200
+ link_issues: linkIssues,
10201
+ youtrack_base_url: youtrackBaseUrl,
10202
+ issues_provider: issuesProvider,
10203
+ link_on_pr: linkOnPr
10204
+ };
10205
+ }
10206
+ if (cfgStatus === "malformed") {
10207
+ return { ok: false, error: cfgError, configPath: cfgPath };
10208
+ }
10209
+ if (cfgStatus === "missing" && provider === null)
10210
+ return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
10211
+ if (provider === null) {
10212
+ return {
10213
+ ok: false,
10214
+ error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
10215
+ configPath: cfgPath
10216
+ };
10217
+ }
10218
+ const prov = cfg[provider] ?? {};
10219
+ const out = {
10220
+ ok: true,
10221
+ configPath: path5.resolve(cfgPath),
10222
+ provider,
10223
+ defaultTargetBranch: workspaceDefault,
10224
+ pr: cfg.pr ?? {},
10225
+ workspace_name: ws?.name ?? null,
10226
+ link_issues: linkIssues,
10227
+ youtrack_base_url: youtrackBaseUrl,
10228
+ issues_provider: issuesProvider,
10229
+ link_on_pr: linkOnPr
10150
10230
  };
10151
- }, shortHash = (value) => createHash5("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
10231
+ if (provider === "gitlab") {
10232
+ out.gitlab = {
10233
+ host: prov.host ?? "gitlab.com",
10234
+ apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
10235
+ };
10236
+ } else if (provider === "github") {
10237
+ out.github = { host: prov.host ?? "github.com" };
10238
+ }
10239
+ return out;
10240
+ }
10241
+ var workspacesPath = () => path5.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
10242
+ const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
10243
+ if (r.status !== 0)
10244
+ return null;
10245
+ const url = (r.stdout ?? "").trim();
10246
+ if (/github\.com[:/]/.test(url))
10247
+ return "github";
10248
+ if (/gitlab\.com[:/]/.test(url))
10249
+ return "gitlab";
10250
+ return null;
10251
+ }, readVcsConfig = () => {
10252
+ const file = vcsConfigPath();
10253
+ let raw;
10152
10254
  try {
10153
- return fs3.realpathSync.native(file);
10255
+ raw = fs4.readFileSync(file, "utf8");
10154
10256
  } catch {
10155
- return file;
10257
+ return { status: "missing", path: file, config: {} };
10156
10258
  }
10157
- }, DISCOVERY_ENV, isGitDir = (dir) => {
10259
+ let parsed;
10158
10260
  try {
10159
- return fs3.statSync(path6.join(dir, "HEAD")).isFile() && fs3.statSync(path6.join(dir, "objects")).isDirectory() && fs3.statSync(path6.join(dir, "refs")).isDirectory();
10261
+ parsed = JSON.parse(raw);
10160
10262
  } catch {
10161
- return false;
10263
+ return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
10162
10264
  }
10163
- }, reftableRefs = (common) => {
10164
- try {
10165
- return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs3.readFileSync(path6.join(common, "config"), "utf8"));
10166
- } catch {
10167
- return false;
10265
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10266
+ return { status: "valid", path: file, config: parsed };
10168
10267
  }
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 {}
10268
+ return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
10269
+ }, trackSummary = (resolution) => resolution.status === "resolved" ? {
10270
+ name: resolution.track?.name ?? null,
10271
+ source: resolution.source,
10272
+ detail: resolution.detail,
10273
+ warnings: resolution.warnings,
10274
+ track: resolution.track,
10275
+ tracks: resolution.tracks,
10276
+ blocking: resolution.blocking
10277
+ } : null;
10278
+ var init_vcs_config = __esm(() => {
10279
+ init_config();
10280
+ init_workspaces();
10281
+ init_release_tracks();
10282
+ });
10283
+
10284
+ // packages/workit-core/src/core/logger.ts
10285
+ import os3 from "node:os";
10286
+ import path6 from "node:path";
10287
+ var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
10288
+ const override = process.env.WORKFLOW_TOOLKIT_STATE;
10289
+ if (override)
10290
+ return override;
10291
+ const home = os3.homedir();
10292
+ if (process.env.XDG_STATE_HOME)
10293
+ return path6.join(process.env.XDG_STATE_HOME, "workit");
10294
+ switch (os3.platform()) {
10295
+ case "darwin":
10296
+ return path6.join(home, "Library", "Application Support", "workit");
10297
+ case "win32":
10298
+ return path6.join(process.env.LOCALAPPDATA ?? path6.join(home, "AppData", "Local"), "workit");
10299
+ default:
10300
+ return path6.join(process.env.HOME ?? home, ".local", "state", "workit");
10177
10301
  }
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
- ];
10302
+ }, homePattern;
10303
+ var init_logger = __esm(() => {
10304
+ SENSITIVE_WORDS = new Set([
10305
+ "token",
10306
+ "secret",
10307
+ "password",
10308
+ "passwd",
10309
+ "authorization",
10310
+ "credential",
10311
+ "credentials",
10312
+ "cookie",
10313
+ "api",
10314
+ "apikey",
10315
+ "apitoken",
10316
+ "accesstoken",
10317
+ "clientsecret",
10318
+ "refreshtoken",
10319
+ "key",
10320
+ "bearer",
10321
+ "prompt",
10322
+ "message",
10323
+ "messages",
10324
+ "content",
10325
+ "body",
10326
+ "args",
10327
+ "argument",
10328
+ "result",
10329
+ "results",
10330
+ "output",
10331
+ "issue",
10332
+ "description",
10333
+ "summary",
10334
+ "payload",
10335
+ "script",
10336
+ "command",
10337
+ "text",
10338
+ "query"
10339
+ ]);
10340
+ STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
10341
+ homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
10195
10342
  });
10196
10343
 
10197
10344
  // packages/workit-core/src/host-session.ts
10198
- import { createHash as createHash6 } from "node:crypto";
10199
- import fs4 from "node:fs";
10345
+ import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
10346
+ import fs5 from "node:fs";
10200
10347
  import path7 from "node:path";
10201
10348
  function hostSessionFromEnv(env) {
10202
10349
  const value = (key) => env[key]?.trim() || null;
@@ -10225,7 +10372,7 @@ function migrateRepoIssued(cwd) {
10225
10372
  if (location instanceof Error)
10226
10373
  return;
10227
10374
  old = path7.join(location.dir, "hooks", "issued");
10228
- if (!fs4.existsSync(old))
10375
+ if (!fs5.existsSync(old))
10229
10376
  return;
10230
10377
  } catch {
10231
10378
  return;
@@ -10233,13 +10380,18 @@ function migrateRepoIssued(cwd) {
10233
10380
  for (const lead of safeList(old)) {
10234
10381
  try {
10235
10382
  const from = path7.join(old, lead);
10236
- if (!fs4.statSync(from).isDirectory())
10383
+ if (!fs5.statSync(from).isDirectory())
10237
10384
  continue;
10238
10385
  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 });
10386
+ fs5.mkdirSync(to, { recursive: true, mode: 448 });
10387
+ for (const id of fs5.readdirSync(from)) {
10388
+ try {
10389
+ fs5.copyFileSync(path7.join(from, id), path7.join(to, id), fs5.constants.COPYFILE_EXCL);
10390
+ } catch (error) {
10391
+ if (error.code !== "EEXIST")
10392
+ throw error;
10393
+ }
10394
+ fs5.rmSync(path7.join(from, id), { force: true });
10243
10395
  }
10244
10396
  removeIfEmpty(from);
10245
10397
  } catch {}
@@ -10247,30 +10399,76 @@ function migrateRepoIssued(cwd) {
10247
10399
  removeIfEmpty(old);
10248
10400
  }
10249
10401
  function recordIssuedSession(cwd, lead, issued) {
10402
+ const token = randomBytes(18).toString("base64url");
10250
10403
  try {
10251
10404
  migrateRepoIssued(cwd);
10252
- const dir = path7.join(userIssuedRoot(), hash(lead));
10253
- fs4.mkdirSync(dir, { recursive: true, mode: 448 });
10254
10405
  const own = hash(issued);
10255
- fs4.writeFileSync(path7.join(dir, own), ISSUED_SESSIONS_MARKER);
10256
- const entries = fs4.readdirSync(dir);
10406
+ const tokens = path7.join(userTokenRoot(), hash(lead));
10407
+ fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
10408
+ fs5.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
10409
+ `, { mode: 384, flag: "wx" });
10410
+ const dir = path7.join(userIssuedRoot(), hash(lead));
10411
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
10412
+ fs5.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
10413
+ `, { mode: 384 });
10414
+ pruneIssued(dir, own, tokens);
10415
+ return token;
10416
+ } catch {
10417
+ return null;
10418
+ }
10419
+ }
10420
+ function pruneIssued(dir, own, tokens) {
10421
+ try {
10422
+ const entries = fs5.readdirSync(dir);
10257
10423
  if (entries.length <= ISSUED_PER_LEAD)
10258
10424
  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 });
10425
+ 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);
10426
+ for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
10427
+ fs5.rmSync(path7.join(dir, name), { force: true });
10428
+ fs5.rmSync(path7.join(tokens, name), { force: true });
10429
+ }
10262
10430
  } catch {}
10263
10431
  }
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) => {
10432
+ function issuedBinding(cwd, claimed) {
10433
+ const record = issuedRecord(cwd, claimed);
10434
+ const { stored, leadBound } = issuedToken(claimed);
10435
+ if (stored)
10436
+ return "token";
10437
+ if (record === null)
10438
+ return null;
10439
+ return leadBound ? "token" : "legacy";
10440
+ }
10441
+ 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
10442
  try {
10266
- return fs4.readdirSync(dir);
10443
+ return fs5.readdirSync(dir);
10267
10444
  } catch {
10268
10445
  return [];
10269
10446
  }
10270
10447
  }, removeIfEmpty = (dir) => {
10271
10448
  try {
10272
- fs4.rmdirSync(dir);
10449
+ fs5.rmdirSync(dir);
10273
10450
  } catch {}
10451
+ }, tokenHash = (token) => createHash6("sha256").update(token).digest("hex"), issuedRecord = (cwd, claimed) => {
10452
+ const cut = claimed.lastIndexOf(":");
10453
+ if (cut <= 0)
10454
+ return null;
10455
+ migrateRepoIssued(cwd);
10456
+ try {
10457
+ return fs5.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
10458
+ } catch {
10459
+ return null;
10460
+ }
10461
+ }, TOKEN_HASH, issuedToken = (claimed) => {
10462
+ const cut = claimed.lastIndexOf(":");
10463
+ if (cut <= 0)
10464
+ return { stored: null, leadBound: false };
10465
+ const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
10466
+ try {
10467
+ const text = fs5.readFileSync(path7.join(dir, hash(claimed)), "utf8");
10468
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
10469
+ } catch {
10470
+ return { stored: null, leadBound: safeList(dir).length > 0 };
10471
+ }
10274
10472
  }, OWN_CHILD_SHELL;
10275
10473
  var init_host_session = __esm(() => {
10276
10474
  init_logger();
@@ -10283,13 +10481,14 @@ var init_host_session = __esm(() => {
10283
10481
  (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
10284
10482
  ]
10285
10483
  ];
10484
+ TOKEN_HASH = /^([0-9a-f]{64})$/mu;
10286
10485
  OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
10287
10486
  });
10288
10487
 
10289
10488
  // 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";
10489
+ import { spawnSync as spawnSync8 } from "node:child_process";
10490
+ import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
10491
+ import fs6 from "node:fs";
10293
10492
  import path8 from "node:path";
10294
10493
  function resolveCommit(cwd, ref) {
10295
10494
  if (!safeRef(ref))
@@ -10365,7 +10564,7 @@ function ledgerPath(cwd) {
10365
10564
  const root = storeRoot(cwd);
10366
10565
  return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
10367
10566
  }
10368
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10567
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
10369
10568
  const lock = `${file}.lock`;
10370
10569
  const deadline = Date.now() + waitMs;
10371
10570
  let token = null;
@@ -10389,13 +10588,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
10389
10588
  }
10390
10589
  function appendLine(file, build, options) {
10391
10590
  const write = () => {
10392
- const fd = fs5.openSync(file, "a+");
10591
+ const fd = fs6.openSync(file, "a+", 384);
10393
10592
  try {
10394
- const size = fs5.fstatSync(fd).size;
10593
+ if (process.platform !== "win32")
10594
+ tighten(fd);
10595
+ const size = fs6.fstatSync(fd).size;
10395
10596
  let fence = "";
10396
10597
  if (size > 0) {
10397
10598
  const last = Buffer.alloc(1);
10398
- fs5.readSync(fd, last, 0, 1, size - 1);
10599
+ fs6.readSync(fd, last, 0, 1, size - 1);
10399
10600
  if (last[0] !== 10)
10400
10601
  fence = `
10401
10602
  `;
@@ -10404,16 +10605,16 @@ function appendLine(file, build, options) {
10404
10605
  if (!line.ok)
10405
10606
  return line;
10406
10607
  const bytes = Buffer.from(fence + line.value);
10407
- const written = fs5.writeSync(fd, bytes);
10608
+ const written = fs6.writeSync(fd, bytes);
10408
10609
  if (written !== bytes.length)
10409
10610
  return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
10410
10611
  return { ok: true, value: undefined };
10411
10612
  } finally {
10412
- fs5.closeSync(fd);
10613
+ fs6.closeSync(fd);
10413
10614
  }
10414
10615
  };
10415
10616
  try {
10416
- fs5.mkdirSync(path8.dirname(file), { recursive: true });
10617
+ fs6.mkdirSync(path8.dirname(file), { recursive: true });
10417
10618
  if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
10418
10619
  return write();
10419
10620
  const locked = withLedgerLock(file, write);
@@ -10467,8 +10668,10 @@ function readLedger(cwd, options = {}) {
10467
10668
  if (!file.ok)
10468
10669
  return file;
10469
10670
  let text = "";
10671
+ let stat = null;
10470
10672
  try {
10471
- text = fs5.readFileSync(file.value, "utf8");
10673
+ stat = fs6.statSync(file.value);
10674
+ text = fs6.readFileSync(file.value, "utf8");
10472
10675
  } catch (error) {
10473
10676
  if (error.code === "ENOENT")
10474
10677
  return {
@@ -10487,6 +10690,7 @@ function readLedger(cwd, options = {}) {
10487
10690
  const integrity = { chained: 0, legacy: 0, unverified: [] };
10488
10691
  const earlier = new Set([null]);
10489
10692
  let chainStarted = false;
10693
+ const ids = new Set;
10490
10694
  for (const raw of text.split(`
10491
10695
  `)) {
10492
10696
  const trimmed = raw.trim();
@@ -10520,6 +10724,10 @@ function readLedger(cwd, options = {}) {
10520
10724
  reason = "unsigned";
10521
10725
  else
10522
10726
  rowIntegrity = "legacy";
10727
+ if (options.integrity && !reason && ids.has(parsed.id))
10728
+ reason = "duplicate_id";
10729
+ if (options.integrity)
10730
+ ids.add(parsed.id);
10523
10731
  if (reason)
10524
10732
  rowIntegrity = "unverified";
10525
10733
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -10543,7 +10751,8 @@ function readLedger(cwd, options = {}) {
10543
10751
  seq: rows.length + 1,
10544
10752
  superseded: false,
10545
10753
  supersedeIgnored: false,
10546
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
10754
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
10755
+ ...reason ? { integrityReason: reason } : {}
10547
10756
  };
10548
10757
  rows.push(row);
10549
10758
  if (!options.integrity)
@@ -10571,18 +10780,44 @@ function readLedger(cwd, options = {}) {
10571
10780
  const byId = new Map;
10572
10781
  for (const row of rows) {
10573
10782
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
10574
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10783
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10575
10784
  target.superseded = true;
10576
10785
  else if (row.supersedes)
10577
10786
  row.supersedeIgnored = true;
10578
10787
  if (!byId.has(row.id))
10579
10788
  byId.set(row.id, row);
10580
10789
  }
10790
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
10791
+ tamperCache.set(file.value, {
10792
+ size: stat.size,
10793
+ stamp: fileStamp(stat),
10794
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
10795
+ });
10581
10796
  return {
10582
10797
  ok: true,
10583
10798
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
10584
10799
  };
10585
10800
  }
10801
+ function ledgerTampering(cwd) {
10802
+ const file = ledgerPath(cwd);
10803
+ if (!file.ok)
10804
+ return [];
10805
+ try {
10806
+ const stat = fs6.statSync(file.value);
10807
+ const cached = tamperCache.get(file.value);
10808
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
10809
+ return cached.breaks;
10810
+ } catch {
10811
+ return [];
10812
+ }
10813
+ const read = readLedger(cwd, { integrity: true });
10814
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
10815
+ }
10816
+ function tamperedUnblock(cwd) {
10817
+ const file = ledgerPath(cwd);
10818
+ const where = file.ok ? file.value : "the ledger file";
10819
+ 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`;
10820
+ }
10586
10821
  function verdictBasis(verdict, current) {
10587
10822
  if (verdict.head && verdict.head === current.head)
10588
10823
  return "fresh";
@@ -10649,15 +10884,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
10649
10884
  sessions.add(session);
10650
10885
  return sessions;
10651
10886
  }
10652
- function effectiveOf(rows, authors, sameCode, isCurrent) {
10887
+ function sessionTrust(cwd, row) {
10888
+ const session = row.actor.session;
10889
+ if (!session || row.self === true)
10890
+ return null;
10891
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
10892
+ const host = row.actor.host;
10893
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
10894
+ if (!issuedOnly)
10895
+ return null;
10896
+ const binding = issuedBinding(cwd, session);
10897
+ if (binding === null)
10898
+ return "unissued_session";
10899
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
10900
+ }
10901
+ function trustReasons(cwd, row) {
10902
+ if (row.integrity === "unverified")
10903
+ return [row.integrityReason ?? "unsigned"];
10904
+ if (row.integrity !== "verified")
10905
+ return ["unsigned"];
10906
+ const session = sessionTrust(cwd, row);
10907
+ return session ? [session] : [];
10908
+ }
10909
+ function withIntegrity(cwd, rows) {
10910
+ if (rows.every((row) => row.integrity !== undefined))
10911
+ return rows;
10912
+ const read = readLedger(cwd, { integrity: true });
10913
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
10914
+ return rows.map((row) => {
10915
+ if (row.integrity !== undefined)
10916
+ return row;
10917
+ const fresh = bySeq.get(row.seq);
10918
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
10919
+ });
10920
+ }
10921
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
10653
10922
  let effective = null;
10654
10923
  for (const row of rows) {
10655
10924
  if (!effective) {
10656
10925
  effective = row;
10657
10926
  continue;
10658
10927
  }
10659
- const rowIndependent = independenceReasons(row, authors).length === 0;
10660
- const effIndependent = independenceReasons(effective, authors).length === 0;
10928
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
10929
+ continue;
10930
+ const rowIndependent = reasonsOf(row).length === 0;
10931
+ const effIndependent = reasonsOf(effective).length === 0;
10661
10932
  if (effIndependent && !rowIndependent && isCurrent(effective))
10662
10933
  continue;
10663
10934
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -10671,7 +10942,7 @@ function restackCarries(rows, branch, from, to) {
10671
10942
  return false;
10672
10943
  const edges = new Map;
10673
10944
  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)
10945
+ 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
10946
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
10676
10947
  const seen = new Set([from]);
10677
10948
  const queue = [from];
@@ -10687,7 +10958,8 @@ function restackCarries(rows, branch, from, to) {
10687
10958
  }
10688
10959
  return false;
10689
10960
  }
10690
- function checkVerdicts(cwd, branch, rows) {
10961
+ function checkVerdicts(cwd, branch, ledgerRows) {
10962
+ const rows = withIntegrity(cwd, ledgerRows);
10691
10963
  const head = branchHead(cwd, branch);
10692
10964
  const fallbackBase = defaultBase(cwd);
10693
10965
  const scope = branchScope(cwd, branch);
@@ -10717,21 +10989,47 @@ function checkVerdicts(cwd, branch, rows) {
10717
10989
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
10718
10990
  };
10719
10991
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
10992
+ const failureAuthorCache = new Map;
10993
+ const failureAuthors = (row) => {
10994
+ let set = failureAuthorCache.get(row.seq);
10995
+ if (!set) {
10996
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
10997
+ failureAuthorCache.set(row.seq, set);
10998
+ }
10999
+ return set;
11000
+ };
11001
+ const reasonCache = new Map;
11002
+ const reasonsOf = (row) => {
11003
+ let reasons = reasonCache.get(row);
11004
+ if (!reasons) {
11005
+ const failing = FAILING.has(String(row.result));
11006
+ reasons = [
11007
+ ...trustReasons(cwd, row),
11008
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
11009
+ ];
11010
+ reasonCache.set(row, reasons);
11011
+ }
11012
+ return reasons;
11013
+ };
11014
+ const retired = new Set;
11015
+ for (const row of rows)
11016
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
11017
+ retired.add(row.supersedes);
10720
11018
  const byKind = new Map;
10721
11019
  for (const row of rows)
10722
- if (isLiveVerdict(row) && scope(row)) {
11020
+ if (isLiveVerdict(row, retired) && scope(row)) {
10723
11021
  const kind = typeof row.kind === "string" ? row.kind : "review";
10724
11022
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
10725
11023
  }
10726
11024
  const verdicts = [];
10727
11025
  const warnings = [];
10728
11026
  for (const [kind, list] of byKind) {
10729
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
11027
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
10730
11028
  if (!row)
10731
11029
  continue;
10732
11030
  const basis = basisOf(row);
10733
11031
  const current = basis !== "stale";
10734
- const independence = independenceReasons(row, authors);
11032
+ const independence = reasonsOf(row);
10735
11033
  const reasons = [
10736
11034
  ...current ? [] : ["stale"],
10737
11035
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -10748,23 +11046,31 @@ function checkVerdicts(cwd, branch, rows) {
10748
11046
  });
10749
11047
  if (reasons.length === 0) {
10750
11048
  for (const later of list)
10751
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
11049
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
10752
11050
  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
11051
  }
10754
11052
  }
10755
11053
  for (const row of rows)
10756
11054
  if (row.integrity === "unverified" && scope(row))
10757
11055
  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)`);
11056
+ const tampered = ledgerTampering(cwd);
11057
+ if (tampered.length) {
11058
+ warnings.push(tamperedWarning(cwd, tampered));
11059
+ for (const entry of verdicts) {
11060
+ entry.accepted = false;
11061
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
11062
+ }
11063
+ }
10758
11064
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
10759
11065
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
10760
11066
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
10761
11067
  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: [] } : {
11068
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
10763
11069
  accepted: false,
10764
11070
  verdict: failing?.verdict ?? null,
10765
11071
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
10766
11072
  };
10767
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
11073
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
10768
11074
  return {
10769
11075
  branch,
10770
11076
  head,
@@ -10785,7 +11091,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10785
11091
  error,
10786
11092
  ...unblock ? { unblock } : {}
10787
11093
  }), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
10788
- const run = spawnSync7("git", args, {
11094
+ const run = spawnSync8("git", args, {
10789
11095
  cwd,
10790
11096
  encoding: "utf8",
10791
11097
  timeout: timeoutMs,
@@ -10819,24 +11125,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10819
11125
  } catch {
10820
11126
  return null;
10821
11127
  }
10822
- }, newId2 = () => `${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
11128
+ }, newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
10823
11129
  if (process.platform !== "linux")
10824
11130
  return false;
10825
11131
  try {
10826
- return isNetworkFsType(fs5.statfsSync(dir).type);
11132
+ return isNetworkFsType(fs6.statfsSync(dir).type);
10827
11133
  } catch {
10828
11134
  return false;
10829
11135
  }
10830
11136
  }, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
10831
11137
  try {
10832
- return fs5.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
11138
+ return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
10833
11139
  } catch {
10834
11140
  return null;
10835
11141
  }
10836
11142
  }, lockAgeMs = (lock) => {
10837
11143
  for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
10838
11144
  try {
10839
- return Date.now() - fs5.statSync(target).mtimeMs;
11145
+ return Date.now() - fs6.statSync(target).mtimeMs;
10840
11146
  } catch {}
10841
11147
  }
10842
11148
  return null;
@@ -10844,7 +11150,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
10844
11150
  ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10845
11151
  for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
10846
11152
  const buffer = Buffer.alloc(span);
10847
- fs5.readSync(fd, buffer, 0, span, size - span);
11153
+ fs6.readSync(fd, buffer, 0, span, size - span);
10848
11154
  const end = buffer.lastIndexOf(10);
10849
11155
  if (end < 0) {
10850
11156
  if (span === size)
@@ -10872,6 +11178,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10872
11178
  return text || null;
10873
11179
  }
10874
11180
  return null;
11181
+ }, tighten = (fd) => {
11182
+ try {
11183
+ if ((fs6.fstatSync(fd).mode & 63) !== 0)
11184
+ fs6.fchmodSync(fd, 384);
11185
+ } catch {}
10875
11186
  }, sealed = (row, id, at, previous) => {
10876
11187
  const { prevHash: _prev, rowHash: _row, ...fields } = row;
10877
11188
  const prevHash = previous === null ? null : sha2562(previous);
@@ -10886,7 +11197,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10886
11197
  session: str(actor.session),
10887
11198
  agentId: str(actor.agentId)
10888
11199
  };
10889
- }, isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded, RENAMED, renameAwareBranch = (scopeOf, branch) => {
11200
+ }, 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
11201
  return (target) => {
10891
11202
  const own = typeof branch === "string" ? branch : null;
10892
11203
  const theirs = typeof target.branch === "string" ? target.branch : null;
@@ -10903,7 +11214,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10903
11214
  if (derivesFrom(author, session) || derivesFrom(session, author))
10904
11215
  return true;
10905
11216
  return false;
10906
- }, independenceReasons = (row, authors) => {
11217
+ }, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
10907
11218
  const reasons = [];
10908
11219
  if (!row.actor.session)
10909
11220
  reasons.push("no_session");
@@ -10914,6 +11225,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
10914
11225
  return reasons;
10915
11226
  }, OBSERVED_ONLY;
10916
11227
  var init_ledger = __esm(() => {
11228
+ init_store_lock();
10917
11229
  init_vcs_config();
10918
11230
  init_rev();
10919
11231
  init_host_session();
@@ -10937,56 +11249,73 @@ var init_ledger = __esm(() => {
10937
11249
  ledgerLock = {
10938
11250
  acquire(lock) {
10939
11251
  try {
10940
- fs5.mkdirSync(lock);
11252
+ fs6.mkdirSync(lock);
10941
11253
  } catch (error) {
10942
11254
  if (error.code === "EEXIST")
10943
11255
  return null;
10944
11256
  throw error;
10945
11257
  }
10946
- const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
10947
- fs5.writeFileSync(path8.join(lock, LOCK_OWNER), token);
11258
+ const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
11259
+ fs6.writeFileSync(path8.join(lock, LOCK_OWNER), token);
10948
11260
  return token;
10949
11261
  },
10950
11262
  release(lock, token) {
10951
11263
  if (lockToken(lock) !== token)
10952
11264
  return false;
10953
- const tomb = `${lock}.release-${randomBytes(6).toString("hex")}`;
11265
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
10954
11266
  try {
10955
- fs5.renameSync(lock, tomb);
11267
+ fs6.renameSync(lock, tomb);
10956
11268
  } catch {
10957
11269
  return false;
10958
11270
  }
10959
11271
  if (lockToken(tomb) !== token) {
10960
11272
  try {
10961
- fs5.renameSync(tomb, lock);
11273
+ fs6.renameSync(tomb, lock);
10962
11274
  } catch {}
10963
11275
  return false;
10964
11276
  }
10965
- fs5.rmSync(tomb, { recursive: true, force: true });
11277
+ fs6.rmSync(tomb, { recursive: true, force: true });
10966
11278
  return true;
10967
11279
  },
10968
11280
  token: lockToken,
10969
11281
  reap(lock, observed) {
10970
- const tomb = `${lock}.reap-${randomBytes(6).toString("hex")}`;
11282
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
10971
11283
  try {
10972
- fs5.renameSync(lock, tomb);
11284
+ fs6.renameSync(lock, tomb);
10973
11285
  } catch {
10974
11286
  return false;
10975
11287
  }
10976
11288
  if (lockToken(tomb) === observed) {
10977
- fs5.rmSync(tomb, { recursive: true, force: true });
11289
+ fs6.rmSync(tomb, { recursive: true, force: true });
10978
11290
  return true;
10979
11291
  }
10980
11292
  try {
10981
- fs5.renameSync(tomb, lock);
11293
+ fs6.renameSync(tomb, lock);
10982
11294
  } catch {
10983
- fs5.rmSync(tomb, { recursive: true, force: true });
11295
+ fs6.rmSync(tomb, { recursive: true, force: true });
10984
11296
  }
10985
11297
  return false;
10986
11298
  }
10987
11299
  };
10988
11300
  TAIL_BYTES = 64 * 1024;
11301
+ TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
11302
+ tamperCache = new Map;
10989
11303
  RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
11304
+ TRUST_REASONS = new Set([
11305
+ "unsigned",
11306
+ "hash_mismatch",
11307
+ "broken_link",
11308
+ "duplicate_id",
11309
+ "unissued_session",
11310
+ "unbound_session"
11311
+ ]);
11312
+ SHELL_VOUCHED_HOSTS = new Set([
11313
+ "cli",
11314
+ "opencode",
11315
+ "pi",
11316
+ "codex_cli",
11317
+ "codex_desktop"
11318
+ ]);
10990
11319
  OBSERVED_ONLY = new Set([
10991
11320
  "check",
10992
11321
  "merge.unverified",
@@ -11936,7 +12265,7 @@ function normalizeMaxBytes(value, options = {}) {
11936
12265
  }
11937
12266
 
11938
12267
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
11939
- import fs6 from "node:fs";
12268
+ import fs7 from "node:fs";
11940
12269
  function createScratchBuffer(maxBytes) {
11941
12270
  const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
11942
12271
  return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
@@ -11959,7 +12288,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
11959
12288
  let total = 0;
11960
12289
  while (true) {
11961
12290
  const length = nextReadLength(total, maxBytes, scratch.length);
11962
- const bytesRead = fs6.readSync(fd, scratch, 0, length, null);
12291
+ const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
11963
12292
  if (bytesRead === 0) {
11964
12293
  return Buffer.concat(chunks, total);
11965
12294
  }
@@ -11973,9 +12302,9 @@ var init_bounded_read = __esm(() => {
11973
12302
  });
11974
12303
 
11975
12304
  // node_modules/@openclaw/fs-safe/dist/read-open-flags.js
11976
- import fs7 from "node:fs";
12305
+ import fs8 from "node:fs";
11977
12306
  function resolveReadOpenFlags(options) {
11978
- const constants = options?.constants ?? fs7.constants;
12307
+ const constants = options?.constants ?? fs8.constants;
11979
12308
  const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
11980
12309
  const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
11981
12310
  return constants.O_RDONLY | noFollow | nonBlocking;
@@ -11983,7 +12312,7 @@ function resolveReadOpenFlags(options) {
11983
12312
  var init_read_open_flags = () => {};
11984
12313
 
11985
12314
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
11986
- import { randomBytes as randomBytes2 } from "node:crypto";
12315
+ import { randomBytes as randomBytes3 } from "node:crypto";
11987
12316
  import fsSync from "node:fs";
11988
12317
  import path11 from "node:path";
11989
12318
  function parseSidecarLockSnapshot(snapshot, parser) {
@@ -11991,7 +12320,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
11991
12320
  }
11992
12321
  function createSidecarLockOwnershipToken() {
11993
12322
  let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
11994
- for (const byte of randomBytes2(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
12323
+ for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
11995
12324
  for (let bit = 7;bit >= 0; bit -= 1) {
11996
12325
  token += byte & 1 << bit ? "\t" : " ";
11997
12326
  }
@@ -12188,7 +12517,7 @@ function sleepSync(ms) {
12188
12517
  }
12189
12518
 
12190
12519
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
12191
- import fs8 from "node:fs";
12520
+ import fs9 from "node:fs";
12192
12521
  import path12 from "node:path";
12193
12522
  function getSyncHeldLocks() {
12194
12523
  const globalWithState = globalThis;
@@ -12206,7 +12535,7 @@ function releaseAllSyncHeldLocks() {
12206
12535
  }
12207
12536
  try {
12208
12537
  if (held.fd !== undefined)
12209
- fs8.closeSync(held.fd);
12538
+ fs9.closeSync(held.fd);
12210
12539
  } catch {}
12211
12540
  try {
12212
12541
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
@@ -12241,7 +12570,7 @@ function releaseSyncHeldLock(held) {
12241
12570
  if (held.fd !== undefined) {
12242
12571
  const fd = held.fd;
12243
12572
  held.fd = undefined;
12244
- fs8.closeSync(fd);
12573
+ fs9.closeSync(fd);
12245
12574
  }
12246
12575
  removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
12247
12576
  heldLocks.delete(held.normalizedTargetPath);
@@ -12264,11 +12593,11 @@ function createSyncHeldLockHandle(held) {
12264
12593
  };
12265
12594
  }
12266
12595
  function canonicalLockParentSync(parent) {
12267
- return process.platform === "win32" ? fs8.realpathSync.native(parent) : fs8.realpathSync(parent);
12596
+ return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
12268
12597
  }
12269
12598
  function normalizeTargetPath(targetPath) {
12270
12599
  const resolved = path12.resolve(targetPath);
12271
- fs8.mkdirSync(path12.dirname(resolved), { recursive: true });
12600
+ fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
12272
12601
  try {
12273
12602
  return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
12274
12603
  } catch {
@@ -12296,7 +12625,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
12296
12625
  }
12297
12626
  function reclaimGuardExists(reclaimGuardPath) {
12298
12627
  try {
12299
- fs8.lstatSync(reclaimGuardPath);
12628
+ fs9.lstatSync(reclaimGuardPath);
12300
12629
  return true;
12301
12630
  } catch (error) {
12302
12631
  if (error.code === "ENOENT")
@@ -12360,19 +12689,19 @@ function acquireFileLockSync(targetPath, options) {
12360
12689
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
12361
12690
  let lockFileCreateDenied = false;
12362
12691
  try {
12363
- const noFollow = process.platform !== "win32" && typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
12692
+ const noFollow = process.platform !== "win32" && typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
12364
12693
  try {
12365
- fd = fs8.openSync(lockPath, fs8.constants.O_WRONLY | fs8.constants.O_CREAT | fs8.constants.O_EXCL | noFollow, 384);
12694
+ fd = fs9.openSync(lockPath, fs9.constants.O_WRONLY | fs9.constants.O_CREAT | fs9.constants.O_EXCL | noFollow, 384);
12366
12695
  } catch (error) {
12367
12696
  lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
12368
12697
  throw error;
12369
12698
  }
12370
- fs8.writeFileSync(fd, raw, "utf8");
12371
- fs8.fsyncSync(fd);
12699
+ fs9.writeFileSync(fd, raw, "utf8");
12700
+ fs9.fsyncSync(fd);
12372
12701
  const snapshot = {
12373
12702
  raw,
12374
12703
  payload,
12375
- stat: fs8.fstatSync(fd),
12704
+ stat: fs9.fstatSync(fd),
12376
12705
  ownershipToken
12377
12706
  };
12378
12707
  const createdHeld = {
@@ -12407,8 +12736,8 @@ function acquireFileLockSync(targetPath, options) {
12407
12736
  return returnedHandle;
12408
12737
  } catch (error) {
12409
12738
  if (fd !== undefined) {
12410
- const failed = { payload: null, stat: fs8.fstatSync(fd) };
12411
- fs8.closeSync(fd);
12739
+ const failed = { payload: null, stat: fs9.fstatSync(fd) };
12740
+ fs9.closeSync(fd);
12412
12741
  fd = undefined;
12413
12742
  removeSidecarLockIfUnchangedSync(lockPath, failed);
12414
12743
  }
@@ -12457,7 +12786,7 @@ function acquireFileLockSync(targetPath, options) {
12457
12786
  })) {
12458
12787
  let ownsReclaimGuard = false;
12459
12788
  try {
12460
- fs8.mkdirSync(reclaimGuardPath);
12789
+ fs9.mkdirSync(reclaimGuardPath);
12461
12790
  ownsReclaimGuard = true;
12462
12791
  if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
12463
12792
  continue;
@@ -12470,182 +12799,51 @@ function acquireFileLockSync(targetPath, options) {
12470
12799
  } finally {
12471
12800
  if (ownsReclaimGuard) {
12472
12801
  try {
12473
- fs8.rmdirSync(reclaimGuardPath);
12802
+ fs9.rmdirSync(reclaimGuardPath);
12474
12803
  } catch {}
12475
12804
  }
12476
12805
  }
12477
12806
  }
12478
12807
  throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
12479
- code: "file_lock_stale",
12480
- lockPath,
12481
- normalizedTargetPath
12482
- });
12483
- }
12484
- waitForRetry();
12485
- }
12486
- }
12487
- }
12488
- var SYNC_HELD_LOCKS_KEY, SYNC_CLEANUP_REGISTERED_KEY, SYNC_CLEANUP_HANDLER_KEY;
12489
- var init_file_lock_sync = __esm(() => {
12490
- init_errors3();
12491
- init_sidecar_lock_reclaim();
12492
- init_sidecar_lock_policy();
12493
- init_lock_config();
12494
- SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
12495
- SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
12496
- SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
12497
- });
12498
-
12499
- // node_modules/@openclaw/fs-safe/dist/file-lock.js
12500
- var init_file_lock = __esm(() => {
12501
- init_file_lock_sync();
12502
- });
12503
-
12504
- // packages/workit-core/src/core/package-root.ts
12505
- import { existsSync as existsSync3 } from "node:fs";
12506
- import path13 from "node:path";
12507
- import { fileURLToPath } from "node:url";
12508
- var packageRoot = () => {
12509
- let dir = path13.dirname(fileURLToPath(import.meta.url));
12510
- while (true) {
12511
- const parent = path13.dirname(dir);
12512
- if (existsSync3(path13.join(dir, "package.json")) || parent === dir)
12513
- return dir;
12514
- dir = parent;
12515
- }
12516
- };
12517
- var init_package_root = () => {};
12518
-
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;
12808
+ code: "file_lock_stale",
12809
+ lockPath,
12810
+ normalizedTargetPath
12811
+ });
12812
+ }
12813
+ waitForRetry();
12563
12814
  }
12564
12815
  }
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;
12816
+ }
12817
+ var SYNC_HELD_LOCKS_KEY, SYNC_CLEANUP_REGISTERED_KEY, SYNC_CLEANUP_HANDLER_KEY;
12818
+ var init_file_lock_sync = __esm(() => {
12819
+ init_errors3();
12820
+ init_sidecar_lock_reclaim();
12821
+ init_sidecar_lock_policy();
12822
+ init_lock_config();
12823
+ SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
12824
+ SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
12825
+ SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
12826
+ });
12827
+
12828
+ // node_modules/@openclaw/fs-safe/dist/file-lock.js
12829
+ var init_file_lock = __esm(() => {
12830
+ init_file_lock_sync();
12831
+ });
12832
+
12833
+ // packages/workit-core/src/core/package-root.ts
12834
+ import { existsSync as existsSync3 } from "node:fs";
12835
+ import path13 from "node:path";
12836
+ import { fileURLToPath } from "node:url";
12837
+ var packageRoot = () => {
12838
+ let dir = path13.dirname(fileURLToPath(import.meta.url));
12839
+ while (true) {
12840
+ const parent = path13.dirname(dir);
12841
+ if (existsSync3(path13.join(dir, "package.json")) || parent === dir)
12842
+ return dir;
12843
+ dir = parent;
12633
12844
  }
12634
12845
  };
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
- });
12846
+ var init_package_root = () => {};
12649
12847
 
12650
12848
  // packages/workit-core/src/store/event-log.ts
12651
12849
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -19869,10 +20067,238 @@ var init_policy = __esm(() => {
19869
20067
  init_shell_words();
19870
20068
  });
19871
20069
 
20070
+ // packages/workit-core/src/core/live-session.ts
20071
+ import { spawnSync as spawnSync13 } from "node:child_process";
20072
+ import { createHash as createHash15 } from "node:crypto";
20073
+ import fs17 from "node:fs";
20074
+ import os6 from "node:os";
20075
+ import path22 from "node:path";
20076
+ var LIVE_SESSION_TTL_MS, HEARTBEAT_EVERY_MS = 30000, liveProbe, leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session, familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, ""), processAlive = (pid) => {
20077
+ try {
20078
+ process.kill(pid, 0);
20079
+ return true;
20080
+ } catch (error) {
20081
+ return error.code === "EPERM";
20082
+ }
20083
+ }, processInfo = (pid) => {
20084
+ try {
20085
+ if (liveProbe.platform === "linux") {
20086
+ const stat = fs17.readFileSync(`/proc/${pid}/stat`, "utf8");
20087
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
20088
+ const command = fs17.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
20089
+ return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
20090
+ }
20091
+ if (liveProbe.platform === "win32")
20092
+ return null;
20093
+ const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
20094
+ return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
20095
+ } catch {
20096
+ return null;
20097
+ }
20098
+ }, WRAPPER, PIDS, hostPid = (transport) => {
20099
+ if (transport !== "hook-process")
20100
+ return process.pid;
20101
+ const key = `${liveProbe.platform}\x00${process.ppid}`;
20102
+ if (PIDS.has(key))
20103
+ return PIDS.get(key) ?? null;
20104
+ if (liveProbe.platform === "win32") {
20105
+ PIDS.set(key, null);
20106
+ return null;
20107
+ }
20108
+ let pid = process.ppid;
20109
+ for (let hop = 0;hop < 4; hop++) {
20110
+ const info = processInfo(pid);
20111
+ if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
20112
+ break;
20113
+ pid = info.ppid;
20114
+ }
20115
+ PIDS.set(key, pid);
20116
+ return pid;
20117
+ }, sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), liveOwner = (input, transport, env = process.env) => {
20118
+ const session = input.session.id?.trim();
20119
+ if (!session)
20120
+ return null;
20121
+ const leads = new Set([leadOf(sessionSafe(session))]);
20122
+ if (input.session.parentId)
20123
+ leads.add(leadOf(sessionSafe(input.session.parentId)));
20124
+ if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
20125
+ leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
20126
+ return {
20127
+ host: input.host,
20128
+ session: sessionSafe(session),
20129
+ leads: [...leads],
20130
+ pid: () => hostPid(transport)
20131
+ };
20132
+ }, checkoutOf = (target) => {
20133
+ let dir = path22.resolve(target);
20134
+ while (!fs17.existsSync(dir) && path22.dirname(dir) !== dir)
20135
+ dir = path22.dirname(dir);
20136
+ if (fs17.existsSync(dir) && !fs17.statSync(dir).isDirectory())
20137
+ dir = path22.dirname(dir);
20138
+ const parts = dir.split(path22.sep);
20139
+ const dotGit = parts.lastIndexOf(".git");
20140
+ if (dotGit > 0)
20141
+ dir = parts.slice(0, dotGit).join(path22.sep) || path22.sep;
20142
+ const location = resolveStore(dir);
20143
+ if (location instanceof Error || !location.shared || !location.top)
20144
+ return null;
20145
+ let top = location.top;
20146
+ try {
20147
+ top = fs17.realpathSync.native(top);
20148
+ } catch {}
20149
+ return top;
20150
+ }, markerFile2 = (top) => path22.join(resolveStateDir(), "live-sessions", `${createHash15("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`), checkoutKey = (top, platform = process.platform) => {
20151
+ let key = path22.resolve(top);
20152
+ try {
20153
+ key = fs17.realpathSync.native(key);
20154
+ } catch {}
20155
+ return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
20156
+ }, readMarker = (file) => {
20157
+ try {
20158
+ const value = JSON.parse(fs17.readFileSync(file, "utf8"));
20159
+ if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
20160
+ return null;
20161
+ return {
20162
+ version: 1,
20163
+ session: value.session,
20164
+ leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
20165
+ host: value.host,
20166
+ pid: typeof value.pid === "number" ? value.pid : null,
20167
+ pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
20168
+ machine: typeof value.machine === "string" ? value.machine : "",
20169
+ checkout: typeof value.checkout === "string" ? value.checkout : "",
20170
+ heartbeatAt: value.heartbeatAt
20171
+ };
20172
+ } catch {
20173
+ return null;
20174
+ }
20175
+ }, ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt), isLive = (marker, deps) => {
20176
+ const age = ageOf2(marker, deps.now ?? new Date);
20177
+ if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
20178
+ return false;
20179
+ if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
20180
+ return true;
20181
+ if (!(deps.alive ?? processAlive)(marker.pid))
20182
+ return false;
20183
+ if (marker.pidStart === null)
20184
+ return true;
20185
+ const start = liveProbe.start(marker.pid);
20186
+ return start === null || start === marker.pidStart;
20187
+ }, sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead)), sameOwner = (marker, owner, machine) => {
20188
+ if (sameLead(marker, owner))
20189
+ return true;
20190
+ if (marker.machine !== machine)
20191
+ return false;
20192
+ const pid = owner.pid();
20193
+ if (pid === null || marker.pid === null || marker.pidStart === null)
20194
+ return familyOf(marker.host) === familyOf(owner.host);
20195
+ return marker.pid === pid;
20196
+ }, liveConflict = (target, owner, deps = {}) => {
20197
+ try {
20198
+ const checkout = checkoutOf(target);
20199
+ if (!checkout)
20200
+ return null;
20201
+ const file = markerFile2(checkout);
20202
+ const marker = readMarker(file);
20203
+ if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
20204
+ return null;
20205
+ if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
20206
+ return null;
20207
+ return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
20208
+ } catch {
20209
+ return null;
20210
+ }
20211
+ }, claimLiveSession = (target, owner, deps = {}) => {
20212
+ try {
20213
+ const checkout = checkoutOf(target);
20214
+ if (!checkout)
20215
+ return;
20216
+ const file = markerFile2(checkout);
20217
+ const now = deps.now ?? new Date;
20218
+ const machine = deps.machine ?? os6.hostname();
20219
+ const current = readMarker(file);
20220
+ if (current) {
20221
+ const age = ageOf2(current, now);
20222
+ if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
20223
+ return;
20224
+ if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
20225
+ return;
20226
+ }
20227
+ const pid = owner.pid();
20228
+ const marker = {
20229
+ version: 1,
20230
+ session: owner.session,
20231
+ leads: owner.leads,
20232
+ host: owner.host,
20233
+ pid,
20234
+ pidStart: pid === null ? null : liveProbe.start(pid),
20235
+ machine,
20236
+ checkout,
20237
+ heartbeatAt: now.toISOString()
20238
+ };
20239
+ fs17.mkdirSync(path22.dirname(file), { recursive: true });
20240
+ const temp = `${file}.${process.pid}.tmp`;
20241
+ fs17.writeFileSync(temp, JSON.stringify(marker));
20242
+ fs17.renameSync(temp, file);
20243
+ } catch {}
20244
+ };
20245
+ var init_live_session = __esm(() => {
20246
+ init_paths();
20247
+ init_logger();
20248
+ init_store_lock();
20249
+ LIVE_SESSION_TTL_MS = 15 * 60000;
20250
+ liveProbe = {
20251
+ platform: process.platform,
20252
+ run: (args) => spawnSync13("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
20253
+ start: (pid) => processStartOf(pid)
20254
+ };
20255
+ WRAPPER = /^(?:\S*\/)?(?:sh|bash|dash|zsh|ash|ksh|fish|env|timeout|npx|npm|bunx|cmd\.exe|pwsh|powershell)(?:\s|$)|launch\.mjs|npx-cli\.js/u;
20256
+ PIDS = new Map;
20257
+ });
20258
+
19872
20259
  // packages/workit-core/src/hooks/write-gate.ts
19873
20260
  import { realpathSync as realpathSync5 } from "node:fs";
19874
- import path22 from "node:path";
19875
- function writeGate(cwd, paths) {
20261
+ import os7 from "node:os";
20262
+ import path23 from "node:path";
20263
+ function writeGate(cwd, paths, owner, deps) {
20264
+ if (!owner)
20265
+ return beforeWriteGate(cwd, paths);
20266
+ const targets = liveTargets(cwd, paths);
20267
+ const live = liveGate(owner, targets, deps);
20268
+ if (live.kind !== "none")
20269
+ return live;
20270
+ const decision = beforeWriteGate(cwd, paths);
20271
+ if (decision.kind === "none")
20272
+ claimAll(owner, targets, deps);
20273
+ return decision;
20274
+ }
20275
+ function shellGate(cwd, command, owner, dialect = "posix", deps) {
20276
+ const writes = shellWrites(command);
20277
+ let targets = [];
20278
+ if (owner) {
20279
+ try {
20280
+ targets = [
20281
+ ...segmentTargets(cwd, command, dialect),
20282
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path23.join(dir, DIR_TARGET))
20283
+ ];
20284
+ } catch {
20285
+ targets = [];
20286
+ }
20287
+ }
20288
+ if (owner && targets.length) {
20289
+ const live = liveGate(owner, targets, deps);
20290
+ if (live.kind !== "none") {
20291
+ if (!overridden(command, dialect))
20292
+ return live;
20293
+ noteOverride(cwd, owner, command);
20294
+ }
20295
+ }
20296
+ const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
20297
+ if (owner && targets.length && decision.kind === "none")
20298
+ claimAll(owner, targets, deps);
20299
+ return decision;
20300
+ }
20301
+ function beforeWriteGate(cwd, paths) {
19876
20302
  try {
19877
20303
  const gate = gateFor(cwd);
19878
20304
  if (!gate || gate.blockers.length === 0)
@@ -19891,21 +20317,21 @@ function writeGate(cwd, paths) {
19891
20317
  }
19892
20318
  var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
19893
20319
  let rest = "";
19894
- for (let dir = path22.resolve(file);; dir = path22.dirname(dir)) {
20320
+ for (let dir = path23.resolve(file);; dir = path23.dirname(dir)) {
19895
20321
  try {
19896
- return path22.join(realpathSync5(dir), rest);
20322
+ return path23.join(realpathSync5(dir), rest);
19897
20323
  } catch {
19898
- if (path22.dirname(dir) === dir)
19899
- return path22.resolve(file);
19900
- rest = rest ? path22.join(path22.basename(dir), rest) : path22.basename(dir);
20324
+ if (path23.dirname(dir) === dir)
20325
+ return path23.resolve(file);
20326
+ rest = rest ? path23.join(path23.basename(dir), rest) : path23.basename(dir);
19901
20327
  }
19902
20328
  }
19903
20329
  }, exempt = (cwd, root, file, plans) => {
19904
20330
  const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
19905
- const relative = path22.relative(fold(canonical2(root)), fold(canonical2(path22.resolve(cwd, file))));
19906
- if (relative.startsWith("..") || path22.isAbsolute(relative))
20331
+ const relative = path23.relative(fold(canonical2(root)), fold(canonical2(path23.resolve(cwd, file))));
20332
+ if (relative.startsWith("..") || path23.isAbsolute(relative))
19907
20333
  return true;
19908
- const posix = relative.split(path22.sep).join("/");
20334
+ const posix = relative.split(path23.sep).join("/");
19909
20335
  const segments = posix.split("/");
19910
20336
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
19911
20337
  }, PREFIXES2, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
@@ -19997,7 +20423,7 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
19997
20423
  if (!unknown && targets.length === 0)
19998
20424
  return { writes: false, targets: null };
19999
20425
  return { writes: true, targets: unknown ? null : targets };
20000
- }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path22.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
20426
+ }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path23.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
20001
20427
  const store = new TaskStore(cwd);
20002
20428
  const listed = store.listTaskIndex();
20003
20429
  if (!listed.ok)
@@ -20033,21 +20459,182 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
20033
20459
  if (cache2.size > 32)
20034
20460
  cache2.delete(cache2.keys().next().value);
20035
20461
  return gate;
20462
+ }, MUTATING_GIT, DRY_N, MUTATING_WORKIT_GIT, DIR_TARGET = ".workit-live-target", OVERRIDE = "WORKIT_LIVE_SESSION=override", inert = (args, sub) => args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run" || DRY_N.has(sub) && /^-[a-zA-Z]*n[a-zA-Z]*$/.test(arg) && !arg.startsWith("--")), gitMutationDirs = (cwd, command, dialect) => {
20463
+ let invocations;
20464
+ try {
20465
+ invocations = rawInvocations(command, dialect);
20466
+ } catch {
20467
+ return [];
20468
+ }
20469
+ return invocations.flatMap((invocation) => {
20470
+ if (invocation.tool !== "git" || invocation.dir === undefined)
20471
+ return [];
20472
+ const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
20473
+ const sub = invocation.args[at] ?? "";
20474
+ const rest = invocation.args.slice(at + 1);
20475
+ if (!MUTATING_GIT.has(sub) || inert(rest, sub))
20476
+ return [];
20477
+ if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
20478
+ return [];
20479
+ return [path23.resolve(cwd, invocation.dir ?? ".")];
20480
+ });
20481
+ }, CD2, PUSHD2, POPD2, cdInto = (dir, words) => {
20482
+ const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
20483
+ if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
20484
+ return;
20485
+ return target === undefined ? os7.homedir() : path23.resolve(dir, target);
20486
+ }, quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`, segmentTargets = (cwd, command, dialect) => {
20487
+ const targets = [];
20488
+ let dir = cwd;
20489
+ let stack = [];
20490
+ const groups = [];
20491
+ const at = (file) => path23.isAbsolute(file) ? file : dir === undefined ? null : path23.resolve(dir, file);
20492
+ for (const segment of segmentsOf(command, dialect)) {
20493
+ if (segment.group === "open") {
20494
+ groups.push({ dir, stack: [...stack] });
20495
+ continue;
20496
+ }
20497
+ if (segment.group === "close") {
20498
+ const saved = groups.pop();
20499
+ if (saved)
20500
+ ({ dir, stack } = saved);
20501
+ continue;
20502
+ }
20503
+ const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
20504
+ const words = lead < 0 ? [] : segment.words.slice(lead);
20505
+ const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
20506
+ if (CD2.has(name)) {
20507
+ dir = cdInto(dir, words);
20508
+ continue;
20509
+ }
20510
+ if (PUSHD2.has(name)) {
20511
+ stack.push(dir);
20512
+ dir = cdInto(dir, words);
20513
+ continue;
20514
+ }
20515
+ if (POPD2.has(name)) {
20516
+ dir = stack.length ? stack.pop() : undefined;
20517
+ continue;
20518
+ }
20519
+ for (const file of segment.redirects)
20520
+ if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
20521
+ const resolved = at(file);
20522
+ if (resolved)
20523
+ targets.push(resolved);
20524
+ }
20525
+ const edits = editsOf(segment.words);
20526
+ if (edits === null) {
20527
+ if (dir !== undefined)
20528
+ targets.push(path23.join(dir, DIR_TARGET));
20529
+ } else
20530
+ for (const file of edits) {
20531
+ const resolved = at(file);
20532
+ if (resolved)
20533
+ targets.push(resolved);
20534
+ }
20535
+ if (dir !== undefined && workitInvocations(segment.words.map(quoteWord).join(" "), "posix").some((args) => args[0] === "git" && MUTATING_WORKIT_GIT.has(args[1] ?? "") && !inert(args.slice(2), "")))
20536
+ targets.push(path23.join(dir, DIR_TARGET));
20537
+ }
20538
+ return targets;
20539
+ }, overridden = (command, dialect) => {
20540
+ const segments = segmentsOf(command, dialect);
20541
+ if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
20542
+ return false;
20543
+ const words = segments[0].words;
20544
+ const lead = words.findIndex((word) => !/^\w+=/.test(word));
20545
+ return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
20546
+ }, HOST_LABELS, ageText = (ms) => {
20547
+ const seconds = Math.max(0, Math.round(ms / 1000));
20548
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
20549
+ }, liveDeny = ({ marker, checkout, ageMs, file }) => {
20550
+ const unblock = `Work in your own worktree (\`git worktree add <path> -b <branch>\`, or \`workit fanout worktree create <slice>\`). Only if the user confirms that session is finished: release it with \`rm '${file}'\`, or prefix one command with ${OVERRIDE}.`;
20551
+ return {
20552
+ kind: "deny",
20553
+ reason: `workit live-session guard: ${checkout} belongs to another live agent session, ${HOST_LABELS[marker.host] ?? marker.host} session ${marker.session}${marker.pid === null ? "" : ` (pid ${marker.pid})`}, last active ${ageText(ageMs)} ago. Do not edit a checkout another session is working in; read-only commands stay allowed. ${unblock}`,
20554
+ unblock
20555
+ };
20556
+ }, guarded = (targets) => {
20557
+ const seen = new Set;
20558
+ return targets.filter((target) => {
20559
+ const dir = path23.dirname(target);
20560
+ if (seen.has(dir))
20561
+ return false;
20562
+ seen.add(dir);
20563
+ return true;
20564
+ });
20565
+ }, liveGate = (owner, targets, deps) => {
20566
+ try {
20567
+ for (const target of guarded(targets)) {
20568
+ const conflict = liveConflict(target, owner, deps);
20569
+ if (conflict && workspaceLiveSessionGuard(conflict.checkout))
20570
+ return liveDeny(conflict);
20571
+ }
20572
+ } catch {}
20573
+ return NONE3;
20574
+ }, claimAll = (owner, targets, deps) => {
20575
+ for (const target of guarded(targets))
20576
+ claimLiveSession(target, owner, deps);
20577
+ }, liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path23.resolve(cwd, file)) : [path23.join(cwd, DIR_TARGET)], noteOverride = (cwd, owner, command) => {
20578
+ try {
20579
+ if (!inWorkitWorkspace(cwd))
20580
+ return;
20581
+ appendHookObserved(cwd, {
20582
+ type: "live_session.override",
20583
+ actor: { host: owner.host, session: owner.session, agentId: null },
20584
+ session: owner.session,
20585
+ command: command.slice(0, 200)
20586
+ });
20587
+ } catch {}
20036
20588
  };
20037
20589
  var init_write_gate = __esm(() => {
20038
20590
  init_task_store();
20039
20591
  init_derive();
20040
20592
  init_task_evaluation();
20593
+ init_live_session();
20594
+ init_workspaces();
20041
20595
  init_ledger();
20596
+ init_raw_git();
20042
20597
  init_shell_words();
20043
20598
  NONE3 = { kind: "none" };
20044
20599
  CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
20045
20600
  PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
20046
20601
  cache2 = new Map;
20602
+ MUTATING_GIT = new Set([
20603
+ "add",
20604
+ "am",
20605
+ "apply",
20606
+ "checkout",
20607
+ "cherry-pick",
20608
+ "clean",
20609
+ "commit",
20610
+ "merge",
20611
+ "mv",
20612
+ "pull",
20613
+ "rebase",
20614
+ "reset",
20615
+ "restore",
20616
+ "revert",
20617
+ "rm",
20618
+ "stash",
20619
+ "switch"
20620
+ ]);
20621
+ DRY_N = new Set(["add", "clean", "rm", "mv"]);
20622
+ MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
20623
+ CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
20624
+ PUSHD2 = new Set(["pushd", "push-location"]);
20625
+ POPD2 = new Set(["popd", "pop-location"]);
20626
+ HOST_LABELS = {
20627
+ claude_code: "Claude Code",
20628
+ opencode: "OpenCode",
20629
+ codex_cli: "Codex CLI",
20630
+ codex_desktop: "Codex Desktop",
20631
+ cursor: "Cursor",
20632
+ pi: "Pi"
20633
+ };
20047
20634
  });
20048
20635
 
20049
20636
  // packages/workit-core/src/hooks/stop.ts
20050
- import { spawnSync as spawnSync13 } from "node:child_process";
20637
+ import { spawnSync as spawnSync14 } from "node:child_process";
20051
20638
  function stopObligation(input, descriptor) {
20052
20639
  try {
20053
20640
  const session = input.session.id;
@@ -20056,10 +20643,10 @@ function stopObligation(input, descriptor) {
20056
20643
  const branch = currentBranch(input.cwd);
20057
20644
  if (!branch || guardedBranch(input.cwd, branch))
20058
20645
  return null;
20059
- const ledger = readLedger(input.cwd);
20646
+ const ledger = readLedger(input.cwd, { integrity: true });
20060
20647
  if (!ledger.ok)
20061
20648
  return null;
20062
- const all = ledger.value.rows;
20649
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
20063
20650
  const rows = all.filter((row) => row.branch === branch);
20064
20651
  const mine = rows.filter((row) => sessionOf(row) === session);
20065
20652
  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"));
@@ -20111,7 +20698,7 @@ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
20111
20698
  `);
20112
20699
  return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
20113
20700
  }, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
20114
- const run = spawnSync13("git", args, {
20701
+ const run = spawnSync14("git", args, {
20115
20702
  cwd,
20116
20703
  encoding: "utf8",
20117
20704
  timeout: 3000,
@@ -20423,9 +21010,9 @@ var init_skill_nudge = __esm(() => {
20423
21010
 
20424
21011
  // packages/workit-core/src/hooks/hosts/fields.ts
20425
21012
  import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
20426
- import path23 from "node:path";
21013
+ import path24 from "node:path";
20427
21014
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
20428
- if (!nonEmpty2(value) || !path23.isAbsolute(value) || !existsSync7(value))
21015
+ if (!nonEmpty2(value) || !path24.isAbsolute(value) || !existsSync7(value))
20429
21016
  return null;
20430
21017
  try {
20431
21018
  return statSync2(value).isDirectory() ? realpathSync6(value) : null;
@@ -20530,8 +21117,7 @@ function handleHook(input, deps) {
20530
21117
  const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
20531
21118
  if (raw.kind === "deny")
20532
21119
  return raw;
20533
- const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
20534
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
21120
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
20535
21121
  if (gate.kind === "deny")
20536
21122
  return gate;
20537
21123
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
@@ -20544,7 +21130,7 @@ function handleHook(input, deps) {
20544
21130
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
20545
21131
  return NONE5;
20546
21132
  case "write.pre":
20547
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
21133
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
20548
21134
  case "subagent.start":
20549
21135
  return {
20550
21136
  kind: "context",
@@ -20587,9 +21173,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
20587
21173
  if (role !== "judge" || !event.agentId)
20588
21174
  return readOnly;
20589
21175
  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.`;
21176
+ const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
21177
+ const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
21178
+ 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
21179
  }, failureDecision = (descriptor, event, error) => {
20594
21180
  if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
20595
21181
  return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
@@ -20627,6 +21213,7 @@ var init_handle = __esm(() => {
20627
21213
  init_raw_git();
20628
21214
  init_skill_nudge();
20629
21215
  init_stop();
21216
+ init_live_session();
20630
21217
  init_write_gate();
20631
21218
  NONE5 = { kind: "none" };
20632
21219
  offered = new Set;
@@ -21165,8 +21752,8 @@ var init_codex = __esm(() => {
21165
21752
  });
21166
21753
 
21167
21754
  // packages/workit-core/src/hooks/hosts/cursor.ts
21168
- import fs17 from "node:fs";
21169
- import path24 from "node:path";
21755
+ import fs18 from "node:fs";
21756
+ import path25 from "node:path";
21170
21757
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
21171
21758
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
21172
21759
  return { ok: false, error: "hook_event_name is required" };
@@ -21247,17 +21834,17 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
21247
21834
  };
21248
21835
  }
21249
21836
  }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
21250
- if (!file || !path24.isAbsolute(file))
21837
+ if (!file || !path25.isAbsolute(file))
21251
21838
  return null;
21252
21839
  try {
21253
- const size = fs17.statSync(file).size;
21840
+ const size = fs18.statSync(file).size;
21254
21841
  const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
21255
21842
  const buffer = Buffer.alloc(size - start);
21256
- const fd = fs17.openSync(file, "r");
21843
+ const fd = fs18.openSync(file, "r");
21257
21844
  try {
21258
- fs17.readSync(fd, buffer, 0, buffer.length, start);
21845
+ fs18.readSync(fd, buffer, 0, buffer.length, start);
21259
21846
  } finally {
21260
- fs17.closeSync(fd);
21847
+ fs18.closeSync(fd);
21261
21848
  }
21262
21849
  const lines = buffer.toString("utf8").split(`
21263
21850
  `);
@@ -21283,7 +21870,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
21283
21870
  } catch {
21284
21871
  return null;
21285
21872
  }
21286
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path24.sep) ? root : `${root}${path24.sep}`), hookCwd = (input) => {
21873
+ }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path25.sep) ? root : `${root}${path25.sep}`), hookCwd = (input) => {
21287
21874
  const cwd = existingDirectory(input.cwd);
21288
21875
  if (cwd && input.hook_event_name === "beforeShellExecution")
21289
21876
  return cwd;
@@ -21509,6 +22096,7 @@ var init_hooks = __esm(async () => {
21509
22096
  init_context();
21510
22097
  init_policy();
21511
22098
  init_write_gate();
22099
+ init_live_session();
21512
22100
  init_stop();
21513
22101
  init_raw_git();
21514
22102
  init_skill_nudge();