@brainervirus/workit-opencode 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/plugin.js CHANGED
@@ -4,7 +4,7 @@ function define(plugin) {
4
4
  }
5
5
 
6
6
  // packages/workit-core/src/core.ts
7
- import { spawnSync as spawnSync13 } from "node:child_process";
7
+ import { spawnSync as spawnSync14 } from "node:child_process";
8
8
  import { existsSync as existsSync7, realpathSync as realpathSync5 } from "node:fs";
9
9
  import path19 from "node:path";
10
10
 
@@ -9597,6 +9597,35 @@ function acquireFileLockSync(targetPath, options) {
9597
9597
  }
9598
9598
  }
9599
9599
  }
9600
+ // packages/workit-core/src/host-session.ts
9601
+ var HOST_SESSION_VARS = [
9602
+ ["OPENCODE_SESSION_ID", () => "opencode"],
9603
+ ["PI_SESSION_ID", () => "pi"],
9604
+ [
9605
+ "CODEX_THREAD_ID",
9606
+ (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9607
+ ]
9608
+ ];
9609
+ var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9610
+ function hostSessionFromEnv(env) {
9611
+ const value = (key) => env[key]?.trim() || null;
9612
+ let inner = null;
9613
+ for (const [key, host] of HOST_SESSION_VARS) {
9614
+ const session = value(key);
9615
+ if (session) {
9616
+ inner = { host: host(env), session };
9617
+ break;
9618
+ }
9619
+ }
9620
+ const declared = value("WORKIT_HOST");
9621
+ if (env.WORKIT_SESSION_ID !== undefined) {
9622
+ const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9623
+ if (!inherited)
9624
+ return { host: declared, session: value("WORKIT_SESSION_ID") };
9625
+ }
9626
+ return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9627
+ }
9628
+
9600
9629
  // packages/workit-core/src/core/package-root.ts
9601
9630
  import { existsSync } from "node:fs";
9602
9631
  import path3 from "node:path";
@@ -10561,9 +10590,10 @@ var drop = (lock) => {
10561
10590
  };
10562
10591
  var envActor = () => {
10563
10592
  const value = (key) => process.env[key]?.trim() || null;
10593
+ const acting = hostSessionFromEnv(process.env);
10564
10594
  return {
10565
- host: value("WORKIT_HOST") ?? "runtime",
10566
- session: value("WORKIT_SESSION_ID"),
10595
+ host: acting.host ?? "runtime",
10596
+ session: acting.session,
10567
10597
  agentId: value("WORKIT_AGENT_ID")
10568
10598
  };
10569
10599
  };
@@ -12487,6 +12517,7 @@ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:cry
12487
12517
  import fs10 from "node:fs";
12488
12518
  import path9 from "node:path";
12489
12519
  var LEDGER_VERSION = 1;
12520
+ var MAX_LINE_BYTES = 4096;
12490
12521
  var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
12491
12522
  var FAILING = new Set(["failed", "blocked"]);
12492
12523
  var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
@@ -12586,6 +12617,7 @@ function ledgerPath(cwd) {
12586
12617
  const root = storeRoot(cwd);
12587
12618
  return root.ok ? { ok: true, value: path9.join(root.value.root, "ledger", "ledger.jsonl") } : root;
12588
12619
  }
12620
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
12589
12621
  var NETWORK_FS_TYPES = new Set([
12590
12622
  26985,
12591
12623
  20859,
@@ -12598,6 +12630,158 @@ var NETWORK_FS_TYPES = new Set([
12598
12630
  1196443219,
12599
12631
  198183888
12600
12632
  ]);
12633
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
12634
+ var onNetworkFs = (dir) => {
12635
+ if (process.platform !== "linux")
12636
+ return false;
12637
+ try {
12638
+ return isNetworkFsType(fs10.statfsSync(dir).type);
12639
+ } catch {
12640
+ return false;
12641
+ }
12642
+ };
12643
+ var LOCK_WAIT_MS = 5000;
12644
+ var LOCK_STALE_MS = 30000;
12645
+ var LOCK_OWNER = "owner";
12646
+ var lockToken = (lock) => {
12647
+ try {
12648
+ return fs10.readFileSync(path9.join(lock, LOCK_OWNER), "utf8");
12649
+ } catch {
12650
+ return null;
12651
+ }
12652
+ };
12653
+ var lockAgeMs = (lock) => {
12654
+ for (const target of [path9.join(lock, LOCK_OWNER), lock]) {
12655
+ try {
12656
+ return Date.now() - fs10.statSync(target).mtimeMs;
12657
+ } catch {}
12658
+ }
12659
+ return null;
12660
+ };
12661
+ var ledgerLock = {
12662
+ acquire(lock) {
12663
+ try {
12664
+ fs10.mkdirSync(lock);
12665
+ } catch (error) {
12666
+ if (error.code === "EEXIST")
12667
+ return null;
12668
+ throw error;
12669
+ }
12670
+ const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
12671
+ fs10.writeFileSync(path9.join(lock, LOCK_OWNER), token);
12672
+ return token;
12673
+ },
12674
+ release(lock, token) {
12675
+ if (lockToken(lock) !== token)
12676
+ return false;
12677
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
12678
+ try {
12679
+ fs10.renameSync(lock, tomb);
12680
+ } catch {
12681
+ return false;
12682
+ }
12683
+ if (lockToken(tomb) !== token) {
12684
+ try {
12685
+ fs10.renameSync(tomb, lock);
12686
+ } catch {}
12687
+ return false;
12688
+ }
12689
+ fs10.rmSync(tomb, { recursive: true, force: true });
12690
+ return true;
12691
+ },
12692
+ token: lockToken,
12693
+ reap(lock, observed) {
12694
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
12695
+ try {
12696
+ fs10.renameSync(lock, tomb);
12697
+ } catch {
12698
+ return false;
12699
+ }
12700
+ if (lockToken(tomb) === observed) {
12701
+ fs10.rmSync(tomb, { recursive: true, force: true });
12702
+ return true;
12703
+ }
12704
+ try {
12705
+ fs10.renameSync(tomb, lock);
12706
+ } catch {
12707
+ fs10.rmSync(tomb, { recursive: true, force: true });
12708
+ }
12709
+ return false;
12710
+ }
12711
+ };
12712
+ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
12713
+ const lock = `${file}.lock`;
12714
+ const deadline = Date.now() + waitMs;
12715
+ let token = null;
12716
+ for (;; ) {
12717
+ token = ledgerLock.acquire(lock);
12718
+ if (token)
12719
+ break;
12720
+ const observed = lockToken(lock);
12721
+ const age = lockAgeMs(lock);
12722
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
12723
+ continue;
12724
+ if (Date.now() >= deadline)
12725
+ return null;
12726
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
12727
+ }
12728
+ try {
12729
+ return fn();
12730
+ } finally {
12731
+ ledgerLock.release(lock, token);
12732
+ }
12733
+ }
12734
+ function appendLine(file, line, options) {
12735
+ const write = () => {
12736
+ const fd = fs10.openSync(file, "a+");
12737
+ try {
12738
+ const size = fs10.fstatSync(fd).size;
12739
+ let fence = "";
12740
+ if (size > 0) {
12741
+ const last = Buffer.alloc(1);
12742
+ fs10.readSync(fd, last, 0, 1, size - 1);
12743
+ if (last[0] !== 10)
12744
+ fence = `
12745
+ `;
12746
+ }
12747
+ const bytes = Buffer.from(fence + line);
12748
+ const written = fs10.writeSync(fd, bytes);
12749
+ if (written !== bytes.length)
12750
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
12751
+ return { ok: true, value: undefined };
12752
+ } finally {
12753
+ fs10.closeSync(fd);
12754
+ }
12755
+ };
12756
+ try {
12757
+ fs10.mkdirSync(path9.dirname(file), { recursive: true });
12758
+ if (!options.forceLock && !onNetworkFs(path9.dirname(file)))
12759
+ return write();
12760
+ const locked = withLedgerLock(file, write);
12761
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
12762
+ } catch (error) {
12763
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
12764
+ }
12765
+ }
12766
+ var serialize = (row, now) => {
12767
+ const full = { v: LEDGER_VERSION, id: newId2(), at: now.toISOString(), ...row };
12768
+ return { full, line: `${JSON.stringify(full)}
12769
+ ` };
12770
+ };
12771
+ function appendRaw(cwd, row, options) {
12772
+ const { full, line } = serialize(row, options.now ?? new Date);
12773
+ const bytes = Buffer.byteLength(line);
12774
+ if (bytes > MAX_LINE_BYTES)
12775
+ 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>`);
12776
+ const file = ledgerPath(cwd);
12777
+ if (!file.ok)
12778
+ return file;
12779
+ const written = appendLine(file.value, line, options);
12780
+ return written.ok ? { ok: true, value: full } : written;
12781
+ }
12782
+ function appendHookObserved(cwd, row) {
12783
+ return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
12784
+ }
12601
12785
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12602
12786
  var str2 = (value) => typeof value === "string" ? value : null;
12603
12787
  var normalizeActor = (value) => {
@@ -14291,6 +14475,7 @@ var gitContext = (workspaceRoot, paths = []) => {
14291
14475
  };
14292
14476
 
14293
14477
  // packages/workit-core/src/core/workspaces.ts
14478
+ import { spawnSync as spawnSync8 } from "node:child_process";
14294
14479
  import { createHash as createHash8 } from "node:crypto";
14295
14480
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
14296
14481
  import path13 from "node:path";
@@ -14535,6 +14720,53 @@ var validateWorkspaceGlob = (glob) => {
14535
14720
  }
14536
14721
  return { ok: true };
14537
14722
  };
14723
+
14724
+ class WorkspaceConfigError extends Error {
14725
+ name = "WorkspaceConfigError";
14726
+ }
14727
+ var mainCheckouts = new Map;
14728
+ var mainCheckoutOf = (cwd) => {
14729
+ const cached = mainCheckouts.get(cwd);
14730
+ if (cached !== undefined)
14731
+ return cached;
14732
+ const env = { ...process.env };
14733
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
14734
+ delete env[name];
14735
+ const git = (args) => {
14736
+ const run = spawnSync8("git", args, {
14737
+ cwd,
14738
+ env,
14739
+ encoding: "utf8",
14740
+ timeout: 5000,
14741
+ windowsHide: true
14742
+ });
14743
+ return run.status === 0 ? run.stdout : null;
14744
+ };
14745
+ let main = null;
14746
+ const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
14747
+ const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
14748
+ if (gitDir && common && path13.resolve(gitDir) !== path13.resolve(common)) {
14749
+ const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
14750
+ main = listed?.trim() || (path13.basename(common) === ".git" ? path13.dirname(common) : common);
14751
+ }
14752
+ mainCheckouts.set(cwd, main);
14753
+ return main;
14754
+ };
14755
+ var matchingEntries = (cwd, entries) => {
14756
+ const match = (where) => {
14757
+ const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
14758
+ return entries.filter((entry) => {
14759
+ const glob = entry.glob.replaceAll("\\", "/");
14760
+ const canonical = canonicalGlob(glob);
14761
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
14762
+ });
14763
+ };
14764
+ const direct = match(cwd);
14765
+ if (direct.length)
14766
+ return direct;
14767
+ const main = mainCheckoutOf(cwd);
14768
+ return main ? match(main) : direct;
14769
+ };
14538
14770
  var realpathOf = (p) => {
14539
14771
  const real = realpathSync3.native ?? realpathSync3;
14540
14772
  try {
@@ -14559,43 +14791,29 @@ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
14559
14791
  if (named.length === 1)
14560
14792
  return named[0];
14561
14793
  if (named.length > 1)
14562
- throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
14794
+ throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
14563
14795
  if (matches.length > 0) {
14564
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
14796
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
14565
14797
  }
14566
- throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
14798
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
14567
14799
  }
14568
14800
  if (matches.length < 2)
14569
14801
  return matches[0];
14570
14802
  const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
14571
14803
  const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
14572
14804
  if (mostSpecific.length > 1) {
14573
- throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
14805
+ throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
14574
14806
  }
14575
14807
  return mostSpecific[0];
14576
14808
  };
14577
14809
  var resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
14578
- const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
14579
- const matches = [];
14580
- for (const ws of entries) {
14581
- const glob = ws.glob.replaceAll("\\", "/");
14582
- const canonical = canonicalGlob(glob);
14583
- let matched = false;
14584
- for (const target of targets) {
14585
- if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
14586
- matched = true;
14587
- break;
14588
- }
14589
- }
14590
- if (matched)
14591
- matches.push(ws);
14592
- }
14810
+ const matches = matchingEntries(cwd, entries);
14593
14811
  return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
14594
14812
  };
14595
14813
  var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
14596
14814
  const result = readWorkspacesResult(dir);
14597
14815
  if (result.status === "malformed" || result.status === "invalid")
14598
- throw new Error(result.error);
14816
+ throw new WorkspaceConfigError(result.error);
14599
14817
  return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
14600
14818
  };
14601
14819
  var runtimeWorkspacePolicySchema = (kind) => {
@@ -14627,31 +14845,26 @@ var matchRuntimeWorkspace = (cwd) => {
14627
14845
  } catch (error) {
14628
14846
  if (error.code === "ENOENT")
14629
14847
  return null;
14630
- throw new Error(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
14848
+ throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
14631
14849
  }
14632
14850
  let parsed;
14633
14851
  try {
14634
14852
  parsed = JSON.parse(raw);
14635
14853
  } catch {
14636
- throw new Error(`${file} is not valid JSON`);
14854
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
14637
14855
  }
14638
14856
  const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
14639
14857
  if (!validated.success) {
14640
14858
  const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
14641
- throw new Error(`${file} has invalid workspace matching data: ${detail}`);
14859
+ throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
14642
14860
  }
14643
14861
  const entries = validated.data.workspaces;
14644
14862
  for (const entry of entries ?? []) {
14645
14863
  const glob = validateWorkspaceGlob(entry.glob);
14646
14864
  if (!glob.ok)
14647
- throw new Error(`${file} workspace ${entry.name}: ${glob.error}`);
14865
+ throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
14648
14866
  }
14649
- const targets = [cwd, realpathOf(cwd)].map((value) => value.replaceAll("\\", "/"));
14650
- const matches = (entries ?? []).filter((entry) => {
14651
- const glob = entry.glob.replaceAll("\\", "/");
14652
- const canonical = canonicalGlob(glob);
14653
- return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
14654
- });
14867
+ const matches = matchingEntries(cwd, entries ?? []);
14655
14868
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
14656
14869
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
14657
14870
  return selected ? { file, selected } : null;
@@ -14668,7 +14881,7 @@ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
14668
14881
  const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
14669
14882
  if (!policy.success) {
14670
14883
  const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
14671
- throw new Error(`${file} has invalid ${kind} policy configuration: ${detail}`);
14884
+ throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
14672
14885
  }
14673
14886
  return policy.data;
14674
14887
  };
@@ -14679,10 +14892,10 @@ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
14679
14892
  const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
14680
14893
  const profileName = requestedProfile?.trim() || candidate.defaultProfile;
14681
14894
  if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
14682
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
14895
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
14683
14896
  const profile = profileName ? candidate.profiles?.[profileName] : undefined;
14684
14897
  if (profileName && !profile)
14685
- throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
14898
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
14686
14899
  const profilePolicy = profile?.[key];
14687
14900
  const workspacePolicy = candidate[key];
14688
14901
  return {
@@ -14702,11 +14915,11 @@ function workspaceDefaultTarget(cwd, globalDefault) {
14702
14915
  // packages/workit-core/src/core/vcs-config.ts
14703
14916
  import fs12 from "node:fs";
14704
14917
  import path14 from "node:path";
14705
- import { spawnSync as spawnSync9 } from "node:child_process";
14918
+ import { spawnSync as spawnSync10 } from "node:child_process";
14706
14919
 
14707
14920
  // packages/workit-core/src/core/release-tracks.ts
14708
14921
  import { createHash as createHash9 } from "node:crypto";
14709
- import { spawnSync as spawnSync8 } from "node:child_process";
14922
+ import { spawnSync as spawnSync9 } from "node:child_process";
14710
14923
  class ReleaseTrackError extends Error {
14711
14924
  }
14712
14925
  var KNOWN_FIELDS = new Set([
@@ -14991,7 +15204,7 @@ function resolveReleaseTrack(input) {
14991
15204
  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 ")}`);
14992
15205
  }
14993
15206
  var git4 = (cwd, args) => {
14994
- const run = spawnSync8("git", args, {
15207
+ const run = spawnSync9("git", args, {
14995
15208
  cwd,
14996
15209
  encoding: "utf8",
14997
15210
  timeout: 20000,
@@ -15106,7 +15319,7 @@ function resolveTrackFor(cwd, options) {
15106
15319
  var workspacesPath = () => path14.join(configDir(), "workspaces.json");
15107
15320
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
15108
15321
  var remoteProvider = (cwd) => {
15109
- const r = spawnSync9("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
15322
+ const r = spawnSync10("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
15110
15323
  if (r.status !== 0)
15111
15324
  return null;
15112
15325
  const url = (r.stdout ?? "").trim();
@@ -15143,7 +15356,7 @@ var remoteEndpoint = (remote) => {
15143
15356
  var configuredSshEndpoint = (host) => {
15144
15357
  if (host.startsWith("-"))
15145
15358
  return null;
15146
- const resolved = spawnSync9("ssh", ["-G", host], {
15359
+ const resolved = spawnSync10("ssh", ["-G", host], {
15147
15360
  encoding: "utf8",
15148
15361
  timeout: 5000,
15149
15362
  maxBuffer: 1024 * 1024,
@@ -15316,7 +15529,7 @@ function mergedPrStyle(limit = 6, cwd) {
15316
15529
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
15317
15530
  descriptionPreview: desc.slice(0, 600)
15318
15531
  });
15319
- const remoteResult = spawnSync9("git", ["remote", "get-url", "--push", "--all", "origin"], {
15532
+ const remoteResult = spawnSync10("git", ["remote", "get-url", "--push", "--all", "origin"], {
15320
15533
  cwd,
15321
15534
  encoding: "utf8"
15322
15535
  });
@@ -15332,7 +15545,7 @@ function mergedPrStyle(limit = 6, cwd) {
15332
15545
  if (!hostingApiHostMatches(remote, host))
15333
15546
  return { ok: false, error: "PR style target does not match the provider host" };
15334
15547
  if (provider === "gitlab") {
15335
- const run = (args) => spawnSync9("glab", ["api", ...args], {
15548
+ const run = (args) => spawnSync10("glab", ["api", ...args], {
15336
15549
  cwd,
15337
15550
  encoding: "utf8",
15338
15551
  env
@@ -15352,7 +15565,7 @@ function mergedPrStyle(limit = 6, cwd) {
15352
15565
  });
15353
15566
  }
15354
15567
  } else if (provider === "github") {
15355
- const r = spawnSync9("gh", [
15568
+ const r = spawnSync10("gh", [
15356
15569
  "pr",
15357
15570
  "list",
15358
15571
  "--repo",
@@ -15476,6 +15689,7 @@ var validateBranchNameFor = (workspaceRoot, name) => {
15476
15689
  };
15477
15690
  }
15478
15691
  };
15692
+ var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
15479
15693
 
15480
15694
  // packages/workit-core/src/core/route-intent.ts
15481
15695
  var shellBranchTarget = (command) => {
@@ -17423,7 +17637,7 @@ class WorkitCore {
17423
17637
  }
17424
17638
  }
17425
17639
  // packages/workit-core/src/core/repo-context.ts
17426
- import { spawnSync as spawnSync10 } from "node:child_process";
17640
+ import { spawnSync as spawnSync11 } from "node:child_process";
17427
17641
  import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
17428
17642
  import path16 from "node:path";
17429
17643
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -17452,7 +17666,7 @@ var invalidRange = (cwd) => ({
17452
17666
  cwd
17453
17667
  });
17454
17668
  var runGit = (cwd, args) => {
17455
- const result = spawnSync10("git", args, { cwd, encoding: "utf8" });
17669
+ const result = spawnSync11("git", args, { cwd, encoding: "utf8" });
17456
17670
  return {
17457
17671
  stdout: (result.stdout ?? "").trimEnd(),
17458
17672
  stderr: (result.stderr ?? "").trimEnd(),
@@ -17893,12 +18107,12 @@ function releaseNotesContext(root, rangeOrTag) {
17893
18107
  }
17894
18108
 
17895
18109
  // packages/workit-core/src/core/tracker-issues.ts
17896
- import { spawnSync as spawnSync12 } from "node:child_process";
18110
+ import { spawnSync as spawnSync13 } from "node:child_process";
17897
18111
 
17898
18112
  // packages/workit-core/src/core/pr-create.ts
17899
18113
  import fs13 from "node:fs";
17900
18114
  import path17 from "node:path";
17901
- import { spawnSync as spawnSync11 } from "node:child_process";
18115
+ import { spawnSync as spawnSync12 } from "node:child_process";
17902
18116
  // packages/workit-core/src/forge/pr-body.ts
17903
18117
  function parseGhIssue(value) {
17904
18118
  const m = /issues\/(\d+)/.exec(value);
@@ -17923,7 +18137,7 @@ function whichOnPath(tool) {
17923
18137
  }
17924
18138
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
17925
18139
  var pushRemote = (cwd) => {
17926
- const result = spawnSync11("git", ["remote", "get-url", "--push", "--all", "origin"], {
18140
+ const result = spawnSync12("git", ["remote", "get-url", "--push", "--all", "origin"], {
17927
18141
  cwd,
17928
18142
  encoding: "utf8"
17929
18143
  });
@@ -17954,7 +18168,7 @@ var defaultCli = (bin, available) => {
17954
18168
  if (!available)
17955
18169
  return null;
17956
18170
  return (args, root) => {
17957
- const out = spawnSync12(bin, args, {
18171
+ const out = spawnSync13(bin, args, {
17958
18172
  cwd: root,
17959
18173
  encoding: "utf8",
17960
18174
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -18530,7 +18744,7 @@ function resolveInside(root, candidate) {
18530
18744
  }
18531
18745
  function run2(root, executable, args, env = {}) {
18532
18746
  const cwd = realpathSync5(root);
18533
- const result = spawnSync13(executable, args, {
18747
+ const result = spawnSync14(executable, args, {
18534
18748
  cwd,
18535
18749
  encoding: "utf8",
18536
18750
  env: { ...process.env, ...env }
@@ -18671,36 +18885,43 @@ var shellPolicy = (cwd, command) => {
18671
18885
  // packages/workit-core/src/hooks/write-gate.ts
18672
18886
  import { realpathSync as realpathSync6 } from "node:fs";
18673
18887
  import path21 from "node:path";
18674
- var NONE2 = { kind: "none" };
18675
- var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18676
- var canonical2 = (file) => {
18677
- let rest = "";
18678
- for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18679
- try {
18680
- return path21.join(realpathSync6(dir), rest);
18681
- } catch {
18682
- if (path21.dirname(dir) === dir)
18683
- return path21.resolve(file);
18684
- rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18888
+
18889
+ // packages/workit-core/src/hooks/shell-words.ts
18890
+ var closingQuote = (command, start, dialect) => {
18891
+ const quote = command[start];
18892
+ for (let index = start + 1;index < command.length; index++) {
18893
+ const char = command[index];
18894
+ if (quote === "'") {
18895
+ if (char !== "'")
18896
+ continue;
18897
+ if (dialect === "powershell" && command[index + 1] === "'") {
18898
+ index++;
18899
+ continue;
18900
+ }
18901
+ return index;
18685
18902
  }
18903
+ if (dialect === "posix" && char === "\\" || dialect === "powershell" && char === "`") {
18904
+ index++;
18905
+ continue;
18906
+ }
18907
+ if (char === '"')
18908
+ return index;
18686
18909
  }
18910
+ return -1;
18687
18911
  };
18688
- var exempt = (cwd, root, file, plans) => {
18689
- const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18690
- const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18691
- if (relative.startsWith("..") || path21.isAbsolute(relative))
18692
- return true;
18693
- const posix = relative.split(path21.sep).join("/");
18694
- const segments = posix.split("/");
18695
- return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18912
+ var unquote = (body, quote, dialect) => {
18913
+ if (quote === "'")
18914
+ return dialect === "powershell" ? body.replaceAll("''", "'") : body;
18915
+ return dialect === "posix" ? body.replace(/\\(["\\$`])/g, "$1") : body.replace(/`(.)/g, "$1");
18696
18916
  };
18697
- var segmentsOf = (command) => {
18917
+ var segmentsOf = (command, dialect = "posix") => {
18698
18918
  const segments = [];
18699
18919
  let current = { words: [], redirects: [] };
18700
18920
  let word = "";
18701
18921
  let inWord = false;
18702
18922
  let pending = null;
18703
18923
  const heredocs = [];
18924
+ const escape = dialect === "posix" ? "\\" : "`";
18704
18925
  const flushWord = () => {
18705
18926
  if (!inWord)
18706
18927
  return;
@@ -18724,11 +18945,11 @@ var segmentsOf = (command) => {
18724
18945
  for (let index = 0;index < command.length; index++) {
18725
18946
  const char = command[index];
18726
18947
  if (char === "'" || char === '"') {
18727
- const end = command.indexOf(char, index + 1);
18728
- word += command.slice(index + 1, end < 0 ? undefined : end);
18948
+ const end = closingQuote(command, index, dialect);
18949
+ word += unquote(command.slice(index + 1, end < 0 ? undefined : end), char, dialect);
18729
18950
  inWord = true;
18730
18951
  index = end < 0 ? command.length : end;
18731
- } else if (char === "\\" && index + 1 < command.length) {
18952
+ } else if (char === escape && index + 1 < command.length) {
18732
18953
  word += command[++index];
18733
18954
  inWord = true;
18734
18955
  } else if (char === `
@@ -18745,7 +18966,14 @@ var segmentsOf = (command) => {
18745
18966
  }
18746
18967
  } else if (/\s/.test(char))
18747
18968
  flushWord();
18748
- else if (";&|()".includes(char)) {
18969
+ else if (char === "(" || char === ")") {
18970
+ if (char === "(" && inWord && word.endsWith("$")) {
18971
+ word = word.slice(0, -1);
18972
+ inWord = word !== "";
18973
+ }
18974
+ flushSegment();
18975
+ segments.push({ words: [], redirects: [], group: char === "(" ? "open" : "close" });
18976
+ } else if (";&|".includes(char)) {
18749
18977
  if (char === "&" && command[index + 1] === ">")
18750
18978
  continue;
18751
18979
  flushSegment();
@@ -18763,7 +18991,7 @@ var segmentsOf = (command) => {
18763
18991
  inWord = true;
18764
18992
  }
18765
18993
  pending = "redirect";
18766
- } else if (char === "<") {
18994
+ } else if (char === "<" && dialect === "posix") {
18767
18995
  flushWord();
18768
18996
  if (command[index + 1] === "<") {
18769
18997
  index++;
@@ -18780,6 +19008,31 @@ var segmentsOf = (command) => {
18780
19008
  flushSegment();
18781
19009
  return segments;
18782
19010
  };
19011
+
19012
+ // packages/workit-core/src/hooks/write-gate.ts
19013
+ var NONE2 = { kind: "none" };
19014
+ var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
19015
+ var canonical2 = (file) => {
19016
+ let rest = "";
19017
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
19018
+ try {
19019
+ return path21.join(realpathSync6(dir), rest);
19020
+ } catch {
19021
+ if (path21.dirname(dir) === dir)
19022
+ return path21.resolve(file);
19023
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
19024
+ }
19025
+ }
19026
+ };
19027
+ var exempt = (cwd, root, file, plans) => {
19028
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
19029
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
19030
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
19031
+ return true;
19032
+ const posix = relative.split(path21.sep).join("/");
19033
+ const segments = posix.split("/");
19034
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
19035
+ };
18783
19036
  var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18784
19037
  var operands = (words) => words.filter((item) => !item.startsWith("-"));
18785
19038
  var editsOf = (input) => {
@@ -18929,8 +19182,454 @@ function writeGate(cwd, paths) {
18929
19182
  return NONE2;
18930
19183
  }
18931
19184
  }
19185
+ // packages/workit-core/src/hooks/raw-git.ts
19186
+ import { spawnSync as spawnSync15 } from "node:child_process";
19187
+ import { createHash as createHash11 } from "node:crypto";
19188
+ import fs15 from "node:fs";
19189
+ import os4 from "node:os";
19190
+ import path22 from "node:path";
19191
+ var NONE3 = { kind: "none" };
19192
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
19193
+ var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
19194
+ var SCRIPT_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/;
19195
+ var CD = new Set(["cd", "chdir", "set-location", "sl"]);
19196
+ var PUSHD = new Set(["pushd", "push-location"]);
19197
+ var POPD = new Set(["popd", "pop-location"]);
19198
+ var GIT_VALUE_OPTIONS = new Set([
19199
+ "-c",
19200
+ "--git-dir",
19201
+ "--work-tree",
19202
+ "--namespace",
19203
+ "--exec-path",
19204
+ "--config-env",
19205
+ "--super-prefix"
19206
+ ]);
19207
+ var isAbsolute = (value) => path22.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19208
+ var joinDir = (base, next) => {
19209
+ if (base === undefined || /[$`*?]/.test(next) || next === "-")
19210
+ return;
19211
+ const expanded = next === "~" || next.startsWith("~/") ? path22.join(os4.homedir(), next.slice(1)) : next;
19212
+ return base === null || isAbsolute(expanded) ? expanded : path22.join(base, expanded);
19213
+ };
19214
+ var cdTarget = (words) => {
19215
+ const args = words.slice(1);
19216
+ const flag = args.findIndex((word) => /^-(?:literal)?path$/i.test(word));
19217
+ if (flag >= 0)
19218
+ return args[flag + 1] ?? null;
19219
+ return args.find((word) => word === "-" || !word.startsWith("-")) ?? null;
19220
+ };
19221
+ var stripPrefixes = (input) => {
19222
+ let words = input;
19223
+ for (;; ) {
19224
+ const first = words[0];
19225
+ if (first === undefined)
19226
+ return words;
19227
+ if (/^\w+=/.test(first))
19228
+ words = words.slice(1);
19229
+ else if (PREFIXES3.has(first)) {
19230
+ words = words.slice(1);
19231
+ while (words[0]?.startsWith("-"))
19232
+ words = words.slice(words[0] === "-u" ? 2 : 1);
19233
+ } else if (first === "timeout") {
19234
+ words = words.slice(1);
19235
+ while (words[0]?.startsWith("-"))
19236
+ words = words.slice(1);
19237
+ words = words.slice(1);
19238
+ } else
19239
+ return words;
19240
+ }
19241
+ };
19242
+ var commandName = (word) => (word?.split(/[\\/]/).at(-1) ?? "").toLowerCase().replace(/\.exe$/, "");
19243
+ function rawInvocations(command, dialect = "posix", depth = 0) {
19244
+ const found = [];
19245
+ let dir = null;
19246
+ const groups = [];
19247
+ let stack = [];
19248
+ for (const segment of segmentsOf(command, dialect)) {
19249
+ if (segment.group === "open") {
19250
+ groups.push({ dir, stack: [...stack] });
19251
+ continue;
19252
+ }
19253
+ if (segment.group === "close") {
19254
+ const saved = groups.pop();
19255
+ if (saved)
19256
+ ({ dir, stack } = saved);
19257
+ continue;
19258
+ }
19259
+ const words = stripPrefixes(segment.words);
19260
+ const name = commandName(words[0]);
19261
+ if (CD.has(name)) {
19262
+ const target = cdTarget(words);
19263
+ dir = target === null ? os4.homedir() : joinDir(dir, target);
19264
+ continue;
19265
+ }
19266
+ if (PUSHD.has(name)) {
19267
+ const target = cdTarget(words);
19268
+ stack.push(dir);
19269
+ dir = target === null || target === "-" ? undefined : joinDir(dir, target);
19270
+ continue;
19271
+ }
19272
+ if (POPD.has(name)) {
19273
+ dir = stack.length ? stack.pop() : undefined;
19274
+ continue;
19275
+ }
19276
+ if (SHELLS.has(name) && depth < 3) {
19277
+ const flag = words.findIndex((word, index) => index > 0 && SCRIPT_FLAG.test(word));
19278
+ const script = flag > 0 ? words[flag + 1] : undefined;
19279
+ if (script)
19280
+ for (const inner of rawInvocations(script, "posix", depth + 1))
19281
+ found.push({
19282
+ ...inner,
19283
+ dir: inner.dir === null ? dir : inner.dir === undefined ? undefined : joinDir(dir, inner.dir)
19284
+ });
19285
+ continue;
19286
+ }
19287
+ if (name === "gh" || name === "glab") {
19288
+ found.push({ tool: name, dir, args: words.slice(1) });
19289
+ continue;
19290
+ }
19291
+ if (name !== "git")
19292
+ continue;
19293
+ let here = dir;
19294
+ let index = 1;
19295
+ for (;index < words.length; index++) {
19296
+ const word = words[index];
19297
+ if (word === "-C")
19298
+ here = words[index + 1] ? joinDir(here, words[++index]) : here;
19299
+ else if (GIT_VALUE_OPTIONS.has(word))
19300
+ index++;
19301
+ else if (!word.startsWith("-"))
19302
+ break;
19303
+ }
19304
+ found.push({ tool: "git", dir: here, args: words.slice(index) });
19305
+ }
19306
+ return found;
19307
+ }
19308
+ var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
19309
+ var pushAction = (args) => {
19310
+ let force = false;
19311
+ let lease = false;
19312
+ let wholesale = false;
19313
+ let remote = null;
19314
+ const positionals = [];
19315
+ for (let index = 1;index < args.length; index++) {
19316
+ const arg = args[index];
19317
+ if (arg === "--") {
19318
+ positionals.push(...args.slice(index + 1));
19319
+ break;
19320
+ }
19321
+ if (arg === "--dry-run" || /^-[a-zA-Z]*n[a-zA-Z]*$/.test(arg))
19322
+ return null;
19323
+ if (arg === "--repo")
19324
+ remote = args[++index] ?? null;
19325
+ else if (arg.startsWith("--repo="))
19326
+ remote = arg.slice("--repo=".length);
19327
+ else if (PUSH_VALUE_OPTIONS.has(arg))
19328
+ index++;
19329
+ else if (arg === "--force" || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(arg))
19330
+ force = true;
19331
+ else if (arg.startsWith("--force-with-lease") || arg === "--force-if-includes")
19332
+ lease = true;
19333
+ else if (["--all", "--mirror", "--branches", "--tags"].includes(arg))
19334
+ wholesale = true;
19335
+ else if (!arg.startsWith("-"))
19336
+ positionals.push(arg);
19337
+ }
19338
+ const specs = remote === null ? positionals.slice(1) : positionals;
19339
+ remote ??= positionals[0] ?? null;
19340
+ const targets = [];
19341
+ for (const raw of specs) {
19342
+ if (raw.startsWith("+"))
19343
+ force = true;
19344
+ const spec = raw.replace(/^\+/, "");
19345
+ const colon = spec.indexOf(":");
19346
+ const destination = colon >= 0 ? spec.slice(colon + 1) : spec;
19347
+ if (!destination)
19348
+ continue;
19349
+ if (destination === "HEAD")
19350
+ targets.push(null);
19351
+ else if (destination.startsWith("refs/heads/"))
19352
+ targets.push(destination.slice(11));
19353
+ else if (!destination.startsWith("refs/"))
19354
+ targets.push(destination);
19355
+ }
19356
+ if (specs.length === 0 && !wholesale)
19357
+ targets.push(null);
19358
+ return { kind: "push", targets, blindForce: force && !lease, remote };
19359
+ };
19360
+ var VALUE_OPTIONS = {
19361
+ git: new Set([
19362
+ "-m",
19363
+ "--message",
19364
+ "-F",
19365
+ "--file",
19366
+ "-C",
19367
+ "-c",
19368
+ "--reuse-message",
19369
+ "--reedit-message",
19370
+ "--author",
19371
+ "--date",
19372
+ "--trailer",
19373
+ "-t",
19374
+ "--template",
19375
+ "--cleanup",
19376
+ "--fixup",
19377
+ "--squash",
19378
+ "-o",
19379
+ "--push-option",
19380
+ "--repo",
19381
+ "--receive-pack",
19382
+ "--exec"
19383
+ ]),
19384
+ 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"])),
19385
+ 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"]))
19386
+ };
19387
+ var asksHelp = (invocation) => {
19388
+ const { tool, args } = invocation;
19389
+ if (args[0] === "help" || tool !== "git" && args[1] === "help")
19390
+ return true;
19391
+ const values = VALUE_OPTIONS[tool];
19392
+ for (let index = 0;index < args.length; index++) {
19393
+ const arg = args[index];
19394
+ if (arg === "--")
19395
+ return false;
19396
+ if (arg === "--help" || arg === "-h")
19397
+ return true;
19398
+ if (values.has(arg))
19399
+ index++;
19400
+ }
19401
+ return false;
19402
+ };
19403
+ function rawAction(invocation) {
19404
+ const args = invocation.args;
19405
+ if (asksHelp(invocation))
19406
+ return null;
19407
+ const [first, second] = args;
19408
+ if (invocation.tool === "git") {
19409
+ if (first === "commit")
19410
+ return args.includes("--dry-run") ? null : { kind: "commit", amend: args.includes("--amend") };
19411
+ if (first === "push")
19412
+ return pushAction(args);
19413
+ return null;
19414
+ }
19415
+ const forge = invocation.tool;
19416
+ const noun = forge === "gh" ? "pr" : "mr";
19417
+ if (first !== noun)
19418
+ return null;
19419
+ if (second === "merge")
19420
+ return { kind: "merge", forge };
19421
+ if (second === "create")
19422
+ return { kind: "pr-create", forge };
19423
+ if (["view", "checks", "status", "ci", "list"].includes(second ?? ""))
19424
+ return { kind: "pr-read", forge };
19425
+ return null;
19426
+ }
19427
+ var repoAt = (dir) => {
19428
+ const location = resolveStore(dir);
19429
+ if (location instanceof Error || !location.shared)
19430
+ return null;
19431
+ return { location, dir, branch: resolveTaskKey(dir, location).branch };
19432
+ };
19433
+ var isWorkitWorkspace = (repo) => {
19434
+ if (fs15.existsSync(repo.location.dir))
19435
+ return true;
19436
+ try {
19437
+ return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
19438
+ } catch {
19439
+ return false;
19440
+ }
19441
+ };
19442
+ var guardedBranch = (dir, branch) => {
19443
+ try {
19444
+ const policy = resolveBranchPolicyFor(dir);
19445
+ return policy.protected.has(branch.toLowerCase()) || policy.defaultTargetBranch.toLowerCase() === branch.toLowerCase();
19446
+ } catch {
19447
+ return false;
19448
+ }
19449
+ };
19450
+ var toPushRemote = (repo, remote) => {
19451
+ if (remote === null)
19452
+ return true;
19453
+ try {
19454
+ const pushRemote = pushRemoteName(repo.dir, repo.branch);
19455
+ return pushRemote === null || pushRemote === remote;
19456
+ } catch {
19457
+ return false;
19458
+ }
19459
+ };
19460
+ var headSha2 = (repo) => {
19461
+ const git = repo.location.git;
19462
+ if (!git || git.reftable)
19463
+ return null;
19464
+ try {
19465
+ const head = fs15.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19466
+ if (/^[0-9a-f]{40,64}$/.test(head))
19467
+ return head;
19468
+ const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
19469
+ if (!ref)
19470
+ return null;
19471
+ const common = path22.dirname(repo.location.dir);
19472
+ try {
19473
+ return fs15.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19474
+ } catch {}
19475
+ const packed = fs15.readFileSync(path22.join(common, "packed-refs"), "utf8");
19476
+ return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
19477
+ } catch {
19478
+ return null;
19479
+ }
19480
+ };
19481
+ var quote = (text) => `\`${text}\``;
19482
+ var refusal = (action, branch) => {
19483
+ if (action.kind === "merge")
19484
+ 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")}).`;
19485
+ 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")}.`;
19486
+ };
19487
+ var nudgeFor = (action, host, session) => {
19488
+ switch (action.kind) {
19489
+ case "commit": {
19490
+ const prefix = host === "cursor" && session ? `WORKIT_SESSION_ID=${session} ` : "";
19491
+ 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.`;
19492
+ }
19493
+ case "push":
19494
+ 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.`;
19495
+ case "pr-create":
19496
+ return `workit: prefer ${quote("workit pr create --fill")} (or --title/--body): it records the PR the merge gate checks.`;
19497
+ case "pr-read":
19498
+ return `workit: ${quote("workit pr status")} shows the PR, its checks and the verdict state; ${quote("workit ci wait")} waits for checks.`;
19499
+ case "merge":
19500
+ return null;
19501
+ }
19502
+ };
19503
+ var actionsIn = (cwd, command, dialect) => {
19504
+ const out = [];
19505
+ const repos = new Map;
19506
+ for (const invocation of rawInvocations(command, dialect)) {
19507
+ if (invocation.dir === undefined)
19508
+ continue;
19509
+ const action = rawAction(invocation);
19510
+ if (!action)
19511
+ continue;
19512
+ const dir = path22.resolve(cwd, invocation.dir ?? ".");
19513
+ if (!repos.has(dir)) {
19514
+ const repo = repoAt(dir);
19515
+ repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
19516
+ }
19517
+ const repo = repos.get(dir);
19518
+ if (repo)
19519
+ out.push({ action, repo });
19520
+ }
19521
+ return out;
19522
+ };
19523
+ var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
19524
+ var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
19525
+ function rawGitPre(input, command) {
19526
+ try {
19527
+ if (!mentionsRawTool(command))
19528
+ return NONE3;
19529
+ const nudges = [];
19530
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19531
+ if (action.kind === "merge")
19532
+ return deny(refusal(action, null));
19533
+ if (action.kind === "push")
19534
+ for (const target of action.targets) {
19535
+ const branch = target ?? repo.branch;
19536
+ if (branch && guardedBranch(repo.dir, branch) && toPushRemote(repo, action.remote))
19537
+ return deny(refusal(action, branch));
19538
+ }
19539
+ const nudge = nudgeFor(action, input.host, input.session.id);
19540
+ if (nudge && !nudges.includes(nudge))
19541
+ nudges.push(nudge);
19542
+ }
19543
+ return nudges.length ? { kind: "context", text: nudges.join(`
19544
+ `) } : NONE3;
19545
+ } catch {
19546
+ return NONE3;
19547
+ }
19548
+ }
19549
+ var deny = (reason) => ({ kind: "deny", reason, unblock: null });
19550
+ var MARKER_TTL_SECONDS = 3600;
19551
+ var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash11("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19552
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash11("sha256").update(command).digest("hex")}`;
19553
+ function noteRawCommit(input, command, key) {
19554
+ try {
19555
+ if (!input.session.id || !/\bcommit\b/.test(command))
19556
+ return;
19557
+ const at = Math.floor(Date.now() / 1000);
19558
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19559
+ if (action.kind !== "commit")
19560
+ continue;
19561
+ const file = markerFile(repo, input.session.id, key);
19562
+ fs15.mkdirSync(path22.dirname(file), { recursive: true });
19563
+ fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19564
+ `);
19565
+ }
19566
+ } catch {}
19567
+ }
19568
+ var headCommit = (dir) => {
19569
+ const run = spawnSync15("git", [
19570
+ "log",
19571
+ "-1",
19572
+ "--format=%H%x1f%ct%x1f%s%x1f%(trailers:key=Workit-Session,valueonly,separator=%x2c)"
19573
+ ], {
19574
+ cwd: dir,
19575
+ encoding: "utf8",
19576
+ timeout: 5000,
19577
+ windowsHide: true,
19578
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
19579
+ });
19580
+ const [sha, committed, subject = "", trailer = ""] = (run.status === 0 ? run.stdout.trim() : "").split("\x1F");
19581
+ return sha && /^[0-9a-f]{40,64}$/.test(sha) ? { sha, committed: Number(committed), subject, trailer: trailer.trim() || null } : null;
19582
+ };
19583
+ var settle2 = (input, repo, file) => {
19584
+ let marker;
19585
+ try {
19586
+ marker = JSON.parse(fs15.readFileSync(file, "utf8"));
19587
+ } catch {
19588
+ return;
19589
+ }
19590
+ fs15.rmSync(file, { force: true });
19591
+ const session = input.session.id;
19592
+ const now = Date.now() / 1000;
19593
+ if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
19594
+ return;
19595
+ if (!session || !repo.branch)
19596
+ return;
19597
+ const head = headCommit(repo.dir);
19598
+ if (!head || head.sha === marker.head || head.committed < marker.at)
19599
+ return;
19600
+ if (head.trailer && head.trailer !== session)
19601
+ return;
19602
+ const ledger = readLedger(repo.dir);
19603
+ if (!ledger.ok)
19604
+ return;
19605
+ const claimed = ledger.value.rows.some((row) => row.type === "commit.recorded" && (row.sha === head.sha || row.head === head.sha));
19606
+ if (claimed)
19607
+ return;
19608
+ appendHookObserved(repo.dir, {
19609
+ type: "commit.recorded",
19610
+ actor: { host: input.host, session, agentId: input.session.agentId },
19611
+ branch: repo.branch,
19612
+ head: head.sha,
19613
+ sha: head.sha,
19614
+ session,
19615
+ agentId: input.session.agentId,
19616
+ subject: head.subject.slice(0, 200),
19617
+ files: [],
19618
+ fileCount: null,
19619
+ via: "raw_shell"
19620
+ });
19621
+ };
19622
+ function rawGitPost(input, command, key) {
19623
+ try {
19624
+ if (!input.session.id || !/\bcommit\b/.test(command))
19625
+ return;
19626
+ for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input)))
19627
+ if (action.kind === "commit")
19628
+ settle2(input, repo, markerFile(repo, input.session.id, key));
19629
+ } catch {}
19630
+ }
18932
19631
  // packages/workit-core/src/hooks/hosts/fields.ts
18933
- var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
19632
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18934
19633
  var WRITE_TOOLS = new Set([
18935
19634
  "write",
18936
19635
  "edit",
@@ -18956,7 +19655,13 @@ var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
18956
19655
  // packages/workit-core/src/hooks/hosts/claude-code.ts
18957
19656
  var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
18958
19657
  var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
18959
- var CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "SubagentStart"]);
19658
+ var CONTEXT_EVENTS = new Set([
19659
+ "SessionStart",
19660
+ "UserPromptSubmit",
19661
+ "SubagentStart",
19662
+ "PreToolUse",
19663
+ "PostToolUse"
19664
+ ]);
18960
19665
 
18961
19666
  // packages/workit-core/src/hooks/hosts/codex.ts
18962
19667
  var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
@@ -19034,11 +19739,11 @@ var OPENCODE_DESCRIPTOR = {
19034
19739
  ]
19035
19740
  };
19036
19741
  // packages/workit-opencode/src/shared/init-apply.ts
19037
- import path26 from "node:path";
19742
+ import path27 from "node:path";
19038
19743
 
19039
19744
  // packages/workit-core/src/core/gitignore.ts
19040
19745
  import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
19041
- import path22 from "node:path";
19746
+ import path23 from "node:path";
19042
19747
  var GITIGNORE_ENTRIES = [
19043
19748
  "# workit: SDD working state (never commit)",
19044
19749
  "docs/*/sdd/",
@@ -19058,7 +19763,7 @@ var GITIGNORE_ENTRIES = [
19058
19763
  var ensureProjectGitignore = (workspaceRoot, confirmed) => {
19059
19764
  if (!confirmed)
19060
19765
  return { ok: false, error: "confirmed: true required" };
19061
- const file = path22.join(workspaceRoot, ".gitignore");
19766
+ const file = path23.join(workspaceRoot, ".gitignore");
19062
19767
  const existing = existsSync8(file) ? readFileSync6(file, "utf8") : "";
19063
19768
  const existingLines = new Set(existing.split(`
19064
19769
  `).map((l) => l.trim()).filter(Boolean));
@@ -19086,25 +19791,25 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
19086
19791
 
19087
19792
  // packages/workit-core/src/core/hygiene.ts
19088
19793
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
19089
- import path23 from "node:path";
19794
+ import path24 from "node:path";
19090
19795
  var repoRoot3 = assetRoot();
19091
- var templatesDir = () => path23.join(repoRoot3, "templates", "hygiene");
19796
+ var templatesDir = () => path24.join(repoRoot3, "templates", "hygiene");
19092
19797
  var packageJson = (root) => {
19093
- if (!existsSync9(path23.join(root, "package.json")))
19798
+ if (!existsSync9(path24.join(root, "package.json")))
19094
19799
  return null;
19095
19800
  try {
19096
- return JSON.parse(readFileSync7(path23.join(root, "package.json"), "utf8"));
19801
+ return JSON.parse(readFileSync7(path24.join(root, "package.json"), "utf8"));
19097
19802
  } catch {
19098
19803
  return null;
19099
19804
  }
19100
19805
  };
19101
19806
  var isOpenSource = (root) => {
19102
- if (existsSync9(path23.join(root, "LICENSE")))
19807
+ if (existsSync9(path24.join(root, "LICENSE")))
19103
19808
  return true;
19104
19809
  const pkg = packageJson(root);
19105
19810
  if (pkg && !pkg.private)
19106
19811
  return true;
19107
- return path23.basename(root) === "workflow-toolkit";
19812
+ return path24.basename(root) === "workflow-toolkit";
19108
19813
  };
19109
19814
  var licenseHolder = (root) => {
19110
19815
  const pkg = packageJson(root);
@@ -19127,13 +19832,13 @@ var planHygieneFiles = (root, opts = {}) => {
19127
19832
  files.push("LICENSE", "CONTRIBUTING.md");
19128
19833
  const planned = [];
19129
19834
  for (const file of files) {
19130
- if (existsSync9(path23.join(root, file)))
19835
+ if (existsSync9(path24.join(root, file)))
19131
19836
  continue;
19132
- const tpl = path23.join(templatesDir(), file);
19837
+ const tpl = path24.join(templatesDir(), file);
19133
19838
  if (!existsSync9(tpl))
19134
19839
  continue;
19135
- const content = readFileSync7(tpl, "utf8").replace(/<PROJECT>/g, path23.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
19136
- planned.push({ path: path23.join(root, file), content });
19840
+ const content = readFileSync7(tpl, "utf8").replace(/<PROJECT>/g, path24.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
19841
+ planned.push({ path: path24.join(root, file), content });
19137
19842
  }
19138
19843
  return planned;
19139
19844
  };
@@ -19143,19 +19848,19 @@ var ensureHygieneFiles = (root, opts) => {
19143
19848
  const created = [];
19144
19849
  for (const planned of planHygieneFiles(root, opts)) {
19145
19850
  writeFileSync5(planned.path, planned.content, "utf8");
19146
- created.push(path23.basename(planned.path));
19851
+ created.push(path24.basename(planned.path));
19147
19852
  }
19148
19853
  return { ok: true, created };
19149
19854
  };
19150
19855
 
19151
19856
  // packages/workit-core/src/core/init.ts
19152
- import fs15 from "node:fs";
19153
- import path25 from "node:path";
19154
- import { spawnSync as spawnSync14 } from "node:child_process";
19857
+ import fs16 from "node:fs";
19858
+ import path26 from "node:path";
19859
+ import { spawnSync as spawnSync16 } from "node:child_process";
19155
19860
 
19156
19861
  // packages/workit-core/src/core/workspace-branch-policy.ts
19157
19862
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
19158
- import path24 from "node:path";
19863
+ import path25 from "node:path";
19159
19864
  import { isDeepStrictEqual } from "node:util";
19160
19865
  var readFileSafe = (p) => {
19161
19866
  try {
@@ -19166,7 +19871,7 @@ var readFileSafe = (p) => {
19166
19871
  };
19167
19872
  function applyWorkspaceBranchPolicy(opts) {
19168
19873
  const { workspace_root, env = process.env } = opts;
19169
- const dir = path24.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
19874
+ const dir = path25.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
19170
19875
  const {
19171
19876
  status,
19172
19877
  path: wsPath,
@@ -19178,7 +19883,7 @@ function applyWorkspaceBranchPolicy(opts) {
19178
19883
  if (status === "malformed" || status === "invalid")
19179
19884
  return { ok: false, error: workspacesError ?? `invalid workspaces.json: ${wsPath}` };
19180
19885
  const detection = detectBranchPolicy(workspace_root);
19181
- const name = String(env.WORKFLOW_BP_NAME ?? path24.basename(workspace_root));
19886
+ const name = String(env.WORKFLOW_BP_NAME ?? path25.basename(workspace_root));
19182
19887
  const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
19183
19888
  const policy = {
19184
19889
  preset: detection.preset,
@@ -19227,7 +19932,7 @@ function applyWorkspaceBranchPolicy(opts) {
19227
19932
  ok: false,
19228
19933
  error: `workspaces.json changed while applying the branch policy: ${wsPath}`
19229
19934
  };
19230
- mkdirSync4(path24.dirname(wsPath), { recursive: true });
19935
+ mkdirSync4(path25.dirname(wsPath), { recursive: true });
19231
19936
  const tmp = `${wsPath}.${process.pid}.tmp`;
19232
19937
  try {
19233
19938
  writeFileSync6(tmp, JSON.stringify({ ...document, workspaces: next }, null, 2) + `
@@ -19262,7 +19967,7 @@ function applyWorkspaceBranchPolicy(opts) {
19262
19967
  var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE";
19263
19968
  var readJson = (p) => {
19264
19969
  try {
19265
- const parsed = JSON.parse(fs15.readFileSync(p, "utf8"));
19970
+ const parsed = JSON.parse(fs16.readFileSync(p, "utf8"));
19266
19971
  return isConfigObject(parsed) ? parsed : null;
19267
19972
  } catch {
19268
19973
  return null;
@@ -19274,7 +19979,7 @@ var resolveInitProvider = (cwd) => {
19274
19979
  return env.toLowerCase();
19275
19980
  const root = cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
19276
19981
  try {
19277
- const out = spawnSync14("git", ["remote", "get-url", "origin"], {
19982
+ const out = spawnSync16("git", ["remote", "get-url", "origin"], {
19278
19983
  cwd: root,
19279
19984
  encoding: "utf8"
19280
19985
  });
@@ -19292,7 +19997,7 @@ var youtrackJsonContent = (dir) => {
19292
19997
  const meetingIssue = process.env.WORKFLOW_YT_MEETING_ISSUE;
19293
19998
  return {
19294
19999
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://youtrack.example.com",
19295
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path25.join(dir, "youtrack.token"),
20000
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path26.join(dir, "youtrack.token"),
19296
20001
  ...meetingIssue ? {
19297
20002
  meetingIssue,
19298
20003
  meetingIssues: { general: { issue: meetingIssue, label: "General meetings" } }
@@ -19322,19 +20027,19 @@ var vcsJsonContent = () => {
19322
20027
  };
19323
20028
  function initApplyData(action, env = process.env) {
19324
20029
  const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
19325
- fs15.mkdirSync(dir, { recursive: true });
20030
+ fs16.mkdirSync(dir, { recursive: true });
19326
20031
  switch (action) {
19327
20032
  case "youtrack_json": {
19328
- const out = path25.join(dir, "youtrack.json");
19329
- fs15.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
20033
+ const out = path26.join(dir, "youtrack.json");
20034
+ fs16.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
19330
20035
  `, "utf8");
19331
20036
  return { action, ok: true, path: out };
19332
20037
  }
19333
20038
  case "youtrack_token_placeholder": {
19334
- const p = path25.join(dir, "youtrack.token");
20039
+ const p = path26.join(dir, "youtrack.token");
19335
20040
  const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER2 + `
19336
20041
  `, 384) === "preserved";
19337
- const abs = path25.resolve(p);
20042
+ const abs = path26.resolve(p);
19338
20043
  return {
19339
20044
  action,
19340
20045
  ok: true,
@@ -19346,14 +20051,14 @@ function initApplyData(action, env = process.env) {
19346
20051
  };
19347
20052
  }
19348
20053
  case "youtrack_scaffold": {
19349
- const jsonOut = path25.join(dir, "youtrack.json");
19350
- const tokenOut = path25.join(dir, "youtrack.token");
19351
- fs15.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
20054
+ const jsonOut = path26.join(dir, "youtrack.json");
20055
+ const tokenOut = path26.join(dir, "youtrack.token");
20056
+ fs16.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
19352
20057
  `, "utf8");
19353
20058
  const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER2 + `
19354
20059
  `, 384) === "preserved";
19355
- const configPath = path25.resolve(jsonOut);
19356
- const tokenPath = path25.resolve(tokenOut);
20060
+ const configPath = path26.resolve(jsonOut);
20061
+ const tokenPath = path26.resolve(tokenOut);
19357
20062
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
19358
20063
  process.env.WORKFLOW_YOUTRACK_CONFIG = configPath;
19359
20064
  let tokenCreate = {};
@@ -19405,10 +20110,10 @@ function initApplyData(action, env = process.env) {
19405
20110
  }
19406
20111
  }
19407
20112
  case "vcs_scaffold": {
19408
- const jsonOut = path25.join(dir, "vcs.json");
19409
- fs15.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
20113
+ const jsonOut = path26.join(dir, "vcs.json");
20114
+ fs16.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
19410
20115
  `, "utf8");
19411
- const configPath = path25.resolve(jsonOut);
20116
+ const configPath = path26.resolve(jsonOut);
19412
20117
  const prev = process.env.WORKFLOW_VCS_CONFIG;
19413
20118
  process.env.WORKFLOW_VCS_CONFIG = configPath;
19414
20119
  try {
@@ -19527,7 +20232,7 @@ var executeInitApply = (args, directory, runtime) => {
19527
20232
  protectedNames: args.branch_policy_protected
19528
20233
  }, current);
19529
20234
  writeConfig(next);
19530
- return output(ok({ action: "config", path: path26.join(configDir(), "config.json"), ...next }));
20235
+ return output(ok({ action: "config", path: path27.join(configDir(), "config.json"), ...next }));
19531
20236
  }
19532
20237
  const env = Object.fromEntries(Object.entries({
19533
20238
  WORKFLOW_YT_BASE_URL: args.base_url,
@@ -19544,14 +20249,14 @@ var executeInitApply = (args, directory, runtime) => {
19544
20249
 
19545
20250
  // packages/workit-opencode/src/shared/session.ts
19546
20251
  import { realpathSync as realpathSync7 } from "node:fs";
19547
- import path27 from "node:path";
20252
+ import path28 from "node:path";
19548
20253
  var sameWorkspace = (expected, observed) => {
19549
20254
  if (typeof observed !== "string" || !observed)
19550
20255
  return false;
19551
20256
  try {
19552
20257
  return realpathSync7(expected) === realpathSync7(observed);
19553
20258
  } catch {
19554
- return path27.resolve(expected) === path27.resolve(observed);
20259
+ return path28.resolve(expected) === path28.resolve(observed);
19555
20260
  }
19556
20261
  };
19557
20262
 
@@ -19623,7 +20328,7 @@ var workitFamilyOf = (toolName) => {
19623
20328
 
19624
20329
  // packages/workit-opencode/src/tools/context.ts
19625
20330
  import { realpathSync as realpathSync8 } from "node:fs";
19626
- import path28 from "node:path";
20331
+ import path29 from "node:path";
19627
20332
  var createContextTool = () => ({
19628
20333
  description: "Read Git, pull request, YouTrack, issue, changelog, release, or affected-file context.",
19629
20334
  execute: async (args, context) => {
@@ -19633,7 +20338,7 @@ var createContextTool = () => ({
19633
20338
  let root;
19634
20339
  try {
19635
20340
  const cwd = parsed.data.cwd;
19636
- root = realpathSync8(cwd ? path28.resolve(context.directory, cwd) : context.directory);
20341
+ root = realpathSync8(cwd ? path29.resolve(context.directory, cwd) : context.directory);
19637
20342
  } catch {
19638
20343
  return JSON.stringify(failure2("invalid_input", "context target directory cannot be resolved"), null, 2);
19639
20344
  }
@@ -20156,7 +20861,7 @@ var injectCompactionContext = (root, sessionID, system) => {
20156
20861
 
20157
20862
  // packages/workit-opencode/src/v2/permissions.ts
20158
20863
  var WRITE_ACTIONS = new Set(["edit", "write", "patch"]);
20159
- var evaluateShellPermission = (root, event) => {
20864
+ var evaluateShellPermission = (root, event, workdirs = new Map) => {
20160
20865
  if (event.effect === "deny")
20161
20866
  return;
20162
20867
  const deny = (reason) => {
@@ -20176,6 +20881,17 @@ var evaluateShellPermission = (root, event) => {
20176
20881
  const decision = shellPolicy(root, resource);
20177
20882
  if (decision.kind === "deny")
20178
20883
  return deny(decision.reason);
20884
+ const cwd = event.sessionID && workdirs.get(event.sessionID) || root;
20885
+ const raw = rawGitPre({
20886
+ host: "opencode",
20887
+ cwd,
20888
+ session: { id: event.sessionID ?? "", agentId: null, agentType: null, parentId: null },
20889
+ permissionMode: null,
20890
+ transcriptPath: null,
20891
+ event: { kind: "shell.pre", command: resource, toolUseId: null }
20892
+ }, resource);
20893
+ if (raw.kind === "deny")
20894
+ return deny(raw.reason);
20179
20895
  const writes = shellWrites(resource);
20180
20896
  const gated = writes.writes ? writeGate(root, writes.targets) : null;
20181
20897
  if (gated?.kind === "deny")
@@ -20183,9 +20899,65 @@ var evaluateShellPermission = (root, event) => {
20183
20899
  }
20184
20900
  };
20185
20901
 
20902
+ // packages/workit-opencode/src/v2/shell.ts
20903
+ import path30 from "node:path";
20904
+ var commandOf = (input) => {
20905
+ if (input === null || typeof input !== "object")
20906
+ return null;
20907
+ const { command, workdir } = input;
20908
+ return typeof command === "string" && command.trim() ? { command, workdir: typeof workdir === "string" && workdir.trim() ? workdir : null } : null;
20909
+ };
20910
+ var hookInput = (root, sessionID, id, shell) => ({
20911
+ host: "opencode",
20912
+ cwd: shell.workdir ? path30.resolve(root, shell.workdir) : root,
20913
+ session: { id: sessionID, agentId: null, agentType: null, parentId: null },
20914
+ permissionMode: null,
20915
+ transcriptPath: null,
20916
+ event: { kind: "shell.pre", command: shell.command, toolUseId: id }
20917
+ });
20918
+ var observeShellBefore = (root, event, workdirs) => {
20919
+ try {
20920
+ if (event.tool !== "shell")
20921
+ return;
20922
+ const shell = commandOf(event.input);
20923
+ if (!shell)
20924
+ return;
20925
+ const sessionID = String(event.sessionID);
20926
+ const input = hookInput(root, sessionID, String(event.id), shell);
20927
+ workdirs.set(sessionID, input.cwd);
20928
+ if (workdirs.size > 256)
20929
+ workdirs.delete(workdirs.keys().next().value);
20930
+ noteRawCommit(input, shell.command, callKey(String(event.id), shell.command));
20931
+ } catch {}
20932
+ };
20933
+ var observeShellResult = (root, event) => {
20934
+ try {
20935
+ if (event.tool !== "shell" || event.status !== "completed")
20936
+ return;
20937
+ const shell = commandOf(event.input);
20938
+ if (!shell)
20939
+ return;
20940
+ const input = hookInput(root, String(event.sessionID), String(event.id), shell);
20941
+ rawGitPost(input, shell.command, callKey(String(event.id), shell.command));
20942
+ const nudge = rawGitPre(input, shell.command);
20943
+ const result = event.result;
20944
+ if (nudge.kind !== "context" || !result)
20945
+ return;
20946
+ if (typeof result.content === "string")
20947
+ event.result = { ...result, content: `${result.content}
20948
+
20949
+ ${nudge.text}` };
20950
+ else if (Array.isArray(result.content))
20951
+ event.result = {
20952
+ ...result,
20953
+ content: [...result.content, { type: "text", text: nudge.text }]
20954
+ };
20955
+ } catch {}
20956
+ };
20957
+
20186
20958
  // packages/workit-opencode/src/v2/registry.ts
20187
20959
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
20188
- import path30 from "node:path";
20960
+ import path32 from "node:path";
20189
20961
 
20190
20962
  // packages/workit-core/src/core/skill-manifests.ts
20191
20963
  var WORKIT_METHOD_SKILLS = [
@@ -20234,33 +21006,33 @@ if (false) {}
20234
21006
 
20235
21007
  // packages/workit-opencode/src/shared/assets.ts
20236
21008
  import { existsSync as existsSync10, realpathSync as realpathSync9 } from "node:fs";
20237
- import path29 from "node:path";
21009
+ import path31 from "node:path";
20238
21010
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20239
21011
  var moduleDir = () => {
20240
21012
  try {
20241
- return realpathSync9(path29.dirname(fileURLToPath2(import.meta.url)));
21013
+ return realpathSync9(path31.dirname(fileURLToPath2(import.meta.url)));
20242
21014
  } catch {
20243
- return path29.dirname(fileURLToPath2(import.meta.url));
21015
+ return path31.dirname(fileURLToPath2(import.meta.url));
20244
21016
  }
20245
21017
  };
20246
21018
  var packageRoot2 = () => {
20247
21019
  const dir = moduleDir();
20248
21020
  for (const candidate of [
20249
- path29.resolve(dir, ".."),
20250
- path29.resolve(dir, "..", ".."),
20251
- path29.resolve(dir, "..", "..", "..")
21021
+ path31.resolve(dir, ".."),
21022
+ path31.resolve(dir, "..", ".."),
21023
+ path31.resolve(dir, "..", "..", "..")
20252
21024
  ]) {
20253
- if (existsSync10(path29.join(candidate, "package.json")))
21025
+ if (existsSync10(path31.join(candidate, "package.json")))
20254
21026
  return candidate;
20255
21027
  }
20256
- return path29.resolve(dir, "..");
21028
+ return path31.resolve(dir, "..");
20257
21029
  };
20258
21030
  var skillsRoot = () => {
20259
21031
  const root = packageRoot2();
20260
- const canonical = path29.resolve(root, "..", "workit-core", "skills");
20261
- if (existsSync10(path29.join(root, "src")) && existsSync10(canonical))
21032
+ const canonical = path31.resolve(root, "..", "workit-core", "skills");
21033
+ if (existsSync10(path31.join(root, "src")) && existsSync10(canonical))
20262
21034
  return canonical;
20263
- return path29.join(root, "assets", "skills");
21035
+ return path31.join(root, "assets", "skills");
20264
21036
  };
20265
21037
 
20266
21038
  // packages/workit-opencode/src/v2/registry.ts
@@ -20287,11 +21059,11 @@ var registerSkills = async (ctx) => {
20287
21059
  if (!existsSync11(skillsDir))
20288
21060
  return new Set;
20289
21061
  const listed = await ctx.skill.list().catch(() => ({ data: [] }));
20290
- const own = (skill) => typeof skill.path === "string" && path30.resolve(skill.path).startsWith(skillsDir + path30.sep);
21062
+ const own = (skill) => typeof skill.path === "string" && path32.resolve(skill.path).startsWith(skillsDir + path32.sep);
20291
21063
  const userSkills = new Set((listed?.data ?? []).filter((skill) => !own(skill)).map((skill) => skill.id));
20292
21064
  const packaged = new Map;
20293
21065
  for (const id of WORKIT_METHOD_SKILLS) {
20294
- const file = path30.join(skillsDir, id, "SKILL.md");
21066
+ const file = path32.join(skillsDir, id, "SKILL.md");
20295
21067
  if (!userSkills.has(id) && existsSync11(file))
20296
21068
  packaged.set(id, file);
20297
21069
  }
@@ -20339,22 +21111,22 @@ var registerCommands = async (ctx, registeredSkills) => {
20339
21111
 
20340
21112
  // packages/workit-opencode/src/stale-sources.ts
20341
21113
  import { existsSync as existsSync12, readdirSync as readdirSync5 } from "node:fs";
20342
- import path31 from "node:path";
21114
+ import path33 from "node:path";
20343
21115
  import { fileURLToPath as fileURLToPath3 } from "node:url";
20344
21116
  var typescriptFiles = (directory) => {
20345
21117
  if (!existsSync12(directory))
20346
21118
  return [];
20347
21119
  return readdirSync5(directory, { withFileTypes: true }).flatMap((entry) => {
20348
- const file = path31.join(directory, entry.name);
21120
+ const file = path33.join(directory, entry.name);
20349
21121
  return entry.isDirectory() ? typescriptFiles(file) : entry.isFile() && file.endsWith(".ts") ? [file] : [];
20350
21122
  });
20351
21123
  };
20352
21124
  var pluginSourceFiles = [
20353
21125
  ...new Set([
20354
21126
  fileURLToPath3(import.meta.url),
20355
- ...typescriptFiles(path31.join(packageRoot2(), "src")),
20356
- path31.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
20357
- ...typescriptFiles(path31.join(packageRoot2(), "..", "workit-core", "src", "core"))
21127
+ ...typescriptFiles(path33.join(packageRoot2(), "src")),
21128
+ path33.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
21129
+ ...typescriptFiles(path33.join(packageRoot2(), "..", "workit-core", "src", "core"))
20358
21130
  ])
20359
21131
  ].filter((file) => existsSync12(file)).toSorted();
20360
21132
 
@@ -20455,7 +21227,10 @@ var setup = async (ctx) => {
20455
21227
  });
20456
21228
  }
20457
21229
  });
21230
+ const shellWorkdirs = new Map;
20458
21231
  await ctx.tool.hook("execute.before", async (event) => {
21232
+ if (event.tool === "shell")
21233
+ return observeShellBefore(root, event, shellWorkdirs);
20459
21234
  if (event.tool !== "subagent")
20460
21235
  return;
20461
21236
  await lifecycle.executeBefore({
@@ -20466,6 +21241,8 @@ var setup = async (ctx) => {
20466
21241
  });
20467
21242
  });
20468
21243
  await ctx.tool.hook("execute.after", async (event) => {
21244
+ if (event.tool === "shell")
21245
+ return observeShellResult(root, event);
20469
21246
  if (event.tool !== "subagent")
20470
21247
  return;
20471
21248
  await lifecycle.executeAfter({
@@ -20477,7 +21254,7 @@ var setup = async (ctx) => {
20477
21254
  });
20478
21255
  });
20479
21256
  await ctx.permission.hook("evaluate", (event) => {
20480
- evaluateShellPermission(root, event);
21257
+ evaluateShellPermission(root, event, shellWorkdirs);
20481
21258
  });
20482
21259
  await ctx.session.hook("context", async (event) => {
20483
21260
  const session = await sessionFacts(ctx, String(event.sessionID));