@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.
@@ -8834,7 +8834,7 @@ var PREFIXES, branchNames = (workspaceRoot) => {
8834
8834
  return new Set;
8835
8835
  return new Set((r.stdout ?? "").split(`
8836
8836
  `).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
8837
- }, detectBranchPolicy = (workspaceRoot) => {
8837
+ }, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
8838
8838
  const names = branchNames(workspaceRoot);
8839
8839
  const develop = names.has("develop");
8840
8840
  const main = names.has("main");
@@ -17647,10 +17647,41 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17647
17647
  ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
17648
17648
  ])
17649
17649
  };
17650
+ }, KIND_SYNONYMS, allowedKinds = (allowed) => [
17651
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
17652
+ ], editDistance = (a, b) => {
17653
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
17654
+ for (let i = 1;i <= a.length; i++) {
17655
+ let previous = row[0];
17656
+ row[0] = i;
17657
+ for (let j = 1;j <= b.length; j++) {
17658
+ const saved = row[j];
17659
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
17660
+ previous = saved;
17661
+ }
17662
+ }
17663
+ return row[b.length];
17664
+ }, suggestBranchName = (name, kinds) => {
17665
+ if (!kinds.length)
17666
+ return null;
17667
+ const slash = name.indexOf("/");
17668
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
17669
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
17670
+ const synonym = KIND_SYNONYMS[prefix];
17671
+ 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;
17672
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
17673
+ return `${kind}/${rest}`;
17674
+ }, kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", "), aliasNote = (kinds) => {
17675
+ const notes = [
17676
+ ["fix", "bugfix"],
17677
+ ["chore", "feature"]
17678
+ ].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
17679
+ return notes.length ? ` (${notes.join(", ")})` : "";
17650
17680
  }, validateBranchNamePolicy = (name, policy, source) => {
17651
17681
  const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
17682
+ const kinds = allowedKinds(policy.allowed);
17652
17683
  if (policy.protected.has(name.toLowerCase())) {
17653
- const correction = `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
17684
+ 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(", ")}`;
17654
17685
  return {
17655
17686
  ok: false,
17656
17687
  rule: "protected_ref",
@@ -17661,7 +17692,8 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17661
17692
  };
17662
17693
  }
17663
17694
  if (!policy.allowed.some((pattern) => pattern.test(name))) {
17664
- const correction = `choose a branch matching one of: ${allowed.join(", ")}`;
17695
+ const suggestion = suggestBranchName(name, kinds);
17696
+ 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(", ")}`;
17665
17697
  return {
17666
17698
  ok: false,
17667
17699
  rule: "allowed_pattern",
@@ -17700,7 +17732,52 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17700
17732
  error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
17701
17733
  };
17702
17734
  }
17703
- }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
17735
+ }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy, gitFlowOf = (root, integration) => {
17736
+ try {
17737
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
17738
+ return null;
17739
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
17740
+ const names = branchNames(root);
17741
+ const production = productionBranchOf(names);
17742
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
17743
+ return { develop, production, developExists: names.has(develop) };
17744
+ } catch {
17745
+ return null;
17746
+ }
17747
+ }, releaseBranchesOf = (cwd) => {
17748
+ try {
17749
+ const resolved = resolveBranchPolicyFor(cwd);
17750
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
17751
+ return {
17752
+ preset: resolved.preset,
17753
+ branches: flow ? [flow.production, flow.develop] : [],
17754
+ production: flow ? flow.production : resolved.defaultTargetBranch
17755
+ };
17756
+ } catch {
17757
+ return { preset: "unknown", branches: [], production: null };
17758
+ }
17759
+ }, releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase()), branchGuidance = (cwd) => {
17760
+ try {
17761
+ const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
17762
+ const kinds = allowedKinds(patterns);
17763
+ const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
17764
+ const anyName = patterns.some((pattern) => pattern.source === "^.*$");
17765
+ const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
17766
+ 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.`;
17767
+ const resolved = vcsConfig("resolve", cwd);
17768
+ const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
17769
+ const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
17770
+ if (flow?.developExists)
17771
+ 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}.`;
17772
+ const names = branchNames(cwd);
17773
+ const configured = String(resolved.baseBranch ?? integration ?? "") || null;
17774
+ const trunk = productionBranchOf(names);
17775
+ 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.";
17776
+ return `${allowed} ${base}`;
17777
+ } catch {
17778
+ return null;
17779
+ }
17780
+ }, globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
17704
17781
  var init_branch = __esm(() => {
17705
17782
  init_branch_policy();
17706
17783
  init_commit_flavors();
@@ -17709,33 +17786,63 @@ var init_branch = __esm(() => {
17709
17786
  init_workspaces();
17710
17787
  init_vcs_config();
17711
17788
  init_release_tracks();
17789
+ KIND_SYNONYMS = {
17790
+ fix: "bugfix",
17791
+ fixes: "bugfix",
17792
+ bug: "bugfix",
17793
+ bugs: "bugfix",
17794
+ bugfixes: "bugfix",
17795
+ feat: "feature",
17796
+ features: "feature",
17797
+ chore: "feature",
17798
+ docs: "feature",
17799
+ doc: "feature",
17800
+ refactor: "feature",
17801
+ test: "feature",
17802
+ tests: "feature",
17803
+ ci: "feature",
17804
+ build: "feature",
17805
+ perf: "feature",
17806
+ style: "feature",
17807
+ deps: "feature",
17808
+ task: "feature",
17809
+ enhancement: "feature",
17810
+ hot: "hotfix",
17811
+ hotfixes: "hotfix",
17812
+ releases: "release",
17813
+ rel: "release"
17814
+ };
17712
17815
  });
17713
17816
 
17714
- // packages/workit-core/src/core/route-intent.ts
17715
- var shellBranchTarget = (command) => {
17716
- if (!command.trim() || /[\r\n;&|<>`'"$()\\*?{}~!#]/u.test(command) || command.includes("[") || command.includes("]"))
17717
- return null;
17718
- const args = command.trim().split(/\s+/u);
17719
- if (args[0] !== "git")
17720
- return null;
17721
- const option = args[2];
17722
- 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;
17723
- return target && !target.startsWith("-") ? target : null;
17724
- }, shellBranchPolicyViolation = (root, command) => {
17725
- const target = shellBranchTarget(command);
17726
- return target === null ? null : validateBranchNameFor(root, target);
17727
- };
17728
- var init_route_intent = __esm(() => {
17729
- init_branch();
17730
- });
17731
-
17732
- // packages/workit-core/src/hooks/policy.ts
17733
- var shellPolicy = (cwd, command) => {
17734
- const policy = shellBranchPolicyViolation(cwd, command);
17735
- return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
17817
+ // packages/workit-core/src/hooks/session-marker.ts
17818
+ import { createHash as createHash13 } from "node:crypto";
17819
+ import fs15 from "node:fs";
17820
+ import path18 from "node:path";
17821
+ var sessionMarkerFile = (input, session, store) => {
17822
+ let dir = store;
17823
+ if (dir === undefined) {
17824
+ const location = resolveStore(input.cwd);
17825
+ if (location instanceof Error || !location.shared)
17826
+ return null;
17827
+ dir = location.dir;
17828
+ }
17829
+ const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
17830
+ return path18.join(dir, "hooks", `skills-${name}.json`);
17831
+ }, list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [], readSessionMarker = (file) => {
17832
+ try {
17833
+ const value = JSON.parse(fs15.readFileSync(file, "utf8"));
17834
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
17835
+ } catch {
17836
+ return { nudged: [], loaded: [], raw: [] };
17837
+ }
17838
+ }, writeSessionMarker = (file, marker) => {
17839
+ try {
17840
+ fs15.mkdirSync(path18.dirname(file), { recursive: true });
17841
+ fs15.writeFileSync(file, JSON.stringify(marker));
17842
+ } catch {}
17736
17843
  };
17737
- var init_policy = __esm(() => {
17738
- init_route_intent();
17844
+ var init_session_marker = __esm(() => {
17845
+ init_paths();
17739
17846
  });
17740
17847
 
17741
17848
  // packages/workit-core/src/hooks/shell-words.ts
@@ -17859,219 +17966,12 @@ var closingQuote = (command, start, dialect) => {
17859
17966
  return segments;
17860
17967
  };
17861
17968
 
17862
- // packages/workit-core/src/hooks/write-gate.ts
17863
- import { realpathSync as realpathSync5 } from "node:fs";
17864
- import path18 from "node:path";
17865
- function writeGate(cwd, paths) {
17866
- try {
17867
- const gate = gateFor(cwd);
17868
- if (!gate || gate.blockers.length === 0)
17869
- return NONE2;
17870
- if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
17871
- return NONE2;
17872
- const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
17873
- return {
17874
- kind: "deny",
17875
- reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
17876
- unblock: gate.blockers[0].reason
17877
- };
17878
- } catch {
17879
- return NONE2;
17880
- }
17881
- }
17882
- var NONE2, CASE_INSENSITIVE, canonical2 = (file) => {
17883
- let rest = "";
17884
- for (let dir = path18.resolve(file);; dir = path18.dirname(dir)) {
17885
- try {
17886
- return path18.join(realpathSync5(dir), rest);
17887
- } catch {
17888
- if (path18.dirname(dir) === dir)
17889
- return path18.resolve(file);
17890
- rest = rest ? path18.join(path18.basename(dir), rest) : path18.basename(dir);
17891
- }
17892
- }
17893
- }, exempt = (cwd, root, file, plans) => {
17894
- const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
17895
- const relative = path18.relative(fold(canonical2(root)), fold(canonical2(path18.resolve(cwd, file))));
17896
- if (relative.startsWith("..") || path18.isAbsolute(relative))
17897
- return true;
17898
- const posix = relative.split(path18.sep).join("/");
17899
- const segments = posix.split("/");
17900
- return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
17901
- }, PREFIXES2, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
17902
- let words = input;
17903
- while (words.length && (PREFIXES2.has(words[0]) || /^\w+=/.test(words[0])))
17904
- words = words.slice(1);
17905
- const [verb, ...args] = words;
17906
- const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
17907
- if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
17908
- return [];
17909
- switch (name) {
17910
- case "touch":
17911
- case "mkdir":
17912
- case "rm":
17913
- case "rmdir":
17914
- case "tee":
17915
- case "mv":
17916
- case "truncate":
17917
- return operands(args);
17918
- case "cp":
17919
- case "ln":
17920
- case "install": {
17921
- const target = args.indexOf("-t");
17922
- if (target >= 0)
17923
- return args[target + 1] ? [args[target + 1]] : null;
17924
- const last = operands(args).at(-1);
17925
- return last ? [last] : null;
17926
- }
17927
- case "dd":
17928
- return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
17929
- case "sed":
17930
- case "perl": {
17931
- if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
17932
- return [];
17933
- const scripted = args.some((item) => item === "-e" || item === "-f");
17934
- const files = [];
17935
- for (let index = 0;index < args.length; index++) {
17936
- if (args[index] === "-e" || args[index] === "-f")
17937
- index++;
17938
- else if (!args[index].startsWith("-"))
17939
- files.push(args[index]);
17940
- }
17941
- return scripted ? files : files.slice(1);
17942
- }
17943
- case "patch":
17944
- return null;
17945
- case "set-content":
17946
- case "add-content":
17947
- case "out-file":
17948
- case "new-item":
17949
- case "ni":
17950
- case "remove-item":
17951
- case "del":
17952
- case "copy-item":
17953
- case "move-item": {
17954
- const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
17955
- if (flag >= 0)
17956
- return args[flag + 1] ? [args[flag + 1]] : null;
17957
- const files = operands(args);
17958
- if (name === "move-item")
17959
- return files.length ? files : null;
17960
- const file = name === "copy-item" ? files.at(-1) : files[0];
17961
- return file ? [file] : null;
17962
- }
17963
- case "git": {
17964
- const sub = operands(args)[0];
17965
- if (sub === "apply")
17966
- return null;
17967
- if (sub === "restore" || sub === "mv" || sub === "rm")
17968
- return operands(args).slice(1);
17969
- if (sub === "checkout" && args.includes("--"))
17970
- return args.slice(args.indexOf("--") + 1);
17971
- return [];
17972
- }
17973
- default:
17974
- return [];
17975
- }
17976
- }, shellWrites = (command) => {
17977
- const targets = [];
17978
- let unknown = false;
17979
- for (const segment of segmentsOf(command)) {
17980
- targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
17981
- const edits = editsOf(segment.words);
17982
- if (edits === null)
17983
- unknown = true;
17984
- else
17985
- targets.push(...edits);
17986
- }
17987
- if (!unknown && targets.length === 0)
17988
- return { writes: false, targets: null };
17989
- return { writes: true, targets: unknown ? null : targets };
17990
- }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path18.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
17991
- const store = new TaskStore(cwd);
17992
- const listed = store.listTaskIndex();
17993
- if (!listed.ok)
17994
- return null;
17995
- const key = store.currentKey();
17996
- if (!key.ok)
17997
- return null;
17998
- const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
17999
- if (!entry)
18000
- return { blockers: [], root: store.root, plans: new Set };
18001
- const ledger = ledgerPath(store.root);
18002
- const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18003
- const hit = cache2.get(store.root);
18004
- if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18005
- return hit;
18006
- const task = store.readTask(entry.id);
18007
- if (!task.ok)
18008
- return null;
18009
- const workspace = store.readWorkspace();
18010
- const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18011
- const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18012
- const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18013
- return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18014
- });
18015
- const gate = {
18016
- blockers,
18017
- root: store.root,
18018
- plans,
18019
- key: cacheKey,
18020
- planKey: planKeyOf(store.root, plans)
18021
- };
18022
- cache2.set(store.root, gate);
18023
- if (cache2.size > 32)
18024
- cache2.delete(cache2.keys().next().value);
18025
- return gate;
18026
- };
18027
- var init_write_gate = __esm(() => {
18028
- init_task_store();
18029
- init_derive();
18030
- init_task_evaluation();
18031
- init_ledger();
18032
- NONE2 = { kind: "none" };
18033
- CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18034
- PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18035
- cache2 = new Map;
18036
- });
18037
-
18038
- // packages/workit-core/src/hooks/session-marker.ts
18039
- import { createHash as createHash13 } from "node:crypto";
18040
- import fs15 from "node:fs";
18041
- import path19 from "node:path";
18042
- var sessionMarkerFile = (input, session, store) => {
18043
- let dir = store;
18044
- if (dir === undefined) {
18045
- const location = resolveStore(input.cwd);
18046
- if (location instanceof Error || !location.shared)
18047
- return null;
18048
- dir = location.dir;
18049
- }
18050
- const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
18051
- return path19.join(dir, "hooks", `skills-${name}.json`);
18052
- }, list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [], readSessionMarker = (file) => {
18053
- try {
18054
- const value = JSON.parse(fs15.readFileSync(file, "utf8"));
18055
- return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
18056
- } catch {
18057
- return { nudged: [], loaded: [], raw: [] };
18058
- }
18059
- }, writeSessionMarker = (file, marker) => {
18060
- try {
18061
- fs15.mkdirSync(path19.dirname(file), { recursive: true });
18062
- fs15.writeFileSync(file, JSON.stringify(marker));
18063
- } catch {}
18064
- };
18065
- var init_session_marker = __esm(() => {
18066
- init_paths();
18067
- });
18068
-
18069
17969
  // packages/workit-core/src/hooks/raw-git.ts
18070
17970
  import { spawnSync as spawnSync11 } from "node:child_process";
18071
17971
  import { createHash as createHash14 } from "node:crypto";
18072
17972
  import fs16 from "node:fs";
18073
17973
  import os4 from "node:os";
18074
- import path20 from "node:path";
17974
+ import path19 from "node:path";
18075
17975
  function rawInvocations(command, dialect = "posix", depth = 0) {
18076
17976
  const found = [];
18077
17977
  let dir = null;
@@ -18181,7 +18081,7 @@ function rawAction(invocation) {
18181
18081
  function rawGitPre(input, command, options = {}) {
18182
18082
  try {
18183
18083
  if (!mentionsRawTool(command))
18184
- return NONE3;
18084
+ return NONE2;
18185
18085
  const dialect = dialectOf(input);
18186
18086
  const nudges = new Map;
18187
18087
  let store = null;
@@ -18189,25 +18089,36 @@ function rawGitPre(input, command, options = {}) {
18189
18089
  store ??= repo.location.dir;
18190
18090
  if (action.kind === "merge")
18191
18091
  return deny(refusal(action, null));
18192
- if (action.kind === "push")
18092
+ if (action.kind === "push" && toPushRemote(repo, action.remote)) {
18093
+ if (action.mirror)
18094
+ 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")}.`);
18193
18095
  for (const target of action.targets) {
18194
18096
  const branch = target ?? repo.branch;
18195
- if (branch && guardedBranch(repo.dir, branch) && toPushRemote(repo, action.remote))
18196
- return deny(refusal(action, branch));
18097
+ if (branch && guardedBranch(repo.dir, branch))
18098
+ return deny(refusal(action, branch, repo.dir));
18197
18099
  }
18100
+ const remote = action.remote ?? pushRemoteName(repo.dir, repo.branch) ?? "origin";
18101
+ const facts = refFacts(repo, remote);
18102
+ const tagDeny = tagRefusal(action, facts);
18103
+ if (tagDeny)
18104
+ return deny(tagDeny);
18105
+ const nameDeny = action.deletes ? null : offPolicyPush(action, repo, facts, remote);
18106
+ if (nameDeny)
18107
+ return deny(nameDeny);
18108
+ }
18198
18109
  const nudge = nudgeFor(action, input.host, input.session.id);
18199
18110
  if (nudge && !nudges.has(nudgeKind(action)))
18200
18111
  nudges.set(nudgeKind(action), nudge);
18201
18112
  }
18202
18113
  if (options.nudge === false || !store || nudges.size === 0)
18203
- return NONE3;
18114
+ return NONE2;
18204
18115
  if (workitInvocations(command, dialect).length > 0)
18205
- return NONE3;
18116
+ return NONE2;
18206
18117
  const shown = unseenNudges(input, nudges, store);
18207
18118
  return shown.length ? { kind: "context", text: shown.join(`
18208
- `) } : NONE3;
18119
+ `) } : NONE2;
18209
18120
  } catch {
18210
- return NONE3;
18121
+ return NONE2;
18211
18122
  }
18212
18123
  }
18213
18124
  function noteRawCommit(input, command, key) {
@@ -18219,7 +18130,7 @@ function noteRawCommit(input, command, key) {
18219
18130
  if (action.kind !== "commit")
18220
18131
  continue;
18221
18132
  const file = markerFile(repo, input.session.id, key);
18222
- fs16.mkdirSync(path20.dirname(file), { recursive: true });
18133
+ fs16.mkdirSync(path19.dirname(file), { recursive: true });
18223
18134
  fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18224
18135
  `);
18225
18136
  }
@@ -18246,11 +18157,11 @@ function settlePendingCommit(input) {
18246
18157
  settle2(input, repo, file);
18247
18158
  } catch {}
18248
18159
  }
18249
- var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, isAbsolute = (value) => path20.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"), joinDir = (base, next) => {
18160
+ var NONE2, SHELLS, PREFIXES2, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, isAbsolute = (value) => path19.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"), joinDir = (base, next) => {
18250
18161
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
18251
18162
  return;
18252
- const expanded = next === "~" || next.startsWith("~/") ? path20.join(os4.homedir(), next.slice(1)) : next;
18253
- return base === null || isAbsolute(expanded) ? expanded : path20.join(base, expanded);
18163
+ const expanded = next === "~" || next.startsWith("~/") ? path19.join(os4.homedir(), next.slice(1)) : next;
18164
+ return base === null || isAbsolute(expanded) ? expanded : path19.join(base, expanded);
18254
18165
  }, cdTarget = (words) => {
18255
18166
  const args = words.slice(1);
18256
18167
  const flag = args.findIndex((word) => /^-(?:literal)?path$/i.test(word));
@@ -18265,7 +18176,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18265
18176
  return words;
18266
18177
  if (/^\w+=/.test(first))
18267
18178
  words = words.slice(1);
18268
- else if (PREFIXES3.has(first)) {
18179
+ else if (PREFIXES2.has(first)) {
18269
18180
  words = words.slice(1);
18270
18181
  while (words[0]?.startsWith("-"))
18271
18182
  words = words.slice(words[0] === "-u" ? 2 : 1);
@@ -18281,6 +18192,10 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18281
18192
  let force = false;
18282
18193
  let lease = false;
18283
18194
  let wholesale = false;
18195
+ let allTags = false;
18196
+ let deletes = false;
18197
+ let allBranches = false;
18198
+ let mirror = false;
18284
18199
  let remote = null;
18285
18200
  const positionals = [];
18286
18201
  for (let index = 1;index < args.length; index++) {
@@ -18301,14 +18216,22 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18301
18216
  force = true;
18302
18217
  else if (arg.startsWith("--force-with-lease") || arg === "--force-if-includes")
18303
18218
  lease = true;
18304
- else if (["--all", "--mirror", "--branches", "--tags"].includes(arg))
18305
- wholesale = true;
18219
+ else if (arg === "--delete" || arg === "-d")
18220
+ deletes = true;
18221
+ else if (arg === "--tags")
18222
+ allTags = wholesale = true;
18223
+ else if (arg === "--all" || arg === "--branches")
18224
+ allBranches = wholesale = true;
18225
+ else if (arg === "--mirror")
18226
+ mirror = wholesale = true;
18306
18227
  else if (!arg.startsWith("-"))
18307
18228
  positionals.push(arg);
18308
18229
  }
18309
18230
  const specs = remote === null ? positionals.slice(1) : positionals;
18310
18231
  remote ??= positionals[0] ?? null;
18311
18232
  const targets = [];
18233
+ const tags = [];
18234
+ const deleted = [];
18312
18235
  for (const raw of specs) {
18313
18236
  if (raw.startsWith("+"))
18314
18237
  force = true;
@@ -18317,16 +18240,29 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18317
18240
  const destination = colon >= 0 ? spec.slice(colon + 1) : spec;
18318
18241
  if (!destination)
18319
18242
  continue;
18320
- if (destination === "HEAD")
18321
- targets.push(null);
18322
- else if (destination.startsWith("refs/heads/"))
18323
- targets.push(destination.slice(11));
18324
- else if (!destination.startsWith("refs/"))
18325
- targets.push(destination);
18243
+ const name = destination === "HEAD" ? null : destination.startsWith("refs/heads/") ? destination.slice(11) : destination.startsWith("refs/") ? undefined : destination;
18244
+ if (destination.startsWith("refs/tags/"))
18245
+ tags.push(destination.slice(10));
18246
+ if (name === undefined)
18247
+ continue;
18248
+ targets.push(name);
18249
+ if (colon === 0 && name !== null)
18250
+ deleted.push(name);
18326
18251
  }
18327
18252
  if (specs.length === 0 && !wholesale)
18328
18253
  targets.push(null);
18329
- return { kind: "push", targets, blindForce: force && !lease, remote };
18254
+ return {
18255
+ kind: "push",
18256
+ targets,
18257
+ blindForce: force && !lease,
18258
+ remote,
18259
+ ...tags.length ? { tags } : {},
18260
+ ...allTags ? { allTags: true } : {},
18261
+ ...deletes ? { deletes: true } : {},
18262
+ ...deleted.length ? { deleted } : {},
18263
+ ...allBranches ? { allBranches: true } : {},
18264
+ ...mirror ? { mirror: true } : {}
18265
+ };
18330
18266
  }, VALUE_OPTIONS, asksHelp = (invocation) => {
18331
18267
  const { tool, args } = invocation;
18332
18268
  if (args[0] === "help" || tool !== "git" && args[1] === "help")
@@ -18376,25 +18312,72 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18376
18312
  if (!git || git.reftable)
18377
18313
  return null;
18378
18314
  try {
18379
- const head = fs16.readFileSync(path20.join(git.gitDir, "HEAD"), "utf8").trim();
18315
+ const head = fs16.readFileSync(path19.join(git.gitDir, "HEAD"), "utf8").trim();
18380
18316
  if (/^[0-9a-f]{40,64}$/.test(head))
18381
18317
  return head;
18382
18318
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
18383
18319
  if (!ref)
18384
18320
  return null;
18385
- const common = path20.dirname(repo.location.dir);
18321
+ const common = path19.dirname(repo.location.dir);
18386
18322
  try {
18387
- return fs16.readFileSync(path20.join(common, ref), "utf8").trim() || null;
18323
+ return fs16.readFileSync(path19.join(common, ref), "utf8").trim() || null;
18388
18324
  } catch {}
18389
- const packed = fs16.readFileSync(path20.join(common, "packed-refs"), "utf8");
18325
+ const packed = fs16.readFileSync(path19.join(common, "packed-refs"), "utf8");
18390
18326
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
18391
18327
  } catch {
18392
18328
  return null;
18393
18329
  }
18394
- }, quote = (text) => `\`${text}\``, refusal = (action, branch) => {
18330
+ }, quote = (text) => `\`${text}\``, refusal = (action, branch, dir) => {
18395
18331
  if (action.kind === "merge")
18396
18332
  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")}).`;
18333
+ const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
18334
+ if (dir && branch && plainPush && releaseBranch(dir, branch))
18335
+ 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")}.`;
18397
18336
  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")}.`;
18337
+ }, refFacts = (repo, remote) => {
18338
+ const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
18339
+ const listed = spawnSync11("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags", `refs/remotes/${remote}`], { cwd: repo.dir, encoding: "utf8", timeout: 5000 });
18340
+ const remotePrefix = `refs/remotes/${remote}/`;
18341
+ for (const ref of (listed.stdout ?? "").split(`
18342
+ `)) {
18343
+ if (ref.startsWith("refs/heads/"))
18344
+ facts.heads.add(ref.slice(11));
18345
+ else if (ref.startsWith("refs/tags/"))
18346
+ facts.tags.add(ref.slice(10));
18347
+ else if (ref.startsWith(remotePrefix))
18348
+ facts.remoteHeads.add(ref.slice(remotePrefix.length));
18349
+ }
18350
+ return facts;
18351
+ }, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts) => {
18352
+ const tags = [
18353
+ ...action.tags ?? [],
18354
+ ...action.targets.filter((name) => name !== null && isTag(facts, name))
18355
+ ];
18356
+ if (!tags.length && !action.allTags)
18357
+ return null;
18358
+ const named = tags.length ? tags.join(" --release ") : "v<version>";
18359
+ 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")}).`;
18360
+ }, remoteHas = (repo, remote, branch) => {
18361
+ const listed = spawnSync11("git", ["ls-remote", "--heads", remote, `refs/heads/${branch}`], {
18362
+ cwd: repo.dir,
18363
+ encoding: "utf8",
18364
+ timeout: 3000,
18365
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
18366
+ });
18367
+ return listed.status !== 0 || (listed.stdout ?? "").trim() !== "";
18368
+ }, offPolicyPush = (action, repo, facts, remote) => {
18369
+ const names = action.allBranches ? [...facts.heads] : action.targets.map((target) => target ?? repo.branch);
18370
+ for (const branch of names) {
18371
+ if (!branch || action.deleted?.includes(branch) || isTag(facts, branch))
18372
+ continue;
18373
+ if (facts.remoteHeads.has(branch))
18374
+ continue;
18375
+ const check = validateBranchNameFor(repo.dir, branch);
18376
+ if (check.ok || check.rule !== "allowed_pattern" || remoteHas(repo, remote, branch))
18377
+ continue;
18378
+ return `workit: raw ${quote("git push")} onto ${branch}: ${check.error}`;
18379
+ }
18380
+ return null;
18398
18381
  }, nudgeFor = (action, host, session) => {
18399
18382
  switch (action.kind) {
18400
18383
  case "commit": {
@@ -18419,7 +18402,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18419
18402
  const action = rawAction(invocation);
18420
18403
  if (!action)
18421
18404
  continue;
18422
- const dir = path20.resolve(cwd, invocation.dir ?? ".");
18405
+ const dir = path19.resolve(cwd, invocation.dir ?? ".");
18423
18406
  if (!repos.has(dir)) {
18424
18407
  const repo = repoAt(dir);
18425
18408
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -18446,7 +18429,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18446
18429
  raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
18447
18430
  });
18448
18431
  return fresh.map((kind) => nudges.get(kind));
18449
- }, deny = (reason) => ({ kind: "deny", reason, unblock: null }), NEXT_COMMAND = "next", MARKER_TTL_SECONDS = 3600, markerFile = (repo, session, key) => path20.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`), callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash14("sha256").update(command).digest("hex")}`, headCommit = (dir) => {
18432
+ }, deny = (reason) => ({ kind: "deny", reason, unblock: null }), NEXT_COMMAND = "next", MARKER_TTL_SECONDS = 3600, markerFile = (repo, session, key) => path19.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`), callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash14("sha256").update(command).digest("hex")}`, headCommit = (dir) => {
18450
18433
  const run = spawnSync11("git", [
18451
18434
  "log",
18452
18435
  "-1",
@@ -18506,9 +18489,9 @@ var init_raw_git = __esm(() => {
18506
18489
  init_ledger();
18507
18490
  init_paths();
18508
18491
  init_session_marker();
18509
- NONE3 = { kind: "none" };
18492
+ NONE2 = { kind: "none" };
18510
18493
  SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18511
- PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18494
+ PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18512
18495
  SCRIPT_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/;
18513
18496
  CD = new Set(["cd", "chdir", "set-location", "sl"]);
18514
18497
  PUSHD = new Set(["pushd", "push-location"]);
@@ -18566,8 +18549,298 @@ var init_raw_git = __esm(() => {
18566
18549
  };
18567
18550
  });
18568
18551
 
18569
- // packages/workit-core/src/hooks/stop.ts
18552
+ // packages/workit-core/src/core/route-intent.ts
18570
18553
  import { spawnSync as spawnSync12 } from "node:child_process";
18554
+ import { existsSync as existsSync6 } from "node:fs";
18555
+ import path20 from "node:path";
18556
+ var UNKNOWABLE, SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, flagValue = (args, flags) => {
18557
+ for (let index = 1;index < args.length; index++) {
18558
+ const arg = args[index];
18559
+ if (arg === "--")
18560
+ return null;
18561
+ if (flags.has(arg))
18562
+ return args[index + 1] ?? null;
18563
+ const eq = arg.indexOf("=");
18564
+ if (arg.startsWith("--") && eq > 0 && flags.has(arg.slice(0, eq)))
18565
+ return arg.slice(eq + 1);
18566
+ if (!arg.startsWith("--") && arg.length > 2 && flags.has(arg.slice(0, 2)))
18567
+ return arg.slice(2);
18568
+ }
18569
+ return null;
18570
+ }, WORKTREE_VALUES, worktreeTarget = (args) => {
18571
+ const named = flagValue(args, WORKTREE_CREATE);
18572
+ if (named !== null)
18573
+ return named;
18574
+ const positionals = [];
18575
+ for (let index = 1;index < args.length; index++) {
18576
+ const arg = args[index];
18577
+ if (arg === "--detach" || arg === "-d")
18578
+ return null;
18579
+ if (WORKTREE_VALUES.has(arg))
18580
+ index++;
18581
+ else if (!arg.startsWith("-"))
18582
+ positionals.push(arg);
18583
+ }
18584
+ if (positionals.length !== 1)
18585
+ return null;
18586
+ const base = positionals[0].split(/[\\/]/u).findLast(Boolean) ?? "";
18587
+ return base && base !== "." && base !== ".." ? base : null;
18588
+ }, branchCommandTarget = (args) => {
18589
+ let rename = false;
18590
+ const positionals = [];
18591
+ for (let index = 1;index < args.length; index++) {
18592
+ const arg = args[index];
18593
+ if (arg === "--") {
18594
+ positionals.push(...args.slice(index + 1));
18595
+ break;
18596
+ }
18597
+ const flag = arg.split("=", 1)[0];
18598
+ if (BRANCH_NO_CREATE.has(flag))
18599
+ return null;
18600
+ if (BRANCH_RENAME.has(flag))
18601
+ rename = true;
18602
+ else if (!arg.startsWith("-"))
18603
+ positionals.push(arg);
18604
+ }
18605
+ if (positionals.length < 1 || positionals.length > 2)
18606
+ return null;
18607
+ return rename ? positionals.at(-1) ?? null : positionals[0];
18608
+ }, gitBranchCreation = (args) => {
18609
+ const [sub, second] = args;
18610
+ 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;
18611
+ return target && !target.startsWith("-") && !UNKNOWABLE.test(target) ? target : null;
18612
+ }, shellBranchTargets = (command, dialect = "posix") => {
18613
+ if (!/\bgit\b/u.test(command))
18614
+ return [];
18615
+ const found = [];
18616
+ for (const invocation of rawInvocations(command, dialect)) {
18617
+ if (invocation.tool !== "git")
18618
+ continue;
18619
+ const branch = gitBranchCreation(invocation.args);
18620
+ if (branch)
18621
+ found.push({ branch, dir: invocation.dir });
18622
+ }
18623
+ return found;
18624
+ }, localBranchExists = (dir, branch) => spawnSync12("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
18625
+ cwd: dir,
18626
+ stdio: "ignore",
18627
+ timeout: 5000
18628
+ }).status === 0, shellBranchPolicyViolation = (root, command, dialect = "posix") => {
18629
+ let judged = false;
18630
+ for (const target of shellBranchTargets(command, dialect)) {
18631
+ if (target.dir === undefined)
18632
+ continue;
18633
+ const dir = target.dir === null ? root : path20.resolve(root, target.dir);
18634
+ if (target.dir !== null && (!existsSync6(dir) || !inWorkitWorkspace(dir)))
18635
+ continue;
18636
+ if (localBranchExists(dir, target.branch))
18637
+ continue;
18638
+ judged = true;
18639
+ const check = validateBranchNameFor(dir, target.branch);
18640
+ if (!check.ok)
18641
+ return check;
18642
+ }
18643
+ return judged ? { ok: true } : null;
18644
+ };
18645
+ var init_route_intent = __esm(() => {
18646
+ init_raw_git();
18647
+ init_branch();
18648
+ UNKNOWABLE = /[$`*?[\]{}~]/u;
18649
+ SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
18650
+ CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
18651
+ WORKTREE_CREATE = new Set(["-b", "-B"]);
18652
+ 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
+ BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
18654
+ WORKTREE_VALUES = new Set(["--reason"]);
18655
+ });
18656
+
18657
+ // packages/workit-core/src/hooks/policy.ts
18658
+ var shellPolicy = (cwd, command, dialect = "posix") => {
18659
+ const policy = shellBranchPolicyViolation(cwd, command, dialect);
18660
+ return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
18661
+ };
18662
+ var init_policy = __esm(() => {
18663
+ init_route_intent();
18664
+ });
18665
+
18666
+ // packages/workit-core/src/hooks/write-gate.ts
18667
+ import { realpathSync as realpathSync5 } from "node:fs";
18668
+ import path21 from "node:path";
18669
+ function writeGate(cwd, paths) {
18670
+ try {
18671
+ const gate = gateFor(cwd);
18672
+ if (!gate || gate.blockers.length === 0)
18673
+ return NONE3;
18674
+ if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
18675
+ return NONE3;
18676
+ const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
18677
+ return {
18678
+ kind: "deny",
18679
+ reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
18680
+ unblock: gate.blockers[0].reason
18681
+ };
18682
+ } catch {
18683
+ return NONE3;
18684
+ }
18685
+ }
18686
+ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
18687
+ let rest = "";
18688
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18689
+ try {
18690
+ return path21.join(realpathSync5(dir), rest);
18691
+ } catch {
18692
+ if (path21.dirname(dir) === dir)
18693
+ return path21.resolve(file);
18694
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18695
+ }
18696
+ }
18697
+ }, exempt = (cwd, root, file, plans) => {
18698
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18699
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18700
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
18701
+ return true;
18702
+ const posix = relative.split(path21.sep).join("/");
18703
+ const segments = posix.split("/");
18704
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18705
+ }, PREFIXES3, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
18706
+ let words = input;
18707
+ while (words.length && (PREFIXES3.has(words[0]) || /^\w+=/.test(words[0])))
18708
+ words = words.slice(1);
18709
+ const [verb, ...args] = words;
18710
+ const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
18711
+ if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
18712
+ return [];
18713
+ switch (name) {
18714
+ case "touch":
18715
+ case "mkdir":
18716
+ case "rm":
18717
+ case "rmdir":
18718
+ case "tee":
18719
+ case "mv":
18720
+ case "truncate":
18721
+ return operands(args);
18722
+ case "cp":
18723
+ case "ln":
18724
+ case "install": {
18725
+ const target = args.indexOf("-t");
18726
+ if (target >= 0)
18727
+ return args[target + 1] ? [args[target + 1]] : null;
18728
+ const last = operands(args).at(-1);
18729
+ return last ? [last] : null;
18730
+ }
18731
+ case "dd":
18732
+ return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
18733
+ case "sed":
18734
+ case "perl": {
18735
+ if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
18736
+ return [];
18737
+ const scripted = args.some((item) => item === "-e" || item === "-f");
18738
+ const files = [];
18739
+ for (let index = 0;index < args.length; index++) {
18740
+ if (args[index] === "-e" || args[index] === "-f")
18741
+ index++;
18742
+ else if (!args[index].startsWith("-"))
18743
+ files.push(args[index]);
18744
+ }
18745
+ return scripted ? files : files.slice(1);
18746
+ }
18747
+ case "patch":
18748
+ return null;
18749
+ case "set-content":
18750
+ case "add-content":
18751
+ case "out-file":
18752
+ case "new-item":
18753
+ case "ni":
18754
+ case "remove-item":
18755
+ case "del":
18756
+ case "copy-item":
18757
+ case "move-item": {
18758
+ const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
18759
+ if (flag >= 0)
18760
+ return args[flag + 1] ? [args[flag + 1]] : null;
18761
+ const files = operands(args);
18762
+ if (name === "move-item")
18763
+ return files.length ? files : null;
18764
+ const file = name === "copy-item" ? files.at(-1) : files[0];
18765
+ return file ? [file] : null;
18766
+ }
18767
+ case "git": {
18768
+ const sub = operands(args)[0];
18769
+ if (sub === "apply")
18770
+ return null;
18771
+ if (sub === "restore" || sub === "mv" || sub === "rm")
18772
+ return operands(args).slice(1);
18773
+ if (sub === "checkout" && args.includes("--"))
18774
+ return args.slice(args.indexOf("--") + 1);
18775
+ return [];
18776
+ }
18777
+ default:
18778
+ return [];
18779
+ }
18780
+ }, shellWrites = (command) => {
18781
+ const targets = [];
18782
+ let unknown = false;
18783
+ for (const segment of segmentsOf(command)) {
18784
+ targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
18785
+ const edits = editsOf(segment.words);
18786
+ if (edits === null)
18787
+ unknown = true;
18788
+ else
18789
+ targets.push(...edits);
18790
+ }
18791
+ if (!unknown && targets.length === 0)
18792
+ return { writes: false, targets: null };
18793
+ return { writes: true, targets: unknown ? null : targets };
18794
+ }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path21.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
18795
+ const store = new TaskStore(cwd);
18796
+ const listed = store.listTaskIndex();
18797
+ if (!listed.ok)
18798
+ return null;
18799
+ const key = store.currentKey();
18800
+ if (!key.ok)
18801
+ return null;
18802
+ const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
18803
+ if (!entry)
18804
+ return { blockers: [], root: store.root, plans: new Set };
18805
+ const ledger = ledgerPath(store.root);
18806
+ const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18807
+ const hit = cache2.get(store.root);
18808
+ if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18809
+ return hit;
18810
+ const task = store.readTask(entry.id);
18811
+ if (!task.ok)
18812
+ return null;
18813
+ const workspace = store.readWorkspace();
18814
+ const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18815
+ const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18816
+ const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18817
+ return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18818
+ });
18819
+ const gate = {
18820
+ blockers,
18821
+ root: store.root,
18822
+ plans,
18823
+ key: cacheKey,
18824
+ planKey: planKeyOf(store.root, plans)
18825
+ };
18826
+ cache2.set(store.root, gate);
18827
+ if (cache2.size > 32)
18828
+ cache2.delete(cache2.keys().next().value);
18829
+ return gate;
18830
+ };
18831
+ var init_write_gate = __esm(() => {
18832
+ init_task_store();
18833
+ init_derive();
18834
+ init_task_evaluation();
18835
+ init_ledger();
18836
+ NONE3 = { kind: "none" };
18837
+ CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18838
+ PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18839
+ cache2 = new Map;
18840
+ });
18841
+
18842
+ // packages/workit-core/src/hooks/stop.ts
18843
+ import { spawnSync as spawnSync13 } from "node:child_process";
18571
18844
  function stopObligation(input, descriptor) {
18572
18845
  try {
18573
18846
  const session = input.session.id;
@@ -18631,7 +18904,7 @@ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
18631
18904
  `);
18632
18905
  return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
18633
18906
  }, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
18634
- const run = spawnSync12("git", args, {
18907
+ const run = spawnSync13("git", args, {
18635
18908
  cwd,
18636
18909
  encoding: "utf8",
18637
18910
  timeout: 3000,
@@ -18941,10 +19214,10 @@ var init_skill_nudge = __esm(() => {
18941
19214
  });
18942
19215
 
18943
19216
  // packages/workit-core/src/hooks/hosts/fields.ts
18944
- import { existsSync as existsSync6, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
18945
- import path21 from "node:path";
19217
+ import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
19218
+ import path22 from "node:path";
18946
19219
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
18947
- if (!nonEmpty2(value) || !path21.isAbsolute(value) || !existsSync6(value))
19220
+ if (!nonEmpty2(value) || !path22.isAbsolute(value) || !existsSync7(value))
18948
19221
  return null;
18949
19222
  try {
18950
19223
  return statSync2(value).isDirectory() ? realpathSync6(value) : null;
@@ -19040,7 +19313,7 @@ function handleHook(input, deps) {
19040
19313
  if (skillRead)
19041
19314
  recordSkillLoad(input, skillRead, "read");
19042
19315
  const canDeny = usable2(descriptor.shellPolicy.deny);
19043
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
19316
+ const policy = canDeny ? shellPolicy(input.cwd, event.command, event.dialect) : NONE5;
19044
19317
  if (policy.kind !== "none")
19045
19318
  return policy;
19046
19319
  const postTool = usable2(descriptor.events["shell.post"].support);
@@ -19094,13 +19367,14 @@ var usable2 = (value) => value === "native" || value === "partial", NONE5, resen
19094
19367
  const role = workitRole(descriptor, event.agentType);
19095
19368
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
19096
19369
  const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
19370
+ const guide = role === "implementer" ? branchGuidance(input.cwd) : null;
19097
19371
  if (role === "implementer" && isolated)
19098
- 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.`;
19372
+ 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.`;
19099
19373
  if (isolated)
19100
19374
  return `${ISOLATED_GIT}
19101
19375
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
19102
19376
  if (role === "implementer")
19103
- 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.`;
19377
+ 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.`;
19104
19378
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
19105
19379
  if (role !== "judge" || !event.agentId)
19106
19380
  return readOnly;
@@ -19138,6 +19412,7 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
19138
19412
  };
19139
19413
  var init_handle = __esm(() => {
19140
19414
  init_context();
19415
+ init_branch();
19141
19416
  init_host_session();
19142
19417
  init_policy();
19143
19418
  init_raw_git();
@@ -19682,7 +19957,7 @@ var init_codex = __esm(() => {
19682
19957
 
19683
19958
  // packages/workit-core/src/hooks/hosts/cursor.ts
19684
19959
  import fs17 from "node:fs";
19685
- import path22 from "node:path";
19960
+ import path23 from "node:path";
19686
19961
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
19687
19962
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
19688
19963
  return { ok: false, error: "hook_event_name is required" };
@@ -19763,7 +20038,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19763
20038
  };
19764
20039
  }
19765
20040
  }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
19766
- if (!file || !path22.isAbsolute(file))
20041
+ if (!file || !path23.isAbsolute(file))
19767
20042
  return null;
19768
20043
  try {
19769
20044
  const size = fs17.statSync(file).size;
@@ -19799,7 +20074,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19799
20074
  } catch {
19800
20075
  return null;
19801
20076
  }
19802
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path22.sep) ? root : `${root}${path22.sep}`), hookCwd = (input) => {
20077
+ }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path23.sep) ? root : `${root}${path23.sep}`), hookCwd = (input) => {
19803
20078
  const cwd = existingDirectory(input.cwd);
19804
20079
  if (cwd && input.hook_event_name === "beforeShellExecution")
19805
20080
  return cwd;