@brainervirus/workit-cursor 8.10.0 → 8.11.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.
@@ -8792,6 +8792,7 @@ var branchNames = (workspaceRoot) => {
8792
8792
  return new Set((r.stdout ?? "").split(`
8793
8793
  `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
8794
8794
  };
8795
+ var productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main";
8795
8796
  var detectBranchPolicy = (workspaceRoot) => {
8796
8797
  const names = branchNames(workspaceRoot);
8797
8798
  const develop = names.has("develop");
@@ -17666,6 +17667,18 @@ var seedTurnContext = (input, descriptor, text = turnContextText(input, descript
17666
17667
  else
17667
17668
  clearTurnDigest(file);
17668
17669
  };
17670
+ // packages/workit-core/src/core/route-intent.ts
17671
+ import { spawnSync as spawnSync12 } from "node:child_process";
17672
+ import { existsSync as existsSync6 } from "node:fs";
17673
+ import path20 from "node:path";
17674
+
17675
+ // packages/workit-core/src/hooks/raw-git.ts
17676
+ import { spawnSync as spawnSync11 } from "node:child_process";
17677
+ import { createHash as createHash14 } from "node:crypto";
17678
+ import fs16 from "node:fs";
17679
+ import os4 from "node:os";
17680
+ import path19 from "node:path";
17681
+
17669
17682
  // packages/workit-core/src/core/branch.ts
17670
17683
  import { createHash as createHash12 } from "node:crypto";
17671
17684
  var withReleaseTracks = (workspaceRoot, policy) => {
@@ -17703,10 +17716,72 @@ function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
17703
17716
  provenance: { commitPolicy: selected.source }
17704
17717
  };
17705
17718
  }
17719
+ var KIND_SYNONYMS = {
17720
+ fix: "bugfix",
17721
+ fixes: "bugfix",
17722
+ bug: "bugfix",
17723
+ bugs: "bugfix",
17724
+ bugfixes: "bugfix",
17725
+ feat: "feature",
17726
+ features: "feature",
17727
+ chore: "feature",
17728
+ docs: "feature",
17729
+ doc: "feature",
17730
+ refactor: "feature",
17731
+ test: "feature",
17732
+ tests: "feature",
17733
+ ci: "feature",
17734
+ build: "feature",
17735
+ perf: "feature",
17736
+ style: "feature",
17737
+ deps: "feature",
17738
+ task: "feature",
17739
+ enhancement: "feature",
17740
+ hot: "hotfix",
17741
+ hotfixes: "hotfix",
17742
+ releases: "release",
17743
+ rel: "release"
17744
+ };
17745
+ var allowedKinds = (allowed) => [
17746
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
17747
+ ];
17748
+ var editDistance = (a, b) => {
17749
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
17750
+ for (let i = 1;i <= a.length; i++) {
17751
+ let previous = row[0];
17752
+ row[0] = i;
17753
+ for (let j = 1;j <= b.length; j++) {
17754
+ const saved = row[j];
17755
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
17756
+ previous = saved;
17757
+ }
17758
+ }
17759
+ return row[b.length];
17760
+ };
17761
+ var suggestBranchName = (name, kinds) => {
17762
+ if (!kinds.length)
17763
+ return null;
17764
+ const slash = name.indexOf("/");
17765
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
17766
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
17767
+ const synonym = KIND_SYNONYMS[prefix];
17768
+ const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
17769
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
17770
+ return `${kind}/${rest}`;
17771
+ };
17772
+ var kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", ");
17773
+ var aliasNote = (kinds) => {
17774
+ const notes = [
17775
+ ["fix", "bugfix"],
17776
+ ["chore", "feature"]
17777
+ ].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
17778
+ return notes.length ? ` (${notes.join(", ")})` : "";
17779
+ };
17706
17780
  var validateBranchNamePolicy = (name, policy, source) => {
17707
17781
  const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
17782
+ const kinds = allowedKinds(policy.allowed);
17708
17783
  if (policy.protected.has(name.toLowerCase())) {
17709
- const correction = `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
17784
+ const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
17710
17785
  return {
17711
17786
  ok: false,
17712
17787
  rule: "protected_ref",
@@ -17717,7 +17792,8 @@ var validateBranchNamePolicy = (name, policy, source) => {
17717
17792
  };
17718
17793
  }
17719
17794
  if (!policy.allowed.some((pattern) => pattern.test(name))) {
17720
- const correction = `choose a branch matching one of: ${allowed.join(", ")}`;
17795
+ const suggestion = suggestBranchName(name, kinds);
17796
+ const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
17721
17797
  return {
17722
17798
  ok: false,
17723
17799
  rule: "allowed_pattern",
@@ -17760,31 +17836,87 @@ var validateBranchNameFor = (workspaceRoot, name) => {
17760
17836
  }
17761
17837
  };
17762
17838
  var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
17763
-
17764
- // packages/workit-core/src/core/route-intent.ts
17765
- var shellBranchTarget = (command) => {
17766
- if (!command.trim() || /[\r\n;&|<>`'"$()\\*?{}~!#]/u.test(command) || command.includes("[") || command.includes("]"))
17767
- return null;
17768
- const args = command.trim().split(/\s+/u);
17769
- if (args[0] !== "git")
17839
+ var gitFlowOf = (root, integration) => {
17840
+ try {
17841
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
17842
+ return null;
17843
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
17844
+ const names = branchNames(root);
17845
+ const production = productionBranchOf(names);
17846
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
17847
+ return { develop, production, developExists: names.has(develop) };
17848
+ } catch {
17770
17849
  return null;
17771
- const option = args[2];
17772
- const target = args[1] === "switch" && ["-c", "--create", "-C", "--force-create"].includes(option ?? "") && (args.length === 4 || args.length === 5) ? args[3] : args[1] === "checkout" && ["-b", "-B"].includes(option ?? "") && (args.length === 4 || args.length === 5) ? args[3] : args[1] === "branch" && (args.length === 3 || args.length === 4) ? args[2] : null;
17773
- return target && !target.startsWith("-") ? target : null;
17850
+ }
17774
17851
  };
17775
- var shellBranchPolicyViolation = (root, command) => {
17776
- const target = shellBranchTarget(command);
17777
- return target === null ? null : validateBranchNameFor(root, target);
17852
+ var releaseBranchesOf = (cwd) => {
17853
+ try {
17854
+ const resolved = resolveBranchPolicyFor(cwd);
17855
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
17856
+ return {
17857
+ preset: resolved.preset,
17858
+ branches: flow ? [flow.production, flow.develop] : [],
17859
+ production: flow ? flow.production : resolved.defaultTargetBranch
17860
+ };
17861
+ } catch {
17862
+ return { preset: "unknown", branches: [], production: null };
17863
+ }
17778
17864
  };
17779
-
17780
- // packages/workit-core/src/hooks/policy.ts
17781
- var shellPolicy = (cwd, command) => {
17782
- const policy = shellBranchPolicyViolation(cwd, command);
17783
- return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
17865
+ var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
17866
+ var branchGuidance = (cwd) => {
17867
+ try {
17868
+ const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
17869
+ const kinds = allowedKinds(patterns);
17870
+ const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
17871
+ const anyName = patterns.some((pattern) => pattern.source === "^.*$");
17872
+ const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
17873
+ const allowed = anyName ? "Any non-protected branch name is allowed here." : `${kinds.length ? `Allowed branch kinds here: ${kindList(kinds)}${aliasNote(kinds)}` : "Allowed branch names here:"}${others.length ? `${kinds.length ? "; also" : ""} names matching ${others.join(", ")}` : ""}; anything else is denied.`;
17874
+ const resolved = vcsConfig("resolve", cwd);
17875
+ const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
17876
+ const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
17877
+ if (flow?.developExists)
17878
+ return `${allowed} git-flow: feature/, bugfix/ and release/ start from origin/${flow.develop}, hotfix/ from origin/${flow.production}; feature/ and bugfix/ land on ${flow.develop}, release/ and hotfix/ on ${flow.production}.`;
17879
+ const names = branchNames(cwd);
17880
+ const configured = String(resolved.baseBranch ?? integration ?? "") || null;
17881
+ const trunk = productionBranchOf(names);
17882
+ const base = configured && names.has(configured) ? `Branch from origin/${configured}.` : names.has(trunk) ? `Branch from origin/${trunk}${configured ? ` (the default target ${configured} does not exist here)` : ""}.` : "No base branch exists here yet; pass the base explicitly.";
17883
+ return `${allowed} ${base}`;
17884
+ } catch {
17885
+ return null;
17886
+ }
17784
17887
  };
17785
- // packages/workit-core/src/hooks/write-gate.ts
17786
- import { realpathSync as realpathSync5 } from "node:fs";
17888
+ var globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
17889
+
17890
+ // packages/workit-core/src/hooks/session-marker.ts
17891
+ import { createHash as createHash13 } from "node:crypto";
17892
+ import fs15 from "node:fs";
17787
17893
  import path18 from "node:path";
17894
+ var sessionMarkerFile = (input, session, store) => {
17895
+ let dir = store;
17896
+ if (dir === undefined) {
17897
+ const location = resolveStore(input.cwd);
17898
+ if (location instanceof Error || !location.shared)
17899
+ return null;
17900
+ dir = location.dir;
17901
+ }
17902
+ const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
17903
+ return path18.join(dir, "hooks", `skills-${name}.json`);
17904
+ };
17905
+ var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
17906
+ var readSessionMarker = (file) => {
17907
+ try {
17908
+ const value = JSON.parse(fs15.readFileSync(file, "utf8"));
17909
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
17910
+ } catch {
17911
+ return { nudged: [], loaded: [], raw: [] };
17912
+ }
17913
+ };
17914
+ var writeSessionMarker = (file, marker) => {
17915
+ try {
17916
+ fs15.mkdirSync(path18.dirname(file), { recursive: true });
17917
+ fs15.writeFileSync(file, JSON.stringify(marker));
17918
+ } catch {}
17919
+ };
17788
17920
 
17789
17921
  // packages/workit-core/src/hooks/shell-words.ts
17790
17922
  var closingQuote = (command, start, dialect) => {
@@ -17909,224 +18041,10 @@ var segmentsOf = (command, dialect = "posix") => {
17909
18041
  return segments;
17910
18042
  };
17911
18043
 
17912
- // packages/workit-core/src/hooks/write-gate.ts
17913
- var NONE2 = { kind: "none" };
17914
- var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
17915
- var canonical2 = (file) => {
17916
- let rest = "";
17917
- for (let dir = path18.resolve(file);; dir = path18.dirname(dir)) {
17918
- try {
17919
- return path18.join(realpathSync5(dir), rest);
17920
- } catch {
17921
- if (path18.dirname(dir) === dir)
17922
- return path18.resolve(file);
17923
- rest = rest ? path18.join(path18.basename(dir), rest) : path18.basename(dir);
17924
- }
17925
- }
17926
- };
17927
- var exempt = (cwd, root, file, plans) => {
17928
- const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
17929
- const relative = path18.relative(fold(canonical2(root)), fold(canonical2(path18.resolve(cwd, file))));
17930
- if (relative.startsWith("..") || path18.isAbsolute(relative))
17931
- return true;
17932
- const posix = relative.split(path18.sep).join("/");
17933
- const segments = posix.split("/");
17934
- return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
17935
- };
17936
- var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
17937
- var operands = (words) => words.filter((item) => !item.startsWith("-"));
17938
- var editsOf = (input) => {
17939
- let words = input;
17940
- while (words.length && (PREFIXES2.has(words[0]) || /^\w+=/.test(words[0])))
17941
- words = words.slice(1);
17942
- const [verb, ...args] = words;
17943
- const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
17944
- if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
17945
- return [];
17946
- switch (name) {
17947
- case "touch":
17948
- case "mkdir":
17949
- case "rm":
17950
- case "rmdir":
17951
- case "tee":
17952
- case "mv":
17953
- case "truncate":
17954
- return operands(args);
17955
- case "cp":
17956
- case "ln":
17957
- case "install": {
17958
- const target = args.indexOf("-t");
17959
- if (target >= 0)
17960
- return args[target + 1] ? [args[target + 1]] : null;
17961
- const last = operands(args).at(-1);
17962
- return last ? [last] : null;
17963
- }
17964
- case "dd":
17965
- return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
17966
- case "sed":
17967
- case "perl": {
17968
- if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
17969
- return [];
17970
- const scripted = args.some((item) => item === "-e" || item === "-f");
17971
- const files = [];
17972
- for (let index = 0;index < args.length; index++) {
17973
- if (args[index] === "-e" || args[index] === "-f")
17974
- index++;
17975
- else if (!args[index].startsWith("-"))
17976
- files.push(args[index]);
17977
- }
17978
- return scripted ? files : files.slice(1);
17979
- }
17980
- case "patch":
17981
- return null;
17982
- case "set-content":
17983
- case "add-content":
17984
- case "out-file":
17985
- case "new-item":
17986
- case "ni":
17987
- case "remove-item":
17988
- case "del":
17989
- case "copy-item":
17990
- case "move-item": {
17991
- const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
17992
- if (flag >= 0)
17993
- return args[flag + 1] ? [args[flag + 1]] : null;
17994
- const files = operands(args);
17995
- if (name === "move-item")
17996
- return files.length ? files : null;
17997
- const file = name === "copy-item" ? files.at(-1) : files[0];
17998
- return file ? [file] : null;
17999
- }
18000
- case "git": {
18001
- const sub = operands(args)[0];
18002
- if (sub === "apply")
18003
- return null;
18004
- if (sub === "restore" || sub === "mv" || sub === "rm")
18005
- return operands(args).slice(1);
18006
- if (sub === "checkout" && args.includes("--"))
18007
- return args.slice(args.indexOf("--") + 1);
18008
- return [];
18009
- }
18010
- default:
18011
- return [];
18012
- }
18013
- };
18014
- var shellWrites = (command) => {
18015
- const targets = [];
18016
- let unknown = false;
18017
- for (const segment of segmentsOf(command)) {
18018
- targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
18019
- const edits = editsOf(segment.words);
18020
- if (edits === null)
18021
- unknown = true;
18022
- else
18023
- targets.push(...edits);
18024
- }
18025
- if (!unknown && targets.length === 0)
18026
- return { writes: false, targets: null };
18027
- return { writes: true, targets: unknown ? null : targets };
18028
- };
18029
- var cache2 = new Map;
18030
- var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path18.resolve(root, file)) ?? "-").join("\x00");
18031
- var gateFor = (cwd) => {
18032
- const store = new TaskStore(cwd);
18033
- const listed = store.listTaskIndex();
18034
- if (!listed.ok)
18035
- return null;
18036
- const key = store.currentKey();
18037
- if (!key.ok)
18038
- return null;
18039
- const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
18040
- if (!entry)
18041
- return { blockers: [], root: store.root, plans: new Set };
18042
- const ledger = ledgerPath(store.root);
18043
- const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18044
- const hit = cache2.get(store.root);
18045
- if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18046
- return hit;
18047
- const task = store.readTask(entry.id);
18048
- if (!task.ok)
18049
- return null;
18050
- const workspace = store.readWorkspace();
18051
- const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18052
- const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18053
- const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18054
- return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18055
- });
18056
- const gate = {
18057
- blockers,
18058
- root: store.root,
18059
- plans,
18060
- key: cacheKey,
18061
- planKey: planKeyOf(store.root, plans)
18062
- };
18063
- cache2.set(store.root, gate);
18064
- if (cache2.size > 32)
18065
- cache2.delete(cache2.keys().next().value);
18066
- return gate;
18067
- };
18068
- function writeGate(cwd, paths) {
18069
- try {
18070
- const gate = gateFor(cwd);
18071
- if (!gate || gate.blockers.length === 0)
18072
- return NONE2;
18073
- if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
18074
- return NONE2;
18075
- const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
18076
- return {
18077
- kind: "deny",
18078
- reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
18079
- unblock: gate.blockers[0].reason
18080
- };
18081
- } catch {
18082
- return NONE2;
18083
- }
18084
- }
18085
- // packages/workit-core/src/hooks/stop.ts
18086
- import { spawnSync as spawnSync12 } from "node:child_process";
18087
-
18088
18044
  // packages/workit-core/src/hooks/raw-git.ts
18089
- import { spawnSync as spawnSync11 } from "node:child_process";
18090
- import { createHash as createHash14 } from "node:crypto";
18091
- import fs16 from "node:fs";
18092
- import os4 from "node:os";
18093
- import path20 from "node:path";
18094
-
18095
- // packages/workit-core/src/hooks/session-marker.ts
18096
- import { createHash as createHash13 } from "node:crypto";
18097
- import fs15 from "node:fs";
18098
- import path19 from "node:path";
18099
- var sessionMarkerFile = (input, session, store) => {
18100
- let dir = store;
18101
- if (dir === undefined) {
18102
- const location = resolveStore(input.cwd);
18103
- if (location instanceof Error || !location.shared)
18104
- return null;
18105
- dir = location.dir;
18106
- }
18107
- const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
18108
- return path19.join(dir, "hooks", `skills-${name}.json`);
18109
- };
18110
- var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
18111
- var readSessionMarker = (file) => {
18112
- try {
18113
- const value = JSON.parse(fs15.readFileSync(file, "utf8"));
18114
- return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
18115
- } catch {
18116
- return { nudged: [], loaded: [], raw: [] };
18117
- }
18118
- };
18119
- var writeSessionMarker = (file, marker) => {
18120
- try {
18121
- fs15.mkdirSync(path19.dirname(file), { recursive: true });
18122
- fs15.writeFileSync(file, JSON.stringify(marker));
18123
- } catch {}
18124
- };
18125
-
18126
- // packages/workit-core/src/hooks/raw-git.ts
18127
- var NONE3 = { kind: "none" };
18045
+ var NONE2 = { kind: "none" };
18128
18046
  var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18129
- var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18047
+ var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18130
18048
  var SCRIPT_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/;
18131
18049
  var CD = new Set(["cd", "chdir", "set-location", "sl"]);
18132
18050
  var PUSHD = new Set(["pushd", "push-location"]);
@@ -18140,12 +18058,12 @@ var GIT_VALUE_OPTIONS = new Set([
18140
18058
  "--config-env",
18141
18059
  "--super-prefix"
18142
18060
  ]);
18143
- var isAbsolute = (value) => path20.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
18061
+ var isAbsolute = (value) => path19.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
18144
18062
  var joinDir = (base, next) => {
18145
18063
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
18146
18064
  return;
18147
- const expanded = next === "~" || next.startsWith("~/") ? path20.join(os4.homedir(), next.slice(1)) : next;
18148
- return base === null || isAbsolute(expanded) ? expanded : path20.join(base, expanded);
18065
+ const expanded = next === "~" || next.startsWith("~/") ? path19.join(os4.homedir(), next.slice(1)) : next;
18066
+ return base === null || isAbsolute(expanded) ? expanded : path19.join(base, expanded);
18149
18067
  };
18150
18068
  var cdTarget = (words) => {
18151
18069
  const args = words.slice(1);
@@ -18162,7 +18080,7 @@ var stripPrefixes = (input) => {
18162
18080
  return words;
18163
18081
  if (/^\w+=/.test(first))
18164
18082
  words = words.slice(1);
18165
- else if (PREFIXES3.has(first)) {
18083
+ else if (PREFIXES2.has(first)) {
18166
18084
  words = words.slice(1);
18167
18085
  while (words[0]?.startsWith("-"))
18168
18086
  words = words.slice(words[0] === "-u" ? 2 : 1);
@@ -18277,6 +18195,10 @@ var pushAction = (args) => {
18277
18195
  let force = false;
18278
18196
  let lease = false;
18279
18197
  let wholesale = false;
18198
+ let allTags = false;
18199
+ let deletes = false;
18200
+ let allBranches = false;
18201
+ let mirror = false;
18280
18202
  let remote = null;
18281
18203
  const positionals = [];
18282
18204
  for (let index = 1;index < args.length; index++) {
@@ -18297,14 +18219,22 @@ var pushAction = (args) => {
18297
18219
  force = true;
18298
18220
  else if (arg.startsWith("--force-with-lease") || arg === "--force-if-includes")
18299
18221
  lease = true;
18300
- else if (["--all", "--mirror", "--branches", "--tags"].includes(arg))
18301
- wholesale = true;
18222
+ else if (arg === "--delete" || arg === "-d")
18223
+ deletes = true;
18224
+ else if (arg === "--tags")
18225
+ allTags = wholesale = true;
18226
+ else if (arg === "--all" || arg === "--branches")
18227
+ allBranches = wholesale = true;
18228
+ else if (arg === "--mirror")
18229
+ mirror = wholesale = true;
18302
18230
  else if (!arg.startsWith("-"))
18303
18231
  positionals.push(arg);
18304
18232
  }
18305
18233
  const specs = remote === null ? positionals.slice(1) : positionals;
18306
18234
  remote ??= positionals[0] ?? null;
18307
18235
  const targets = [];
18236
+ const tags = [];
18237
+ const deleted = [];
18308
18238
  for (const raw of specs) {
18309
18239
  if (raw.startsWith("+"))
18310
18240
  force = true;
@@ -18313,16 +18243,29 @@ var pushAction = (args) => {
18313
18243
  const destination = colon >= 0 ? spec.slice(colon + 1) : spec;
18314
18244
  if (!destination)
18315
18245
  continue;
18316
- if (destination === "HEAD")
18317
- targets.push(null);
18318
- else if (destination.startsWith("refs/heads/"))
18319
- targets.push(destination.slice(11));
18320
- else if (!destination.startsWith("refs/"))
18321
- targets.push(destination);
18246
+ const name = destination === "HEAD" ? null : destination.startsWith("refs/heads/") ? destination.slice(11) : destination.startsWith("refs/") ? undefined : destination;
18247
+ if (destination.startsWith("refs/tags/"))
18248
+ tags.push(destination.slice(10));
18249
+ if (name === undefined)
18250
+ continue;
18251
+ targets.push(name);
18252
+ if (colon === 0 && name !== null)
18253
+ deleted.push(name);
18322
18254
  }
18323
18255
  if (specs.length === 0 && !wholesale)
18324
18256
  targets.push(null);
18325
- return { kind: "push", targets, blindForce: force && !lease, remote };
18257
+ return {
18258
+ kind: "push",
18259
+ targets,
18260
+ blindForce: force && !lease,
18261
+ remote,
18262
+ ...tags.length ? { tags } : {},
18263
+ ...allTags ? { allTags: true } : {},
18264
+ ...deletes ? { deletes: true } : {},
18265
+ ...deleted.length ? { deleted } : {},
18266
+ ...allBranches ? { allBranches: true } : {},
18267
+ ...mirror ? { mirror: true } : {}
18268
+ };
18326
18269
  };
18327
18270
  var VALUE_OPTIONS = {
18328
18271
  git: new Set([
@@ -18429,28 +18372,80 @@ var headSha2 = (repo) => {
18429
18372
  if (!git || git.reftable)
18430
18373
  return null;
18431
18374
  try {
18432
- const head = fs16.readFileSync(path20.join(git.gitDir, "HEAD"), "utf8").trim();
18375
+ const head = fs16.readFileSync(path19.join(git.gitDir, "HEAD"), "utf8").trim();
18433
18376
  if (/^[0-9a-f]{40,64}$/.test(head))
18434
18377
  return head;
18435
18378
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
18436
18379
  if (!ref)
18437
18380
  return null;
18438
- const common = path20.dirname(repo.location.dir);
18381
+ const common = path19.dirname(repo.location.dir);
18439
18382
  try {
18440
- return fs16.readFileSync(path20.join(common, ref), "utf8").trim() || null;
18383
+ return fs16.readFileSync(path19.join(common, ref), "utf8").trim() || null;
18441
18384
  } catch {}
18442
- const packed = fs16.readFileSync(path20.join(common, "packed-refs"), "utf8");
18385
+ const packed = fs16.readFileSync(path19.join(common, "packed-refs"), "utf8");
18443
18386
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
18444
18387
  } catch {
18445
18388
  return null;
18446
18389
  }
18447
18390
  };
18448
18391
  var quote = (text) => `\`${text}\``;
18449
- var refusal = (action, branch) => {
18392
+ var refusal = (action, branch, dir) => {
18450
18393
  if (action.kind === "merge")
18451
18394
  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")}).`;
18395
+ const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
18396
+ if (dir && branch && plainPush && releaseBranch(dir, branch))
18397
+ return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release v<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
18452
18398
  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")}.`;
18453
18399
  };
18400
+ var refFacts = (repo, remote) => {
18401
+ const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
18402
+ const listed = spawnSync11("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags", `refs/remotes/${remote}`], { cwd: repo.dir, encoding: "utf8", timeout: 5000 });
18403
+ const remotePrefix = `refs/remotes/${remote}/`;
18404
+ for (const ref of (listed.stdout ?? "").split(`
18405
+ `)) {
18406
+ if (ref.startsWith("refs/heads/"))
18407
+ facts.heads.add(ref.slice(11));
18408
+ else if (ref.startsWith("refs/tags/"))
18409
+ facts.tags.add(ref.slice(10));
18410
+ else if (ref.startsWith(remotePrefix))
18411
+ facts.remoteHeads.add(ref.slice(remotePrefix.length));
18412
+ }
18413
+ return facts;
18414
+ };
18415
+ var isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name);
18416
+ var tagRefusal = (action, facts) => {
18417
+ const tags = [
18418
+ ...action.tags ?? [],
18419
+ ...action.targets.filter((name) => name !== null && isTag(facts, name))
18420
+ ];
18421
+ if (!tags.length && !action.allTags)
18422
+ return null;
18423
+ const named = tags.length ? tags.join(" --release ") : "v<version>";
18424
+ return `workit: raw ${quote(`git push${action.allTags ? " --tags" : ""}`)} of ${tags.length ? `tag ${tags.join(", ")}` : "tags"} bypasses the release grant. Push a release tag with ${quote(`workit git push --release ${named}`)} (v<version> tags whose commit the production branch already has; a tag never moves), under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}).`;
18425
+ };
18426
+ var remoteHas = (repo, remote, branch) => {
18427
+ const listed = spawnSync11("git", ["ls-remote", "--heads", remote, `refs/heads/${branch}`], {
18428
+ cwd: repo.dir,
18429
+ encoding: "utf8",
18430
+ timeout: 3000,
18431
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
18432
+ });
18433
+ return listed.status !== 0 || (listed.stdout ?? "").trim() !== "";
18434
+ };
18435
+ var offPolicyPush = (action, repo, facts, remote) => {
18436
+ const names = action.allBranches ? [...facts.heads] : action.targets.map((target) => target ?? repo.branch);
18437
+ for (const branch of names) {
18438
+ if (!branch || action.deleted?.includes(branch) || isTag(facts, branch))
18439
+ continue;
18440
+ if (facts.remoteHeads.has(branch))
18441
+ continue;
18442
+ const check = validateBranchNameFor(repo.dir, branch);
18443
+ if (check.ok || check.rule !== "allowed_pattern" || remoteHas(repo, remote, branch))
18444
+ continue;
18445
+ return `workit: raw ${quote("git push")} onto ${branch}: ${check.error}`;
18446
+ }
18447
+ return null;
18448
+ };
18454
18449
  var nudgeFor = (action, host, session) => {
18455
18450
  switch (action.kind) {
18456
18451
  case "commit": {
@@ -18476,7 +18471,7 @@ var actionsIn = (cwd, command, dialect) => {
18476
18471
  const action = rawAction(invocation);
18477
18472
  if (!action)
18478
18473
  continue;
18479
- const dir = path20.resolve(cwd, invocation.dir ?? ".");
18474
+ const dir = path19.resolve(cwd, invocation.dir ?? ".");
18480
18475
  if (!repos.has(dir)) {
18481
18476
  const repo = repoAt(dir);
18482
18477
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -18513,7 +18508,7 @@ var unseenNudges = (input, nudges, store) => {
18513
18508
  function rawGitPre(input, command, options = {}) {
18514
18509
  try {
18515
18510
  if (!mentionsRawTool(command))
18516
- return NONE3;
18511
+ return NONE2;
18517
18512
  const dialect = dialectOf(input);
18518
18513
  const nudges = new Map;
18519
18514
  let store = null;
@@ -18521,31 +18516,42 @@ function rawGitPre(input, command, options = {}) {
18521
18516
  store ??= repo.location.dir;
18522
18517
  if (action.kind === "merge")
18523
18518
  return deny(refusal(action, null));
18524
- if (action.kind === "push")
18519
+ if (action.kind === "push" && toPushRemote(repo, action.remote)) {
18520
+ if (action.mirror)
18521
+ return deny(`workit: raw ${quote("git push --mirror")} rewrites and deletes every remote ref, protected branches and tags included. Push one branch with ${quote("workit git push")}.`);
18525
18522
  for (const target of action.targets) {
18526
18523
  const branch = target ?? repo.branch;
18527
- if (branch && guardedBranch(repo.dir, branch) && toPushRemote(repo, action.remote))
18528
- return deny(refusal(action, branch));
18524
+ if (branch && guardedBranch(repo.dir, branch))
18525
+ return deny(refusal(action, branch, repo.dir));
18529
18526
  }
18527
+ const remote = action.remote ?? pushRemoteName(repo.dir, repo.branch) ?? "origin";
18528
+ const facts = refFacts(repo, remote);
18529
+ const tagDeny = tagRefusal(action, facts);
18530
+ if (tagDeny)
18531
+ return deny(tagDeny);
18532
+ const nameDeny = action.deletes ? null : offPolicyPush(action, repo, facts, remote);
18533
+ if (nameDeny)
18534
+ return deny(nameDeny);
18535
+ }
18530
18536
  const nudge = nudgeFor(action, input.host, input.session.id);
18531
18537
  if (nudge && !nudges.has(nudgeKind(action)))
18532
18538
  nudges.set(nudgeKind(action), nudge);
18533
18539
  }
18534
18540
  if (options.nudge === false || !store || nudges.size === 0)
18535
- return NONE3;
18541
+ return NONE2;
18536
18542
  if (workitInvocations(command, dialect).length > 0)
18537
- return NONE3;
18543
+ return NONE2;
18538
18544
  const shown = unseenNudges(input, nudges, store);
18539
18545
  return shown.length ? { kind: "context", text: shown.join(`
18540
- `) } : NONE3;
18546
+ `) } : NONE2;
18541
18547
  } catch {
18542
- return NONE3;
18548
+ return NONE2;
18543
18549
  }
18544
18550
  }
18545
18551
  var deny = (reason) => ({ kind: "deny", reason, unblock: null });
18546
18552
  var NEXT_COMMAND = "next";
18547
18553
  var MARKER_TTL_SECONDS = 3600;
18548
- var markerFile = (repo, session, key) => path20.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
18554
+ var markerFile = (repo, session, key) => path19.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
18549
18555
  var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash14("sha256").update(command).digest("hex")}`;
18550
18556
  function noteRawCommit(input, command, key) {
18551
18557
  try {
@@ -18556,7 +18562,7 @@ function noteRawCommit(input, command, key) {
18556
18562
  if (action.kind !== "commit")
18557
18563
  continue;
18558
18564
  const file = markerFile(repo, input.session.id, key);
18559
- fs16.mkdirSync(path20.dirname(file), { recursive: true });
18565
+ fs16.mkdirSync(path19.dirname(file), { recursive: true });
18560
18566
  fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18561
18567
  `);
18562
18568
  }
@@ -18638,7 +18644,292 @@ function settlePendingCommit(input) {
18638
18644
  } catch {}
18639
18645
  }
18640
18646
 
18647
+ // packages/workit-core/src/core/route-intent.ts
18648
+ var UNKNOWABLE = /[$`*?[\]{}~]/u;
18649
+ var SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
18650
+ var CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
18651
+ var WORKTREE_CREATE = new Set(["-b", "-B"]);
18652
+ var BRANCH_NO_CREATE = new Set(["-d", "-D", "--delete", "-l", "--list", "-a", "--all", "-r", "--remotes", "--show-current"].concat(["-v", "-vv", "--verbose", "--contains", "--no-contains", "--merged", "--no-merged"]).concat(["--points-at", "--sort", "--format", "--column", "--no-column", "-u"]).concat(["--set-upstream-to", "--unset-upstream", "--edit-description", "-i", "--ignore-case"]));
18653
+ var BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
18654
+ var flagValue = (args, flags) => {
18655
+ for (let index = 1;index < args.length; index++) {
18656
+ const arg = args[index];
18657
+ if (arg === "--")
18658
+ return null;
18659
+ if (flags.has(arg))
18660
+ return args[index + 1] ?? null;
18661
+ const eq = arg.indexOf("=");
18662
+ if (arg.startsWith("--") && eq > 0 && flags.has(arg.slice(0, eq)))
18663
+ return arg.slice(eq + 1);
18664
+ if (!arg.startsWith("--") && arg.length > 2 && flags.has(arg.slice(0, 2)))
18665
+ return arg.slice(2);
18666
+ }
18667
+ return null;
18668
+ };
18669
+ var WORKTREE_VALUES = new Set(["--reason"]);
18670
+ var worktreeTarget = (args) => {
18671
+ const named = flagValue(args, WORKTREE_CREATE);
18672
+ if (named !== null)
18673
+ return named;
18674
+ const positionals = [];
18675
+ for (let index = 1;index < args.length; index++) {
18676
+ const arg = args[index];
18677
+ if (arg === "--detach" || arg === "-d")
18678
+ return null;
18679
+ if (WORKTREE_VALUES.has(arg))
18680
+ index++;
18681
+ else if (!arg.startsWith("-"))
18682
+ positionals.push(arg);
18683
+ }
18684
+ if (positionals.length !== 1)
18685
+ return null;
18686
+ const base = positionals[0].split(/[\\/]/u).findLast(Boolean) ?? "";
18687
+ return base && base !== "." && base !== ".." ? base : null;
18688
+ };
18689
+ var branchCommandTarget = (args) => {
18690
+ let rename = false;
18691
+ const positionals = [];
18692
+ for (let index = 1;index < args.length; index++) {
18693
+ const arg = args[index];
18694
+ if (arg === "--") {
18695
+ positionals.push(...args.slice(index + 1));
18696
+ break;
18697
+ }
18698
+ const flag = arg.split("=", 1)[0];
18699
+ if (BRANCH_NO_CREATE.has(flag))
18700
+ return null;
18701
+ if (BRANCH_RENAME.has(flag))
18702
+ rename = true;
18703
+ else if (!arg.startsWith("-"))
18704
+ positionals.push(arg);
18705
+ }
18706
+ if (positionals.length < 1 || positionals.length > 2)
18707
+ return null;
18708
+ return rename ? positionals.at(-1) ?? null : positionals[0];
18709
+ };
18710
+ var gitBranchCreation = (args) => {
18711
+ const [sub, second] = args;
18712
+ const target = sub === "switch" ? flagValue(args, SWITCH_CREATE) : sub === "checkout" ? flagValue(args, CHECKOUT_CREATE) : sub === "branch" ? branchCommandTarget(args) : sub === "worktree" && second === "add" ? worktreeTarget(args.slice(1)) : null;
18713
+ return target && !target.startsWith("-") && !UNKNOWABLE.test(target) ? target : null;
18714
+ };
18715
+ var shellBranchTargets = (command, dialect = "posix") => {
18716
+ if (!/\bgit\b/u.test(command))
18717
+ return [];
18718
+ const found = [];
18719
+ for (const invocation of rawInvocations(command, dialect)) {
18720
+ if (invocation.tool !== "git")
18721
+ continue;
18722
+ const branch = gitBranchCreation(invocation.args);
18723
+ if (branch)
18724
+ found.push({ branch, dir: invocation.dir });
18725
+ }
18726
+ return found;
18727
+ };
18728
+ var localBranchExists = (dir, branch) => spawnSync12("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
18729
+ cwd: dir,
18730
+ stdio: "ignore",
18731
+ timeout: 5000
18732
+ }).status === 0;
18733
+ var shellBranchPolicyViolation = (root, command, dialect = "posix") => {
18734
+ let judged = false;
18735
+ for (const target of shellBranchTargets(command, dialect)) {
18736
+ if (target.dir === undefined)
18737
+ continue;
18738
+ const dir = target.dir === null ? root : path20.resolve(root, target.dir);
18739
+ if (target.dir !== null && (!existsSync6(dir) || !inWorkitWorkspace(dir)))
18740
+ continue;
18741
+ if (localBranchExists(dir, target.branch))
18742
+ continue;
18743
+ judged = true;
18744
+ const check = validateBranchNameFor(dir, target.branch);
18745
+ if (!check.ok)
18746
+ return check;
18747
+ }
18748
+ return judged ? { ok: true } : null;
18749
+ };
18750
+
18751
+ // packages/workit-core/src/hooks/policy.ts
18752
+ var shellPolicy = (cwd, command, dialect = "posix") => {
18753
+ const policy = shellBranchPolicyViolation(cwd, command, dialect);
18754
+ return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
18755
+ };
18756
+ // packages/workit-core/src/hooks/write-gate.ts
18757
+ import { realpathSync as realpathSync5 } from "node:fs";
18758
+ import path21 from "node:path";
18759
+ var NONE3 = { kind: "none" };
18760
+ var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18761
+ var canonical2 = (file) => {
18762
+ let rest = "";
18763
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18764
+ try {
18765
+ return path21.join(realpathSync5(dir), rest);
18766
+ } catch {
18767
+ if (path21.dirname(dir) === dir)
18768
+ return path21.resolve(file);
18769
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18770
+ }
18771
+ }
18772
+ };
18773
+ var exempt = (cwd, root, file, plans) => {
18774
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18775
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18776
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
18777
+ return true;
18778
+ const posix = relative.split(path21.sep).join("/");
18779
+ const segments = posix.split("/");
18780
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18781
+ };
18782
+ var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18783
+ var operands = (words) => words.filter((item) => !item.startsWith("-"));
18784
+ var editsOf = (input) => {
18785
+ let words = input;
18786
+ while (words.length && (PREFIXES3.has(words[0]) || /^\w+=/.test(words[0])))
18787
+ words = words.slice(1);
18788
+ const [verb, ...args] = words;
18789
+ const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
18790
+ if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
18791
+ return [];
18792
+ switch (name) {
18793
+ case "touch":
18794
+ case "mkdir":
18795
+ case "rm":
18796
+ case "rmdir":
18797
+ case "tee":
18798
+ case "mv":
18799
+ case "truncate":
18800
+ return operands(args);
18801
+ case "cp":
18802
+ case "ln":
18803
+ case "install": {
18804
+ const target = args.indexOf("-t");
18805
+ if (target >= 0)
18806
+ return args[target + 1] ? [args[target + 1]] : null;
18807
+ const last = operands(args).at(-1);
18808
+ return last ? [last] : null;
18809
+ }
18810
+ case "dd":
18811
+ return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
18812
+ case "sed":
18813
+ case "perl": {
18814
+ if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
18815
+ return [];
18816
+ const scripted = args.some((item) => item === "-e" || item === "-f");
18817
+ const files = [];
18818
+ for (let index = 0;index < args.length; index++) {
18819
+ if (args[index] === "-e" || args[index] === "-f")
18820
+ index++;
18821
+ else if (!args[index].startsWith("-"))
18822
+ files.push(args[index]);
18823
+ }
18824
+ return scripted ? files : files.slice(1);
18825
+ }
18826
+ case "patch":
18827
+ return null;
18828
+ case "set-content":
18829
+ case "add-content":
18830
+ case "out-file":
18831
+ case "new-item":
18832
+ case "ni":
18833
+ case "remove-item":
18834
+ case "del":
18835
+ case "copy-item":
18836
+ case "move-item": {
18837
+ const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
18838
+ if (flag >= 0)
18839
+ return args[flag + 1] ? [args[flag + 1]] : null;
18840
+ const files = operands(args);
18841
+ if (name === "move-item")
18842
+ return files.length ? files : null;
18843
+ const file = name === "copy-item" ? files.at(-1) : files[0];
18844
+ return file ? [file] : null;
18845
+ }
18846
+ case "git": {
18847
+ const sub = operands(args)[0];
18848
+ if (sub === "apply")
18849
+ return null;
18850
+ if (sub === "restore" || sub === "mv" || sub === "rm")
18851
+ return operands(args).slice(1);
18852
+ if (sub === "checkout" && args.includes("--"))
18853
+ return args.slice(args.indexOf("--") + 1);
18854
+ return [];
18855
+ }
18856
+ default:
18857
+ return [];
18858
+ }
18859
+ };
18860
+ var shellWrites = (command) => {
18861
+ const targets = [];
18862
+ let unknown = false;
18863
+ for (const segment of segmentsOf(command)) {
18864
+ targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
18865
+ const edits = editsOf(segment.words);
18866
+ if (edits === null)
18867
+ unknown = true;
18868
+ else
18869
+ targets.push(...edits);
18870
+ }
18871
+ if (!unknown && targets.length === 0)
18872
+ return { writes: false, targets: null };
18873
+ return { writes: true, targets: unknown ? null : targets };
18874
+ };
18875
+ var cache2 = new Map;
18876
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path21.resolve(root, file)) ?? "-").join("\x00");
18877
+ var gateFor = (cwd) => {
18878
+ const store = new TaskStore(cwd);
18879
+ const listed = store.listTaskIndex();
18880
+ if (!listed.ok)
18881
+ return null;
18882
+ const key = store.currentKey();
18883
+ if (!key.ok)
18884
+ return null;
18885
+ const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
18886
+ if (!entry)
18887
+ return { blockers: [], root: store.root, plans: new Set };
18888
+ const ledger = ledgerPath(store.root);
18889
+ const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18890
+ const hit = cache2.get(store.root);
18891
+ if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18892
+ return hit;
18893
+ const task = store.readTask(entry.id);
18894
+ if (!task.ok)
18895
+ return null;
18896
+ const workspace = store.readWorkspace();
18897
+ const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18898
+ const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18899
+ const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18900
+ return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18901
+ });
18902
+ const gate = {
18903
+ blockers,
18904
+ root: store.root,
18905
+ plans,
18906
+ key: cacheKey,
18907
+ planKey: planKeyOf(store.root, plans)
18908
+ };
18909
+ cache2.set(store.root, gate);
18910
+ if (cache2.size > 32)
18911
+ cache2.delete(cache2.keys().next().value);
18912
+ return gate;
18913
+ };
18914
+ function writeGate(cwd, paths) {
18915
+ try {
18916
+ const gate = gateFor(cwd);
18917
+ if (!gate || gate.blockers.length === 0)
18918
+ return NONE3;
18919
+ if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
18920
+ return NONE3;
18921
+ const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
18922
+ return {
18923
+ kind: "deny",
18924
+ reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
18925
+ unblock: gate.blockers[0].reason
18926
+ };
18927
+ } catch {
18928
+ return NONE3;
18929
+ }
18930
+ }
18641
18931
  // packages/workit-core/src/hooks/stop.ts
18932
+ import { spawnSync as spawnSync13 } from "node:child_process";
18642
18933
  var NONE4 = { kind: "none" };
18643
18934
  var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
18644
18935
  var OFFER = /\b(?:let me know|want me to|should I|shall I|would you like|do you want|if you(?:'d| would) like|may I|can I go ahead)\b/i;
@@ -18659,7 +18950,7 @@ var asksUser = (message) => {
18659
18950
  };
18660
18951
  var short = (sha) => (sha ?? "?").slice(0, 12);
18661
18952
  var gitList = (cwd, args) => {
18662
- const run = spawnSync12("git", args, {
18953
+ const run = spawnSync13("git", args, {
18663
18954
  cwd,
18664
18955
  encoding: "utf8",
18665
18956
  timeout: 3000,
@@ -19022,13 +19313,13 @@ ${line}` };
19022
19313
  return decision;
19023
19314
  };
19024
19315
  // packages/workit-core/src/hooks/hosts/fields.ts
19025
- import { existsSync as existsSync6, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
19026
- import path21 from "node:path";
19316
+ import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
19317
+ import path22 from "node:path";
19027
19318
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
19028
19319
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
19029
19320
  var optionalText = (value) => typeof value === "string" ? value : null;
19030
19321
  var existingDirectory = (value) => {
19031
- if (!nonEmpty2(value) || !path21.isAbsolute(value) || !existsSync6(value))
19322
+ if (!nonEmpty2(value) || !path22.isAbsolute(value) || !existsSync7(value))
19032
19323
  return null;
19033
19324
  try {
19034
19325
  return statSync2(value).isDirectory() ? realpathSync6(value) : null;
@@ -19117,13 +19408,14 @@ var subagentStartText = (input, descriptor, event) => {
19117
19408
  const role = workitRole(descriptor, event.agentType);
19118
19409
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
19119
19410
  const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
19411
+ const guide = role === "implementer" ? branchGuidance(input.cwd) : null;
19120
19412
  if (role === "implementer" && isolated)
19121
- return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${ISOLATED_GIT} Use a policy-compliant branch (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
19413
+ return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${ISOLATED_GIT} Plain git is expected here, and the branch name is still policy-checked: a raw \`git switch -c\`, \`git checkout -b\`, \`git branch\` or \`git worktree add -b\` onto a protected or non-compliant name is denied, so use a policy-compliant branch.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
19122
19414
  if (isolated)
19123
19415
  return `${ISOLATED_GIT}
19124
19416
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
19125
19417
  if (role === "implementer")
19126
- return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree (the lead makes one with \`workit fanout worktree create <slice>\`), never in the lead's checkout, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
19418
+ return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree (the lead makes one with \`workit fanout worktree create <slice>\`), never in the lead's checkout, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (or \`--kind <kind> --slug <slug>\`, which picks the base); branch policy hooks still deny protected or non-compliant branches, raw git included.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
19127
19419
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
19128
19420
  if (role !== "judge" || !event.agentId)
19129
19421
  return readOnly;
@@ -19163,7 +19455,7 @@ function handleHook(input, deps) {
19163
19455
  if (skillRead)
19164
19456
  recordSkillLoad(input, skillRead, "read");
19165
19457
  const canDeny = usable2(descriptor.shellPolicy.deny);
19166
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
19458
+ const policy = canDeny ? shellPolicy(input.cwd, event.command, event.dialect) : NONE5;
19167
19459
  if (policy.kind !== "none")
19168
19460
  return policy;
19169
19461
  const postTool = usable2(descriptor.events["shell.post"].support);
@@ -19766,7 +20058,7 @@ var codexAdapter = {
19766
20058
 
19767
20059
  // packages/workit-core/src/hooks/hosts/cursor.ts
19768
20060
  import fs17 from "node:fs";
19769
- import path22 from "node:path";
20061
+ import path23 from "node:path";
19770
20062
  var undocumented = { support: "undocumented", native: null };
19771
20063
  var none = { support: "none", native: null };
19772
20064
  var CURSOR_DESCRIPTOR = {
@@ -19963,7 +20255,7 @@ var protocolEvent2 = (input) => {
19963
20255
  };
19964
20256
  var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
19965
20257
  var lastAssistantText = (file) => {
19966
- if (!file || !path22.isAbsolute(file))
20258
+ if (!file || !path23.isAbsolute(file))
19967
20259
  return null;
19968
20260
  try {
19969
20261
  const size = fs17.statSync(file).size;
@@ -20000,7 +20292,7 @@ var lastAssistantText = (file) => {
20000
20292
  return null;
20001
20293
  }
20002
20294
  };
20003
- var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path22.sep) ? root : `${root}${path22.sep}`);
20295
+ var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path23.sep) ? root : `${root}${path23.sep}`);
20004
20296
  var hookCwd = (input) => {
20005
20297
  const cwd = existingDirectory(input.cwd);
20006
20298
  if (cwd && input.hook_event_name === "beforeShellExecution")