auto-mode 0.10.0 → 0.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.
@@ -1,8 +1,22 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
2
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
2
4
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, normalize, parse, relative, resolve } from "node:path";
4
5
  import { spawn } from "node:child_process";
5
6
  import { existsSync } from "node:fs";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/bypass/resolve-edit-target.ts
9
+ async function resolveEditTarget(path) {
10
+ const missing = [];
11
+ for (let current = path;; current = dirname(current)) try {
12
+ const real = await realpath(current);
13
+ return join(real, ...missing.toReversed());
14
+ } catch {
15
+ if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
16
+ missing.push(basename(current));
17
+ }
18
+ }
19
+ //#endregion
6
20
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
7
21
  function getEnumValues(entries) {
8
22
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -2922,7 +2936,7 @@ function bucketFor(state, inst) {
2922
2936
  return bucket;
2923
2937
  }
2924
2938
  let handoff;
2925
- const open = [];
2939
+ const open$1 = [];
2926
2940
  const memo = {
2927
2941
  alloc(_inst, payload, empty) {
2928
2942
  const bucket = handoff;
@@ -2933,7 +2947,7 @@ const memo = {
2933
2947
  issues: null
2934
2948
  };
2935
2949
  bucket.set(payload.value, entry);
2936
- open.push(entry);
2950
+ open$1.push(entry);
2937
2951
  return empty;
2938
2952
  },
2939
2953
  guard(inst) {
@@ -2999,10 +3013,10 @@ const memo = {
2999
3013
  return payload;
3000
3014
  }
3001
3015
  handoff = bucket;
3002
- const depth = open.length;
3016
+ const depth = open$1.length;
3003
3017
  const result = base(payload, ctx);
3004
3018
  handoff = void 0;
3005
- const entry = open.length > depth ? open.pop() : void 0;
3019
+ const entry = open$1.length > depth ? open$1.pop() : void 0;
3006
3020
  if (result instanceof Promise) return result.then((r) => {
3007
3021
  if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
3008
3022
  return r;
@@ -5526,79 +5540,414 @@ function refine(fn, _params = {}) {
5526
5540
  function superRefine(fn, params) {
5527
5541
  return /* @__PURE__ */ _superRefine(fn, params);
5528
5542
  }
5543
+ const PRESETS = {
5544
+ jev: {
5545
+ protocol: "system-one",
5546
+ baseURL: "https://api.typesafe.ai",
5547
+ model: "jev-1.13.0",
5548
+ apiKeyEnv: "TYPESAFE_API_KEY",
5549
+ reasoning: false,
5550
+ maxTokens: 3e3,
5551
+ timeoutMs: 5e3
5552
+ },
5553
+ spark: {
5554
+ baseURL: "https://api.meta.ai",
5555
+ model: "muse-spark-1.3-contributor",
5556
+ apiKeyEnv: "META_API_KEY",
5557
+ reasoning: true,
5558
+ maxTokens: 3e3,
5559
+ timeoutMs: 45e3
5560
+ },
5561
+ claude: {
5562
+ baseURL: "https://api.anthropic.com",
5563
+ model: "claude-haiku-4-5-20251001",
5564
+ apiKeyEnv: "ANTHROPIC_API_KEY",
5565
+ reasoning: true,
5566
+ maxTokens: 3e3,
5567
+ timeoutMs: 45e3
5568
+ },
5569
+ glm: {
5570
+ baseURL: "https://api.z.ai/api/anthropic",
5571
+ model: "glm-5.3-flash",
5572
+ apiKeyEnv: "ZAI_API_KEY",
5573
+ reasoning: true,
5574
+ maxTokens: 3e3,
5575
+ timeoutMs: 6e4
5576
+ }
5577
+ };
5578
+ const MESSAGES_DEFAULTS = {
5579
+ protocol: "messages",
5580
+ baseURL: "https://api.anthropic.com",
5581
+ reasoning: true,
5582
+ maxTokens: 3e3,
5583
+ timeoutMs: 45e3
5584
+ };
5585
+ function findPreset(id) {
5586
+ return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5587
+ }
5529
5588
  //#endregion
5530
- //#region src/config/load-claude-rules.ts
5531
- async function loadClaudeRules(path) {
5532
- const empty = {
5533
- environment: [],
5534
- allow: [],
5535
- soft_deny: [],
5536
- hard_deny: []
5537
- };
5538
- if (path === null) return empty;
5539
- const configDir = process.env["CLAUDE_CONFIG_DIR"];
5540
- const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5541
- const settingsPath = path ?? join(base, "settings.json");
5589
+ //#region src/config/read-api-key-from-command.ts
5590
+ function readApiKeyFromCommand(command, options = {}) {
5591
+ const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
5592
+ if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
5593
+ return new Promise((resolve) => {
5594
+ const grouped = process.platform !== "win32";
5595
+ const child = spawn("/bin/sh", ["-c", command], {
5596
+ detached: grouped,
5597
+ stdio: [
5598
+ "ignore",
5599
+ "pipe",
5600
+ "ignore"
5601
+ ]
5602
+ });
5603
+ let output = "";
5604
+ let settled = false;
5605
+ let stopped = false;
5606
+ const setResult = (value) => {
5607
+ if (!settled) {
5608
+ settled = true;
5609
+ clearTimeout(timer);
5610
+ options.signal?.removeEventListener("abort", stopChild);
5611
+ resolve(value);
5612
+ }
5613
+ };
5614
+ const stopChild = () => {
5615
+ if (stopped) return;
5616
+ stopped = true;
5617
+ try {
5618
+ if (grouped && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
5619
+ else child.kill("SIGKILL");
5620
+ } catch {
5621
+ child.kill("SIGKILL");
5622
+ }
5623
+ };
5624
+ const timer = setTimeout(stopChild, Math.min(5e3, remainingMs));
5625
+ options.signal?.addEventListener("abort", stopChild, { once: true });
5626
+ child.stdout.setEncoding("utf8");
5627
+ child.stdout.on("data", (chunk) => {
5628
+ if (stopped) return;
5629
+ output += chunk;
5630
+ if (output.length > 65536) stopChild();
5631
+ });
5632
+ child.on("error", () => {
5633
+ setResult(null);
5634
+ });
5635
+ child.on("close", (code) => {
5636
+ const key = !stopped && code === 0 && output.trim() !== "" ? output.trim() : null;
5637
+ setResult(key);
5638
+ });
5639
+ });
5640
+ }
5641
+ //#endregion
5642
+ //#region src/config/config.ts
5643
+ const SCOPE_SOURCE_KINDS = [
5644
+ "cwd",
5645
+ "session",
5646
+ "globs",
5647
+ "atc"
5648
+ ];
5649
+ const DEFAULT_PROVIDER = findPreset("jev");
5650
+ if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
5651
+ const DEFAULT_DENIAL_BUDGET = {
5652
+ consecutive: 3,
5653
+ perSession: 20
5654
+ };
5655
+ const DEFAULT_SCOPE_SOURCES = {
5656
+ cwd: { kind: "cwd" },
5657
+ session: { kind: "session" }
5658
+ };
5659
+ const DEFAULT_CONFIG = {
5660
+ provider: DEFAULT_PROVIDER,
5661
+ onFailure: "defer",
5662
+ minConfidence: .8,
5663
+ denialBudget: DEFAULT_DENIAL_BUDGET,
5664
+ scopeSources: DEFAULT_SCOPE_SOURCES
5665
+ };
5666
+ function resolveConfigPath() {
5667
+ const xdg = process.env["XDG_CONFIG_HOME"];
5668
+ const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
5669
+ return join(base, "auto-mode", "config.json");
5670
+ }
5671
+ async function loadConfig(path = resolveConfigPath()) {
5542
5672
  let raw;
5543
5673
  try {
5544
- raw = await readFile(settingsPath, "utf8");
5674
+ raw = await readFile(path, "utf8");
5545
5675
  } catch (error) {
5546
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return empty;
5547
- throw new Error("Claude settings unreadable", { cause: error });
5676
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return DEFAULT_CONFIG;
5677
+ throw new Error("auto-mode configuration unreadable", { cause: error });
5548
5678
  }
5549
- const entries = array(string()).optional();
5550
- const schema = object({ autoMode: object({
5551
- environment: entries,
5552
- allow: entries,
5553
- soft_deny: entries,
5554
- hard_deny: entries
5555
- }).optional() });
5556
- let body;
5679
+ let json;
5557
5680
  try {
5558
- body = JSON.parse(raw);
5681
+ json = JSON.parse(raw);
5559
5682
  } catch {
5560
- throw new Error("Claude settings contain invalid JSON");
5683
+ throw new Error(`${path} is not valid JSON`);
5561
5684
  }
5562
- const parsed = schema.safeParse(body);
5563
- if (!parsed.success) throw new Error("Claude autoMode settings are invalid");
5564
- const rules = parsed.data;
5685
+ return buildConfig(json, path);
5686
+ }
5687
+ const text = string().min(1);
5688
+ const positive = number().positive();
5689
+ const denialBudgetSchema = strictObject({
5690
+ consecutive: number().int().positive().optional(),
5691
+ perSession: number().int().positive().optional()
5692
+ });
5693
+ const configFileSchema = strictObject({
5694
+ classifiers: record(text, unknown()).optional(),
5695
+ scopeSources: record(text, unknown()).optional(),
5696
+ decision: strictObject({
5697
+ classifier: text.optional(),
5698
+ judge: text.nullable().optional(),
5699
+ minConfidence: number().min(.5).max(1).optional(),
5700
+ onFailure: _enum(["defer", "deny"]).optional(),
5701
+ denialBudget: denialBudgetSchema.optional()
5702
+ }).optional(),
5703
+ policy: strictObject({
5704
+ rulesPath: text.nullable().optional(),
5705
+ frameworkPath: text.nullable().optional(),
5706
+ claudeSettingsPath: text.nullable().optional()
5707
+ }).optional()
5708
+ });
5709
+ function buildConfig(json, path) {
5710
+ const parsed = configFileSchema.safeParse(json);
5711
+ if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5712
+ const file = parsed.data;
5713
+ const warnings = [];
5714
+ const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5715
+ const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5716
+ const configuredSources = file.scopeSources === void 0 ? null : scopeSources.entries;
5717
+ warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`), ...collectScopeWarnings(configuredSources).map((line) => `${path}: scopeSources ${line}`));
5718
+ const decision = file.decision ?? {};
5719
+ const policy = file.policy ?? {};
5720
+ const judgeID = decision.judge ?? null;
5565
5721
  return {
5566
- environment: (rules.autoMode?.environment ?? []).filter((entry) => entry !== "$defaults"),
5567
- allow: (rules.autoMode?.allow ?? []).filter((entry) => entry !== "$defaults"),
5568
- soft_deny: (rules.autoMode?.soft_deny ?? []).filter((entry) => entry !== "$defaults"),
5569
- hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5722
+ provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5723
+ judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5724
+ scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
5725
+ classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5726
+ rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5727
+ onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
5728
+ claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
5729
+ minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
5730
+ denialBudget: {
5731
+ consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
5732
+ perSession: decision.denialBudget?.perSession ?? DEFAULT_DENIAL_BUDGET.perSession
5733
+ },
5734
+ warnings
5570
5735
  };
5571
5736
  }
5572
- //#endregion
5573
- //#region src/rules/split-shell-command.ts
5574
- function splitShellCommand(command) {
5575
- const segments = [];
5576
- let current = "";
5577
- let quote = null;
5578
- let unparsed = false;
5579
- let i = 0;
5580
- const collectSegment = () => {
5581
- const text = current.trim();
5582
- if (text !== "") segments.push({ text });
5583
- current = "";
5737
+ function collectScopeWarnings(entries) {
5738
+ if (entries === null) return [];
5739
+ const sources = Object.values(entries);
5740
+ return [...sources.some((source) => source.kind === "cwd") ? [] : ["has no cwd entry, so the task owns only what the other sources name"], ...sources.flatMap((source) => source.paths ?? []).filter((glob) => glob.includes(".worktrees")).map((glob) => `glob ${glob} covers worktrees that other tasks own`)];
5741
+ }
5742
+ function buildRegistry(raw, parseEntry) {
5743
+ const entries = /* @__PURE__ */ new Map();
5744
+ const dropped = [];
5745
+ const droppedIDs = [];
5746
+ for (const [id, entry] of Object.entries(raw)) {
5747
+ const result = parseEntry(id, entry);
5748
+ if (typeof result === "string") {
5749
+ dropped.push(`${id} dropped: ${result}`);
5750
+ droppedIDs.push(id);
5751
+ } else entries.set(id, result);
5752
+ }
5753
+ return {
5754
+ entries: Object.fromEntries(entries),
5755
+ dropped,
5756
+ droppedIDs
5584
5757
  };
5585
- while (i < command.length) {
5586
- const ch = command.charAt(i);
5587
- if (quote !== null) {
5588
- if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
5589
- if (ch === quote) quote = null;
5590
- current += ch;
5591
- i += 1;
5592
- continue;
5593
- }
5594
- if (ch === "'" || ch === "\"") {
5595
- quote = ch;
5596
- current += ch;
5597
- i += 1;
5598
- continue;
5599
- }
5600
- if (ch === "\\") {
5601
- current += command.slice(i, i + 2);
5758
+ }
5759
+ const CLASSIFIER_KINDS = [...Object.keys(PRESETS), "messages"];
5760
+ const classifierEntrySchema = strictObject({
5761
+ kind: text.optional(),
5762
+ baseURL: text.optional(),
5763
+ model: text.optional(),
5764
+ apiKeyEnv: text.optional(),
5765
+ apiKeyCommand: text.optional(),
5766
+ reasoning: boolean().optional(),
5767
+ maxTokens: positive.optional(),
5768
+ timeoutMs: positive.optional()
5769
+ });
5770
+ const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
5771
+ "apiKey",
5772
+ "key",
5773
+ "token",
5774
+ "secret"
5775
+ ]);
5776
+ function parseClassifierEntry(id, entry) {
5777
+ if (typeof entry === "object" && entry !== null) {
5778
+ const credential = Object.keys(entry).find((key) => CREDENTIAL_KEYS.has(key));
5779
+ if (credential !== void 0) return `holds a literal ${credential}; name the key with apiKeyEnv or apiKeyCommand`;
5780
+ }
5781
+ const parsed = classifierEntrySchema.safeParse(entry);
5782
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5783
+ const kind = parsed.data.kind ?? id;
5784
+ if (!CLASSIFIER_KINDS.includes(kind)) return `unknown kind '${kind}'; known kinds are ${CLASSIFIER_KINDS.join(", ")}`;
5785
+ const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind);
5786
+ const model = parsed.data.model ?? base?.model;
5787
+ if (base === void 0 || model === void 0) return `kind '${kind}' needs a model`;
5788
+ return {
5789
+ protocol: base.protocol ?? "messages",
5790
+ baseURL: parsed.data.baseURL ?? base.baseURL,
5791
+ model,
5792
+ apiKeyEnv: parsed.data.apiKeyEnv ?? base.apiKeyEnv,
5793
+ apiKeyCommand: parsed.data.apiKeyCommand ?? base.apiKeyCommand,
5794
+ reasoning: parsed.data.reasoning ?? base.reasoning,
5795
+ maxTokens: parsed.data.maxTokens ?? base.maxTokens,
5796
+ timeoutMs: parsed.data.timeoutMs ?? base.timeoutMs
5797
+ };
5798
+ }
5799
+ const scopeSourceEntrySchema = strictObject({
5800
+ kind: text.optional(),
5801
+ paths: array(text).min(1).optional()
5802
+ });
5803
+ function parseScopeSourceEntry(id, entry) {
5804
+ const parsed = scopeSourceEntrySchema.safeParse(entry);
5805
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5806
+ const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
5807
+ if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5808
+ if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5809
+ kind,
5810
+ paths: parsed.data.paths.map((glob) => expandHomePath(glob) ?? glob)
5811
+ };
5812
+ return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5813
+ }
5814
+ function formatFirstIssue(issues) {
5815
+ const [issue] = issues;
5816
+ if (issue === void 0) return "invalid entry";
5817
+ return issue.path.length === 0 ? issue.message : `${issue.path.map(String).join(".")}: ${issue.message}`;
5818
+ }
5819
+ function resolveRole(role, id, entries, droppedIDs, path) {
5820
+ const entry = Object.hasOwn(entries, id) ? entries[id] : void 0;
5821
+ if (entry !== void 0) return entry;
5822
+ if (droppedIDs.includes(id)) throw new Error(`${path}: decision.${role} names '${id}', whose entry was dropped`);
5823
+ if (findPreset(id) === void 0) throw new Error(`${path}: decision.${role} names '${id}', which is neither a classifiers entry nor a built-in kind (${Object.keys(PRESETS).join(", ")})`);
5824
+ return parseBuiltIn(id);
5825
+ }
5826
+ function parseBuiltIn(id) {
5827
+ const resolved = parseClassifierEntry(id, {});
5828
+ if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
5829
+ return resolved;
5830
+ }
5831
+ function expandHomePath(path) {
5832
+ return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
5833
+ }
5834
+ function resolveApiKey(provider, options = {}) {
5835
+ const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
5836
+ if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
5837
+ if (provider.apiKeyCommand === void 0 || provider.apiKeyCommand === "") return Promise.resolve(null);
5838
+ return readApiKeyFromCommand(provider.apiKeyCommand, options);
5839
+ }
5840
+ //#endregion
5841
+ //#region src/config/load-claude-rules.ts
5842
+ async function loadClaudeRules(path) {
5843
+ const empty = {
5844
+ environment: [],
5845
+ allow: [],
5846
+ soft_deny: [],
5847
+ hard_deny: []
5848
+ };
5849
+ if (path === null) return empty;
5850
+ const configDir = process.env["CLAUDE_CONFIG_DIR"];
5851
+ const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5852
+ const settingsPath = path ?? join(base, "settings.json");
5853
+ let raw;
5854
+ try {
5855
+ raw = await readFile(settingsPath, "utf8");
5856
+ } catch (error) {
5857
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return empty;
5858
+ throw new Error("Claude settings unreadable", { cause: error });
5859
+ }
5860
+ const entries = array(string()).optional();
5861
+ const schema = object({ autoMode: object({
5862
+ environment: entries,
5863
+ allow: entries,
5864
+ soft_deny: entries,
5865
+ hard_deny: entries
5866
+ }).optional() });
5867
+ let body;
5868
+ try {
5869
+ body = JSON.parse(raw);
5870
+ } catch {
5871
+ throw new Error("Claude settings contain invalid JSON");
5872
+ }
5873
+ const parsed = schema.safeParse(body);
5874
+ if (!parsed.success) throw new Error("Claude autoMode settings are invalid");
5875
+ const rules = parsed.data;
5876
+ return {
5877
+ environment: (rules.autoMode?.environment ?? []).filter((entry) => entry !== "$defaults"),
5878
+ allow: (rules.autoMode?.allow ?? []).filter((entry) => entry !== "$defaults"),
5879
+ soft_deny: (rules.autoMode?.soft_deny ?? []).filter((entry) => entry !== "$defaults"),
5880
+ hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5881
+ };
5882
+ }
5883
+ //#endregion
5884
+ //#region src/rules/split-command-words.ts
5885
+ function splitCommandWords(text) {
5886
+ const words = splitWords$1(text);
5887
+ let index = 0;
5888
+ while (index < words.length) {
5889
+ const word = words[index] ?? "";
5890
+ if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
5891
+ else if (word === "env") {
5892
+ index += 1;
5893
+ while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
5894
+ } else if (word === "timeout") index += 2;
5895
+ else break;
5896
+ }
5897
+ return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
5898
+ }
5899
+ function splitWords$1(text) {
5900
+ const words = [];
5901
+ let current = "";
5902
+ let quote = null;
5903
+ let started = false;
5904
+ for (const ch of text) if (quote !== null) {
5905
+ if (ch === quote) quote = null;
5906
+ else current += ch;
5907
+ } else if (ch === "'" || ch === "\"") {
5908
+ quote = ch;
5909
+ started = true;
5910
+ } else if (/\s/u.test(ch)) {
5911
+ if (started) words.push(current);
5912
+ current = "";
5913
+ started = false;
5914
+ } else {
5915
+ current += ch;
5916
+ started = true;
5917
+ }
5918
+ if (started) words.push(current);
5919
+ return words;
5920
+ }
5921
+ //#endregion
5922
+ //#region src/rules/split-shell-command.ts
5923
+ function splitShellCommand(command) {
5924
+ const segments = [];
5925
+ let current = "";
5926
+ let quote = null;
5927
+ let unparsed = false;
5928
+ let i = 0;
5929
+ const collectSegment = () => {
5930
+ const text = current.trim();
5931
+ if (text !== "") segments.push({ text });
5932
+ current = "";
5933
+ };
5934
+ while (i < command.length) {
5935
+ const ch = command.charAt(i);
5936
+ if (quote !== null) {
5937
+ if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
5938
+ if (ch === quote) quote = null;
5939
+ current += ch;
5940
+ i += 1;
5941
+ continue;
5942
+ }
5943
+ if (ch === "'" || ch === "\"") {
5944
+ quote = ch;
5945
+ current += ch;
5946
+ i += 1;
5947
+ continue;
5948
+ }
5949
+ if (ch === "\\") {
5950
+ current += command.slice(i, i + 2);
5602
5951
  i += 2;
5603
5952
  continue;
5604
5953
  }
@@ -5628,6 +5977,14 @@ function splitShellCommand(command) {
5628
5977
  };
5629
5978
  }
5630
5979
  //#endregion
5980
+ //#region src/containment/to-repository-slug.ts
5981
+ function toRepositorySlug(reference) {
5982
+ const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
5983
+ if (match?.groups === void 0) return null;
5984
+ const parts = match.groups;
5985
+ return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
5986
+ }
5987
+ //#endregion
5631
5988
  //#region src/containment/collect-scope-findings.ts
5632
5989
  function collectScopeFindings(action, scope) {
5633
5990
  const filePath = action.input["file_path"] ?? action.input["notebook_path"];
@@ -5672,7 +6029,10 @@ function collectToolFindings(action, scope) {
5672
6029
  const verb = action.tool.split("__").at(-1) ?? "";
5673
6030
  if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
5674
6031
  const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
5675
- return !verb.includes("merge") && typeof number === "number" && scope.pullRequests.includes(number) ? [] : [{
6032
+ const owner = action.input["owner"];
6033
+ const name = action.input["repo"];
6034
+ const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
6035
+ return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
5676
6036
  kind: "remote-write",
5677
6037
  target: action.tool
5678
6038
  }];
@@ -5696,7 +6056,7 @@ function collectCommandFindings(command, cwd, scope) {
5696
6056
  const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
5697
6057
  let directory = cwd;
5698
6058
  for (const segment of splitShellCommand(command).segments) {
5699
- const [name, ...args] = getCommandWords(splitWords$1(segment.text));
6059
+ const [name, ...args] = splitCommandWords(segment.text);
5700
6060
  for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
5701
6061
  if (name === void 0) continue;
5702
6062
  if (name === "cd" && hasPipe) directory = null;
@@ -5729,6 +6089,11 @@ function collectProgramFindings(name, args, directory, cwd, scope) {
5729
6089
  return resolve(destination);
5730
6090
  }
5731
6091
  if (name === "curl") return collectCurlFindings(args);
6092
+ const launcher = [name, ...args].findIndex((word) => word.includes("auto-mode"));
6093
+ if (launcher !== -1 && [name, ...args].slice(launcher + 1).includes("record")) return [{
6094
+ kind: "credential",
6095
+ target: "auto-mode record"
6096
+ }];
5732
6097
  const finding = findProgramFinding(name, args);
5733
6098
  return finding === null ? [] : [finding];
5734
6099
  }
@@ -5903,12 +6268,6 @@ function isOwnedRemote(remote, scope) {
5903
6268
  const slug = toRepositorySlug(remote);
5904
6269
  return slug !== null && scope.remotes.some((entry) => toRepositorySlug(entry.url) === slug);
5905
6270
  }
5906
- function toRepositorySlug(reference) {
5907
- const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
5908
- if (match?.groups === void 0) return null;
5909
- const parts = match.groups;
5910
- return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
5911
- }
5912
6271
  const GH_PR_WRITES = /* @__PURE__ */ new Set([
5913
6272
  "comment",
5914
6273
  "merge",
@@ -5947,7 +6306,7 @@ function collectGhFindings(args, scope) {
5947
6306
  const number = Number(args.slice(2).find((word) => /^\d+$/u.test(word)));
5948
6307
  const repoIndex = args.findIndex((word) => word === "--repo" || word === "-R");
5949
6308
  const repo = repoIndex === -1 ? void 0 : args[repoIndex + 1];
5950
- return verb !== "merge" && scope.pullRequests.includes(number) && (repo === void 0 || isCheckoutRepository(repo, scope)) ? [] : [{
6309
+ return verb !== "merge" && isOwnedPullRequest(number, repo, scope) ? [] : [{
5951
6310
  kind: "remote-write",
5952
6311
  target: `gh pr ${verb}`
5953
6312
  }];
@@ -5975,12 +6334,10 @@ function collectGhFindings(args, scope) {
5975
6334
  }];
5976
6335
  return [];
5977
6336
  }
5978
- function isCheckoutRepository(repo, scope) {
5979
- const wanted = repo.toLowerCase().replace(/\.git$/u, "");
5980
- return scope.remotes.some((remote) => {
5981
- const slug = toRepositorySlug(remote.url);
5982
- return slug !== null && (slug === wanted || slug.endsWith(`/${wanted}`));
5983
- });
6337
+ function isOwnedPullRequest(number, repo, scope) {
6338
+ const wanted = repo?.toLowerCase().replace(/\.git$/u, "");
6339
+ const slugs = new Set(scope.remotes.map((remote) => toRepositorySlug(remote.url)));
6340
+ return scope.pullRequests.some((pull) => pull.number === number && (wanted === void 0 ? slugs.size === 1 && slugs.has(pull.repository) : pull.repository === wanted || pull.repository.endsWith(`/${wanted}`)));
5984
6341
  }
5985
6342
  function collectGhAPIFindings(args, scope) {
5986
6343
  const endpoint = args.find((word) => !word.startsWith("-")) ?? "";
@@ -5993,7 +6350,7 @@ function collectGhAPIFindings(args, scope) {
5993
6350
  const hasFields = args.some((word) => /^(?:-f|-F|--field|--raw-field|--input)$/u.test(word));
5994
6351
  if (method === "GET" || method === null && !hasFields) return [];
5995
6352
  const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(endpoint)?.groups;
5996
- if (pull !== void 0 && scope.pullRequests.includes(Number(pull["number"])) && isCheckoutRepository(pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
6353
+ if (pull !== void 0 && isOwnedPullRequest(Number(pull["number"]), pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
5997
6354
  return [{
5998
6355
  kind: /secret|collaborator|protection|key|permission/u.test(endpoint) ? "credential" : "remote-write",
5999
6356
  target: `gh api ${endpoint}`
@@ -6090,528 +6447,64 @@ function collectInPlaceFiles(args) {
6090
6447
  else if (!word.startsWith("-")) {
6091
6448
  if (sawScript) files.push(word);
6092
6449
  else sawScript = true;
6093
- }
6094
- return files;
6095
- }
6096
- function getCommandWords(words) {
6097
- let index = 0;
6098
- while (index < words.length) {
6099
- const word = words[index] ?? "";
6100
- if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6101
- else if (word === "env") {
6102
- index += 1;
6103
- while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6104
- } else if (word === "timeout") index += 2;
6105
- else break;
6106
- }
6107
- return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
6108
- }
6109
- function splitWords$1(text) {
6110
- const words = [];
6111
- let current = "";
6112
- let quote = null;
6113
- let started = false;
6114
- for (const ch of text) if (quote !== null) {
6115
- if (ch === quote) quote = null;
6116
- else current += ch;
6117
- } else if (ch === "'" || ch === "\"") {
6118
- quote = ch;
6119
- started = true;
6120
- } else if (/\s/u.test(ch)) {
6121
- if (started) words.push(current);
6122
- current = "";
6123
- started = false;
6124
- } else {
6125
- current += ch;
6126
- started = true;
6127
- }
6128
- if (started) words.push(current);
6129
- return words;
6130
- }
6131
- function resolvePath(cwd, path, home) {
6132
- const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6133
- if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
6134
- if (cwd === null && !isAbsolute(expanded)) return null;
6135
- const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6136
- return normalize(absolute).replace(/(?<=.)\/$/u, "");
6137
- }
6138
- const SCRATCH_PATHS = [
6139
- "/tmp",
6140
- "/dev/null",
6141
- "/dev/stdout",
6142
- "/dev/stderr"
6143
- ];
6144
- function isInScope(path, scope) {
6145
- if (SCRATCH_PATHS.some((entry) => isUnder(path, entry))) return true;
6146
- return scope.worktrees.some((worktree) => {
6147
- const isNested = isUnder(path, join(worktree, ".worktrees"));
6148
- const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
6149
- return isUnder(path, worktree) && (!isNested || isOtherOwned);
6150
- });
6151
- }
6152
- function isUnder(path, root) {
6153
- return path === root || path.startsWith(`${root}/`);
6154
- }
6155
- //#endregion
6156
- //#region src/model/load-repository-context.ts
6157
- async function loadRepositoryContext(cwd) {
6158
- if ([
6159
- "GIT_DIR",
6160
- "GIT_WORK_TREE",
6161
- "GIT_COMMON_DIR"
6162
- ].some((name) => process.env[name] !== void 0)) return null;
6163
- let directory = resolve(cwd);
6164
- for (;;) {
6165
- const gitPath = join(directory, ".git");
6166
- try {
6167
- await stat(gitPath);
6168
- } catch (error) {
6169
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6170
- const parent = dirname(directory);
6171
- if (parent === directory) return null;
6172
- directory = parent;
6173
- continue;
6174
- }
6175
- let gitDir = gitPath;
6176
- let head;
6177
- try {
6178
- try {
6179
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6180
- } catch {
6181
- const link = await readFile(gitPath, "utf8");
6182
- const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6183
- if (path === void 0) return null;
6184
- gitDir = resolve(directory, path.trim());
6185
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6186
- }
6187
- } catch {
6188
- return null;
6189
- }
6190
- let commonDir = gitDir;
6191
- try {
6192
- const common = await readFile(join(gitDir, "commondir"), "utf8");
6193
- commonDir = resolve(gitDir, common.trim());
6194
- } catch {
6195
- commonDir = gitDir;
6196
- }
6197
- let defaultBranch = null;
6198
- try {
6199
- defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6200
- } catch {
6201
- defaultBranch = null;
6202
- }
6203
- return {
6204
- cwd: resolve(cwd),
6205
- branch: parseReference(head, "refs/heads/"),
6206
- defaultBranch
6207
- };
6208
- }
6209
- }
6210
- function parseReference(content, prefix) {
6211
- const value = content.trim();
6212
- const marker = `ref: ${prefix}`;
6213
- if (!value.startsWith(marker)) return null;
6214
- const branch = value.slice(marker.length);
6215
- return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6216
- }
6217
- //#endregion
6218
- //#region src/containment/build-cwd-scope.ts
6219
- function buildCWDScope(input) {
6220
- const isDefault = input.branch === input.defaultBranch || input.defaultBranch === null && DEFAULT_NAMES.has(input.branch ?? "");
6221
- return {
6222
- home: input.home,
6223
- worktrees: [input.worktree],
6224
- branches: input.branch !== null && !isDefault ? [input.branch] : [],
6225
- currentBranch: input.branch,
6226
- remotes: input.remotes,
6227
- pullRequests: []
6228
- };
6229
- }
6230
- const DEFAULT_NAMES = /* @__PURE__ */ new Set([
6231
- "main",
6232
- "master",
6233
- "trunk",
6234
- "develop"
6235
- ]);
6236
- //#endregion
6237
- //#region src/containment/load-task-scope.ts
6238
- async function loadTaskScope(cwd) {
6239
- const [checkout, context] = await Promise.all([findCheckout(resolve(cwd)), loadRepositoryContext(cwd)]);
6240
- return buildCWDScope({
6241
- home: homedir(),
6242
- worktree: checkout?.worktree ?? resolve(cwd),
6243
- branch: context?.branch ?? null,
6244
- defaultBranch: context?.defaultBranch ?? null,
6245
- remotes: checkout === null ? [] : await readRemotes(checkout.commonDir)
6246
- });
6247
- }
6248
- const GIT_OVERRIDES = [
6249
- "GIT_DIR",
6250
- "GIT_WORK_TREE",
6251
- "GIT_COMMON_DIR"
6252
- ];
6253
- async function findCheckout(start) {
6254
- if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
6255
- for (let directory = start;; directory = dirname(directory)) {
6256
- const gitPath = join(directory, ".git");
6257
- const info = await stat(gitPath).catch(() => null);
6258
- if (info !== null) {
6259
- const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
6260
- return gitDir === null ? null : {
6261
- worktree: directory,
6262
- commonDir: await findCommonDir(gitDir)
6263
- };
6264
- }
6265
- if (dirname(directory) === directory) return null;
6266
- }
6267
- }
6268
- async function findLinkedGitDir(directory, gitPath) {
6269
- const link = await readFile(gitPath, "utf8").catch(() => "");
6270
- const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
6271
- return path === void 0 ? null : resolve(directory, path.trim());
6272
- }
6273
- async function findCommonDir(gitDir) {
6274
- const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
6275
- return common === null ? gitDir : resolve(gitDir, common.trim());
6276
- }
6277
- async function readRemotes(commonDir) {
6278
- const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
6279
- const remotes = [];
6280
- let section = null;
6281
- for (const line of config.split("\n")) {
6282
- const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
6283
- if (header !== void 0) section = header;
6284
- else if (/^\s*\[/u.test(line)) section = null;
6285
- else if (section !== null) {
6286
- const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
6287
- if (url !== void 0) remotes.push({
6288
- name: section,
6289
- url
6290
- });
6291
- }
6292
- }
6293
- return remotes;
6294
- }
6295
- //#endregion
6296
- //#region src/containment/check-containment.ts
6297
- const CONTAINMENT_RULE = "Outside Task Scope";
6298
- async function checkContainment(request) {
6299
- let scope;
6300
- try {
6301
- scope = await loadTaskScope(request.cwd);
6302
- } catch {
6303
- return null;
6304
- }
6305
- const findings = collectScopeFindings({
6306
- tool: request.toolName,
6307
- cwd: request.cwd,
6308
- input: request.toolInput
6309
- }, scope);
6310
- if (findings.length === 0) return null;
6311
- return {
6312
- rule: CONTAINMENT_RULE,
6313
- reason: formatContainmentReason(findings, scope),
6314
- findings
6315
- };
6316
- }
6317
- const KIND_LABELS = {
6318
- path: "path",
6319
- branch: "branch",
6320
- "remote-write": "remote target",
6321
- credential: "credential or global setting",
6322
- prune: "shared resource"
6323
- };
6324
- function formatContainmentReason(findings, scope) {
6325
- const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
6326
- const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
6327
- const [branch] = scope.branches;
6328
- return `This action writes outside the task scope: ${targets}${more}. The task owns ${`the worktree ${scope.worktrees.join(", ")}${branch === void 0 ? "" : ` and the branch ${branch}`}`}. Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.`;
6329
- }
6330
- const PRESETS = {
6331
- jev: {
6332
- protocol: "system-one",
6333
- baseURL: "https://api.typesafe.ai",
6334
- model: "jev-1.13.0",
6335
- apiKeyEnv: "TYPESAFE_API_KEY",
6336
- reasoning: false,
6337
- maxTokens: 3e3,
6338
- timeoutMs: 5e3
6339
- },
6340
- spark: {
6341
- baseURL: "https://api.meta.ai",
6342
- model: "muse-spark-1.3-contributor",
6343
- apiKeyEnv: "META_API_KEY",
6344
- reasoning: true,
6345
- maxTokens: 3e3,
6346
- timeoutMs: 45e3
6347
- },
6348
- claude: {
6349
- baseURL: "https://api.anthropic.com",
6350
- model: "claude-haiku-4-5-20251001",
6351
- apiKeyEnv: "ANTHROPIC_API_KEY",
6352
- reasoning: true,
6353
- maxTokens: 3e3,
6354
- timeoutMs: 45e3
6355
- },
6356
- glm: {
6357
- baseURL: "https://api.z.ai/api/anthropic",
6358
- model: "glm-5.3-flash",
6359
- apiKeyEnv: "ZAI_API_KEY",
6360
- reasoning: true,
6361
- maxTokens: 3e3,
6362
- timeoutMs: 6e4
6363
- }
6364
- };
6365
- const MESSAGES_DEFAULTS = {
6366
- protocol: "messages",
6367
- baseURL: "https://api.anthropic.com",
6368
- reasoning: true,
6369
- maxTokens: 3e3,
6370
- timeoutMs: 45e3
6371
- };
6372
- function findPreset(id) {
6373
- return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
6374
- }
6375
- //#endregion
6376
- //#region src/config/read-api-key-from-command.ts
6377
- function readApiKeyFromCommand(command, options = {}) {
6378
- const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
6379
- if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
6380
- return new Promise((resolve) => {
6381
- const grouped = process.platform !== "win32";
6382
- const child = spawn("/bin/sh", ["-c", command], {
6383
- detached: grouped,
6384
- stdio: [
6385
- "ignore",
6386
- "pipe",
6387
- "ignore"
6388
- ]
6389
- });
6390
- let output = "";
6391
- let settled = false;
6392
- let stopped = false;
6393
- const setResult = (value) => {
6394
- if (!settled) {
6395
- settled = true;
6396
- clearTimeout(timer);
6397
- options.signal?.removeEventListener("abort", stopChild);
6398
- resolve(value);
6399
- }
6400
- };
6401
- const stopChild = () => {
6402
- if (stopped) return;
6403
- stopped = true;
6404
- try {
6405
- if (grouped && child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
6406
- else child.kill("SIGKILL");
6407
- } catch {
6408
- child.kill("SIGKILL");
6409
- }
6410
- };
6411
- const timer = setTimeout(stopChild, Math.min(5e3, remainingMs));
6412
- options.signal?.addEventListener("abort", stopChild, { once: true });
6413
- child.stdout.setEncoding("utf8");
6414
- child.stdout.on("data", (chunk) => {
6415
- if (stopped) return;
6416
- output += chunk;
6417
- if (output.length > 65536) stopChild();
6418
- });
6419
- child.on("error", () => {
6420
- setResult(null);
6421
- });
6422
- child.on("close", (code) => {
6423
- const key = !stopped && code === 0 && output.trim() !== "" ? output.trim() : null;
6424
- setResult(key);
6425
- });
6426
- });
6427
- }
6428
- //#endregion
6429
- //#region src/config/config.ts
6430
- const SCOPE_SOURCE_KINDS = [
6431
- "cwd",
6432
- "session",
6433
- "globs",
6434
- "atc"
6435
- ];
6436
- const DEFAULT_PROVIDER = findPreset("jev");
6437
- if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
6438
- const DEFAULT_DENIAL_BUDGET = {
6439
- consecutive: 3,
6440
- perSession: 20
6441
- };
6442
- const DEFAULT_CONFIG = {
6443
- provider: DEFAULT_PROVIDER,
6444
- onFailure: "defer",
6445
- minConfidence: .8,
6446
- denialBudget: DEFAULT_DENIAL_BUDGET
6447
- };
6448
- function resolveConfigPath() {
6449
- const xdg = process.env["XDG_CONFIG_HOME"];
6450
- const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
6451
- return join(base, "auto-mode", "config.json");
6452
- }
6453
- async function loadConfig(path = resolveConfigPath()) {
6454
- let raw;
6455
- try {
6456
- raw = await readFile(path, "utf8");
6457
- } catch (error) {
6458
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return DEFAULT_CONFIG;
6459
- throw new Error("auto-mode configuration unreadable", { cause: error });
6460
- }
6461
- let json;
6462
- try {
6463
- json = JSON.parse(raw);
6464
- } catch {
6465
- throw new Error(`${path} is not valid JSON`);
6466
- }
6467
- return buildConfig(json, path);
6468
- }
6469
- const text = string().min(1);
6470
- const positive = number().positive();
6471
- const denialBudgetSchema = strictObject({
6472
- consecutive: number().int().positive().optional(),
6473
- perSession: number().int().positive().optional()
6474
- });
6475
- const configFileSchema = strictObject({
6476
- classifiers: record(text, unknown()).optional(),
6477
- scopeSources: record(text, unknown()).optional(),
6478
- decision: strictObject({
6479
- classifier: text.optional(),
6480
- judge: text.nullable().optional(),
6481
- minConfidence: number().min(.5).max(1).optional(),
6482
- onFailure: _enum(["defer", "deny"]).optional(),
6483
- denialBudget: denialBudgetSchema.optional()
6484
- }).optional(),
6485
- policy: strictObject({
6486
- rulesPath: text.nullable().optional(),
6487
- frameworkPath: text.nullable().optional(),
6488
- claudeSettingsPath: text.nullable().optional()
6489
- }).optional()
6490
- });
6491
- function buildConfig(json, path) {
6492
- const parsed = configFileSchema.safeParse(json);
6493
- if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
6494
- const file = parsed.data;
6495
- const warnings = [];
6496
- const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
6497
- const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
6498
- warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
6499
- const decision = file.decision ?? {};
6500
- const policy = file.policy ?? {};
6501
- const judgeID = decision.judge ?? null;
6502
- return {
6503
- provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
6504
- judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
6505
- scopeSources: scopeSources.entries,
6506
- classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
6507
- rulesPath: expandHomePath(policy.rulesPath ?? void 0),
6508
- onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
6509
- claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
6510
- minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
6511
- denialBudget: {
6512
- consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
6513
- perSession: decision.denialBudget?.perSession ?? DEFAULT_DENIAL_BUDGET.perSession
6514
- },
6515
- warnings
6516
- };
6517
- }
6518
- function buildRegistry(raw, parseEntry) {
6519
- const entries = /* @__PURE__ */ new Map();
6520
- const dropped = [];
6521
- const droppedIDs = [];
6522
- for (const [id, entry] of Object.entries(raw)) {
6523
- const result = parseEntry(id, entry);
6524
- if (typeof result === "string") {
6525
- dropped.push(`${id} dropped: ${result}`);
6526
- droppedIDs.push(id);
6527
- } else entries.set(id, result);
6528
- }
6529
- return {
6530
- entries: Object.fromEntries(entries),
6531
- dropped,
6532
- droppedIDs
6533
- };
6534
- }
6535
- const CLASSIFIER_KINDS = [...Object.keys(PRESETS), "messages"];
6536
- const classifierEntrySchema = strictObject({
6537
- kind: text.optional(),
6538
- baseURL: text.optional(),
6539
- model: text.optional(),
6540
- apiKeyEnv: text.optional(),
6541
- apiKeyCommand: text.optional(),
6542
- reasoning: boolean().optional(),
6543
- maxTokens: positive.optional(),
6544
- timeoutMs: positive.optional()
6545
- });
6546
- const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
6547
- "apiKey",
6548
- "key",
6549
- "token",
6550
- "secret"
6551
- ]);
6552
- function parseClassifierEntry(id, entry) {
6553
- if (typeof entry === "object" && entry !== null) {
6554
- const credential = Object.keys(entry).find((key) => CREDENTIAL_KEYS.has(key));
6555
- if (credential !== void 0) return `holds a literal ${credential}; name the key with apiKeyEnv or apiKeyCommand`;
6556
- }
6557
- const parsed = classifierEntrySchema.safeParse(entry);
6558
- if (!parsed.success) return formatFirstIssue(parsed.error.issues);
6559
- const kind = parsed.data.kind ?? id;
6560
- if (!CLASSIFIER_KINDS.includes(kind)) return `unknown kind '${kind}'; known kinds are ${CLASSIFIER_KINDS.join(", ")}`;
6561
- const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind);
6562
- const model = parsed.data.model ?? base?.model;
6563
- if (base === void 0 || model === void 0) return `kind '${kind}' needs a model`;
6564
- return {
6565
- protocol: base.protocol ?? "messages",
6566
- baseURL: parsed.data.baseURL ?? base.baseURL,
6567
- model,
6568
- apiKeyEnv: parsed.data.apiKeyEnv ?? base.apiKeyEnv,
6569
- apiKeyCommand: parsed.data.apiKeyCommand ?? base.apiKeyCommand,
6570
- reasoning: parsed.data.reasoning ?? base.reasoning,
6571
- maxTokens: parsed.data.maxTokens ?? base.maxTokens,
6572
- timeoutMs: parsed.data.timeoutMs ?? base.timeoutMs
6573
- };
6450
+ }
6451
+ return files;
6574
6452
  }
6575
- const scopeSourceEntrySchema = strictObject({
6576
- kind: text.optional(),
6577
- paths: array(text).min(1).optional()
6578
- });
6579
- function parseScopeSourceEntry(id, entry) {
6580
- const parsed = scopeSourceEntrySchema.safeParse(entry);
6581
- if (!parsed.success) return formatFirstIssue(parsed.error.issues);
6582
- const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
6583
- if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
6584
- if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
6585
- kind,
6586
- paths: parsed.data.paths
6587
- };
6588
- return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
6453
+ function resolvePath(cwd, path, home) {
6454
+ const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6455
+ if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
6456
+ if (cwd === null && !isAbsolute(expanded)) return null;
6457
+ const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6458
+ return normalize(absolute).replace(/(?<=.)\/$/u, "");
6589
6459
  }
6590
- function formatFirstIssue(issues) {
6591
- const [issue] = issues;
6592
- if (issue === void 0) return "invalid entry";
6593
- return issue.path.length === 0 ? issue.message : `${issue.path.map(String).join(".")}: ${issue.message}`;
6460
+ const SCRATCH_PATHS = [
6461
+ "/tmp",
6462
+ "/dev/null",
6463
+ "/dev/stdout",
6464
+ "/dev/stderr"
6465
+ ];
6466
+ function isInScope(path, scope) {
6467
+ if (SCRATCH_PATHS.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6468
+ return scope.worktrees.some((worktree) => {
6469
+ const isNested = isUnder(path, join(worktree, ".worktrees"));
6470
+ const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
6471
+ return isUnder(path, worktree) && (!isNested || isOtherOwned);
6472
+ });
6594
6473
  }
6595
- function resolveRole(role, id, entries, droppedIDs, path) {
6596
- const entry = Object.hasOwn(entries, id) ? entries[id] : void 0;
6597
- if (entry !== void 0) return entry;
6598
- if (droppedIDs.includes(id)) throw new Error(`${path}: decision.${role} names '${id}', whose entry was dropped`);
6599
- if (findPreset(id) === void 0) throw new Error(`${path}: decision.${role} names '${id}', which is neither a classifiers entry nor a built-in kind (${Object.keys(PRESETS).join(", ")})`);
6600
- return parseBuiltIn(id);
6474
+ function isUnder(path, root) {
6475
+ return path === root || path.startsWith(`${root}/`);
6601
6476
  }
6602
- function parseBuiltIn(id) {
6603
- const resolved = parseClassifierEntry(id, {});
6604
- if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
6605
- return resolved;
6477
+ //#endregion
6478
+ //#region src/containment/check-containment.ts
6479
+ const CONTAINMENT_RULE = "Outside Task Scope";
6480
+ function checkContainment(request, scope) {
6481
+ const findings = collectScopeFindings({
6482
+ tool: request.toolName,
6483
+ cwd: request.cwd,
6484
+ input: request.toolInput
6485
+ }, scope);
6486
+ if (findings.length === 0) return null;
6487
+ return {
6488
+ rule: CONTAINMENT_RULE,
6489
+ reason: formatContainmentReason(findings, scope),
6490
+ findings
6491
+ };
6606
6492
  }
6607
- function expandHomePath(path) {
6608
- return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
6493
+ const KIND_LABELS = {
6494
+ path: "path",
6495
+ branch: "branch",
6496
+ "remote-write": "remote target",
6497
+ credential: "credential or global setting",
6498
+ prune: "shared resource"
6499
+ };
6500
+ function formatContainmentReason(findings, scope) {
6501
+ const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
6502
+ const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
6503
+ const branches = scope.branches.length === 0 ? "" : ` and ${formatList("branch", "branches", scope.branches)}`;
6504
+ return `This action writes outside the task scope: ${targets}${more}. The task owns ${`${formatList("worktree", "worktrees", scope.worktrees)}${branches}`}. Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.`;
6609
6505
  }
6610
- function resolveApiKey(provider, options = {}) {
6611
- const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
6612
- if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
6613
- if (provider.apiKeyCommand === void 0 || provider.apiKeyCommand === "") return Promise.resolve(null);
6614
- return readApiKeyFromCommand(provider.apiKeyCommand, options);
6506
+ function formatList(one, many, values) {
6507
+ return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
6615
6508
  }
6616
6509
  //#endregion
6617
6510
  //#region src/policy/resolve-shipped-policy-dir.ts
@@ -6882,6 +6775,68 @@ function formatClassifierNote(note, apiKey) {
6882
6775
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6883
6776
  }
6884
6777
  //#endregion
6778
+ //#region src/model/load-repository-context.ts
6779
+ async function loadRepositoryContext(cwd) {
6780
+ if ([
6781
+ "GIT_DIR",
6782
+ "GIT_WORK_TREE",
6783
+ "GIT_COMMON_DIR"
6784
+ ].some((name) => process.env[name] !== void 0)) return null;
6785
+ let directory = resolve(cwd);
6786
+ for (;;) {
6787
+ const gitPath = join(directory, ".git");
6788
+ try {
6789
+ await stat(gitPath);
6790
+ } catch (error) {
6791
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6792
+ const parent = dirname(directory);
6793
+ if (parent === directory) return null;
6794
+ directory = parent;
6795
+ continue;
6796
+ }
6797
+ let gitDir = gitPath;
6798
+ let head;
6799
+ try {
6800
+ try {
6801
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6802
+ } catch {
6803
+ const link = await readFile(gitPath, "utf8");
6804
+ const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6805
+ if (path === void 0) return null;
6806
+ gitDir = resolve(directory, path.trim());
6807
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6808
+ }
6809
+ } catch {
6810
+ return null;
6811
+ }
6812
+ let commonDir = gitDir;
6813
+ try {
6814
+ const common = await readFile(join(gitDir, "commondir"), "utf8");
6815
+ commonDir = resolve(gitDir, common.trim());
6816
+ } catch {
6817
+ commonDir = gitDir;
6818
+ }
6819
+ let defaultBranch = null;
6820
+ try {
6821
+ defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6822
+ } catch {
6823
+ defaultBranch = null;
6824
+ }
6825
+ return {
6826
+ cwd: resolve(cwd),
6827
+ branch: parseReference(head, "refs/heads/"),
6828
+ defaultBranch
6829
+ };
6830
+ }
6831
+ }
6832
+ function parseReference(content, prefix) {
6833
+ const value = content.trim();
6834
+ const marker = `ref: ${prefix}`;
6835
+ if (!value.startsWith(marker)) return null;
6836
+ const branch = value.slice(marker.length);
6837
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6838
+ }
6839
+ //#endregion
6885
6840
  //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
6886
6841
  const t = Symbol.for("@ts-pattern/matcher");
6887
6842
  const e = Symbol.for("@ts-pattern/isVariadic");
@@ -7726,6 +7681,199 @@ function toUnquoted(word) {
7726
7681
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
7727
7682
  }
7728
7683
  //#endregion
7684
+ //#region src/scope/find-checkout.ts
7685
+ const GIT_OVERRIDES$1 = [
7686
+ "GIT_DIR",
7687
+ "GIT_WORK_TREE",
7688
+ "GIT_COMMON_DIR"
7689
+ ];
7690
+ async function findCheckout(start) {
7691
+ if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
7692
+ for (let directory = start;; directory = dirname(directory)) {
7693
+ const gitPath = join(directory, ".git");
7694
+ const info = await stat(gitPath).catch(() => null);
7695
+ if (info !== null) {
7696
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
7697
+ return gitDir === null ? null : {
7698
+ worktree: directory,
7699
+ commonDir: await findCommonDir(gitDir)
7700
+ };
7701
+ }
7702
+ if (dirname(directory) === directory) return null;
7703
+ }
7704
+ }
7705
+ async function findLinkedGitDir(directory, gitPath) {
7706
+ const link = await readFile(gitPath, "utf8").catch(() => "");
7707
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
7708
+ return path === void 0 ? null : resolve(directory, path.trim());
7709
+ }
7710
+ async function findCommonDir(gitDir) {
7711
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
7712
+ return common === null ? gitDir : resolve(gitDir, common.trim());
7713
+ }
7714
+ //#endregion
7715
+ //#region src/scope/merge-scope-facts.ts
7716
+ function mergeScopeFacts(all) {
7717
+ const pullRequests = /* @__PURE__ */ new Map();
7718
+ for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
7719
+ return {
7720
+ worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
7721
+ branches: [...new Set(all.flatMap((facts) => facts.branches))],
7722
+ pullRequests: [...pullRequests.values()],
7723
+ pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
7724
+ };
7725
+ }
7726
+ //#endregion
7727
+ //#region src/scope/build-task-scope.ts
7728
+ function buildTaskScope(input) {
7729
+ const merged = mergeScopeFacts(input.facts);
7730
+ const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
7731
+ const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
7732
+ const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
7733
+ number: pull.number,
7734
+ repository: pull.repository
7735
+ }));
7736
+ return {
7737
+ home: input.home,
7738
+ worktrees: merged.worktrees,
7739
+ branches,
7740
+ currentBranch: input.currentBranch,
7741
+ remotes: input.remotes,
7742
+ pullRequests,
7743
+ pathGlobs: merged.pathGlobs
7744
+ };
7745
+ }
7746
+ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
7747
+ "main",
7748
+ "master",
7749
+ "trunk",
7750
+ "develop"
7751
+ ]);
7752
+ //#endregion
7753
+ //#region src/scope/load-checkout-remotes.ts
7754
+ async function loadCheckoutRemotes(commonDir) {
7755
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
7756
+ const remotes = [];
7757
+ let section = null;
7758
+ for (const line of config.split("\n")) {
7759
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
7760
+ if (header !== void 0) section = header;
7761
+ else if (/^\s*\[/u.test(line)) section = null;
7762
+ else if (section !== null) {
7763
+ const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
7764
+ if (url !== void 0) remotes.push({
7765
+ name: section,
7766
+ url
7767
+ });
7768
+ }
7769
+ }
7770
+ return remotes;
7771
+ }
7772
+ //#endregion
7773
+ //#region src/scope/types.ts
7774
+ const EMPTY_SCOPE_FACTS = {
7775
+ worktrees: [],
7776
+ branches: [],
7777
+ pullRequests: [],
7778
+ pathGlobs: []
7779
+ };
7780
+ const EMPTY_SESSION_SCOPE = {
7781
+ worktrees: [],
7782
+ branches: [],
7783
+ pullRequests: []
7784
+ };
7785
+ //#endregion
7786
+ //#region src/scope/load-session-scope.ts
7787
+ const name = string().min(1);
7788
+ const pullRequestSchema = strictObject({
7789
+ number: number().int().positive(),
7790
+ head: name,
7791
+ repository: name
7792
+ });
7793
+ const branchSchema = strictObject({
7794
+ name,
7795
+ commonDir: name
7796
+ });
7797
+ const sessionScopeSchema = strictObject({
7798
+ worktrees: array(name),
7799
+ branches: array(branchSchema),
7800
+ pullRequests: array(pullRequestSchema)
7801
+ });
7802
+ async function loadSessionScope(path) {
7803
+ let raw;
7804
+ try {
7805
+ raw = await readFile(path, "utf8");
7806
+ } catch {
7807
+ return EMPTY_SESSION_SCOPE;
7808
+ }
7809
+ try {
7810
+ const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
7811
+ return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
7812
+ } catch {
7813
+ return EMPTY_SESSION_SCOPE;
7814
+ }
7815
+ }
7816
+ //#endregion
7817
+ //#region src/scope/resolve-session-scope-path.ts
7818
+ function resolveSessionScopePath(stateDir, sessionID) {
7819
+ const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
7820
+ return join(stateDir, "session-scope", `${key}.json`);
7821
+ }
7822
+ //#endregion
7823
+ //#region src/scope/pick-scope-source-reader.ts
7824
+ function pickScopeSourceReader(source) {
7825
+ if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
7826
+ worktrees: [context.worktree],
7827
+ branches: context.branch === null ? [] : [context.branch]
7828
+ }));
7829
+ if (source.kind === "session") return async (context) => {
7830
+ const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
7831
+ return buildFacts({
7832
+ worktrees: scope.worktrees,
7833
+ branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
7834
+ pullRequests: scope.pullRequests
7835
+ });
7836
+ };
7837
+ if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
7838
+ return () => Promise.resolve(EMPTY_SCOPE_FACTS);
7839
+ }
7840
+ function buildFacts(facts) {
7841
+ return {
7842
+ ...EMPTY_SCOPE_FACTS,
7843
+ ...facts
7844
+ };
7845
+ }
7846
+ //#endregion
7847
+ //#region src/scope/load-task-scope.ts
7848
+ async function loadTaskScope(request, sources) {
7849
+ const cwd = resolve(request.cwd);
7850
+ const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
7851
+ const branch = context?.branch ?? null;
7852
+ const sourceContext = {
7853
+ sessionID: request.sessionID,
7854
+ cwd,
7855
+ worktree: checkout?.worktree ?? cwd,
7856
+ commonDir: checkout?.commonDir ?? null,
7857
+ branch,
7858
+ stateDir: request.stateDir
7859
+ };
7860
+ const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
7861
+ return buildTaskScope({
7862
+ home: homedir(),
7863
+ currentBranch: branch,
7864
+ defaultBranch: context?.defaultBranch ?? null,
7865
+ remotes,
7866
+ facts
7867
+ });
7868
+ }
7869
+ //#endregion
7870
+ //#region src/state/resolve-state-dir.ts
7871
+ function resolveStateDir() {
7872
+ const stateHome = process.env["XDG_STATE_HOME"];
7873
+ const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
7874
+ return join(base, "auto-mode");
7875
+ }
7876
+ //#endregion
7729
7877
  //#region src/classify-action.ts
7730
7878
  async function classifyAction(request, config, options = {}) {
7731
7879
  let configured = null;
@@ -7741,9 +7889,11 @@ async function classifyAction(request, config, options = {}) {
7741
7889
  unavailable: true
7742
7890
  };
7743
7891
  }
7744
- const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
7892
+ const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
7893
+ const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
7745
7894
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7746
- const containment = await checkContainment(request);
7895
+ const scope = await tryLoadTaskScope(request, config);
7896
+ const containment = scope === null ? null : checkContainment(request, scope);
7747
7897
  if (containment !== null) {
7748
7898
  const guidance = await tryReadDenialGuidance();
7749
7899
  return {
@@ -7754,6 +7904,13 @@ async function classifyAction(request, config, options = {}) {
7754
7904
  };
7755
7905
  }
7756
7906
  if (local.kind === "allow") return buildLocalAllow(local.exception);
7907
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
7908
+ if (edit?.kind === "bypass") return {
7909
+ verdict: { kind: "allow" },
7910
+ decidingStage: "bypass",
7911
+ note: `allowed by the edit bypass: ${edit.target}`,
7912
+ status: "allow"
7913
+ };
7757
7914
  if (options.localOnly === true) return {
7758
7915
  verdict: null,
7759
7916
  decidingStage: "local",
@@ -7769,6 +7926,51 @@ async function classifyAction(request, config, options = {}) {
7769
7926
  };
7770
7927
  }
7771
7928
  const READ_ONLY_EXCEPTION = "Read-only actions";
7929
+ async function tryLoadTaskScope(request, config) {
7930
+ try {
7931
+ return await loadTaskScope({
7932
+ sessionID: request.sessionID,
7933
+ cwd: request.cwd,
7934
+ stateDir: resolveStateDir()
7935
+ }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
7936
+ } catch {
7937
+ return null;
7938
+ }
7939
+ }
7940
+ const GIT_OVERRIDES = [
7941
+ "GIT_DIR",
7942
+ "GIT_WORK_TREE",
7943
+ "GIT_COMMON_DIR"
7944
+ ];
7945
+ async function tryClassifyEdit(request, scope) {
7946
+ const fields = getEditFields(request.toolName);
7947
+ const path = fields === null ? void 0 : request.toolInput[fields.path];
7948
+ if (typeof path !== "string" || path === "") return null;
7949
+ try {
7950
+ const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
7951
+ const requested = resolve(request.cwd, path);
7952
+ const [target, worktrees, protectedDirs] = await Promise.all([
7953
+ resolveEditTarget(requested),
7954
+ Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
7955
+ Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
7956
+ ]);
7957
+ if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
7958
+ const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
7959
+ return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
7960
+ toolName: request.toolName,
7961
+ toolInput: request.toolInput,
7962
+ requested,
7963
+ target,
7964
+ checkout: checkout?.worktree ?? null,
7965
+ current
7966
+ }, {
7967
+ worktrees: worktrees.filter((worktree) => worktree !== null),
7968
+ protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
7969
+ });
7970
+ } catch {
7971
+ return null;
7972
+ }
7973
+ }
7772
7974
  function buildLocalAllow(exception) {
7773
7975
  return {
7774
7976
  verdict: { kind: "allow" },
@@ -7850,4 +8052,4 @@ function renderVerdict(verdict) {
7850
8052
  })).exhaustive();
7851
8053
  }
7852
8054
  //#endregion
7853
- export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, string as g, strictObject as h, classifyLocally as i, DEFAULT_DENIAL_BUDGET as l, number as m, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
8055
+ export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, splitCommandWords as _, resolveSessionScopePath as a, DEFAULT_SCOPE_SOURCES as b, loadCheckoutRemotes as c, classifyWithModel as d, parseModelVerdict as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, findCheckout as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadRepositoryContext as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyLocally as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };