@brainervirus/workit-pi 8.1.0 → 8.3.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.
package/dist/workit.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID as randomUUID5 } from "node:crypto";
3
3
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4
4
 
5
5
  // packages/workit-core/src/core.ts
6
- import { spawnSync as spawnSync13 } from "node:child_process";
6
+ import { spawnSync as spawnSync14 } from "node:child_process";
7
7
  import { existsSync as existsSync7, realpathSync as realpathSync5 } from "node:fs";
8
8
  import path19 from "node:path";
9
9
 
@@ -9596,6 +9596,35 @@ function acquireFileLockSync(targetPath, options) {
9596
9596
  }
9597
9597
  }
9598
9598
  }
9599
+ // packages/workit-core/src/host-session.ts
9600
+ var HOST_SESSION_VARS = [
9601
+ ["OPENCODE_SESSION_ID", () => "opencode"],
9602
+ ["PI_SESSION_ID", () => "pi"],
9603
+ [
9604
+ "CODEX_THREAD_ID",
9605
+ (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9606
+ ]
9607
+ ];
9608
+ var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9609
+ function hostSessionFromEnv(env) {
9610
+ const value = (key) => env[key]?.trim() || null;
9611
+ let inner = null;
9612
+ for (const [key, host] of HOST_SESSION_VARS) {
9613
+ const session = value(key);
9614
+ if (session) {
9615
+ inner = { host: host(env), session };
9616
+ break;
9617
+ }
9618
+ }
9619
+ const declared = value("WORKIT_HOST");
9620
+ if (env.WORKIT_SESSION_ID !== undefined) {
9621
+ const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9622
+ if (!inherited)
9623
+ return { host: declared, session: value("WORKIT_SESSION_ID") };
9624
+ }
9625
+ return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9626
+ }
9627
+
9599
9628
  // packages/workit-core/src/core/package-root.ts
9600
9629
  import { existsSync } from "node:fs";
9601
9630
  import path3 from "node:path";
@@ -10560,9 +10589,10 @@ var drop = (lock) => {
10560
10589
  };
10561
10590
  var envActor = () => {
10562
10591
  const value = (key) => process.env[key]?.trim() || null;
10592
+ const acting = hostSessionFromEnv(process.env);
10563
10593
  return {
10564
- host: value("WORKIT_HOST") ?? "runtime",
10565
- session: value("WORKIT_SESSION_ID"),
10594
+ host: acting.host ?? "runtime",
10595
+ session: acting.session,
10566
10596
  agentId: value("WORKIT_AGENT_ID")
10567
10597
  };
10568
10598
  };
@@ -12486,6 +12516,7 @@ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:cry
12486
12516
  import fs10 from "node:fs";
12487
12517
  import path9 from "node:path";
12488
12518
  var LEDGER_VERSION = 1;
12519
+ var MAX_LINE_BYTES = 4096;
12489
12520
  var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
12490
12521
  var FAILING = new Set(["failed", "blocked"]);
12491
12522
  var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
@@ -12585,6 +12616,7 @@ function ledgerPath(cwd) {
12585
12616
  const root = storeRoot(cwd);
12586
12617
  return root.ok ? { ok: true, value: path9.join(root.value.root, "ledger", "ledger.jsonl") } : root;
12587
12618
  }
12619
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
12588
12620
  var NETWORK_FS_TYPES = new Set([
12589
12621
  26985,
12590
12622
  20859,
@@ -12597,6 +12629,158 @@ var NETWORK_FS_TYPES = new Set([
12597
12629
  1196443219,
12598
12630
  198183888
12599
12631
  ]);
12632
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
12633
+ var onNetworkFs = (dir) => {
12634
+ if (process.platform !== "linux")
12635
+ return false;
12636
+ try {
12637
+ return isNetworkFsType(fs10.statfsSync(dir).type);
12638
+ } catch {
12639
+ return false;
12640
+ }
12641
+ };
12642
+ var LOCK_WAIT_MS = 5000;
12643
+ var LOCK_STALE_MS = 30000;
12644
+ var LOCK_OWNER = "owner";
12645
+ var lockToken = (lock) => {
12646
+ try {
12647
+ return fs10.readFileSync(path9.join(lock, LOCK_OWNER), "utf8");
12648
+ } catch {
12649
+ return null;
12650
+ }
12651
+ };
12652
+ var lockAgeMs = (lock) => {
12653
+ for (const target of [path9.join(lock, LOCK_OWNER), lock]) {
12654
+ try {
12655
+ return Date.now() - fs10.statSync(target).mtimeMs;
12656
+ } catch {}
12657
+ }
12658
+ return null;
12659
+ };
12660
+ var ledgerLock = {
12661
+ acquire(lock) {
12662
+ try {
12663
+ fs10.mkdirSync(lock);
12664
+ } catch (error) {
12665
+ if (error.code === "EEXIST")
12666
+ return null;
12667
+ throw error;
12668
+ }
12669
+ const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
12670
+ fs10.writeFileSync(path9.join(lock, LOCK_OWNER), token);
12671
+ return token;
12672
+ },
12673
+ release(lock, token) {
12674
+ if (lockToken(lock) !== token)
12675
+ return false;
12676
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
12677
+ try {
12678
+ fs10.renameSync(lock, tomb);
12679
+ } catch {
12680
+ return false;
12681
+ }
12682
+ if (lockToken(tomb) !== token) {
12683
+ try {
12684
+ fs10.renameSync(tomb, lock);
12685
+ } catch {}
12686
+ return false;
12687
+ }
12688
+ fs10.rmSync(tomb, { recursive: true, force: true });
12689
+ return true;
12690
+ },
12691
+ token: lockToken,
12692
+ reap(lock, observed) {
12693
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
12694
+ try {
12695
+ fs10.renameSync(lock, tomb);
12696
+ } catch {
12697
+ return false;
12698
+ }
12699
+ if (lockToken(tomb) === observed) {
12700
+ fs10.rmSync(tomb, { recursive: true, force: true });
12701
+ return true;
12702
+ }
12703
+ try {
12704
+ fs10.renameSync(tomb, lock);
12705
+ } catch {
12706
+ fs10.rmSync(tomb, { recursive: true, force: true });
12707
+ }
12708
+ return false;
12709
+ }
12710
+ };
12711
+ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
12712
+ const lock = `${file}.lock`;
12713
+ const deadline = Date.now() + waitMs;
12714
+ let token = null;
12715
+ for (;; ) {
12716
+ token = ledgerLock.acquire(lock);
12717
+ if (token)
12718
+ break;
12719
+ const observed = lockToken(lock);
12720
+ const age = lockAgeMs(lock);
12721
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
12722
+ continue;
12723
+ if (Date.now() >= deadline)
12724
+ return null;
12725
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
12726
+ }
12727
+ try {
12728
+ return fn();
12729
+ } finally {
12730
+ ledgerLock.release(lock, token);
12731
+ }
12732
+ }
12733
+ function appendLine(file, line, options) {
12734
+ const write = () => {
12735
+ const fd = fs10.openSync(file, "a+");
12736
+ try {
12737
+ const size = fs10.fstatSync(fd).size;
12738
+ let fence = "";
12739
+ if (size > 0) {
12740
+ const last = Buffer.alloc(1);
12741
+ fs10.readSync(fd, last, 0, 1, size - 1);
12742
+ if (last[0] !== 10)
12743
+ fence = `
12744
+ `;
12745
+ }
12746
+ const bytes = Buffer.from(fence + line);
12747
+ const written = fs10.writeSync(fd, bytes);
12748
+ if (written !== bytes.length)
12749
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
12750
+ return { ok: true, value: undefined };
12751
+ } finally {
12752
+ fs10.closeSync(fd);
12753
+ }
12754
+ };
12755
+ try {
12756
+ fs10.mkdirSync(path9.dirname(file), { recursive: true });
12757
+ if (!options.forceLock && !onNetworkFs(path9.dirname(file)))
12758
+ return write();
12759
+ const locked = withLedgerLock(file, write);
12760
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
12761
+ } catch (error) {
12762
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
12763
+ }
12764
+ }
12765
+ var serialize = (row, now) => {
12766
+ const full = { v: LEDGER_VERSION, id: newId2(), at: now.toISOString(), ...row };
12767
+ return { full, line: `${JSON.stringify(full)}
12768
+ ` };
12769
+ };
12770
+ function appendRaw(cwd, row, options) {
12771
+ const { full, line } = serialize(row, options.now ?? new Date);
12772
+ const bytes = Buffer.byteLength(line);
12773
+ if (bytes > MAX_LINE_BYTES)
12774
+ return err("invalid_input", `ledger row is ${bytes} bytes; the limit is ${MAX_LINE_BYTES}. Shorten the text and pass details by --ref <path|url>`);
12775
+ const file = ledgerPath(cwd);
12776
+ if (!file.ok)
12777
+ return file;
12778
+ const written = appendLine(file.value, line, options);
12779
+ return written.ok ? { ok: true, value: full } : written;
12780
+ }
12781
+ function appendHookObserved(cwd, row) {
12782
+ return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
12783
+ }
12600
12784
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12601
12785
  var str2 = (value) => typeof value === "string" ? value : null;
12602
12786
  var normalizeActor = (value) => {
@@ -14263,6 +14447,7 @@ var gitContext = (workspaceRoot, paths = []) => {
14263
14447
  };
14264
14448
 
14265
14449
  // packages/workit-core/src/core/workspaces.ts
14450
+ import { spawnSync as spawnSync8 } from "node:child_process";
14266
14451
  import { createHash as createHash8 } from "node:crypto";
14267
14452
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
14268
14453
  import path13 from "node:path";
@@ -14507,6 +14692,53 @@ var validateWorkspaceGlob = (glob) => {
14507
14692
  }
14508
14693
  return { ok: true };
14509
14694
  };
14695
+
14696
+ class WorkspaceConfigError extends Error {
14697
+ name = "WorkspaceConfigError";
14698
+ }
14699
+ var mainCheckouts = new Map;
14700
+ var mainCheckoutOf = (cwd) => {
14701
+ const cached = mainCheckouts.get(cwd);
14702
+ if (cached !== undefined)
14703
+ return cached;
14704
+ const env = { ...process.env };
14705
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
14706
+ delete env[name];
14707
+ const git = (args) => {
14708
+ const run = spawnSync8("git", args, {
14709
+ cwd,
14710
+ env,
14711
+ encoding: "utf8",
14712
+ timeout: 5000,
14713
+ windowsHide: true
14714
+ });
14715
+ return run.status === 0 ? run.stdout : null;
14716
+ };
14717
+ let main = null;
14718
+ const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
14719
+ const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
14720
+ if (gitDir && common && path13.resolve(gitDir) !== path13.resolve(common)) {
14721
+ const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
14722
+ main = listed?.trim() || (path13.basename(common) === ".git" ? path13.dirname(common) : common);
14723
+ }
14724
+ mainCheckouts.set(cwd, main);
14725
+ return main;
14726
+ };
14727
+ var matchingEntries = (cwd, entries) => {
14728
+ const match = (where) => {
14729
+ const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
14730
+ return entries.filter((entry) => {
14731
+ const glob = entry.glob.replaceAll("\\", "/");
14732
+ const canonical = canonicalGlob(glob);
14733
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
14734
+ });
14735
+ };
14736
+ const direct = match(cwd);
14737
+ if (direct.length)
14738
+ return direct;
14739
+ const main = mainCheckoutOf(cwd);
14740
+ return main ? match(main) : direct;
14741
+ };
14510
14742
  var realpathOf = (p) => {
14511
14743
  const real = realpathSync3.native ?? realpathSync3;
14512
14744
  try {
@@ -14531,43 +14763,29 @@ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
14531
14763
  if (named.length === 1)
14532
14764
  return named[0];
14533
14765
  if (named.length > 1)
14534
- throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
14766
+ throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
14535
14767
  if (matches.length > 0) {
14536
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
14768
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
14537
14769
  }
14538
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
14770
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
14539
14771
  }
14540
14772
  if (matches.length < 2)
14541
14773
  return matches[0];
14542
14774
  const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
14543
14775
  const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
14544
14776
  if (mostSpecific.length > 1) {
14545
- throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
14777
+ throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
14546
14778
  }
14547
14779
  return mostSpecific[0];
14548
14780
  };
14549
14781
  var resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
14550
- const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
14551
- const matches = [];
14552
- for (const ws of entries) {
14553
- const glob = ws.glob.replaceAll("\\", "/");
14554
- const canonical = canonicalGlob(glob);
14555
- let matched = false;
14556
- for (const target of targets) {
14557
- if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
14558
- matched = true;
14559
- break;
14560
- }
14561
- }
14562
- if (matched)
14563
- matches.push(ws);
14564
- }
14782
+ const matches = matchingEntries(cwd, entries);
14565
14783
  return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
14566
14784
  };
14567
14785
  var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
14568
14786
  const result = readWorkspacesResult(dir);
14569
14787
  if (result.status === "malformed" || result.status === "invalid")
14570
- throw new Error(result.error);
14788
+ throw new WorkspaceConfigError(result.error);
14571
14789
  return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
14572
14790
  };
14573
14791
  var runtimeWorkspacePolicySchema = (kind) => {
@@ -14599,31 +14817,26 @@ var matchRuntimeWorkspace = (cwd) => {
14599
14817
  } catch (error) {
14600
14818
  if (error.code === "ENOENT")
14601
14819
  return null;
14602
- throw new Error(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
14820
+ throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
14603
14821
  }
14604
14822
  let parsed;
14605
14823
  try {
14606
14824
  parsed = JSON.parse(raw);
14607
14825
  } catch {
14608
- throw new Error(`${file} is not valid JSON`);
14826
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
14609
14827
  }
14610
14828
  const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
14611
14829
  if (!validated.success) {
14612
14830
  const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
14613
- throw new Error(`${file} has invalid workspace matching data: ${detail}`);
14831
+ throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
14614
14832
  }
14615
14833
  const entries = validated.data.workspaces;
14616
14834
  for (const entry of entries ?? []) {
14617
14835
  const glob = validateWorkspaceGlob(entry.glob);
14618
14836
  if (!glob.ok)
14619
- throw new Error(`${file} workspace ${entry.name}: ${glob.error}`);
14837
+ throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
14620
14838
  }
14621
- const targets = [cwd, realpathOf(cwd)].map((value) => value.replaceAll("\\", "/"));
14622
- const matches = (entries ?? []).filter((entry) => {
14623
- const glob = entry.glob.replaceAll("\\", "/");
14624
- const canonical = canonicalGlob(glob);
14625
- return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
14626
- });
14839
+ const matches = matchingEntries(cwd, entries ?? []);
14627
14840
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
14628
14841
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
14629
14842
  return selected ? { file, selected } : null;
@@ -14640,7 +14853,7 @@ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
14640
14853
  const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
14641
14854
  if (!policy.success) {
14642
14855
  const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
14643
- throw new Error(`${file} has invalid ${kind} policy configuration: ${detail}`);
14856
+ throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
14644
14857
  }
14645
14858
  return policy.data;
14646
14859
  };
@@ -14651,10 +14864,10 @@ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
14651
14864
  const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
14652
14865
  const profileName = requestedProfile?.trim() || candidate.defaultProfile;
14653
14866
  if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
14654
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
14867
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
14655
14868
  const profile = profileName ? candidate.profiles?.[profileName] : undefined;
14656
14869
  if (profileName && !profile)
14657
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
14870
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
14658
14871
  const profilePolicy = profile?.[key];
14659
14872
  const workspacePolicy = candidate[key];
14660
14873
  return {
@@ -14674,11 +14887,11 @@ function workspaceDefaultTarget(cwd, globalDefault) {
14674
14887
  // packages/workit-core/src/core/vcs-config.ts
14675
14888
  import fs12 from "node:fs";
14676
14889
  import path14 from "node:path";
14677
- import { spawnSync as spawnSync9 } from "node:child_process";
14890
+ import { spawnSync as spawnSync10 } from "node:child_process";
14678
14891
 
14679
14892
  // packages/workit-core/src/core/release-tracks.ts
14680
14893
  import { createHash as createHash9 } from "node:crypto";
14681
- import { spawnSync as spawnSync8 } from "node:child_process";
14894
+ import { spawnSync as spawnSync9 } from "node:child_process";
14682
14895
  class ReleaseTrackError extends Error {
14683
14896
  }
14684
14897
  var KNOWN_FIELDS = new Set([
@@ -14963,7 +15176,7 @@ function resolveReleaseTrack(input) {
14963
15176
  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 ")}`);
14964
15177
  }
14965
15178
  var git4 = (cwd, args) => {
14966
- const run = spawnSync8("git", args, {
15179
+ const run = spawnSync9("git", args, {
14967
15180
  cwd,
14968
15181
  encoding: "utf8",
14969
15182
  timeout: 20000,
@@ -15078,7 +15291,7 @@ function resolveTrackFor(cwd, options) {
15078
15291
  var workspacesPath = () => path14.join(configDir(), "workspaces.json");
15079
15292
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
15080
15293
  var remoteProvider = (cwd) => {
15081
- const r = spawnSync9("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
15294
+ const r = spawnSync10("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
15082
15295
  if (r.status !== 0)
15083
15296
  return null;
15084
15297
  const url = (r.stdout ?? "").trim();
@@ -15115,7 +15328,7 @@ var remoteEndpoint = (remote) => {
15115
15328
  var configuredSshEndpoint = (host) => {
15116
15329
  if (host.startsWith("-"))
15117
15330
  return null;
15118
- const resolved = spawnSync9("ssh", ["-G", host], {
15331
+ const resolved = spawnSync10("ssh", ["-G", host], {
15119
15332
  encoding: "utf8",
15120
15333
  timeout: 5000,
15121
15334
  maxBuffer: 1024 * 1024,
@@ -15288,7 +15501,7 @@ function mergedPrStyle(limit = 6, cwd) {
15288
15501
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
15289
15502
  descriptionPreview: desc.slice(0, 600)
15290
15503
  });
15291
- const remoteResult = spawnSync9("git", ["remote", "get-url", "--push", "--all", "origin"], {
15504
+ const remoteResult = spawnSync10("git", ["remote", "get-url", "--push", "--all", "origin"], {
15292
15505
  cwd,
15293
15506
  encoding: "utf8"
15294
15507
  });
@@ -15304,7 +15517,7 @@ function mergedPrStyle(limit = 6, cwd) {
15304
15517
  if (!hostingApiHostMatches(remote, host))
15305
15518
  return { ok: false, error: "PR style target does not match the provider host" };
15306
15519
  if (provider === "gitlab") {
15307
- const run = (args) => spawnSync9("glab", ["api", ...args], {
15520
+ const run = (args) => spawnSync10("glab", ["api", ...args], {
15308
15521
  cwd,
15309
15522
  encoding: "utf8",
15310
15523
  env
@@ -15324,7 +15537,7 @@ function mergedPrStyle(limit = 6, cwd) {
15324
15537
  });
15325
15538
  }
15326
15539
  } else if (provider === "github") {
15327
- const r = spawnSync9("gh", [
15540
+ const r = spawnSync10("gh", [
15328
15541
  "pr",
15329
15542
  "list",
15330
15543
  "--repo",
@@ -15448,6 +15661,7 @@ var validateBranchNameFor = (workspaceRoot, name) => {
15448
15661
  };
15449
15662
  }
15450
15663
  };
15664
+ var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
15451
15665
 
15452
15666
  // packages/workit-core/src/core/route-intent.ts
15453
15667
  var shellBranchTarget = (command) => {
@@ -17370,7 +17584,7 @@ class WorkitCore {
17370
17584
  }
17371
17585
  }
17372
17586
  // packages/workit-core/src/core/repo-context.ts
17373
- import { spawnSync as spawnSync10 } from "node:child_process";
17587
+ import { spawnSync as spawnSync11 } from "node:child_process";
17374
17588
  import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
17375
17589
  import path16 from "node:path";
17376
17590
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -17399,7 +17613,7 @@ var invalidRange = (cwd) => ({
17399
17613
  cwd
17400
17614
  });
17401
17615
  var runGit = (cwd, args) => {
17402
- const result = spawnSync10("git", args, { cwd, encoding: "utf8" });
17616
+ const result = spawnSync11("git", args, { cwd, encoding: "utf8" });
17403
17617
  return {
17404
17618
  stdout: (result.stdout ?? "").trimEnd(),
17405
17619
  stderr: (result.stderr ?? "").trimEnd(),
@@ -17840,12 +18054,12 @@ function releaseNotesContext(root, rangeOrTag) {
17840
18054
  }
17841
18055
 
17842
18056
  // packages/workit-core/src/core/tracker-issues.ts
17843
- import { spawnSync as spawnSync12 } from "node:child_process";
18057
+ import { spawnSync as spawnSync13 } from "node:child_process";
17844
18058
 
17845
18059
  // packages/workit-core/src/core/pr-create.ts
17846
18060
  import fs13 from "node:fs";
17847
18061
  import path17 from "node:path";
17848
- import { spawnSync as spawnSync11 } from "node:child_process";
18062
+ import { spawnSync as spawnSync12 } from "node:child_process";
17849
18063
  // packages/workit-core/src/forge/pr-body.ts
17850
18064
  function parseGhIssue(value) {
17851
18065
  const m = /issues\/(\d+)/.exec(value);
@@ -17870,7 +18084,7 @@ function whichOnPath(tool) {
17870
18084
  }
17871
18085
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
17872
18086
  var pushRemote = (cwd) => {
17873
- const result = spawnSync11("git", ["remote", "get-url", "--push", "--all", "origin"], {
18087
+ const result = spawnSync12("git", ["remote", "get-url", "--push", "--all", "origin"], {
17874
18088
  cwd,
17875
18089
  encoding: "utf8"
17876
18090
  });
@@ -17901,7 +18115,7 @@ var defaultCli = (bin, available) => {
17901
18115
  if (!available)
17902
18116
  return null;
17903
18117
  return (args, root) => {
17904
- const out = spawnSync12(bin, args, {
18118
+ const out = spawnSync13(bin, args, {
17905
18119
  cwd: root,
17906
18120
  encoding: "utf8",
17907
18121
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -18434,7 +18648,7 @@ function resolveInside(root, candidate) {
18434
18648
  }
18435
18649
  function run2(root, executable, args, env = {}) {
18436
18650
  const cwd = realpathSync5(root);
18437
- const result = spawnSync13(executable, args, {
18651
+ const result = spawnSync14(executable, args, {
18438
18652
  cwd,
18439
18653
  encoding: "utf8",
18440
18654
  env: { ...process.env, ...env }
@@ -18626,36 +18840,43 @@ var shellPolicy = (cwd, command) => {
18626
18840
  // packages/workit-core/src/hooks/write-gate.ts
18627
18841
  import { realpathSync as realpathSync6 } from "node:fs";
18628
18842
  import path21 from "node:path";
18629
- var NONE2 = { kind: "none" };
18630
- var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18631
- var canonical2 = (file) => {
18632
- let rest = "";
18633
- for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18634
- try {
18635
- return path21.join(realpathSync6(dir), rest);
18636
- } catch {
18637
- if (path21.dirname(dir) === dir)
18638
- return path21.resolve(file);
18639
- rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18843
+
18844
+ // packages/workit-core/src/hooks/shell-words.ts
18845
+ var closingQuote = (command, start, dialect) => {
18846
+ const quote = command[start];
18847
+ for (let index = start + 1;index < command.length; index++) {
18848
+ const char = command[index];
18849
+ if (quote === "'") {
18850
+ if (char !== "'")
18851
+ continue;
18852
+ if (dialect === "powershell" && command[index + 1] === "'") {
18853
+ index++;
18854
+ continue;
18855
+ }
18856
+ return index;
18640
18857
  }
18858
+ if (dialect === "posix" && char === "\\" || dialect === "powershell" && char === "`") {
18859
+ index++;
18860
+ continue;
18861
+ }
18862
+ if (char === '"')
18863
+ return index;
18641
18864
  }
18865
+ return -1;
18642
18866
  };
18643
- var exempt = (cwd, root, file, plans) => {
18644
- const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18645
- const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18646
- if (relative.startsWith("..") || path21.isAbsolute(relative))
18647
- return true;
18648
- const posix = relative.split(path21.sep).join("/");
18649
- const segments = posix.split("/");
18650
- return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18867
+ var unquote = (body, quote, dialect) => {
18868
+ if (quote === "'")
18869
+ return dialect === "powershell" ? body.replaceAll("''", "'") : body;
18870
+ return dialect === "posix" ? body.replace(/\\(["\\$`])/g, "$1") : body.replace(/`(.)/g, "$1");
18651
18871
  };
18652
- var segmentsOf = (command) => {
18872
+ var segmentsOf = (command, dialect = "posix") => {
18653
18873
  const segments = [];
18654
18874
  let current = { words: [], redirects: [] };
18655
18875
  let word = "";
18656
18876
  let inWord = false;
18657
18877
  let pending = null;
18658
18878
  const heredocs = [];
18879
+ const escape = dialect === "posix" ? "\\" : "`";
18659
18880
  const flushWord = () => {
18660
18881
  if (!inWord)
18661
18882
  return;
@@ -18679,11 +18900,11 @@ var segmentsOf = (command) => {
18679
18900
  for (let index = 0;index < command.length; index++) {
18680
18901
  const char = command[index];
18681
18902
  if (char === "'" || char === '"') {
18682
- const end = command.indexOf(char, index + 1);
18683
- word += command.slice(index + 1, end < 0 ? undefined : end);
18903
+ const end = closingQuote(command, index, dialect);
18904
+ word += unquote(command.slice(index + 1, end < 0 ? undefined : end), char, dialect);
18684
18905
  inWord = true;
18685
18906
  index = end < 0 ? command.length : end;
18686
- } else if (char === "\\" && index + 1 < command.length) {
18907
+ } else if (char === escape && index + 1 < command.length) {
18687
18908
  word += command[++index];
18688
18909
  inWord = true;
18689
18910
  } else if (char === `
@@ -18700,7 +18921,14 @@ var segmentsOf = (command) => {
18700
18921
  }
18701
18922
  } else if (/\s/.test(char))
18702
18923
  flushWord();
18703
- else if (";&|()".includes(char)) {
18924
+ else if (char === "(" || char === ")") {
18925
+ if (char === "(" && inWord && word.endsWith("$")) {
18926
+ word = word.slice(0, -1);
18927
+ inWord = word !== "";
18928
+ }
18929
+ flushSegment();
18930
+ segments.push({ words: [], redirects: [], group: char === "(" ? "open" : "close" });
18931
+ } else if (";&|".includes(char)) {
18704
18932
  if (char === "&" && command[index + 1] === ">")
18705
18933
  continue;
18706
18934
  flushSegment();
@@ -18718,7 +18946,7 @@ var segmentsOf = (command) => {
18718
18946
  inWord = true;
18719
18947
  }
18720
18948
  pending = "redirect";
18721
- } else if (char === "<") {
18949
+ } else if (char === "<" && dialect === "posix") {
18722
18950
  flushWord();
18723
18951
  if (command[index + 1] === "<") {
18724
18952
  index++;
@@ -18735,6 +18963,31 @@ var segmentsOf = (command) => {
18735
18963
  flushSegment();
18736
18964
  return segments;
18737
18965
  };
18966
+
18967
+ // packages/workit-core/src/hooks/write-gate.ts
18968
+ var NONE2 = { kind: "none" };
18969
+ var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18970
+ var canonical2 = (file) => {
18971
+ let rest = "";
18972
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18973
+ try {
18974
+ return path21.join(realpathSync6(dir), rest);
18975
+ } catch {
18976
+ if (path21.dirname(dir) === dir)
18977
+ return path21.resolve(file);
18978
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18979
+ }
18980
+ }
18981
+ };
18982
+ var exempt = (cwd, root, file, plans) => {
18983
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18984
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18985
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
18986
+ return true;
18987
+ const posix = relative.split(path21.sep).join("/");
18988
+ const segments = posix.split("/");
18989
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18990
+ };
18738
18991
  var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18739
18992
  var operands = (words) => words.filter((item) => !item.startsWith("-"));
18740
18993
  var editsOf = (input) => {
@@ -18884,10 +19137,469 @@ function writeGate(cwd, paths) {
18884
19137
  return NONE2;
18885
19138
  }
18886
19139
  }
19140
+ // packages/workit-core/src/hooks/raw-git.ts
19141
+ import { spawnSync as spawnSync15 } from "node:child_process";
19142
+ import { createHash as createHash11 } from "node:crypto";
19143
+ import fs15 from "node:fs";
19144
+ import os4 from "node:os";
19145
+ import path22 from "node:path";
19146
+ var NONE3 = { kind: "none" };
19147
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
19148
+ var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
19149
+ var SCRIPT_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/;
19150
+ var CD = new Set(["cd", "chdir", "set-location", "sl"]);
19151
+ var PUSHD = new Set(["pushd", "push-location"]);
19152
+ var POPD = new Set(["popd", "pop-location"]);
19153
+ var GIT_VALUE_OPTIONS = new Set([
19154
+ "-c",
19155
+ "--git-dir",
19156
+ "--work-tree",
19157
+ "--namespace",
19158
+ "--exec-path",
19159
+ "--config-env",
19160
+ "--super-prefix"
19161
+ ]);
19162
+ var isAbsolute = (value) => path22.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19163
+ var joinDir = (base, next) => {
19164
+ if (base === undefined || /[$`*?]/.test(next) || next === "-")
19165
+ return;
19166
+ const expanded = next === "~" || next.startsWith("~/") ? path22.join(os4.homedir(), next.slice(1)) : next;
19167
+ return base === null || isAbsolute(expanded) ? expanded : path22.join(base, expanded);
19168
+ };
19169
+ var cdTarget = (words) => {
19170
+ const args = words.slice(1);
19171
+ const flag = args.findIndex((word) => /^-(?:literal)?path$/i.test(word));
19172
+ if (flag >= 0)
19173
+ return args[flag + 1] ?? null;
19174
+ return args.find((word) => word === "-" || !word.startsWith("-")) ?? null;
19175
+ };
19176
+ var stripPrefixes = (input) => {
19177
+ let words = input;
19178
+ for (;; ) {
19179
+ const first = words[0];
19180
+ if (first === undefined)
19181
+ return words;
19182
+ if (/^\w+=/.test(first))
19183
+ words = words.slice(1);
19184
+ else if (PREFIXES3.has(first)) {
19185
+ words = words.slice(1);
19186
+ while (words[0]?.startsWith("-"))
19187
+ words = words.slice(words[0] === "-u" ? 2 : 1);
19188
+ } else if (first === "timeout") {
19189
+ words = words.slice(1);
19190
+ while (words[0]?.startsWith("-"))
19191
+ words = words.slice(1);
19192
+ words = words.slice(1);
19193
+ } else
19194
+ return words;
19195
+ }
19196
+ };
19197
+ var commandName = (word) => (word?.split(/[\\/]/).at(-1) ?? "").toLowerCase().replace(/\.exe$/, "");
19198
+ function rawInvocations(command, dialect = "posix", depth = 0) {
19199
+ const found = [];
19200
+ let dir = null;
19201
+ const groups = [];
19202
+ let stack = [];
19203
+ for (const segment of segmentsOf(command, dialect)) {
19204
+ if (segment.group === "open") {
19205
+ groups.push({ dir, stack: [...stack] });
19206
+ continue;
19207
+ }
19208
+ if (segment.group === "close") {
19209
+ const saved = groups.pop();
19210
+ if (saved)
19211
+ ({ dir, stack } = saved);
19212
+ continue;
19213
+ }
19214
+ const words = stripPrefixes(segment.words);
19215
+ const name = commandName(words[0]);
19216
+ if (CD.has(name)) {
19217
+ const target = cdTarget(words);
19218
+ dir = target === null ? os4.homedir() : joinDir(dir, target);
19219
+ continue;
19220
+ }
19221
+ if (PUSHD.has(name)) {
19222
+ const target = cdTarget(words);
19223
+ stack.push(dir);
19224
+ dir = target === null || target === "-" ? undefined : joinDir(dir, target);
19225
+ continue;
19226
+ }
19227
+ if (POPD.has(name)) {
19228
+ dir = stack.length ? stack.pop() : undefined;
19229
+ continue;
19230
+ }
19231
+ if (SHELLS.has(name) && depth < 3) {
19232
+ const flag = words.findIndex((word, index) => index > 0 && SCRIPT_FLAG.test(word));
19233
+ const script = flag > 0 ? words[flag + 1] : undefined;
19234
+ if (script)
19235
+ for (const inner of rawInvocations(script, "posix", depth + 1))
19236
+ found.push({
19237
+ ...inner,
19238
+ dir: inner.dir === null ? dir : inner.dir === undefined ? undefined : joinDir(dir, inner.dir)
19239
+ });
19240
+ continue;
19241
+ }
19242
+ if (name === "gh" || name === "glab") {
19243
+ found.push({ tool: name, dir, args: words.slice(1) });
19244
+ continue;
19245
+ }
19246
+ if (name !== "git")
19247
+ continue;
19248
+ let here = dir;
19249
+ let index = 1;
19250
+ for (;index < words.length; index++) {
19251
+ const word = words[index];
19252
+ if (word === "-C")
19253
+ here = words[index + 1] ? joinDir(here, words[++index]) : here;
19254
+ else if (GIT_VALUE_OPTIONS.has(word))
19255
+ index++;
19256
+ else if (!word.startsWith("-"))
19257
+ break;
19258
+ }
19259
+ found.push({ tool: "git", dir: here, args: words.slice(index) });
19260
+ }
19261
+ return found;
19262
+ }
19263
+ var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
19264
+ var pushAction = (args) => {
19265
+ let force = false;
19266
+ let lease = false;
19267
+ let wholesale = false;
19268
+ let remote = null;
19269
+ const positionals = [];
19270
+ for (let index = 1;index < args.length; index++) {
19271
+ const arg = args[index];
19272
+ if (arg === "--") {
19273
+ positionals.push(...args.slice(index + 1));
19274
+ break;
19275
+ }
19276
+ if (arg === "--dry-run" || /^-[a-zA-Z]*n[a-zA-Z]*$/.test(arg))
19277
+ return null;
19278
+ if (arg === "--repo")
19279
+ remote = args[++index] ?? null;
19280
+ else if (arg.startsWith("--repo="))
19281
+ remote = arg.slice("--repo=".length);
19282
+ else if (PUSH_VALUE_OPTIONS.has(arg))
19283
+ index++;
19284
+ else if (arg === "--force" || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(arg))
19285
+ force = true;
19286
+ else if (arg.startsWith("--force-with-lease") || arg === "--force-if-includes")
19287
+ lease = true;
19288
+ else if (["--all", "--mirror", "--branches", "--tags"].includes(arg))
19289
+ wholesale = true;
19290
+ else if (!arg.startsWith("-"))
19291
+ positionals.push(arg);
19292
+ }
19293
+ const specs = remote === null ? positionals.slice(1) : positionals;
19294
+ remote ??= positionals[0] ?? null;
19295
+ const targets = [];
19296
+ for (const raw of specs) {
19297
+ if (raw.startsWith("+"))
19298
+ force = true;
19299
+ const spec = raw.replace(/^\+/, "");
19300
+ const colon = spec.indexOf(":");
19301
+ const destination = colon >= 0 ? spec.slice(colon + 1) : spec;
19302
+ if (!destination)
19303
+ continue;
19304
+ if (destination === "HEAD")
19305
+ targets.push(null);
19306
+ else if (destination.startsWith("refs/heads/"))
19307
+ targets.push(destination.slice(11));
19308
+ else if (!destination.startsWith("refs/"))
19309
+ targets.push(destination);
19310
+ }
19311
+ if (specs.length === 0 && !wholesale)
19312
+ targets.push(null);
19313
+ return { kind: "push", targets, blindForce: force && !lease, remote };
19314
+ };
19315
+ var VALUE_OPTIONS = {
19316
+ git: new Set([
19317
+ "-m",
19318
+ "--message",
19319
+ "-F",
19320
+ "--file",
19321
+ "-C",
19322
+ "-c",
19323
+ "--reuse-message",
19324
+ "--reedit-message",
19325
+ "--author",
19326
+ "--date",
19327
+ "--trailer",
19328
+ "-t",
19329
+ "--template",
19330
+ "--cleanup",
19331
+ "--fixup",
19332
+ "--squash",
19333
+ "-o",
19334
+ "--push-option",
19335
+ "--repo",
19336
+ "--receive-pack",
19337
+ "--exec"
19338
+ ]),
19339
+ gh: new Set(["-b", "--body", "-F", "--body-file", "-t", "--title", "--subject", "-A", "--author-email"].concat(["-B", "--base", "-H", "--head", "-l", "--label", "-a", "--assignee"]).concat(["-r", "--reviewer", "-m", "--milestone", "-p", "--project", "-T", "--template"]).concat(["--match-head-commit", "-R", "--repo"])),
19340
+ glab: new Set(["-m", "--message", "--squash-message", "--sha", "-t", "--title", "-d", "--description"].concat(["-b", "--target-branch", "-s", "--source-branch", "-l", "--label"]).concat(["-a", "--assignee", "--reviewer", "-R", "--repo"]))
19341
+ };
19342
+ var asksHelp = (invocation) => {
19343
+ const { tool, args } = invocation;
19344
+ if (args[0] === "help" || tool !== "git" && args[1] === "help")
19345
+ return true;
19346
+ const values = VALUE_OPTIONS[tool];
19347
+ for (let index = 0;index < args.length; index++) {
19348
+ const arg = args[index];
19349
+ if (arg === "--")
19350
+ return false;
19351
+ if (arg === "--help" || arg === "-h")
19352
+ return true;
19353
+ if (values.has(arg))
19354
+ index++;
19355
+ }
19356
+ return false;
19357
+ };
19358
+ function rawAction(invocation) {
19359
+ const args = invocation.args;
19360
+ if (asksHelp(invocation))
19361
+ return null;
19362
+ const [first, second] = args;
19363
+ if (invocation.tool === "git") {
19364
+ if (first === "commit")
19365
+ return args.includes("--dry-run") ? null : { kind: "commit", amend: args.includes("--amend") };
19366
+ if (first === "push")
19367
+ return pushAction(args);
19368
+ return null;
19369
+ }
19370
+ const forge = invocation.tool;
19371
+ const noun = forge === "gh" ? "pr" : "mr";
19372
+ if (first !== noun)
19373
+ return null;
19374
+ if (second === "merge")
19375
+ return { kind: "merge", forge };
19376
+ if (second === "create")
19377
+ return { kind: "pr-create", forge };
19378
+ if (["view", "checks", "status", "ci", "list"].includes(second ?? ""))
19379
+ return { kind: "pr-read", forge };
19380
+ return null;
19381
+ }
19382
+ var repoAt = (dir) => {
19383
+ const location = resolveStore(dir);
19384
+ if (location instanceof Error || !location.shared)
19385
+ return null;
19386
+ return { location, dir, branch: resolveTaskKey(dir, location).branch };
19387
+ };
19388
+ var isWorkitWorkspace = (repo) => {
19389
+ if (fs15.existsSync(repo.location.dir))
19390
+ return true;
19391
+ try {
19392
+ return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
19393
+ } catch {
19394
+ return false;
19395
+ }
19396
+ };
19397
+ var guardedBranch = (dir, branch) => {
19398
+ try {
19399
+ const policy = resolveBranchPolicyFor(dir);
19400
+ return policy.protected.has(branch.toLowerCase()) || policy.defaultTargetBranch.toLowerCase() === branch.toLowerCase();
19401
+ } catch {
19402
+ return false;
19403
+ }
19404
+ };
19405
+ var toPushRemote = (repo, remote) => {
19406
+ if (remote === null)
19407
+ return true;
19408
+ try {
19409
+ const pushRemote = pushRemoteName(repo.dir, repo.branch);
19410
+ return pushRemote === null || pushRemote === remote;
19411
+ } catch {
19412
+ return false;
19413
+ }
19414
+ };
19415
+ var headSha2 = (repo) => {
19416
+ const git = repo.location.git;
19417
+ if (!git || git.reftable)
19418
+ return null;
19419
+ try {
19420
+ const head = fs15.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19421
+ if (/^[0-9a-f]{40,64}$/.test(head))
19422
+ return head;
19423
+ const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
19424
+ if (!ref)
19425
+ return null;
19426
+ const common = path22.dirname(repo.location.dir);
19427
+ try {
19428
+ return fs15.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19429
+ } catch {}
19430
+ const packed = fs15.readFileSync(path22.join(common, "packed-refs"), "utf8");
19431
+ return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
19432
+ } catch {
19433
+ return null;
19434
+ }
19435
+ };
19436
+ var quote = (text) => `\`${text}\``;
19437
+ var refusal = (action, branch) => {
19438
+ if (action.kind === "merge")
19439
+ return `workit: raw ${quote(action.forge === "gh" ? "gh pr merge" : "glab mr merge")} bypasses the merge grant and the verdict gate. Run ${quote("workit pr merge")} instead (it checks both; ${quote("workit help pr")}).`;
19440
+ return `workit: raw ${quote("git push")}${action.kind === "push" && action.blindForce ? " --force" : ""} onto ${branch}, a protected branch or the default target, bypasses workit's protected-branch check. Push your feature branch with ${quote("workit git push")} (${quote("workit git push --force-with-lease")} to rewrite it) and land it with ${quote("workit pr merge")}.`;
19441
+ };
19442
+ var nudgeFor = (action, host, session) => {
19443
+ switch (action.kind) {
19444
+ case "commit": {
19445
+ const prefix = host === "cursor" && session ? `WORKIT_SESSION_ID=${session} ` : "";
19446
+ return action.amend ? `workit: raw ${quote("git commit --amend")} is recorded for this session; for new commits prefer ${quote(`${prefix}workit git commit -m '<msg>'`)} (adds the Workit-Session trailer and checks branch and commit policy).` : `workit: prefer ${quote(`${prefix}workit git commit -m '<msg>' [--all | -- <paths>]`)}: it adds the Workit-Session trailer and checks branch and commit policy.`;
19447
+ }
19448
+ case "push":
19449
+ return action.blindForce ? `workit: ${quote("git push --force")} overwrites without a lease; use ${quote("workit git push --force-with-lease")}.` : `workit: prefer ${quote("workit git push")}: it refuses protected branches and verifies the remote tip.`;
19450
+ case "pr-create":
19451
+ return `workit: prefer ${quote("workit pr create --fill")} (or --title/--body): it records the PR the merge gate checks.`;
19452
+ case "pr-read":
19453
+ return `workit: ${quote("workit pr status")} shows the PR, its checks and the verdict state; ${quote("workit ci wait")} waits for checks.`;
19454
+ case "merge":
19455
+ return null;
19456
+ }
19457
+ };
19458
+ var actionsIn = (cwd, command, dialect) => {
19459
+ const out = [];
19460
+ const repos = new Map;
19461
+ for (const invocation of rawInvocations(command, dialect)) {
19462
+ if (invocation.dir === undefined)
19463
+ continue;
19464
+ const action = rawAction(invocation);
19465
+ if (!action)
19466
+ continue;
19467
+ const dir = path22.resolve(cwd, invocation.dir ?? ".");
19468
+ if (!repos.has(dir)) {
19469
+ const repo = repoAt(dir);
19470
+ repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
19471
+ }
19472
+ const repo = repos.get(dir);
19473
+ if (repo)
19474
+ out.push({ action, repo });
19475
+ }
19476
+ return out;
19477
+ };
19478
+ var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
19479
+ var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
19480
+ function rawGitPre(input, command) {
19481
+ try {
19482
+ if (!mentionsRawTool(command))
19483
+ return NONE3;
19484
+ const nudges = [];
19485
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19486
+ if (action.kind === "merge")
19487
+ return deny(refusal(action, null));
19488
+ if (action.kind === "push")
19489
+ for (const target of action.targets) {
19490
+ const branch = target ?? repo.branch;
19491
+ if (branch && guardedBranch(repo.dir, branch) && toPushRemote(repo, action.remote))
19492
+ return deny(refusal(action, branch));
19493
+ }
19494
+ const nudge = nudgeFor(action, input.host, input.session.id);
19495
+ if (nudge && !nudges.includes(nudge))
19496
+ nudges.push(nudge);
19497
+ }
19498
+ return nudges.length ? { kind: "context", text: nudges.join(`
19499
+ `) } : NONE3;
19500
+ } catch {
19501
+ return NONE3;
19502
+ }
19503
+ }
19504
+ var deny = (reason) => ({ kind: "deny", reason, unblock: null });
19505
+ var NEXT_COMMAND = "next";
19506
+ var MARKER_TTL_SECONDS = 3600;
19507
+ var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash11("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19508
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash11("sha256").update(command).digest("hex")}`;
19509
+ function noteRawCommit(input, command, key) {
19510
+ try {
19511
+ if (!input.session.id || !/\bcommit\b/.test(command))
19512
+ return;
19513
+ const at = Math.floor(Date.now() / 1000);
19514
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19515
+ if (action.kind !== "commit")
19516
+ continue;
19517
+ const file = markerFile(repo, input.session.id, key);
19518
+ fs15.mkdirSync(path22.dirname(file), { recursive: true });
19519
+ fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19520
+ `);
19521
+ }
19522
+ } catch {}
19523
+ }
19524
+ var headCommit = (dir) => {
19525
+ const run = spawnSync15("git", [
19526
+ "log",
19527
+ "-1",
19528
+ "--format=%H%x1f%ct%x1f%s%x1f%(trailers:key=Workit-Session,valueonly,separator=%x2c)"
19529
+ ], {
19530
+ cwd: dir,
19531
+ encoding: "utf8",
19532
+ timeout: 5000,
19533
+ windowsHide: true,
19534
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
19535
+ });
19536
+ const [sha, committed, subject = "", trailer = ""] = (run.status === 0 ? run.stdout.trim() : "").split("\x1F");
19537
+ return sha && /^[0-9a-f]{40,64}$/.test(sha) ? { sha, committed: Number(committed), subject, trailer: trailer.trim() || null } : null;
19538
+ };
19539
+ var settle2 = (input, repo, file) => {
19540
+ let marker;
19541
+ try {
19542
+ marker = JSON.parse(fs15.readFileSync(file, "utf8"));
19543
+ } catch {
19544
+ return;
19545
+ }
19546
+ fs15.rmSync(file, { force: true });
19547
+ const session = input.session.id;
19548
+ const now = Date.now() / 1000;
19549
+ if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
19550
+ return;
19551
+ if (!session || !repo.branch)
19552
+ return;
19553
+ const head = headCommit(repo.dir);
19554
+ if (!head || head.sha === marker.head || head.committed < marker.at)
19555
+ return;
19556
+ if (head.trailer && head.trailer !== session)
19557
+ return;
19558
+ const ledger = readLedger(repo.dir);
19559
+ if (!ledger.ok)
19560
+ return;
19561
+ const claimed = ledger.value.rows.some((row) => row.type === "commit.recorded" && (row.sha === head.sha || row.head === head.sha));
19562
+ if (claimed)
19563
+ return;
19564
+ appendHookObserved(repo.dir, {
19565
+ type: "commit.recorded",
19566
+ actor: { host: input.host, session, agentId: input.session.agentId },
19567
+ branch: repo.branch,
19568
+ head: head.sha,
19569
+ sha: head.sha,
19570
+ session,
19571
+ agentId: input.session.agentId,
19572
+ subject: head.subject.slice(0, 200),
19573
+ files: [],
19574
+ fileCount: null,
19575
+ via: "raw_shell"
19576
+ });
19577
+ };
19578
+ function rawGitPost(input, command, key) {
19579
+ try {
19580
+ if (!input.session.id || !/\bcommit\b/.test(command))
19581
+ return;
19582
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input)))
19583
+ if (action.kind === "commit")
19584
+ settle2(input, repo, markerFile(repo, input.session.id, key));
19585
+ } catch {}
19586
+ }
19587
+ function settlePendingCommit(input) {
19588
+ try {
19589
+ if (!input.session.id)
19590
+ return;
19591
+ const repo = repoAt(input.cwd);
19592
+ if (!repo)
19593
+ return;
19594
+ const file = markerFile(repo, input.session.id, NEXT_COMMAND);
19595
+ if (fs15.existsSync(file))
19596
+ settle2(input, repo, file);
19597
+ } catch {}
19598
+ }
18887
19599
  // packages/workit-core/src/hooks/hosts/fields.ts
18888
19600
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
18889
19601
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
18890
- var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
19602
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18891
19603
  var WRITE_TOOLS = new Set([
18892
19604
  "write",
18893
19605
  "edit",
@@ -18920,7 +19632,7 @@ var writePaths = (input) => {
18920
19632
  };
18921
19633
  // packages/workit-core/src/hooks/handle.ts
18922
19634
  var usable2 = (value) => value === "native" || value === "partial";
18923
- var NONE3 = { kind: "none" };
19635
+ var NONE4 = { kind: "none" };
18924
19636
  var offered = new Set;
18925
19637
  var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
18926
19638
  var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
@@ -18939,7 +19651,7 @@ function handleHook(input, deps) {
18939
19651
  switch (event.kind) {
18940
19652
  case "session.start": {
18941
19653
  if (!usable2(descriptor.context.sessionStart))
18942
- return NONE3;
19654
+ return NONE4;
18943
19655
  const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
18944
19656
  const offer = event.source === "startup" && (key === null || !offered.has(key));
18945
19657
  if (offer && key !== null)
@@ -18954,19 +19666,34 @@ function handleHook(input, deps) {
18954
19666
  }
18955
19667
  case "context.turn": {
18956
19668
  if (!usable2(descriptor.context.perTurn))
18957
- return NONE3;
19669
+ return NONE4;
18958
19670
  const text = turnContextText(input, descriptor);
18959
- return text ? { kind: "context", text } : NONE3;
19671
+ return text ? { kind: "context", text } : NONE4;
18960
19672
  }
18961
19673
  case "shell.pre": {
18962
- const policy = usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE3;
18963
- if (policy.kind !== "none" || !usable2(descriptor.events["write.pre"].support))
19674
+ const canDeny = usable2(descriptor.shellPolicy.deny);
19675
+ const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
19676
+ if (policy.kind !== "none")
18964
19677
  return policy;
18965
- const writes = shellWrites(event.command);
18966
- return writes.writes ? writeGate(input.cwd, writes.targets) : NONE3;
19678
+ const postTool = usable2(descriptor.events["shell.post"].support);
19679
+ if (!postTool)
19680
+ settlePendingCommit(input);
19681
+ const raw = canDeny ? rawGitPre(input, event.command) : NONE4;
19682
+ if (raw.kind === "deny")
19683
+ return raw;
19684
+ const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
19685
+ const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE4;
19686
+ if (gate.kind === "deny")
19687
+ return gate;
19688
+ noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
19689
+ return raw;
18967
19690
  }
19691
+ case "shell.post":
19692
+ if (usable2(descriptor.events["shell.post"].support))
19693
+ rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
19694
+ return NONE4;
18968
19695
  case "write.pre":
18969
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE3;
19696
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE4;
18970
19697
  case "subagent.start":
18971
19698
  return {
18972
19699
  kind: "context",
@@ -18978,17 +19705,22 @@ function handleHook(input, deps) {
18978
19705
  userMessage: "Workit context may be stale after compaction; re-run inspection or resume before acting."
18979
19706
  };
18980
19707
  case "tool.pre":
18981
- case "shell.post":
18982
19708
  case "subagent.stop":
18983
19709
  case "prompt.submit":
18984
19710
  case "stop":
18985
- return NONE3;
19711
+ return NONE4;
18986
19712
  }
18987
19713
  }
18988
19714
  // packages/workit-core/src/hooks/hosts/claude-code.ts
18989
19715
  var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
18990
19716
  var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
18991
- var CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "SubagentStart"]);
19717
+ var CONTEXT_EVENTS = new Set([
19718
+ "SessionStart",
19719
+ "UserPromptSubmit",
19720
+ "SubagentStart",
19721
+ "PreToolUse",
19722
+ "PostToolUse"
19723
+ ]);
18992
19724
 
18993
19725
  // packages/workit-core/src/hooks/hosts/codex.ts
18994
19726
  var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
@@ -19190,21 +19922,30 @@ var registerWorkitTools = (pi, options = {}) => {
19190
19922
  }
19191
19923
  });
19192
19924
  };
19925
+ var hookInput = (ctx, event) => ({
19926
+ host: "pi",
19927
+ cwd: ctx.cwd,
19928
+ session: {
19929
+ id: process.env.WORKIT_PI_WORKER_SESSION || ctx.sessionManager?.getSessionId() || "",
19930
+ agentId: null,
19931
+ agentType: null,
19932
+ parentId: null
19933
+ },
19934
+ permissionMode: null,
19935
+ transcriptPath: null,
19936
+ event
19937
+ });
19193
19938
  var enforceToolPolicy = (event, ctx) => {
19194
19939
  const gate = (hookEvent) => {
19195
- const decision = handleHook({
19196
- host: "pi",
19197
- cwd: ctx.cwd,
19198
- session: { id: "", agentId: null, agentType: null, parentId: null },
19199
- permissionMode: null,
19200
- transcriptPath: null,
19201
- event: hookEvent
19202
- }, { descriptor: PI_DESCRIPTOR, addendum: null });
19940
+ const decision = handleHook(hookInput(ctx, hookEvent), {
19941
+ descriptor: PI_DESCRIPTOR,
19942
+ addendum: null
19943
+ });
19203
19944
  return decision.kind === "deny" ? { block: true, reason: decision.reason } : undefined;
19204
19945
  };
19205
19946
  if (event.toolName === "bash") {
19206
19947
  const command = event.input?.command;
19207
- return typeof command === "string" ? gate({ kind: "shell.pre", command, toolUseId: null }) : undefined;
19948
+ return typeof command === "string" ? gate({ kind: "shell.pre", command, toolUseId: event.toolCallId ?? null }) : undefined;
19208
19949
  }
19209
19950
  if (event.toolName !== "write" && event.toolName !== "edit")
19210
19951
  return;
@@ -19217,16 +19958,37 @@ var enforceToolPolicy = (event, ctx) => {
19217
19958
  toolUseId: null
19218
19959
  });
19219
19960
  };
19961
+ var observeToolResult = (event, ctx) => {
19962
+ try {
19963
+ if (event.toolName !== "bash" || event.isError)
19964
+ return;
19965
+ const command = event.input?.command;
19966
+ if (typeof command !== "string")
19967
+ return;
19968
+ const post = hookInput(ctx, {
19969
+ kind: "shell.post",
19970
+ command,
19971
+ stdout: "",
19972
+ exitCode: null,
19973
+ toolUseId: event.toolCallId
19974
+ });
19975
+ handleHook(post, { descriptor: PI_DESCRIPTOR, addendum: null });
19976
+ const nudge = rawGitPre(post, command);
19977
+ return nudge.kind === "context" ? { content: [...event.content, { type: "text", text: nudge.text }] } : undefined;
19978
+ } catch {
19979
+ return;
19980
+ }
19981
+ };
19220
19982
 
19221
19983
  // packages/workit-pi/src/launches.ts
19222
19984
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
19223
- import os4 from "node:os";
19224
- import path22 from "node:path";
19985
+ import os5 from "node:os";
19986
+ import path23 from "node:path";
19225
19987
  var launchDir = (root) => {
19226
19988
  const location = resolveStore(root);
19227
- return location instanceof Error ? null : path22.join(location.dir, "pi-launches");
19989
+ return location instanceof Error ? null : path23.join(location.dir, "pi-launches");
19228
19990
  };
19229
- var fileFor = (dir, workerId) => path22.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
19991
+ var fileFor = (dir, workerId) => path23.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
19230
19992
  var recordLaunch = (root, attempt) => {
19231
19993
  const dir = launchDir(root);
19232
19994
  if (!dir)
@@ -19236,7 +19998,7 @@ var recordLaunch = (root, attempt) => {
19236
19998
  writeFileSync3(fileFor(dir, attempt.workerId), JSON.stringify({
19237
19999
  ...attempt,
19238
20000
  pid: process.pid,
19239
- host: os4.hostname(),
20001
+ host: os5.hostname(),
19240
20002
  at: new Date().toISOString()
19241
20003
  }), { flag: "wx" });
19242
20004
  return true;
@@ -19250,7 +20012,7 @@ var clearLaunch = (root, workerId) => {
19250
20012
  rmSync2(fileFor(dir, workerId), { force: true });
19251
20013
  };
19252
20014
  var alive = (attempt) => {
19253
- if (attempt.host !== os4.hostname())
20015
+ if (attempt.host !== os5.hostname())
19254
20016
  return true;
19255
20017
  try {
19256
20018
  process.kill(attempt.pid, 0);
@@ -19272,7 +20034,7 @@ var abandonedLaunches = (root) => {
19272
20034
  const found = [];
19273
20035
  for (const name of names) {
19274
20036
  try {
19275
- const attempt = JSON.parse(readFileSync6(path22.join(dir, name), "utf8"));
20037
+ const attempt = JSON.parse(readFileSync6(path23.join(dir, name), "utf8"));
19276
20038
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
19277
20039
  if (!alive(attempt))
19278
20040
  found.push(attempt);
@@ -19445,6 +20207,8 @@ function launchWorker(assignment, options) {
19445
20207
  WORKIT_PI_TASK_ID: taskId ?? "",
19446
20208
  WORKIT_PI_WORKER_ID: workerId ?? "",
19447
20209
  WORKIT_PI_WORKER_SESSION: sessionId,
20210
+ WORKIT_HOST: "pi",
20211
+ WORKIT_SESSION_ID: sessionId,
19448
20212
  WORKIT_PI_WORKER_ROLE: assignment.role,
19449
20213
  WORKIT_PI_WORKER_SCOPE: JSON.stringify(assignment.scope)
19450
20214
  };
@@ -20138,6 +20902,7 @@ function extension(pi) {
20138
20902
  toolCallId: event.toolCallId,
20139
20903
  isError: event.isError
20140
20904
  });
20905
+ return observeToolResult(event, ctx);
20141
20906
  });
20142
20907
  pi.on("session_shutdown", (_event, ctx) => {
20143
20908
  sessions.delete(ctx.sessionManager.getSessionId());