auto-mode 0.13.0 → 0.15.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,22 +1,10 @@
1
1
  import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
2
2
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
4
- import { homedir } from "node:os";
5
4
  import { spawn } from "node:child_process";
5
+ import { homedir } from "node:os";
6
6
  import { existsSync } from "node:fs";
7
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
20
8
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
21
9
  function getEnumValues(entries) {
22
10
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5586,9 +5574,15 @@ function findPreset(id) {
5586
5574
  return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5587
5575
  }
5588
5576
  //#endregion
5577
+ //#region src/config/to-timer-delay.ts
5578
+ function toTimerDelay(ms) {
5579
+ return Math.max(1, Math.ceil(ms));
5580
+ }
5581
+ //#endregion
5589
5582
  //#region src/config/read-api-key-from-command.ts
5590
5583
  function readApiKeyFromCommand(command, options = {}) {
5591
- const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
5584
+ const now = options.now ?? Date.now;
5585
+ const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - now();
5592
5586
  if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
5593
5587
  return new Promise((resolve) => {
5594
5588
  const grouped = process.platform !== "win32";
@@ -5598,7 +5592,11 @@ function readApiKeyFromCommand(command, options = {}) {
5598
5592
  "ignore",
5599
5593
  "pipe",
5600
5594
  "ignore"
5601
- ]
5595
+ ],
5596
+ ...options.host === void 0 ? {} : { env: {
5597
+ HOME: options.host.home,
5598
+ ...options.host.env
5599
+ } }
5602
5600
  });
5603
5601
  let output = "";
5604
5602
  let settled = false;
@@ -5606,7 +5604,7 @@ function readApiKeyFromCommand(command, options = {}) {
5606
5604
  const setResult = (value) => {
5607
5605
  if (!settled) {
5608
5606
  settled = true;
5609
- clearTimeout(timer);
5607
+ timer.removeEventListener("abort", stopChild);
5610
5608
  options.signal?.removeEventListener("abort", stopChild);
5611
5609
  resolve(value);
5612
5610
  }
@@ -5621,8 +5619,10 @@ function readApiKeyFromCommand(command, options = {}) {
5621
5619
  child.kill("SIGKILL");
5622
5620
  }
5623
5621
  };
5624
- const timer = setTimeout(stopChild, Math.min(5e3, remainingMs));
5622
+ const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(5e3, remainingMs)));
5623
+ timer.addEventListener("abort", stopChild, { once: true });
5625
5624
  options.signal?.addEventListener("abort", stopChild, { once: true });
5625
+ if (timer.aborted) stopChild();
5626
5626
  child.stdout.setEncoding("utf8");
5627
5627
  child.stdout.on("data", (chunk) => {
5628
5628
  if (stopped) return;
@@ -5639,6 +5639,23 @@ function readApiKeyFromCommand(command, options = {}) {
5639
5639
  });
5640
5640
  }
5641
5641
  //#endregion
5642
+ //#region src/containment/scratch-paths.ts
5643
+ const SCRATCH_PATHS = [
5644
+ "/tmp",
5645
+ "/dev/null",
5646
+ "/dev/stdout",
5647
+ "/dev/stderr"
5648
+ ];
5649
+ //#endregion
5650
+ //#region src/config/read-host-environment.ts
5651
+ function readHostEnvironment() {
5652
+ return {
5653
+ env: process.env,
5654
+ home: homedir(),
5655
+ scratchPaths: SCRATCH_PATHS
5656
+ };
5657
+ }
5658
+ //#endregion
5642
5659
  //#region src/config/config.ts
5643
5660
  const SCOPE_SOURCE_KINDS = [
5644
5661
  "cwd",
@@ -5654,7 +5671,8 @@ const DEFAULT_DENIAL_BUDGET = {
5654
5671
  };
5655
5672
  const DEFAULT_SCOPE_SOURCES = {
5656
5673
  cwd: { kind: "cwd" },
5657
- session: { kind: "session" }
5674
+ session: { kind: "session" },
5675
+ atc: { kind: "atc" }
5658
5676
  };
5659
5677
  const DEFAULT_CONFIG = {
5660
5678
  provider: DEFAULT_PROVIDER,
@@ -5663,12 +5681,13 @@ const DEFAULT_CONFIG = {
5663
5681
  denialBudget: DEFAULT_DENIAL_BUDGET,
5664
5682
  scopeSources: DEFAULT_SCOPE_SOURCES
5665
5683
  };
5666
- function resolveConfigPath() {
5667
- const xdg = process.env["XDG_CONFIG_HOME"];
5668
- const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
5684
+ function resolveConfigPath(host = readHostEnvironment()) {
5685
+ const xdg = host.env["XDG_CONFIG_HOME"];
5686
+ const base = xdg !== void 0 && xdg !== "" ? xdg : join(host.home, ".config");
5669
5687
  return join(base, "auto-mode", "config.json");
5670
5688
  }
5671
- async function loadConfig(path = resolveConfigPath()) {
5689
+ async function loadConfig(configPath, host = readHostEnvironment()) {
5690
+ const path = configPath ?? resolveConfigPath(host);
5672
5691
  let raw;
5673
5692
  try {
5674
5693
  raw = await readFile(path, "utf8");
@@ -5682,7 +5701,7 @@ async function loadConfig(path = resolveConfigPath()) {
5682
5701
  } catch {
5683
5702
  throw new Error(`${path} is not valid JSON`);
5684
5703
  }
5685
- return buildConfig(json, path);
5704
+ return buildConfig(json, path, host.home);
5686
5705
  }
5687
5706
  const text = string().min(1);
5688
5707
  const positive = number().positive();
@@ -5706,13 +5725,13 @@ const configFileSchema = strictObject({
5706
5725
  claudeSettingsPath: text.nullable().optional()
5707
5726
  }).optional()
5708
5727
  });
5709
- function buildConfig(json, path) {
5728
+ function buildConfig(json, path, home) {
5710
5729
  const parsed = configFileSchema.safeParse(json);
5711
5730
  if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5712
5731
  const file = parsed.data;
5713
5732
  const warnings = [];
5714
5733
  const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5715
- const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5734
+ const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry, home));
5716
5735
  const configuredSources = file.scopeSources === void 0 ? null : scopeSources.entries;
5717
5736
  warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`), ...collectScopeWarnings(configuredSources).map((line) => `${path}: scopeSources ${line}`));
5718
5737
  const decision = file.decision ?? {};
@@ -5722,10 +5741,10 @@ function buildConfig(json, path) {
5722
5741
  provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5723
5742
  judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5724
5743
  scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
5725
- classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5726
- rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5744
+ classifierPath: expandHomePath(policy.frameworkPath ?? void 0, home),
5745
+ rulesPath: expandHomePath(policy.rulesPath ?? void 0, home),
5727
5746
  onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
5728
- claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
5747
+ claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath, home),
5729
5748
  minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
5730
5749
  denialBudget: {
5731
5750
  consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
@@ -5800,14 +5819,14 @@ const scopeSourceEntrySchema = strictObject({
5800
5819
  kind: text.optional(),
5801
5820
  paths: array(text).min(1).optional()
5802
5821
  });
5803
- function parseScopeSourceEntry(id, entry) {
5822
+ function parseScopeSourceEntry(id, entry, home) {
5804
5823
  const parsed = scopeSourceEntrySchema.safeParse(entry);
5805
5824
  if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5806
5825
  const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
5807
5826
  if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5808
5827
  if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5809
5828
  kind,
5810
- paths: parsed.data.paths.map((glob) => expandHomePath(glob) ?? glob)
5829
+ paths: parsed.data.paths.map((glob) => expandHomePath(glob, home) ?? glob)
5811
5830
  };
5812
5831
  return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5813
5832
  }
@@ -5828,18 +5847,113 @@ function parseBuiltIn(id) {
5828
5847
  if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
5829
5848
  return resolved;
5830
5849
  }
5831
- function expandHomePath(path) {
5832
- return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
5850
+ function expandHomePath(path, home) {
5851
+ return path?.startsWith("~/") === true ? join(home, path.slice(2)) : path;
5833
5852
  }
5834
5853
  function resolveApiKey(provider, options = {}) {
5835
- const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
5854
+ const host = options.host ?? readHostEnvironment();
5855
+ const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : host.env[provider.apiKeyEnv];
5836
5856
  if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
5837
5857
  if (provider.apiKeyCommand === void 0 || provider.apiKeyCommand === "") return Promise.resolve(null);
5838
- return readApiKeyFromCommand(provider.apiKeyCommand, options);
5858
+ return readApiKeyFromCommand(provider.apiKeyCommand, {
5859
+ ...options,
5860
+ host
5861
+ });
5862
+ }
5863
+ //#endregion
5864
+ //#region src/scope/find-checkout.ts
5865
+ const GIT_OVERRIDES$1 = [
5866
+ "GIT_DIR",
5867
+ "GIT_WORK_TREE",
5868
+ "GIT_COMMON_DIR"
5869
+ ];
5870
+ async function findCheckout(start, env) {
5871
+ if (GIT_OVERRIDES$1.some((name) => env[name] !== void 0)) return null;
5872
+ for (let directory = start;; directory = dirname(directory)) {
5873
+ const gitPath = join(directory, ".git");
5874
+ const info = await stat(gitPath).catch(() => null);
5875
+ if (info !== null) {
5876
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
5877
+ return gitDir === null ? null : {
5878
+ worktree: directory,
5879
+ commonDir: await findCommonDir(gitDir)
5880
+ };
5881
+ }
5882
+ if (dirname(directory) === directory) return null;
5883
+ }
5884
+ }
5885
+ async function findLinkedGitDir(directory, gitPath) {
5886
+ const link = await readFile(gitPath, "utf8").catch(() => "");
5887
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
5888
+ return path === void 0 ? null : resolve(directory, path.trim());
5889
+ }
5890
+ async function findCommonDir(gitDir) {
5891
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
5892
+ return common === null ? gitDir : resolve(gitDir, common.trim());
5893
+ }
5894
+ //#endregion
5895
+ //#region src/state/resolve-state-dir.ts
5896
+ function resolveStateDir(host) {
5897
+ const stateHome = host.env["XDG_STATE_HOME"];
5898
+ const base = stateHome === void 0 || stateHome === "" ? join(host.home, ".local", "state") : stateHome;
5899
+ return join(base, "auto-mode");
5900
+ }
5901
+ //#endregion
5902
+ //#region src/bypass/resolve-edit-target.ts
5903
+ async function resolveEditTarget(path) {
5904
+ const missing = [];
5905
+ for (let current = path;; current = dirname(current)) try {
5906
+ const real = await realpath(current);
5907
+ return join(real, ...missing.toReversed());
5908
+ } catch {
5909
+ if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
5910
+ missing.push(basename(current));
5911
+ }
5912
+ }
5913
+ //#endregion
5914
+ //#region src/bypass/try-classify-edit.ts
5915
+ const DISK_EDIT_FILE_READER = {
5916
+ resolveEditTarget,
5917
+ findCheckout,
5918
+ readFile: (path) => readFile(path, "utf8")
5919
+ };
5920
+ const GIT_OVERRIDES = [
5921
+ "GIT_DIR",
5922
+ "GIT_WORK_TREE",
5923
+ "GIT_COMMON_DIR"
5924
+ ];
5925
+ async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READER) {
5926
+ const fields = getEditFields(request.toolName);
5927
+ const path = fields === null ? void 0 : request.toolInput[fields.path];
5928
+ if (typeof path !== "string" || path === "") return null;
5929
+ try {
5930
+ const ownDirs = [dirname(resolveConfigPath(host)), resolveStateDir(host)];
5931
+ const requested = resolve(request.cwd, path);
5932
+ const [target, worktrees, protectedDirs] = await Promise.all([
5933
+ files.resolveEditTarget(requested),
5934
+ Promise.all(scope.worktrees.map((worktree) => files.resolveEditTarget(worktree))),
5935
+ Promise.all(ownDirs.map((dir) => files.resolveEditTarget(dir)))
5936
+ ]);
5937
+ if (target === null || GIT_OVERRIDES.some((name) => host.env[name] !== void 0)) return null;
5938
+ const [checkout, current] = await Promise.all([files.findCheckout(dirname(target), host.env), request.toolName === "Edit" ? files.readFile(target).catch(() => null) : null]);
5939
+ return (await import("./classify-edit-LpoeenGt.js")).classifyEdit({
5940
+ toolName: request.toolName,
5941
+ toolInput: request.toolInput,
5942
+ requested,
5943
+ target,
5944
+ checkout: checkout?.worktree ?? null,
5945
+ current
5946
+ }, {
5947
+ worktrees: worktrees.filter((worktree) => worktree !== null),
5948
+ protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
5949
+ });
5950
+ } catch {
5951
+ return null;
5952
+ }
5839
5953
  }
5840
5954
  //#endregion
5841
5955
  //#region src/config/load-claude-rules.ts
5842
- async function loadClaudeRules(path) {
5956
+ async function loadClaudeRules(path, host) {
5843
5957
  const empty = {
5844
5958
  environment: [],
5845
5959
  allow: [],
@@ -5847,8 +5961,8 @@ async function loadClaudeRules(path) {
5847
5961
  hard_deny: []
5848
5962
  };
5849
5963
  if (path === null) return empty;
5850
- const configDir = process.env["CLAUDE_CONFIG_DIR"];
5851
- const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5964
+ const configDir = host.env["CLAUDE_CONFIG_DIR"];
5965
+ const base = configDir === void 0 || configDir === "" ? join(host.home, ".claude") : configDir;
5852
5966
  const settingsPath = path ?? join(base, "settings.json");
5853
5967
  let raw;
5854
5968
  try {
@@ -5986,13 +6100,13 @@ function toRepositorySlug(reference) {
5986
6100
  }
5987
6101
  //#endregion
5988
6102
  //#region src/containment/collect-scope-findings.ts
5989
- function collectScopeFindings(action, scope) {
6103
+ function collectScopeFindings(action, scope, scratchPaths = SCRATCH_PATHS) {
5990
6104
  const filePath = action.input["file_path"] ?? action.input["notebook_path"];
5991
6105
  const command = action.input["command"];
5992
6106
  const findings = [
5993
- ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
6107
+ ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope, scratchPaths) : [],
5994
6108
  ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
5995
- ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
6109
+ ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope, scratchPaths) : []
5996
6110
  ];
5997
6111
  return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
5998
6112
  }
@@ -6002,9 +6116,9 @@ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6002
6116
  "Write",
6003
6117
  "NotebookEdit"
6004
6118
  ]);
6005
- function collectTargetFindings(directory, target, scope) {
6119
+ function collectTargetFindings(directory, target, scope, scratchPaths) {
6006
6120
  const path = resolvePath(directory, target, scope.home);
6007
- return path === null ? [] : collectPathFindings(path, scope);
6121
+ return path === null ? [] : collectPathFindings(path, scope, scratchPaths);
6008
6122
  }
6009
6123
  const CREDENTIAL_PATHS = [
6010
6124
  ".ssh",
@@ -6015,12 +6129,12 @@ const CREDENTIAL_PATHS = [
6015
6129
  ".npmrc",
6016
6130
  ".config/gh"
6017
6131
  ];
6018
- function collectPathFindings(path, scope) {
6132
+ function collectPathFindings(path, scope, scratchPaths) {
6019
6133
  if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
6020
6134
  kind: "credential",
6021
6135
  target: path
6022
6136
  }];
6023
- return isInScope(path, scope) ? [] : [{
6137
+ return isInScope(path, scope, scratchPaths) ? [] : [{
6024
6138
  kind: "path",
6025
6139
  target: path
6026
6140
  }];
@@ -6051,24 +6165,24 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
6051
6165
  "rsync",
6052
6166
  "ln"
6053
6167
  ]);
6054
- function collectCommandFindings(command, cwd, scope) {
6168
+ function collectCommandFindings(command, cwd, scope, scratchPaths) {
6055
6169
  const findings = [];
6056
6170
  const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
6057
6171
  let directory = cwd;
6058
6172
  for (const segment of splitShellCommand(command).segments) {
6059
6173
  const [name, ...args] = splitCommandWords(segment.text);
6060
- for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
6174
+ for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope, scratchPaths));
6061
6175
  if (name === void 0) continue;
6062
6176
  if (name === "cd" && hasPipe) directory = null;
6063
6177
  else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
6064
- else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
6178
+ else findings.push(...collectProgramFindings(name, args, directory, cwd, scope, scratchPaths));
6065
6179
  }
6066
6180
  return findings;
6067
6181
  }
6068
- function collectProgramFindings(name, args, directory, cwd, scope) {
6069
- const resolve = (path) => collectTargetFindings(directory, path, scope);
6182
+ function collectProgramFindings(name, args, directory, cwd, scope, scratchPaths) {
6183
+ const resolve = (path) => collectTargetFindings(directory, path, scope, scratchPaths);
6070
6184
  const operands = args.filter((word) => !word.startsWith("-"));
6071
- if (name === "git") return collectGitFindings(args, directory, cwd, scope);
6185
+ if (name === "git") return collectGitFindings(args, directory, cwd, scope, scratchPaths);
6072
6186
  if (name === "gh") return collectGhFindings(args, scope);
6073
6187
  if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
6074
6188
  if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
@@ -6206,7 +6320,7 @@ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
6206
6320
  "stash",
6207
6321
  "push"
6208
6322
  ]);
6209
- function collectGitFindings(args, directory, cwd, scope) {
6323
+ function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
6210
6324
  let gitDirectory = directory;
6211
6325
  let index = 0;
6212
6326
  while (index < args.length && args[index]?.startsWith("-") === true) {
@@ -6218,11 +6332,11 @@ function collectGitFindings(args, directory, cwd, scope) {
6218
6332
  const rest = args.slice(index + 1);
6219
6333
  const operands = rest.filter((word) => !word.startsWith("-"));
6220
6334
  const findings = [];
6221
- if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
6335
+ if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope, scratchPaths)) findings.push({
6222
6336
  kind: "path",
6223
6337
  target: gitDirectory
6224
6338
  });
6225
- if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
6339
+ if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope, scratchPaths));
6226
6340
  if (subcommand === "branch" && rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) for (const branch of operands.filter((word) => !scope.branches.includes(word))) findings.push({
6227
6341
  kind: "branch",
6228
6342
  target: branch
@@ -6457,14 +6571,8 @@ function resolvePath(cwd, path, home) {
6457
6571
  const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6458
6572
  return normalize(absolute).replace(/(?<=.)\/$/u, "");
6459
6573
  }
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;
6574
+ function isInScope(path, scope, scratchPaths) {
6575
+ if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6468
6576
  return scope.worktrees.some((worktree) => {
6469
6577
  const isNested = isUnder(path, join(worktree, ".worktrees"));
6470
6578
  const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
@@ -6477,12 +6585,12 @@ function isUnder(path, root) {
6477
6585
  //#endregion
6478
6586
  //#region src/containment/check-containment.ts
6479
6587
  const CONTAINMENT_RULE = "Outside Task Scope";
6480
- function checkContainment(request, scope) {
6588
+ function checkContainment(request, scope, scratchPaths) {
6481
6589
  const findings = collectScopeFindings({
6482
6590
  tool: request.toolName,
6483
6591
  cwd: request.cwd,
6484
6592
  input: request.toolInput
6485
- }, scope);
6593
+ }, scope, scratchPaths);
6486
6594
  if (findings.length === 0) return null;
6487
6595
  return {
6488
6596
  rule: CONTAINMENT_RULE,
@@ -6520,12 +6628,12 @@ function buildTaskScopeSummary(scope) {
6520
6628
  }
6521
6629
  //#endregion
6522
6630
  //#region src/policy/resolve-shipped-policy-dir.ts
6523
- function resolveShippedPolicyDir() {
6524
- let dir = import.meta.dirname;
6631
+ function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
6632
+ let dir = startDir;
6525
6633
  for (;;) {
6526
6634
  if (existsSync(join(dir, "policy", "classifier.md"))) return join(dir, "policy");
6527
6635
  const parent = dirname(dir);
6528
- if (parent === dir || dir === parse(dir).root) throw new Error("auto-mode cannot find its shipped policy/ directory");
6636
+ if (parent === dir || dir === stopDir) throw new Error("auto-mode cannot find its shipped policy/ directory");
6529
6637
  dir = parent;
6530
6638
  }
6531
6639
  }
@@ -6542,11 +6650,13 @@ async function loadPolicy(paths = {}, framework = "classifier.md") {
6542
6650
  }
6543
6651
  //#endregion
6544
6652
  //#region src/model/anthropic-client.ts
6545
- async function sendMessage(provider, apiKey, request) {
6653
+ async function sendMessage(provider, apiKey, request, signal) {
6546
6654
  const controller = new AbortController();
6547
- const timer = setTimeout(() => {
6655
+ const stopRequest = () => {
6548
6656
  controller.abort();
6549
- }, provider.timeoutMs);
6657
+ };
6658
+ signal.addEventListener("abort", stopRequest, { once: true });
6659
+ if (signal.aborted) controller.abort();
6550
6660
  try {
6551
6661
  const response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/messages`, {
6552
6662
  method: "POST",
@@ -6579,7 +6689,7 @@ async function sendMessage(provider, apiKey, request) {
6579
6689
  }
6580
6690
  return readResult(body);
6581
6691
  } finally {
6582
- clearTimeout(timer);
6692
+ signal.removeEventListener("abort", stopRequest);
6583
6693
  }
6584
6694
  }
6585
6695
  const contentBlockSchema = union([object({
@@ -6622,7 +6732,7 @@ function readResult(body) {
6622
6732
  //#endregion
6623
6733
  //#region src/model/build-request.ts
6624
6734
  const MAX_INPUT_CHARS = 4e3;
6625
- function buildUserMessage(payload, transcript, reasoning, repositoryContext = null) {
6735
+ function buildUserMessage(payload, transcript, reasoning, repositoryContext = null, mcpServers = []) {
6626
6736
  const history = transcript.length === 0 ? "(no transcript available from this harness)" : transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n\n");
6627
6737
  const input = JSON.stringify(payload.toolInput, null, 2).slice(0, MAX_INPUT_CHARS);
6628
6738
  const closing = reasoning ? "Work through the classification process, then end your reply with the output contract tags." : "Reply with the output contract tags and nothing else.";
@@ -6630,7 +6740,7 @@ function buildUserMessage(payload, transcript, reasoning, repositoryContext = nu
6630
6740
  ${history}
6631
6741
  </transcript>
6632
6742
 
6633
- ${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}<action>
6743
+ ${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}${mcpServers.length === 0 ? "" : `<mcp-servers>\n${JSON.stringify(mcpServers, null, 2)}\n</mcp-servers>\n\n`}<action>
6634
6744
  tool: ${payload.toolName}
6635
6745
  cwd: ${payload.cwd}
6636
6746
  input:
@@ -6662,7 +6772,7 @@ function buildTaskContext(context) {
6662
6772
  }
6663
6773
  //#endregion
6664
6774
  //#region src/model/build-decision-request.ts
6665
- function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null) {
6775
+ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null, mcpServers = []) {
6666
6776
  const rules = {};
6667
6777
  let tier = null;
6668
6778
  for (const section of policy.split(/(?=^## |^### )/mu)) if (section.startsWith("## HARD BLOCK rules")) tier = "hard";
@@ -6710,6 +6820,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
6710
6820
  rulesSource,
6711
6821
  configuredRules,
6712
6822
  ...repositoryContext === null ? {} : { repositoryContext },
6823
+ ...mcpServers.length === 0 ? {} : { mcpServers },
6713
6824
  lastUserMessage: payload.decisionContext === void 0 || payload.decisionContext.agentID === null ? lastUserMessage : null,
6714
6825
  ...payload.decisionContext === void 0 ? {} : { taskContext: buildTaskContext(payload.decisionContext) },
6715
6826
  action: {
@@ -6728,6 +6839,17 @@ function buildEvidenceGuidance(name) {
6728
6839
  return "";
6729
6840
  }
6730
6841
  //#endregion
6842
+ //#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
6843
+ var isProduction = process.env.NODE_ENV === "production";
6844
+ var prefix = "Invariant failed";
6845
+ function invariant(condition, message) {
6846
+ if (condition) return;
6847
+ if (isProduction) throw new Error(prefix);
6848
+ var provided = typeof message === "function" ? message() : message;
6849
+ var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
6850
+ throw new Error(value);
6851
+ }
6852
+ //#endregion
6731
6853
  //#region src/model/classify-decision-answers.ts
6732
6854
  function classifyDecisionAnswers(request, result, minConfidence) {
6733
6855
  const entries = Object.entries(request.rules).map(([id, rule]) => {
@@ -6758,7 +6880,7 @@ function collectDecisionContributors(request, result, minConfidence) {
6758
6880
  const decision = classifyDecisionAnswers(request, result, minConfidence);
6759
6881
  return Object.entries(request.rules).flatMap(([id, rule]) => {
6760
6882
  const answer = result.answers[id];
6761
- if (answer === void 0) throw new Error("Decision answer missing");
6883
+ invariant(answer, "the classification refused a result with a rule unanswered");
6762
6884
  return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
6763
6885
  rule: rule.source === "shipped" ? rule.name : id,
6764
6886
  source: rule.source,
@@ -6787,6 +6909,83 @@ function formatClassifierNote(note, apiKey) {
6787
6909
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6788
6910
  }
6789
6911
  //#endregion
6912
+ //#region src/model/load-mcp-servers.ts
6913
+ const serverSchema = looseObject({
6914
+ type: string().optional(),
6915
+ url: string().optional()
6916
+ });
6917
+ const serversSchema = record(string(), unknown()).optional();
6918
+ const approvalSchema = looseObject({
6919
+ enableAllProjectMcpServers: boolean().optional(),
6920
+ enabledMcpjsonServers: array(string()).optional(),
6921
+ disabledMcpjsonServers: array(string()).optional()
6922
+ });
6923
+ const stateSchema = looseObject({
6924
+ mcpServers: serversSchema,
6925
+ projects: record(string(), looseObject({
6926
+ ...approvalSchema.shape,
6927
+ mcpServers: serversSchema
6928
+ })).optional()
6929
+ });
6930
+ const projectFileSchema = looseObject({ mcpServers: serversSchema });
6931
+ async function loadMCPServers(cwd, host) {
6932
+ const configDir = host.env["CLAUDE_CONFIG_DIR"];
6933
+ const hasConfigDir = configDir !== void 0 && configDir !== "";
6934
+ const statePath = hasConfigDir ? join(configDir, ".claude.json") : join(host.home, ".claude.json");
6935
+ const settingsDir = hasConfigDir ? configDir : join(host.home, ".claude");
6936
+ const settingsPath = join(settingsDir, "settings.json");
6937
+ const [state, settings] = await Promise.all([readJSON(statePath, stateSchema), readJSON(settingsPath, approvalSchema)]);
6938
+ const directory = resolve(cwd);
6939
+ const projectDir = (await findCheckout(directory, host.env))?.worktree ?? directory;
6940
+ const project = state?.projects?.[projectDir];
6941
+ const projectFile = await readJSON(join(projectDir, ".mcp.json"), projectFileSchema);
6942
+ const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
6943
+ const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
6944
+ const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
6945
+ const facts = [
6946
+ ...buildServerFacts(project?.mcpServers, "local"),
6947
+ ...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
6948
+ ...buildServerFacts(state?.mcpServers, "user")
6949
+ ];
6950
+ return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
6951
+ }
6952
+ async function readJSON(path, schema) {
6953
+ try {
6954
+ const text = await readFile(path, "utf8");
6955
+ const parsed = schema.safeParse(JSON.parse(text));
6956
+ return parsed.success ? parsed.data : null;
6957
+ } catch {
6958
+ return null;
6959
+ }
6960
+ }
6961
+ const MAX_NAME_LENGTH = 128;
6962
+ const TRANSPORTS = {
6963
+ stdio: "stdio",
6964
+ http: "http",
6965
+ "streamable-http": "http",
6966
+ sse: "sse",
6967
+ ws: "ws"
6968
+ };
6969
+ function buildServerFacts(servers, scope) {
6970
+ return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
6971
+ const server = serverSchema.safeParse(entry);
6972
+ if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
6973
+ const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
6974
+ if (transport === void 0) return [];
6975
+ return [{
6976
+ name,
6977
+ scope,
6978
+ transport,
6979
+ host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
6980
+ }];
6981
+ });
6982
+ }
6983
+ function findEndpointHost(url) {
6984
+ const parsed = URL.parse(url);
6985
+ if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
6986
+ return parsed.host;
6987
+ }
6988
+ //#endregion
6790
6989
  //#region src/scope/load-checkout-remotes.ts
6791
6990
  async function loadCheckoutRemotes(commonDir) {
6792
6991
  const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
@@ -6828,12 +7027,12 @@ function parseConfigValue(raw) {
6828
7027
  }
6829
7028
  //#endregion
6830
7029
  //#region src/model/load-repository-context.ts
6831
- async function loadRepositoryContext(cwd) {
7030
+ async function loadRepositoryContext(cwd, env, stopDir = parse(resolve(cwd)).root) {
6832
7031
  if ([
6833
7032
  "GIT_DIR",
6834
7033
  "GIT_WORK_TREE",
6835
7034
  "GIT_COMMON_DIR"
6836
- ].some((name) => process.env[name] !== void 0)) return null;
7035
+ ].some((name) => env[name] !== void 0)) return null;
6837
7036
  let directory = resolve(cwd);
6838
7037
  for (;;) {
6839
7038
  const gitPath = join(directory, ".git");
@@ -6842,7 +7041,7 @@ async function loadRepositoryContext(cwd) {
6842
7041
  } catch (error) {
6843
7042
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6844
7043
  const parent = dirname(directory);
6845
- if (parent === directory) return null;
7044
+ if (parent === directory || directory === resolve(stopDir)) return null;
6846
7045
  directory = parent;
6847
7046
  continue;
6848
7047
  }
@@ -6913,8 +7112,8 @@ function normalizeRemoteURL(url) {
6913
7112
  }
6914
7113
  //#endregion
6915
7114
  //#region src/model/load-repository-evidence.ts
6916
- async function loadRepositoryEvidence(cwd, taskScope) {
6917
- const context = await loadRepositoryContext(cwd);
7115
+ async function loadRepositoryEvidence(cwd, taskScope, env, stopDir) {
7116
+ const context = await loadRepositoryContext(cwd, env, stopDir);
6918
7117
  if (taskScope === void 0) return context;
6919
7118
  return {
6920
7119
  ...context ?? {
@@ -7323,6 +7522,28 @@ function pickDecisionVerdict(request, result, minConfidence) {
7323
7522
  })).exhaustive();
7324
7523
  }
7325
7524
  //#endregion
7525
+ //#region src/model/decision-response-schema.ts
7526
+ const probability = number().min(0).max(1);
7527
+ const answerSchema = object({
7528
+ type: literal("choice"),
7529
+ choice: _enum([
7530
+ "allow",
7531
+ "block",
7532
+ "ask"
7533
+ ]),
7534
+ confidence: probability,
7535
+ probabilities: strictObject({
7536
+ allow: probability,
7537
+ block: probability,
7538
+ ask: probability
7539
+ })
7540
+ });
7541
+ const decisionResponseSchema = object({
7542
+ model: string().min(1),
7543
+ answers: record(string(), answerSchema),
7544
+ usage: object({ input_tokens: number().int().nonnegative() })
7545
+ });
7546
+ //#endregion
7326
7547
  //#region src/model/send-decision.ts
7327
7548
  async function sendDecision(provider, apiKey, request, signal) {
7328
7549
  const input = {
@@ -7350,11 +7571,8 @@ async function sendDecision(provider, apiKey, request, signal) {
7350
7571
  const stopRequest = () => {
7351
7572
  controller.abort();
7352
7573
  };
7353
- signal?.addEventListener("abort", stopRequest, { once: true });
7354
- if (signal?.aborted === true) controller.abort();
7355
- const timer = setTimeout(() => {
7356
- controller.abort();
7357
- }, provider.timeoutMs);
7574
+ signal.addEventListener("abort", stopRequest, { once: true });
7575
+ if (signal.aborted) controller.abort();
7358
7576
  try {
7359
7577
  let response;
7360
7578
  try {
@@ -7372,26 +7590,6 @@ async function sendDecision(provider, apiKey, request, signal) {
7372
7590
  throw new DecisionRequestError("network", requestBytes, "Decision API request failed");
7373
7591
  }
7374
7592
  if (!response.ok) throw new DecisionRequestError("http-status", requestBytes, `Decision API returned HTTP ${response.status}`);
7375
- const probability = number().min(0).max(1);
7376
- const answer = object({
7377
- type: literal("choice"),
7378
- choice: _enum([
7379
- "allow",
7380
- "block",
7381
- "ask"
7382
- ]),
7383
- confidence: probability,
7384
- probabilities: strictObject({
7385
- allow: probability,
7386
- block: probability,
7387
- ask: probability
7388
- })
7389
- });
7390
- const schema = object({
7391
- model: string().min(1),
7392
- answers: record(string(), answer),
7393
- usage: object({ input_tokens: number().int().nonnegative() })
7394
- });
7395
7593
  let responseBody;
7396
7594
  try {
7397
7595
  responseBody = await response.json();
@@ -7399,7 +7597,7 @@ async function sendDecision(provider, apiKey, request, signal) {
7399
7597
  if (error instanceof Error && error.name === "AbortError") throw new DecisionRequestError("aborted", requestBytes, "Decision request aborted", { cause: error });
7400
7598
  throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned invalid JSON");
7401
7599
  }
7402
- const parsedResponse = schema.safeParse(responseBody);
7600
+ const parsedResponse = decisionResponseSchema.safeParse(responseBody);
7403
7601
  if (!parsedResponse.success) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned a malformed response");
7404
7602
  const parsed = parsedResponse.data;
7405
7603
  if (Object.keys(parsed.answers).length !== Object.keys(request.questions).length) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned an incomplete answer set");
@@ -7416,19 +7614,19 @@ async function sendDecision(provider, apiKey, request, signal) {
7416
7614
  requestBytes
7417
7615
  };
7418
7616
  } finally {
7419
- clearTimeout(timer);
7420
- signal?.removeEventListener("abort", stopRequest);
7617
+ signal.removeEventListener("abort", stopRequest);
7421
7618
  }
7422
7619
  }
7423
7620
  //#endregion
7424
7621
  //#region src/model/classify-with-jev.ts
7425
- async function classifyWithJev(payload, config, options = {}) {
7622
+ async function classifyWithJev(payload, config, options) {
7426
7623
  const start = performance.now();
7624
+ const now = options.now ?? Date.now;
7427
7625
  let key = null;
7428
7626
  let stage = "credential";
7429
7627
  const minConfidence = config.minConfidence ?? .8;
7430
7628
  let keySource = "none";
7431
- const envKey = config.provider.apiKeyEnv === void 0 ? void 0 : process.env[config.provider.apiKeyEnv];
7629
+ const envKey = config.provider.apiKeyEnv === void 0 ? void 0 : options.host.env[config.provider.apiKeyEnv];
7432
7630
  if (envKey !== void 0 && envKey !== "") keySource = "environment";
7433
7631
  else if (config.provider.apiKeyCommand !== void 0 && config.provider.apiKeyCommand !== "") keySource = "command";
7434
7632
  try {
@@ -7436,23 +7634,22 @@ async function classifyWithJev(payload, config, options = {}) {
7436
7634
  if (key === null) throw new Error("no API key: set the configured environment variable or key command");
7437
7635
  stage = "evidence";
7438
7636
  const directUserText = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage?.text ?? null : null;
7439
- const [policy, rules, repositoryContext] = await Promise.all([
7637
+ const [policy, rules, repositoryContext, mcpServers] = await Promise.all([
7440
7638
  loadPolicy({
7441
7639
  classifierPath: config.classifierPath,
7442
7640
  rulesPath: config.rulesPath
7443
7641
  }, "decision.md"),
7444
- loadClaudeRules(config.claudeSettingsPath),
7445
- loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null)
7642
+ loadClaudeRules(config.claudeSettingsPath, options.host),
7643
+ loadRepositoryEvidence(payload.cwd, options.taskScope, options.host.env).catch(() => null),
7644
+ loadMCPServers(payload.cwd, options.host).catch(() => [])
7446
7645
  ]);
7447
- const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext);
7448
- const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - Date.now();
7646
+ const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext, mcpServers);
7647
+ const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - now();
7449
7648
  if (remainingMs <= 0 || options.signal?.aborted === true) throw new DOMException("Evaluation deadline expired", "AbortError");
7450
- const provider = {
7451
- ...config.provider,
7452
- timeoutMs: Math.min(config.provider.timeoutMs, remainingMs)
7453
- };
7649
+ const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(config.provider.timeoutMs, remainingMs)));
7650
+ const signal = options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]);
7454
7651
  stage = "request";
7455
- const result = await sendDecision(provider, key, request, options.signal);
7652
+ const result = await sendDecision(config.provider, key, request, signal);
7456
7653
  stage = "response";
7457
7654
  const verdict = pickDecisionVerdict(request, result, minConfidence);
7458
7655
  return {
@@ -7476,7 +7673,7 @@ async function classifyWithJev(payload, config, options = {}) {
7476
7673
  if (options.signal?.aborted === true) {
7477
7674
  reason = "evaluation cancelled";
7478
7675
  status = "cancelled";
7479
- } else if (options.deadlineAt !== void 0 && Date.now() >= options.deadlineAt) {
7676
+ } else if (options.deadlineAt !== void 0 && now() >= options.deadlineAt) {
7480
7677
  reason = "evaluation deadline expired";
7481
7678
  status = "timeout";
7482
7679
  } else if (error instanceof Error) {
@@ -7537,8 +7734,15 @@ function parseModelVerdict(text) {
7537
7734
  //#endregion
7538
7735
  //#region src/model/classify-with-model.ts
7539
7736
  async function classifyWithModel(payload, config, options = {}) {
7540
- if (config.provider.protocol === "system-one") return classifyWithJev(payload, config, options);
7541
- const apiKey = await resolveApiKey(config.provider);
7737
+ const host = options.host ?? readHostEnvironment();
7738
+ if (config.provider.protocol === "system-one") return classifyWithJev(payload, config, {
7739
+ ...options,
7740
+ host
7741
+ });
7742
+ const apiKey = await resolveApiKey(config.provider, {
7743
+ ...options,
7744
+ host
7745
+ });
7542
7746
  if (apiKey === null) return buildFailure(config, "no API key: set the configured environment variable or key command");
7543
7747
  let system;
7544
7748
  try {
@@ -7554,13 +7758,14 @@ async function classifyWithModel(payload, config, options = {}) {
7554
7758
  role: "user",
7555
7759
  text: directMessage.text
7556
7760
  }];
7557
- const repositoryContext = await loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null);
7558
- const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext);
7761
+ const [repositoryContext, mcpServers] = await Promise.all([loadRepositoryEvidence(payload.cwd, options.taskScope, host.env).catch(() => null), loadMCPServers(payload.cwd, host).catch(() => [])]);
7762
+ const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext, mcpServers);
7559
7763
  try {
7764
+ const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
7560
7765
  const result = await sendMessage(config.provider, apiKey, {
7561
7766
  system,
7562
7767
  user
7563
- });
7768
+ }, timeout(toTimerDelay(config.provider.timeoutMs)));
7564
7769
  if (result.text.trim() === "") return buildFailure(config, `${config.provider.model} returned no text; raise maxTokens`);
7565
7770
  const verdict = parseModelVerdict(formatClassifierNote(result.text, apiKey));
7566
7771
  const cache = `${result.cachedInputTokens} cached / ${result.cacheWriteTokens} written / ${result.newInputTokens} new / ${result.outputTokens} out`;
@@ -7592,19 +7797,8 @@ function toMessage(error) {
7592
7797
  }
7593
7798
  //#endregion
7594
7799
  //#region src/policy/read-denial-guidance.ts
7595
- async function readDenialGuidance() {
7596
- return (await readFile(join(resolveShippedPolicyDir(), "denial.md"), "utf8")).trim();
7597
- }
7598
- //#endregion
7599
- //#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
7600
- var isProduction = process.env.NODE_ENV === "production";
7601
- var prefix = "Invariant failed";
7602
- function invariant(condition, message) {
7603
- if (condition) return;
7604
- if (isProduction) throw new Error(prefix);
7605
- var provided = typeof message === "function" ? message() : message;
7606
- var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
7607
- throw new Error(value);
7800
+ async function readDenialGuidance(policyDir = resolveShippedPolicyDir()) {
7801
+ return (await readFile(join(policyDir, "denial.md"), "utf8")).trim();
7608
7802
  }
7609
7803
  //#endregion
7610
7804
  //#region src/rules/local-allowlists.ts
@@ -7768,37 +7962,6 @@ function toUnquoted(word) {
7768
7962
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
7769
7963
  }
7770
7964
  //#endregion
7771
- //#region src/scope/find-checkout.ts
7772
- const GIT_OVERRIDES$1 = [
7773
- "GIT_DIR",
7774
- "GIT_WORK_TREE",
7775
- "GIT_COMMON_DIR"
7776
- ];
7777
- async function findCheckout(start) {
7778
- if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
7779
- for (let directory = start;; directory = dirname(directory)) {
7780
- const gitPath = join(directory, ".git");
7781
- const info = await stat(gitPath).catch(() => null);
7782
- if (info !== null) {
7783
- const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
7784
- return gitDir === null ? null : {
7785
- worktree: directory,
7786
- commonDir: await findCommonDir(gitDir)
7787
- };
7788
- }
7789
- if (dirname(directory) === directory) return null;
7790
- }
7791
- }
7792
- async function findLinkedGitDir(directory, gitPath) {
7793
- const link = await readFile(gitPath, "utf8").catch(() => "");
7794
- const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
7795
- return path === void 0 ? null : resolve(directory, path.trim());
7796
- }
7797
- async function findCommonDir(gitDir) {
7798
- const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
7799
- return common === null ? gitDir : resolve(gitDir, common.trim());
7800
- }
7801
- //#endregion
7802
7965
  //#region src/scope/merge-scope-facts.ts
7803
7966
  function mergeScopeFacts(all) {
7804
7967
  const pullRequests = /* @__PURE__ */ new Map();
@@ -7837,6 +8000,106 @@ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
7837
8000
  "develop"
7838
8001
  ]);
7839
8002
  //#endregion
8003
+ //#region src/scope/build-atc-scope-facts.ts
8004
+ function buildAtcScopeFacts(record, isInRepository) {
8005
+ const scope = record.scope;
8006
+ const checkouts = [scope.workspace, ...scope.worktrees];
8007
+ return {
8008
+ worktrees: checkouts.map((checkout) => checkout.path),
8009
+ branches: [...checkouts.flatMap((checkout) => checkout.branch !== null && isInRepository(checkout.path) ? [checkout.branch] : []), ...scope.branches.flatMap((branch) => isInRepository(branch.repo ?? scope.workspace.path) ? [branch.name] : [])],
8010
+ pullRequests: scope.pullRequests.flatMap((pull) => {
8011
+ const repository = toRepositorySlug(`https://${findHost(pull.url)}/${pull.repo}`);
8012
+ return pull.branch === null || repository === null ? [] : [{
8013
+ number: pull.number,
8014
+ head: pull.branch,
8015
+ repository
8016
+ }];
8017
+ }),
8018
+ pathGlobs: []
8019
+ };
8020
+ }
8021
+ function findHost(url) {
8022
+ return (url === void 0 ? null : URL.parse(url)?.host) ?? "github.com";
8023
+ }
8024
+ //#endregion
8025
+ //#region src/scope/load-atc-session-record.ts
8026
+ const name$1 = string().min(1);
8027
+ const path = name$1.refine(isAbsolute, "an absolute path");
8028
+ const checkoutSchema = object({
8029
+ path,
8030
+ branch: name$1.nullable()
8031
+ }).readonly();
8032
+ const branchSchema$1 = object({
8033
+ name: name$1,
8034
+ repo: path.optional()
8035
+ }).readonly();
8036
+ const pullRequestSchema$1 = object({
8037
+ repo: name$1,
8038
+ number: number().int().positive(),
8039
+ url: name$1.optional(),
8040
+ branch: name$1.nullable()
8041
+ }).readonly();
8042
+ const scopeSchema = object({
8043
+ workspace: checkoutSchema,
8044
+ worktrees: array(checkoutSchema).readonly(),
8045
+ branches: array(branchSchema$1).readonly(),
8046
+ pullRequests: array(pullRequestSchema$1).readonly()
8047
+ }).readonly();
8048
+ const recordSchema = object({
8049
+ format: literal("atc.session-record"),
8050
+ version: literal(1),
8051
+ session: name$1,
8052
+ scope: scopeSchema
8053
+ }).readonly();
8054
+ async function loadAtcSessionRecord(location, sessionID) {
8055
+ if (location === void 0 || location === "") return { kind: "absent" };
8056
+ if (!isAbsolute(location)) return {
8057
+ kind: "malformed",
8058
+ diagnostic: `atc session record path is not absolute: ${location}`
8059
+ };
8060
+ let raw;
8061
+ try {
8062
+ raw = await readFile(location, "utf8");
8063
+ } catch (error) {
8064
+ return isMissing(error) ? { kind: "absent" } : {
8065
+ kind: "malformed",
8066
+ diagnostic: `atc session record unreadable: ${location}`
8067
+ };
8068
+ }
8069
+ let parsed;
8070
+ try {
8071
+ parsed = JSON.parse(raw);
8072
+ } catch {
8073
+ return {
8074
+ kind: "malformed",
8075
+ diagnostic: `atc session record is not JSON: ${location}`
8076
+ };
8077
+ }
8078
+ const result = recordSchema.safeParse(parsed);
8079
+ if (!result.success) {
8080
+ const [issue] = result.error.issues;
8081
+ return {
8082
+ kind: "malformed",
8083
+ diagnostic: `atc session record ignored: ${issue === void 0 ? "record" : issue.path.join(".") || "record"} does not match version 1: ${location}`
8084
+ };
8085
+ }
8086
+ if (sessionID === void 0 || sessionID === "") return {
8087
+ kind: "malformed",
8088
+ diagnostic: `atc session record has no session to match: ${location}`
8089
+ };
8090
+ if (result.data.session !== sessionID) return {
8091
+ kind: "malformed",
8092
+ diagnostic: `atc session record belongs to another session: ${location}`
8093
+ };
8094
+ return {
8095
+ kind: "record",
8096
+ record: result.data
8097
+ };
8098
+ }
8099
+ function isMissing(error) {
8100
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
8101
+ }
8102
+ //#endregion
7840
8103
  //#region src/scope/types.ts
7841
8104
  const EMPTY_SCOPE_FACTS = {
7842
8105
  worktrees: [],
@@ -7888,7 +8151,7 @@ function resolveSessionScopePath(stateDir, sessionID) {
7888
8151
  }
7889
8152
  //#endregion
7890
8153
  //#region src/scope/pick-scope-source-reader.ts
7891
- function pickScopeSourceReader(source) {
8154
+ function pickScopeSourceReader(source, findSourceCheckout = findCheckout) {
7892
8155
  if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
7893
8156
  worktrees: [context.worktree],
7894
8157
  branches: context.branch === null ? [] : [context.branch]
@@ -7902,7 +8165,23 @@ function pickScopeSourceReader(source) {
7902
8165
  });
7903
8166
  };
7904
8167
  if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
7905
- return () => Promise.resolve(EMPTY_SCOPE_FACTS);
8168
+ return async (context) => {
8169
+ const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
8170
+ if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
8171
+ if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
8172
+ const scope = loaded.record.scope;
8173
+ const paths = /* @__PURE__ */ new Set([
8174
+ scope.workspace.path,
8175
+ ...scope.worktrees.map((worktree) => worktree.path),
8176
+ ...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
8177
+ ]);
8178
+ const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
8179
+ const commonDirs = new Map(entries);
8180
+ return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
8181
+ };
8182
+ }
8183
+ async function findCommonDirEntry(path, env, findSourceCheckout) {
8184
+ return [path, (await findSourceCheckout(path, env))?.commonDir ?? null];
7906
8185
  }
7907
8186
  function buildFacts(facts) {
7908
8187
  return {
@@ -7912,21 +8191,25 @@ function buildFacts(facts) {
7912
8191
  }
7913
8192
  //#endregion
7914
8193
  //#region src/scope/load-task-scope.ts
7915
- async function loadTaskScope(request, sources) {
8194
+ async function loadTaskScope(request, sources, host, stderr) {
7916
8195
  const cwd = resolve(request.cwd);
7917
- const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
8196
+ const [checkout, context] = await Promise.all([findCheckout(cwd, host.env), loadRepositoryContext(cwd, host.env)]);
7918
8197
  const branch = context?.branch ?? null;
7919
8198
  const sourceContext = {
8199
+ env: host.env,
7920
8200
  sessionID: request.sessionID,
7921
8201
  cwd,
7922
8202
  worktree: checkout?.worktree ?? cwd,
7923
8203
  commonDir: checkout?.commonDir ?? null,
7924
8204
  branch,
7925
- stateDir: request.stateDir
8205
+ stateDir: request.stateDir,
8206
+ atcRecordPath: host.env["ATC_SESSION_RECORD"],
8207
+ atcSessionID: host.env["ATC_SESSION_ID"],
8208
+ stderr
7926
8209
  };
7927
8210
  const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
7928
8211
  return buildTaskScope({
7929
- home: homedir(),
8212
+ home: host.home,
7930
8213
  currentBranch: branch,
7931
8214
  defaultBranch: context?.defaultBranch ?? null,
7932
8215
  remotes,
@@ -7934,18 +8217,12 @@ async function loadTaskScope(request, sources) {
7934
8217
  });
7935
8218
  }
7936
8219
  //#endregion
7937
- //#region src/state/resolve-state-dir.ts
7938
- function resolveStateDir() {
7939
- const stateHome = process.env["XDG_STATE_HOME"];
7940
- const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
7941
- return join(base, "auto-mode");
7942
- }
7943
- //#endregion
7944
8220
  //#region src/classify-action.ts
7945
8221
  async function classifyAction(request, config, options = {}) {
8222
+ const host = options.host ?? readHostEnvironment();
7946
8223
  let configured = null;
7947
8224
  if (config.provider.protocol === "system-one") try {
7948
- configured = await loadClaudeRules(config.claudeSettingsPath);
8225
+ configured = await loadClaudeRules(config.claudeSettingsPath, host);
7949
8226
  } catch {
7950
8227
  const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
7951
8228
  return {
@@ -7959,8 +8236,8 @@ async function classifyAction(request, config, options = {}) {
7959
8236
  const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
7960
8237
  const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
7961
8238
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7962
- const scope = await tryLoadTaskScope(request, config);
7963
- const containment = scope === null ? null : checkContainment(request, scope);
8239
+ const scope = await tryLoadTaskScope(request, config, host, options.stderr ?? process.stderr);
8240
+ const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
7964
8241
  if (containment !== null) {
7965
8242
  const guidance = await tryReadDenialGuidance();
7966
8243
  return {
@@ -7971,7 +8248,7 @@ async function classifyAction(request, config, options = {}) {
7971
8248
  };
7972
8249
  }
7973
8250
  if (local.kind === "allow") return buildLocalAllow(local.exception);
7974
- const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
8251
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host);
7975
8252
  if (edit?.kind === "bypass") return {
7976
8253
  verdict: { kind: "allow" },
7977
8254
  decidingStage: "bypass",
@@ -7987,7 +8264,8 @@ async function classifyAction(request, config, options = {}) {
7987
8264
  const taskScope = scope === null ? void 0 : buildTaskScopeSummary(scope);
7988
8265
  const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, {
7989
8266
  ...options,
7990
- taskScope
8267
+ taskScope,
8268
+ host
7991
8269
  }), tryReadDenialGuidance()]);
7992
8270
  return {
7993
8271
  ...outcome,
@@ -7997,47 +8275,13 @@ async function classifyAction(request, config, options = {}) {
7997
8275
  };
7998
8276
  }
7999
8277
  const READ_ONLY_EXCEPTION = "Read-only actions";
8000
- async function tryLoadTaskScope(request, config) {
8278
+ async function tryLoadTaskScope(request, config, host, stderr) {
8001
8279
  try {
8002
8280
  return await loadTaskScope({
8003
8281
  sessionID: request.sessionID,
8004
8282
  cwd: request.cwd,
8005
- stateDir: resolveStateDir()
8006
- }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
8007
- } catch {
8008
- return null;
8009
- }
8010
- }
8011
- const GIT_OVERRIDES = [
8012
- "GIT_DIR",
8013
- "GIT_WORK_TREE",
8014
- "GIT_COMMON_DIR"
8015
- ];
8016
- async function tryClassifyEdit(request, scope) {
8017
- const fields = getEditFields(request.toolName);
8018
- const path = fields === null ? void 0 : request.toolInput[fields.path];
8019
- if (typeof path !== "string" || path === "") return null;
8020
- try {
8021
- const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
8022
- const requested = resolve(request.cwd, path);
8023
- const [target, worktrees, protectedDirs] = await Promise.all([
8024
- resolveEditTarget(requested),
8025
- Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
8026
- Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
8027
- ]);
8028
- if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
8029
- const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
8030
- return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
8031
- toolName: request.toolName,
8032
- toolInput: request.toolInput,
8033
- requested,
8034
- target,
8035
- checkout: checkout?.worktree ?? null,
8036
- current
8037
- }, {
8038
- worktrees: worktrees.filter((worktree) => worktree !== null),
8039
- protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
8040
- });
8283
+ stateDir: resolveStateDir(host)
8284
+ }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
8041
8285
  } catch {
8042
8286
  return null;
8043
8287
  }
@@ -8066,7 +8310,7 @@ async function tryReadDenialGuidance() {
8066
8310
  }
8067
8311
  }
8068
8312
  //#endregion
8069
- //#region src/request/parse-action-request.ts
8313
+ //#region src/request/action-request-schema.ts
8070
8314
  const userTaskSchema = strictObject({
8071
8315
  text: string(),
8072
8316
  origin: _enum([
@@ -8090,7 +8334,7 @@ const decisionContextSchema = strictObject({
8090
8334
  lastDirectUserMessage: userTaskSchema.nullable(),
8091
8335
  omittedTaskContext: array(omissionSchema)
8092
8336
  });
8093
- const requestSchema = strictObject({
8337
+ const actionRequestSchema = strictObject({
8094
8338
  sessionID: string().min(1),
8095
8339
  toolUseID: string().min(1).optional(),
8096
8340
  cwd: string().min(1),
@@ -8098,8 +8342,10 @@ const requestSchema = strictObject({
8098
8342
  toolInput: looseObject({}),
8099
8343
  context: decisionContextSchema
8100
8344
  });
8345
+ //#endregion
8346
+ //#region src/request/parse-action-request.ts
8101
8347
  function parseActionRequest(body) {
8102
- const parsed = requestSchema.safeParse(body);
8348
+ const parsed = actionRequestSchema.safeParse(body);
8103
8349
  if (!parsed.success) return null;
8104
8350
  const context = parsed.data.context;
8105
8351
  return {
@@ -8123,4 +8369,4 @@ function renderVerdict(verdict) {
8123
8369
  })).exhaustive();
8124
8370
  }
8125
8371
  //#endregion
8126
- 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, findCheckout as c, parseModelVerdict as d, loadRepositoryContext as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, classifyLocally as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadCheckoutRemotes as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyWithModel as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };
8372
+ export { resolveConfigPath as C, strictObject as D, number as E, string as O, resolveApiKey as S, PRESETS as T, findCheckout as _, loadSessionScope as a, DEFAULT_SCOPE_SOURCES as b, classifyWithModel as c, loadCheckoutRemotes as d, loadPolicy as f, resolveStateDir as g, splitCommandWords as h, resolveSessionScopePath as i, parseModelVerdict as l, splitShellCommand as m, parseActionRequest as n, EMPTY_SESSION_SCOPE as o, toRepositorySlug as p, classifyAction as r, classifyLocally as s, renderVerdict as t, loadRepositoryContext as u, DEFAULT_CONFIG as v, readHostEnvironment as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };