@brainervirus/workit-cursor 8.10.0 → 8.12.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");
@@ -9120,8 +9120,8 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
9120
9120
  profiles: record(nonBlank, profileSchema).optional()
9121
9121
  }).passthrough();
9122
9122
  return entrySchema;
9123
- }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd) => {
9124
- const file = path3.join(configDir(), "workspaces.json");
9123
+ }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
9124
+ const file = path3.join(dir, "workspaces.json");
9125
9125
  let raw;
9126
9126
  try {
9127
9127
  raw = readFileSync2(file, "utf8");
@@ -9150,10 +9150,27 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
9150
9150
  const matches = matchingEntries(cwd, entries ?? []);
9151
9151
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
9152
9152
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
9153
- return selected ? { file, selected } : null;
9153
+ return selected ? { file, selected, matches } : null;
9154
9154
  }, resolveRuntimeReleaseTracks = (cwd) => {
9155
9155
  const matched = matchRuntimeWorkspace(cwd);
9156
9156
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
9157
+ }, workspaceTrailers = (cwd, dir) => {
9158
+ let matched;
9159
+ try {
9160
+ matched = matchRuntimeWorkspace(cwd, dir);
9161
+ } catch {
9162
+ return { enabled: true, workspace: null, from: null };
9163
+ }
9164
+ if (!matched)
9165
+ return { enabled: true, workspace: null, from: null };
9166
+ const { selected, matches } = matched;
9167
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9168
+ const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
9169
+ return {
9170
+ enabled: source?.trailers !== false,
9171
+ workspace: selected.name,
9172
+ from: source?.name ?? null
9173
+ };
9157
9174
  }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9158
9175
  const matched = matchRuntimeWorkspace(cwd);
9159
9176
  if (!matched)
@@ -9264,6 +9281,7 @@ var init_workspaces = __esm(() => {
9264
9281
  autoApprove: union([boolean2(), array(nonBlank)]).optional(),
9265
9282
  defaultEndpoint: unknown().optional(),
9266
9283
  verification: _enum(["self", "independent"]).optional(),
9284
+ trailers: boolean2().optional(),
9267
9285
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9268
9286
  kind,
9269
9287
  union([boolean2(), literal("verified")]).optional()
@@ -14783,7 +14801,7 @@ function loadCheckConfig(cwd) {
14783
14801
  gates[gate] = names;
14784
14802
  }
14785
14803
  }
14786
- return { source: CHECKS_FILE, root, checks, gates, error: null };
14804
+ return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
14787
14805
  }
14788
14806
  function matchesNamedCheck(config, name, argv, cwd) {
14789
14807
  if (config.error || name === null || cwd !== ".")
@@ -15550,9 +15568,9 @@ Autonomy contract:
15550
15568
  ship), go to the effective endpoint in \`workit grant show\`: \`pr\` opens
15551
15569
  the PR; \`green\` babysits it without asking (workit-ship) to merge-ready,
15552
15570
  never merging; \`merged\` also lands it with \`workit pr merge\`.
15553
- Merge and release need a workspace grant, which only the user raises from
15554
- their own terminal (a blocked verb names how); never edit workspaces.json or
15555
- route around a grant_required.
15571
+ Merge and release need a workspace grant only the user raises from their
15572
+ own terminal (a blocked verb names how); never edit workspaces.json or route
15573
+ around a grant_required.
15556
15574
  - Ask only for a product or preference choice, or for authority you lack, with
15557
15575
  your recommended answer. Facts are yours: read, run or prototype.
15558
15576
  - Label claims measured (run and seen this session), inferred, or guess.
@@ -15561,17 +15579,19 @@ Autonomy contract:
15561
15579
  - Author is not verifier: your own verdict is \`--self\`, shown as
15562
15580
  self-reviewed, never verified. High risk, \`verification: independent\` and
15563
15581
  merge need a verifier with its own host-derived session (a subagent's
15564
- \`<lead>:<child>\`, set by the host); never hand-mint one such as \`<lead>-v1\`.
15582
+ \`<lead>:<child>\`); never hand-mint one (\`<lead>-v1\`).
15565
15583
  - A local-commit endpoint does not imply PR readiness, and a local commit alone
15566
15584
  is not evidence of a requested remote push. Before reporting delivery,
15567
15585
  reconcile every requested item and observe it (\`workit verify-delivery\`).
15568
15586
 
15569
15587
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
15570
15588
  hand-running git, gh or glab (\`workit git|pr|ci|check\`; \`workit help\` lists
15571
- the rest; without \`workit\` on PATH, \`npx -y @brainervirus/workit-cli\`).
15589
+ the rest; lacking \`workit\`, \`npx -y @brainervirus/workit-cli\`).
15590
+ Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
15591
+ an amend.
15572
15592
  The task families (task, policy, evidence, finding, decision, worker, state)
15573
15593
  are optional continuity for tracked work; each branch has one implicit task,
15574
- created by the first recording, and a solo edit needs no task. A decision
15594
+ and a solo edit needs no task. A decision
15575
15595
  records a user choice; it never authorizes an effect. Judge work once with
15576
15596
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
15577
15597
  Across repositories, bind each item to its checkout, branch and endpoint,
@@ -17647,10 +17667,41 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17647
17667
  ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
17648
17668
  ])
17649
17669
  };
17670
+ }, KIND_SYNONYMS, allowedKinds = (allowed) => [
17671
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
17672
+ ], editDistance = (a, b) => {
17673
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
17674
+ for (let i = 1;i <= a.length; i++) {
17675
+ let previous = row[0];
17676
+ row[0] = i;
17677
+ for (let j = 1;j <= b.length; j++) {
17678
+ const saved = row[j];
17679
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
17680
+ previous = saved;
17681
+ }
17682
+ }
17683
+ return row[b.length];
17684
+ }, suggestBranchName = (name, kinds) => {
17685
+ if (!kinds.length)
17686
+ return null;
17687
+ const slash = name.indexOf("/");
17688
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
17689
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
17690
+ const synonym = KIND_SYNONYMS[prefix];
17691
+ 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;
17692
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
17693
+ return `${kind}/${rest}`;
17694
+ }, kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", "), aliasNote = (kinds) => {
17695
+ const notes = [
17696
+ ["fix", "bugfix"],
17697
+ ["chore", "feature"]
17698
+ ].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
17699
+ return notes.length ? ` (${notes.join(", ")})` : "";
17650
17700
  }, validateBranchNamePolicy = (name, policy, source) => {
17651
17701
  const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
17702
+ const kinds = allowedKinds(policy.allowed);
17652
17703
  if (policy.protected.has(name.toLowerCase())) {
17653
- const correction = `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
17704
+ 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
17705
  return {
17655
17706
  ok: false,
17656
17707
  rule: "protected_ref",
@@ -17661,7 +17712,8 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17661
17712
  };
17662
17713
  }
17663
17714
  if (!policy.allowed.some((pattern) => pattern.test(name))) {
17664
- const correction = `choose a branch matching one of: ${allowed.join(", ")}`;
17715
+ const suggestion = suggestBranchName(name, kinds);
17716
+ 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
17717
  return {
17666
17718
  ok: false,
17667
17719
  rule: "allowed_pattern",
@@ -17700,7 +17752,52 @@ var withReleaseTracks = (workspaceRoot, policy) => {
17700
17752
  error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
17701
17753
  };
17702
17754
  }
17703
- }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
17755
+ }, resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy, gitFlowOf = (root, integration) => {
17756
+ try {
17757
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
17758
+ return null;
17759
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
17760
+ const names = branchNames(root);
17761
+ const production = productionBranchOf(names);
17762
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
17763
+ return { develop, production, developExists: names.has(develop) };
17764
+ } catch {
17765
+ return null;
17766
+ }
17767
+ }, releaseBranchesOf = (cwd) => {
17768
+ try {
17769
+ const resolved = resolveBranchPolicyFor(cwd);
17770
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
17771
+ return {
17772
+ preset: resolved.preset,
17773
+ branches: flow ? [flow.production, flow.develop] : [],
17774
+ production: flow ? flow.production : resolved.defaultTargetBranch
17775
+ };
17776
+ } catch {
17777
+ return { preset: "unknown", branches: [], production: null };
17778
+ }
17779
+ }, releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase()), branchGuidance = (cwd) => {
17780
+ try {
17781
+ const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
17782
+ const kinds = allowedKinds(patterns);
17783
+ const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
17784
+ const anyName = patterns.some((pattern) => pattern.source === "^.*$");
17785
+ const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
17786
+ 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.`;
17787
+ const resolved = vcsConfig("resolve", cwd);
17788
+ const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
17789
+ const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
17790
+ if (flow?.developExists)
17791
+ 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}.`;
17792
+ const names = branchNames(cwd);
17793
+ const configured = String(resolved.baseBranch ?? integration ?? "") || null;
17794
+ const trunk = productionBranchOf(names);
17795
+ 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.";
17796
+ return `${allowed} ${base}`;
17797
+ } catch {
17798
+ return null;
17799
+ }
17800
+ }, globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
17704
17801
  var init_branch = __esm(() => {
17705
17802
  init_branch_policy();
17706
17803
  init_commit_flavors();
@@ -17709,33 +17806,63 @@ var init_branch = __esm(() => {
17709
17806
  init_workspaces();
17710
17807
  init_vcs_config();
17711
17808
  init_release_tracks();
17809
+ KIND_SYNONYMS = {
17810
+ fix: "bugfix",
17811
+ fixes: "bugfix",
17812
+ bug: "bugfix",
17813
+ bugs: "bugfix",
17814
+ bugfixes: "bugfix",
17815
+ feat: "feature",
17816
+ features: "feature",
17817
+ chore: "feature",
17818
+ docs: "feature",
17819
+ doc: "feature",
17820
+ refactor: "feature",
17821
+ test: "feature",
17822
+ tests: "feature",
17823
+ ci: "feature",
17824
+ build: "feature",
17825
+ perf: "feature",
17826
+ style: "feature",
17827
+ deps: "feature",
17828
+ task: "feature",
17829
+ enhancement: "feature",
17830
+ hot: "hotfix",
17831
+ hotfixes: "hotfix",
17832
+ releases: "release",
17833
+ rel: "release"
17834
+ };
17712
17835
  });
17713
17836
 
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" };
17837
+ // packages/workit-core/src/hooks/session-marker.ts
17838
+ import { createHash as createHash13 } from "node:crypto";
17839
+ import fs15 from "node:fs";
17840
+ import path18 from "node:path";
17841
+ var sessionMarkerFile = (input, session, store) => {
17842
+ let dir = store;
17843
+ if (dir === undefined) {
17844
+ const location = resolveStore(input.cwd);
17845
+ if (location instanceof Error || !location.shared)
17846
+ return null;
17847
+ dir = location.dir;
17848
+ }
17849
+ const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
17850
+ return path18.join(dir, "hooks", `skills-${name}.json`);
17851
+ }, list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [], readSessionMarker = (file) => {
17852
+ try {
17853
+ const value = JSON.parse(fs15.readFileSync(file, "utf8"));
17854
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
17855
+ } catch {
17856
+ return { nudged: [], loaded: [], raw: [] };
17857
+ }
17858
+ }, writeSessionMarker = (file, marker) => {
17859
+ try {
17860
+ fs15.mkdirSync(path18.dirname(file), { recursive: true });
17861
+ fs15.writeFileSync(file, JSON.stringify(marker));
17862
+ } catch {}
17736
17863
  };
17737
- var init_policy = __esm(() => {
17738
- init_route_intent();
17864
+ var init_session_marker = __esm(() => {
17865
+ init_paths();
17739
17866
  });
17740
17867
 
17741
17868
  // packages/workit-core/src/hooks/shell-words.ts
@@ -17859,219 +17986,12 @@ var closingQuote = (command, start, dialect) => {
17859
17986
  return segments;
17860
17987
  };
17861
17988
 
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
17989
  // packages/workit-core/src/hooks/raw-git.ts
18070
17990
  import { spawnSync as spawnSync11 } from "node:child_process";
18071
17991
  import { createHash as createHash14 } from "node:crypto";
18072
17992
  import fs16 from "node:fs";
18073
17993
  import os4 from "node:os";
18074
- import path20 from "node:path";
17994
+ import path19 from "node:path";
18075
17995
  function rawInvocations(command, dialect = "posix", depth = 0) {
18076
17996
  const found = [];
18077
17997
  let dir = null;
@@ -18181,7 +18101,7 @@ function rawAction(invocation) {
18181
18101
  function rawGitPre(input, command, options = {}) {
18182
18102
  try {
18183
18103
  if (!mentionsRawTool(command))
18184
- return NONE3;
18104
+ return NONE2;
18185
18105
  const dialect = dialectOf(input);
18186
18106
  const nudges = new Map;
18187
18107
  let store = null;
@@ -18189,25 +18109,36 @@ function rawGitPre(input, command, options = {}) {
18189
18109
  store ??= repo.location.dir;
18190
18110
  if (action.kind === "merge")
18191
18111
  return deny(refusal(action, null));
18192
- if (action.kind === "push")
18112
+ if (action.kind === "push" && toPushRemote(repo, action.remote)) {
18113
+ if (action.mirror)
18114
+ 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
18115
  for (const target of action.targets) {
18194
18116
  const branch = target ?? repo.branch;
18195
- if (branch && guardedBranch(repo.dir, branch) && toPushRemote(repo, action.remote))
18196
- return deny(refusal(action, branch));
18117
+ if (branch && guardedBranch(repo.dir, branch))
18118
+ return deny(refusal(action, branch, repo.dir));
18197
18119
  }
18120
+ const remote = action.remote ?? pushRemoteName(repo.dir, repo.branch) ?? "origin";
18121
+ const facts = refFacts(repo, remote);
18122
+ const tagDeny = tagRefusal(action, facts);
18123
+ if (tagDeny)
18124
+ return deny(tagDeny);
18125
+ const nameDeny = action.deletes ? null : offPolicyPush(action, repo, facts, remote);
18126
+ if (nameDeny)
18127
+ return deny(nameDeny);
18128
+ }
18198
18129
  const nudge = nudgeFor(action, input.host, input.session.id);
18199
18130
  if (nudge && !nudges.has(nudgeKind(action)))
18200
18131
  nudges.set(nudgeKind(action), nudge);
18201
18132
  }
18202
18133
  if (options.nudge === false || !store || nudges.size === 0)
18203
- return NONE3;
18134
+ return NONE2;
18204
18135
  if (workitInvocations(command, dialect).length > 0)
18205
- return NONE3;
18136
+ return NONE2;
18206
18137
  const shown = unseenNudges(input, nudges, store);
18207
18138
  return shown.length ? { kind: "context", text: shown.join(`
18208
- `) } : NONE3;
18139
+ `) } : NONE2;
18209
18140
  } catch {
18210
- return NONE3;
18141
+ return NONE2;
18211
18142
  }
18212
18143
  }
18213
18144
  function noteRawCommit(input, command, key) {
@@ -18219,7 +18150,7 @@ function noteRawCommit(input, command, key) {
18219
18150
  if (action.kind !== "commit")
18220
18151
  continue;
18221
18152
  const file = markerFile(repo, input.session.id, key);
18222
- fs16.mkdirSync(path20.dirname(file), { recursive: true });
18153
+ fs16.mkdirSync(path19.dirname(file), { recursive: true });
18223
18154
  fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18224
18155
  `);
18225
18156
  }
@@ -18246,11 +18177,11 @@ function settlePendingCommit(input) {
18246
18177
  settle2(input, repo, file);
18247
18178
  } catch {}
18248
18179
  }
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) => {
18180
+ 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
18181
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
18251
18182
  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);
18183
+ const expanded = next === "~" || next.startsWith("~/") ? path19.join(os4.homedir(), next.slice(1)) : next;
18184
+ return base === null || isAbsolute(expanded) ? expanded : path19.join(base, expanded);
18254
18185
  }, cdTarget = (words) => {
18255
18186
  const args = words.slice(1);
18256
18187
  const flag = args.findIndex((word) => /^-(?:literal)?path$/i.test(word));
@@ -18265,7 +18196,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18265
18196
  return words;
18266
18197
  if (/^\w+=/.test(first))
18267
18198
  words = words.slice(1);
18268
- else if (PREFIXES3.has(first)) {
18199
+ else if (PREFIXES2.has(first)) {
18269
18200
  words = words.slice(1);
18270
18201
  while (words[0]?.startsWith("-"))
18271
18202
  words = words.slice(words[0] === "-u" ? 2 : 1);
@@ -18281,6 +18212,10 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18281
18212
  let force = false;
18282
18213
  let lease = false;
18283
18214
  let wholesale = false;
18215
+ let allTags = false;
18216
+ let deletes = false;
18217
+ let allBranches = false;
18218
+ let mirror = false;
18284
18219
  let remote = null;
18285
18220
  const positionals = [];
18286
18221
  for (let index = 1;index < args.length; index++) {
@@ -18301,14 +18236,22 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18301
18236
  force = true;
18302
18237
  else if (arg.startsWith("--force-with-lease") || arg === "--force-if-includes")
18303
18238
  lease = true;
18304
- else if (["--all", "--mirror", "--branches", "--tags"].includes(arg))
18305
- wholesale = true;
18239
+ else if (arg === "--delete" || arg === "-d")
18240
+ deletes = true;
18241
+ else if (arg === "--tags")
18242
+ allTags = wholesale = true;
18243
+ else if (arg === "--all" || arg === "--branches")
18244
+ allBranches = wholesale = true;
18245
+ else if (arg === "--mirror")
18246
+ mirror = wholesale = true;
18306
18247
  else if (!arg.startsWith("-"))
18307
18248
  positionals.push(arg);
18308
18249
  }
18309
18250
  const specs = remote === null ? positionals.slice(1) : positionals;
18310
18251
  remote ??= positionals[0] ?? null;
18311
18252
  const targets = [];
18253
+ const tags = [];
18254
+ const deleted = [];
18312
18255
  for (const raw of specs) {
18313
18256
  if (raw.startsWith("+"))
18314
18257
  force = true;
@@ -18317,16 +18260,29 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18317
18260
  const destination = colon >= 0 ? spec.slice(colon + 1) : spec;
18318
18261
  if (!destination)
18319
18262
  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);
18263
+ const name = destination === "HEAD" ? null : destination.startsWith("refs/heads/") ? destination.slice(11) : destination.startsWith("refs/") ? undefined : destination;
18264
+ if (destination.startsWith("refs/tags/"))
18265
+ tags.push(destination.slice(10));
18266
+ if (name === undefined)
18267
+ continue;
18268
+ targets.push(name);
18269
+ if (colon === 0 && name !== null)
18270
+ deleted.push(name);
18326
18271
  }
18327
18272
  if (specs.length === 0 && !wholesale)
18328
18273
  targets.push(null);
18329
- return { kind: "push", targets, blindForce: force && !lease, remote };
18274
+ return {
18275
+ kind: "push",
18276
+ targets,
18277
+ blindForce: force && !lease,
18278
+ remote,
18279
+ ...tags.length ? { tags } : {},
18280
+ ...allTags ? { allTags: true } : {},
18281
+ ...deletes ? { deletes: true } : {},
18282
+ ...deleted.length ? { deleted } : {},
18283
+ ...allBranches ? { allBranches: true } : {},
18284
+ ...mirror ? { mirror: true } : {}
18285
+ };
18330
18286
  }, VALUE_OPTIONS, asksHelp = (invocation) => {
18331
18287
  const { tool, args } = invocation;
18332
18288
  if (args[0] === "help" || tool !== "git" && args[1] === "help")
@@ -18376,25 +18332,72 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18376
18332
  if (!git || git.reftable)
18377
18333
  return null;
18378
18334
  try {
18379
- const head = fs16.readFileSync(path20.join(git.gitDir, "HEAD"), "utf8").trim();
18335
+ const head = fs16.readFileSync(path19.join(git.gitDir, "HEAD"), "utf8").trim();
18380
18336
  if (/^[0-9a-f]{40,64}$/.test(head))
18381
18337
  return head;
18382
18338
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
18383
18339
  if (!ref)
18384
18340
  return null;
18385
- const common = path20.dirname(repo.location.dir);
18341
+ const common = path19.dirname(repo.location.dir);
18386
18342
  try {
18387
- return fs16.readFileSync(path20.join(common, ref), "utf8").trim() || null;
18343
+ return fs16.readFileSync(path19.join(common, ref), "utf8").trim() || null;
18388
18344
  } catch {}
18389
- const packed = fs16.readFileSync(path20.join(common, "packed-refs"), "utf8");
18345
+ const packed = fs16.readFileSync(path19.join(common, "packed-refs"), "utf8");
18390
18346
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
18391
18347
  } catch {
18392
18348
  return null;
18393
18349
  }
18394
- }, quote = (text) => `\`${text}\``, refusal = (action, branch) => {
18350
+ }, quote = (text) => `\`${text}\``, refusal = (action, branch, dir) => {
18395
18351
  if (action.kind === "merge")
18396
18352
  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")}).`;
18353
+ const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
18354
+ if (dir && branch && plainPush && releaseBranch(dir, branch))
18355
+ 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
18356
  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")}.`;
18357
+ }, refFacts = (repo, remote) => {
18358
+ const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
18359
+ const listed = spawnSync11("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags", `refs/remotes/${remote}`], { cwd: repo.dir, encoding: "utf8", timeout: 5000 });
18360
+ const remotePrefix = `refs/remotes/${remote}/`;
18361
+ for (const ref of (listed.stdout ?? "").split(`
18362
+ `)) {
18363
+ if (ref.startsWith("refs/heads/"))
18364
+ facts.heads.add(ref.slice(11));
18365
+ else if (ref.startsWith("refs/tags/"))
18366
+ facts.tags.add(ref.slice(10));
18367
+ else if (ref.startsWith(remotePrefix))
18368
+ facts.remoteHeads.add(ref.slice(remotePrefix.length));
18369
+ }
18370
+ return facts;
18371
+ }, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts) => {
18372
+ const tags = [
18373
+ ...action.tags ?? [],
18374
+ ...action.targets.filter((name) => name !== null && isTag(facts, name))
18375
+ ];
18376
+ if (!tags.length && !action.allTags)
18377
+ return null;
18378
+ const named = tags.length ? tags.join(" --release ") : "v<version>";
18379
+ 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")}).`;
18380
+ }, remoteHas = (repo, remote, branch) => {
18381
+ const listed = spawnSync11("git", ["ls-remote", "--heads", remote, `refs/heads/${branch}`], {
18382
+ cwd: repo.dir,
18383
+ encoding: "utf8",
18384
+ timeout: 3000,
18385
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
18386
+ });
18387
+ return listed.status !== 0 || (listed.stdout ?? "").trim() !== "";
18388
+ }, offPolicyPush = (action, repo, facts, remote) => {
18389
+ const names = action.allBranches ? [...facts.heads] : action.targets.map((target) => target ?? repo.branch);
18390
+ for (const branch of names) {
18391
+ if (!branch || action.deleted?.includes(branch) || isTag(facts, branch))
18392
+ continue;
18393
+ if (facts.remoteHeads.has(branch))
18394
+ continue;
18395
+ const check = validateBranchNameFor(repo.dir, branch);
18396
+ if (check.ok || check.rule !== "allowed_pattern" || remoteHas(repo, remote, branch))
18397
+ continue;
18398
+ return `workit: raw ${quote("git push")} onto ${branch}: ${check.error}`;
18399
+ }
18400
+ return null;
18398
18401
  }, nudgeFor = (action, host, session) => {
18399
18402
  switch (action.kind) {
18400
18403
  case "commit": {
@@ -18419,7 +18422,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18419
18422
  const action = rawAction(invocation);
18420
18423
  if (!action)
18421
18424
  continue;
18422
- const dir = path20.resolve(cwd, invocation.dir ?? ".");
18425
+ const dir = path19.resolve(cwd, invocation.dir ?? ".");
18423
18426
  if (!repos.has(dir)) {
18424
18427
  const repo = repoAt(dir);
18425
18428
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -18446,7 +18449,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18446
18449
  raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
18447
18450
  });
18448
18451
  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) => {
18452
+ }, 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
18453
  const run = spawnSync11("git", [
18451
18454
  "log",
18452
18455
  "-1",
@@ -18506,9 +18509,9 @@ var init_raw_git = __esm(() => {
18506
18509
  init_ledger();
18507
18510
  init_paths();
18508
18511
  init_session_marker();
18509
- NONE3 = { kind: "none" };
18512
+ NONE2 = { kind: "none" };
18510
18513
  SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18511
- PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18514
+ PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
18512
18515
  SCRIPT_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/;
18513
18516
  CD = new Set(["cd", "chdir", "set-location", "sl"]);
18514
18517
  PUSHD = new Set(["pushd", "push-location"]);
@@ -18566,8 +18569,298 @@ var init_raw_git = __esm(() => {
18566
18569
  };
18567
18570
  });
18568
18571
 
18569
- // packages/workit-core/src/hooks/stop.ts
18572
+ // packages/workit-core/src/core/route-intent.ts
18570
18573
  import { spawnSync as spawnSync12 } from "node:child_process";
18574
+ import { existsSync as existsSync6 } from "node:fs";
18575
+ import path20 from "node:path";
18576
+ var UNKNOWABLE, SWITCH_CREATE, CHECKOUT_CREATE, WORKTREE_CREATE, BRANCH_NO_CREATE, BRANCH_RENAME, flagValue = (args, flags) => {
18577
+ for (let index = 1;index < args.length; index++) {
18578
+ const arg = args[index];
18579
+ if (arg === "--")
18580
+ return null;
18581
+ if (flags.has(arg))
18582
+ return args[index + 1] ?? null;
18583
+ const eq = arg.indexOf("=");
18584
+ if (arg.startsWith("--") && eq > 0 && flags.has(arg.slice(0, eq)))
18585
+ return arg.slice(eq + 1);
18586
+ if (!arg.startsWith("--") && arg.length > 2 && flags.has(arg.slice(0, 2)))
18587
+ return arg.slice(2);
18588
+ }
18589
+ return null;
18590
+ }, WORKTREE_VALUES, worktreeTarget = (args) => {
18591
+ const named = flagValue(args, WORKTREE_CREATE);
18592
+ if (named !== null)
18593
+ return named;
18594
+ const positionals = [];
18595
+ for (let index = 1;index < args.length; index++) {
18596
+ const arg = args[index];
18597
+ if (arg === "--detach" || arg === "-d")
18598
+ return null;
18599
+ if (WORKTREE_VALUES.has(arg))
18600
+ index++;
18601
+ else if (!arg.startsWith("-"))
18602
+ positionals.push(arg);
18603
+ }
18604
+ if (positionals.length !== 1)
18605
+ return null;
18606
+ const base = positionals[0].split(/[\\/]/u).findLast(Boolean) ?? "";
18607
+ return base && base !== "." && base !== ".." ? base : null;
18608
+ }, branchCommandTarget = (args) => {
18609
+ let rename = false;
18610
+ const positionals = [];
18611
+ for (let index = 1;index < args.length; index++) {
18612
+ const arg = args[index];
18613
+ if (arg === "--") {
18614
+ positionals.push(...args.slice(index + 1));
18615
+ break;
18616
+ }
18617
+ const flag = arg.split("=", 1)[0];
18618
+ if (BRANCH_NO_CREATE.has(flag))
18619
+ return null;
18620
+ if (BRANCH_RENAME.has(flag))
18621
+ rename = true;
18622
+ else if (!arg.startsWith("-"))
18623
+ positionals.push(arg);
18624
+ }
18625
+ if (positionals.length < 1 || positionals.length > 2)
18626
+ return null;
18627
+ return rename ? positionals.at(-1) ?? null : positionals[0];
18628
+ }, gitBranchCreation = (args) => {
18629
+ const [sub, second] = args;
18630
+ 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;
18631
+ return target && !target.startsWith("-") && !UNKNOWABLE.test(target) ? target : null;
18632
+ }, shellBranchTargets = (command, dialect = "posix") => {
18633
+ if (!/\bgit\b/u.test(command))
18634
+ return [];
18635
+ const found = [];
18636
+ for (const invocation of rawInvocations(command, dialect)) {
18637
+ if (invocation.tool !== "git")
18638
+ continue;
18639
+ const branch = gitBranchCreation(invocation.args);
18640
+ if (branch)
18641
+ found.push({ branch, dir: invocation.dir });
18642
+ }
18643
+ return found;
18644
+ }, localBranchExists = (dir, branch) => spawnSync12("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
18645
+ cwd: dir,
18646
+ stdio: "ignore",
18647
+ timeout: 5000
18648
+ }).status === 0, shellBranchPolicyViolation = (root, command, dialect = "posix") => {
18649
+ let judged = false;
18650
+ for (const target of shellBranchTargets(command, dialect)) {
18651
+ if (target.dir === undefined)
18652
+ continue;
18653
+ const dir = target.dir === null ? root : path20.resolve(root, target.dir);
18654
+ if (target.dir !== null && (!existsSync6(dir) || !inWorkitWorkspace(dir)))
18655
+ continue;
18656
+ if (localBranchExists(dir, target.branch))
18657
+ continue;
18658
+ judged = true;
18659
+ const check = validateBranchNameFor(dir, target.branch);
18660
+ if (!check.ok)
18661
+ return check;
18662
+ }
18663
+ return judged ? { ok: true } : null;
18664
+ };
18665
+ var init_route_intent = __esm(() => {
18666
+ init_raw_git();
18667
+ init_branch();
18668
+ UNKNOWABLE = /[$`*?[\]{}~]/u;
18669
+ SWITCH_CREATE = new Set(["-c", "--create", "-C", "--force-create", "--orphan"]);
18670
+ CHECKOUT_CREATE = new Set(["-b", "-B", "--orphan"]);
18671
+ WORKTREE_CREATE = new Set(["-b", "-B"]);
18672
+ 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"]));
18673
+ BRANCH_RENAME = new Set(["-m", "-M", "--move", "-c", "-C", "--copy"]);
18674
+ WORKTREE_VALUES = new Set(["--reason"]);
18675
+ });
18676
+
18677
+ // packages/workit-core/src/hooks/policy.ts
18678
+ var shellPolicy = (cwd, command, dialect = "posix") => {
18679
+ const policy = shellBranchPolicyViolation(cwd, command, dialect);
18680
+ return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
18681
+ };
18682
+ var init_policy = __esm(() => {
18683
+ init_route_intent();
18684
+ });
18685
+
18686
+ // packages/workit-core/src/hooks/write-gate.ts
18687
+ import { realpathSync as realpathSync5 } from "node:fs";
18688
+ import path21 from "node:path";
18689
+ function writeGate(cwd, paths) {
18690
+ try {
18691
+ const gate = gateFor(cwd);
18692
+ if (!gate || gate.blockers.length === 0)
18693
+ return NONE3;
18694
+ if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
18695
+ return NONE3;
18696
+ const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
18697
+ return {
18698
+ kind: "deny",
18699
+ reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
18700
+ unblock: gate.blockers[0].reason
18701
+ };
18702
+ } catch {
18703
+ return NONE3;
18704
+ }
18705
+ }
18706
+ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
18707
+ let rest = "";
18708
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18709
+ try {
18710
+ return path21.join(realpathSync5(dir), rest);
18711
+ } catch {
18712
+ if (path21.dirname(dir) === dir)
18713
+ return path21.resolve(file);
18714
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18715
+ }
18716
+ }
18717
+ }, exempt = (cwd, root, file, plans) => {
18718
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18719
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18720
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
18721
+ return true;
18722
+ const posix = relative.split(path21.sep).join("/");
18723
+ const segments = posix.split("/");
18724
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18725
+ }, PREFIXES3, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
18726
+ let words = input;
18727
+ while (words.length && (PREFIXES3.has(words[0]) || /^\w+=/.test(words[0])))
18728
+ words = words.slice(1);
18729
+ const [verb, ...args] = words;
18730
+ const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
18731
+ if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
18732
+ return [];
18733
+ switch (name) {
18734
+ case "touch":
18735
+ case "mkdir":
18736
+ case "rm":
18737
+ case "rmdir":
18738
+ case "tee":
18739
+ case "mv":
18740
+ case "truncate":
18741
+ return operands(args);
18742
+ case "cp":
18743
+ case "ln":
18744
+ case "install": {
18745
+ const target = args.indexOf("-t");
18746
+ if (target >= 0)
18747
+ return args[target + 1] ? [args[target + 1]] : null;
18748
+ const last = operands(args).at(-1);
18749
+ return last ? [last] : null;
18750
+ }
18751
+ case "dd":
18752
+ return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
18753
+ case "sed":
18754
+ case "perl": {
18755
+ if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
18756
+ return [];
18757
+ const scripted = args.some((item) => item === "-e" || item === "-f");
18758
+ const files = [];
18759
+ for (let index = 0;index < args.length; index++) {
18760
+ if (args[index] === "-e" || args[index] === "-f")
18761
+ index++;
18762
+ else if (!args[index].startsWith("-"))
18763
+ files.push(args[index]);
18764
+ }
18765
+ return scripted ? files : files.slice(1);
18766
+ }
18767
+ case "patch":
18768
+ return null;
18769
+ case "set-content":
18770
+ case "add-content":
18771
+ case "out-file":
18772
+ case "new-item":
18773
+ case "ni":
18774
+ case "remove-item":
18775
+ case "del":
18776
+ case "copy-item":
18777
+ case "move-item": {
18778
+ const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
18779
+ if (flag >= 0)
18780
+ return args[flag + 1] ? [args[flag + 1]] : null;
18781
+ const files = operands(args);
18782
+ if (name === "move-item")
18783
+ return files.length ? files : null;
18784
+ const file = name === "copy-item" ? files.at(-1) : files[0];
18785
+ return file ? [file] : null;
18786
+ }
18787
+ case "git": {
18788
+ const sub = operands(args)[0];
18789
+ if (sub === "apply")
18790
+ return null;
18791
+ if (sub === "restore" || sub === "mv" || sub === "rm")
18792
+ return operands(args).slice(1);
18793
+ if (sub === "checkout" && args.includes("--"))
18794
+ return args.slice(args.indexOf("--") + 1);
18795
+ return [];
18796
+ }
18797
+ default:
18798
+ return [];
18799
+ }
18800
+ }, shellWrites = (command) => {
18801
+ const targets = [];
18802
+ let unknown = false;
18803
+ for (const segment of segmentsOf(command)) {
18804
+ targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
18805
+ const edits = editsOf(segment.words);
18806
+ if (edits === null)
18807
+ unknown = true;
18808
+ else
18809
+ targets.push(...edits);
18810
+ }
18811
+ if (!unknown && targets.length === 0)
18812
+ return { writes: false, targets: null };
18813
+ return { writes: true, targets: unknown ? null : targets };
18814
+ }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path21.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
18815
+ const store = new TaskStore(cwd);
18816
+ const listed = store.listTaskIndex();
18817
+ if (!listed.ok)
18818
+ return null;
18819
+ const key = store.currentKey();
18820
+ if (!key.ok)
18821
+ return null;
18822
+ const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
18823
+ if (!entry)
18824
+ return { blockers: [], root: store.root, plans: new Set };
18825
+ const ledger = ledgerPath(store.root);
18826
+ const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18827
+ const hit = cache2.get(store.root);
18828
+ if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18829
+ return hit;
18830
+ const task = store.readTask(entry.id);
18831
+ if (!task.ok)
18832
+ return null;
18833
+ const workspace = store.readWorkspace();
18834
+ const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18835
+ const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18836
+ const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18837
+ return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18838
+ });
18839
+ const gate = {
18840
+ blockers,
18841
+ root: store.root,
18842
+ plans,
18843
+ key: cacheKey,
18844
+ planKey: planKeyOf(store.root, plans)
18845
+ };
18846
+ cache2.set(store.root, gate);
18847
+ if (cache2.size > 32)
18848
+ cache2.delete(cache2.keys().next().value);
18849
+ return gate;
18850
+ };
18851
+ var init_write_gate = __esm(() => {
18852
+ init_task_store();
18853
+ init_derive();
18854
+ init_task_evaluation();
18855
+ init_ledger();
18856
+ NONE3 = { kind: "none" };
18857
+ CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18858
+ PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18859
+ cache2 = new Map;
18860
+ });
18861
+
18862
+ // packages/workit-core/src/hooks/stop.ts
18863
+ import { spawnSync as spawnSync13 } from "node:child_process";
18571
18864
  function stopObligation(input, descriptor) {
18572
18865
  try {
18573
18866
  const session = input.session.id;
@@ -18631,7 +18924,7 @@ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
18631
18924
  `);
18632
18925
  return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
18633
18926
  }, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
18634
- const run = spawnSync12("git", args, {
18927
+ const run = spawnSync13("git", args, {
18635
18928
  cwd,
18636
18929
  encoding: "utf8",
18637
18930
  timeout: 3000,
@@ -18941,10 +19234,10 @@ var init_skill_nudge = __esm(() => {
18941
19234
  });
18942
19235
 
18943
19236
  // 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";
19237
+ import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
19238
+ import path22 from "node:path";
18946
19239
  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))
19240
+ if (!nonEmpty2(value) || !path22.isAbsolute(value) || !existsSync7(value))
18948
19241
  return null;
18949
19242
  try {
18950
19243
  return statSync2(value).isDirectory() ? realpathSync6(value) : null;
@@ -19040,7 +19333,7 @@ function handleHook(input, deps) {
19040
19333
  if (skillRead)
19041
19334
  recordSkillLoad(input, skillRead, "read");
19042
19335
  const canDeny = usable2(descriptor.shellPolicy.deny);
19043
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
19336
+ const policy = canDeny ? shellPolicy(input.cwd, event.command, event.dialect) : NONE5;
19044
19337
  if (policy.kind !== "none")
19045
19338
  return policy;
19046
19339
  const postTool = usable2(descriptor.events["shell.post"].support);
@@ -19089,18 +19382,19 @@ var usable2 = (value) => value === "native" || value === "partial", NONE5, resen
19089
19382
  const prefix = descriptor.subagents.agentPrefix;
19090
19383
  const name = agentType.startsWith(prefix) ? agentType.slice(prefix.length) : "";
19091
19384
  return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
19092
- }, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), CLAUDE_WORKTREE, ISOLATED_GIT = 'Run git as plain, separate commands inside your worktree (`git switch -c <branch> <base>`, `git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a commit-msg hook that adds the trailer), never `workit git …`: Claude Code\'s worktree isolation refuses a command that wraps git and cannot be turned off (https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation). Never dodge that check by renaming or re-wrapping git. Use workit for the non-git verbs (`workit check`, `workit ledger`, `workit pr`).', subagentStartText = (input, descriptor, event) => {
19385
+ }, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), CLAUDE_WORKTREE, isolatedGit = (cwd) => `Run git as plain, separate commands inside your worktree (\`git switch -c <branch> <base>\`, ${workspaceTrailers(cwd).enabled ? '`git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a prepare-commit-msg hook that adds the trailer' : "`git commit` (this workspace sets trailers: false: add no Workit-Session trailer), `git push`"}; add Co-Authored-By with \`git commit --trailer\` in the same commit, never by amending), never \`workit git …\`: Claude Code's worktree isolation refuses a command that wraps git and cannot be turned off (https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation). Never dodge that check by renaming or re-wrapping git. Use workit for the non-git verbs (\`workit check\`, \`workit ledger\`, \`workit pr\`).`, subagentStartText = (input, descriptor, event) => {
19093
19386
  const session = input.session.id ?? null;
19094
19387
  const role = workitRole(descriptor, event.agentType);
19095
19388
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
19096
19389
  const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
19390
+ const guide = role === "implementer" ? branchGuidance(input.cwd) : null;
19097
19391
  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.`;
19392
+ return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${isolatedGit(input.cwd)} 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
19393
  if (isolated)
19100
- return `${ISOLATED_GIT}
19394
+ return `${isolatedGit(input.cwd)}
19101
19395
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
19102
19396
  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.`;
19397
+ 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
19398
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
19105
19399
  if (role !== "judge" || !event.agentId)
19106
19400
  return readOnly;
@@ -19138,6 +19432,8 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
19138
19432
  };
19139
19433
  var init_handle = __esm(() => {
19140
19434
  init_context();
19435
+ init_branch();
19436
+ init_workspaces();
19141
19437
  init_host_session();
19142
19438
  init_policy();
19143
19439
  init_raw_git();
@@ -19682,7 +19978,7 @@ var init_codex = __esm(() => {
19682
19978
 
19683
19979
  // packages/workit-core/src/hooks/hosts/cursor.ts
19684
19980
  import fs17 from "node:fs";
19685
- import path22 from "node:path";
19981
+ import path23 from "node:path";
19686
19982
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
19687
19983
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
19688
19984
  return { ok: false, error: "hook_event_name is required" };
@@ -19763,7 +20059,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19763
20059
  };
19764
20060
  }
19765
20061
  }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
19766
- if (!file || !path22.isAbsolute(file))
20062
+ if (!file || !path23.isAbsolute(file))
19767
20063
  return null;
19768
20064
  try {
19769
20065
  const size = fs17.statSync(file).size;
@@ -19799,7 +20095,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19799
20095
  } catch {
19800
20096
  return null;
19801
20097
  }
19802
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path22.sep) ? root : `${root}${path22.sep}`), hookCwd = (input) => {
20098
+ }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path23.sep) ? root : `${root}${path23.sep}`), hookCwd = (input) => {
19803
20099
  const cwd = existingDirectory(input.cwd);
19804
20100
  if (cwd && input.hook_event_name === "beforeShellExecution")
19805
20101
  return cwd;