auto-mode 0.19.0 → 0.19.2

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,10 +1,11 @@
1
1
  import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
2
- import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
+ import { appendFile, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { homedir, tmpdir } from "node:os";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { setTimeout } from "node:timers/promises";
6
8
  import { existsSync } from "node:fs";
7
- import { createHash } from "node:crypto";
8
9
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
9
10
  function getEnumValues(entries) {
10
11
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5529,6 +5530,24 @@ function superRefine(fn, params) {
5529
5530
  return /* @__PURE__ */ _superRefine(fn, params);
5530
5531
  }
5531
5532
  //#endregion
5533
+ //#region src/state/resolve-xdg-dir.ts
5534
+ const BASE_DIRS = {
5535
+ config: {
5536
+ variable: "XDG_CONFIG_HOME",
5537
+ fallback: [".config"]
5538
+ },
5539
+ state: {
5540
+ variable: "XDG_STATE_HOME",
5541
+ fallback: [".local", "state"]
5542
+ }
5543
+ };
5544
+ function resolveXDGDir(host, kind) {
5545
+ const dir = BASE_DIRS[kind];
5546
+ const base = host.env[dir.variable];
5547
+ const root = base === void 0 || base === "" ? join(host.home, ...dir.fallback) : base;
5548
+ return join(root, "auto-mode");
5549
+ }
5550
+ //#endregion
5532
5551
  //#region src/config/presets.ts
5533
5552
  const MESSAGES_DEFAULTS = {
5534
5553
  protocol: "messages",
@@ -5580,40 +5599,36 @@ function findPreset(id) {
5580
5599
  return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5581
5600
  }
5582
5601
  //#endregion
5583
- //#region src/config/to-timer-delay.ts
5584
- function toTimerDelay(ms) {
5585
- return Math.max(1, Math.ceil(ms));
5586
- }
5587
- //#endregion
5588
- //#region src/config/read-api-key-from-command.ts
5589
- function readApiKeyFromCommand(command, options = {}) {
5590
- const now = options.now ?? Date.now;
5591
- const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - now();
5592
- if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
5602
+ //#region src/process/run-capped.ts
5603
+ function runCapped(file, args, options) {
5593
5604
  return new Promise((resolve) => {
5594
5605
  const grouped = process.platform !== "win32";
5595
- const child = spawn("/bin/sh", ["-c", command], {
5606
+ const child = spawn(file, args, {
5596
5607
  detached: grouped,
5597
5608
  stdio: [
5598
- "ignore",
5609
+ options.input === void 0 ? "ignore" : "pipe",
5599
5610
  "pipe",
5600
5611
  "ignore"
5601
5612
  ],
5602
- ...options.host === void 0 ? {} : { env: {
5603
- HOME: options.host.home,
5604
- ...options.host.env
5605
- } }
5613
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd },
5614
+ ...options.env === void 0 ? {} : { env: options.env }
5606
5615
  });
5607
- let output = "";
5616
+ const chunks = [];
5617
+ let size = 0;
5608
5618
  let settled = false;
5609
5619
  let stopped = false;
5610
- const setResult = (value) => {
5611
- if (!settled) {
5612
- settled = true;
5613
- timer.removeEventListener("abort", stopChild);
5614
- options.signal?.removeEventListener("abort", stopChild);
5615
- resolve(value);
5616
- }
5620
+ let exited;
5621
+ const setResult = (exitCode, failedToStart) => {
5622
+ if (settled) return;
5623
+ settled = true;
5624
+ options.signal.removeEventListener("abort", stopChild);
5625
+ if (stopped) child.stdout?.destroy();
5626
+ resolve({
5627
+ exitCode,
5628
+ stdout: stopped || failedToStart ? "" : Buffer.concat(chunks).toString("utf8"),
5629
+ stopped,
5630
+ failedToStart
5631
+ });
5617
5632
  };
5618
5633
  const stopChild = () => {
5619
5634
  if (stopped) return;
@@ -5624,28 +5639,57 @@ function readApiKeyFromCommand(command, options = {}) {
5624
5639
  } catch {
5625
5640
  child.kill("SIGKILL");
5626
5641
  }
5642
+ if (exited !== void 0) setResult(exited, false);
5627
5643
  };
5628
- const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(5e3, remainingMs)));
5629
- timer.addEventListener("abort", stopChild, { once: true });
5630
- options.signal?.addEventListener("abort", stopChild, { once: true });
5631
- if (timer.aborted) stopChild();
5632
- child.stdout.setEncoding("utf8");
5633
- child.stdout.on("data", (chunk) => {
5644
+ options.signal.addEventListener("abort", stopChild, { once: true });
5645
+ if (options.signal.aborted) stopChild();
5646
+ child.stdout?.on("data", (chunk) => {
5634
5647
  if (stopped) return;
5635
- output += chunk;
5636
- if (output.length > 65536) stopChild();
5648
+ chunks.push(chunk);
5649
+ size += chunk.length;
5650
+ if (size > options.maxOutputBytes) stopChild();
5637
5651
  });
5652
+ child.stdin?.on("error", () => {});
5653
+ child.stdin?.end(options.input);
5638
5654
  child.on("error", () => {
5639
- setResult(null);
5655
+ setResult(null, !stopped);
5656
+ });
5657
+ child.on("exit", (code) => {
5658
+ exited = code;
5659
+ if (stopped) setResult(code, false);
5640
5660
  });
5641
5661
  child.on("close", (code) => {
5642
- const key = !stopped && code === 0 && output.trim() !== "" ? output.trim() : null;
5643
- setResult(key);
5662
+ setResult(code, false);
5644
5663
  });
5645
5664
  });
5646
5665
  }
5647
5666
  //#endregion
5648
- //#region src/containment/scratch-paths.ts
5667
+ //#region src/config/to-timer-delay.ts
5668
+ function toTimerDelay(ms) {
5669
+ return Math.max(1, Math.ceil(ms));
5670
+ }
5671
+ //#endregion
5672
+ //#region src/config/read-api-key-from-command.ts
5673
+ const KEY_COMMAND_TIMEOUT_MS = 5e3;
5674
+ const MAX_KEY_OUTPUT_BYTES = 65536;
5675
+ async function readApiKeyFromCommand(command, options = {}) {
5676
+ const now = options.now ?? Date.now;
5677
+ const remainingMs = options.deadlineAt === void 0 ? KEY_COMMAND_TIMEOUT_MS : options.deadlineAt - now();
5678
+ if (remainingMs <= 0 || options.signal?.aborted === true) return null;
5679
+ const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(KEY_COMMAND_TIMEOUT_MS, remainingMs)));
5680
+ const result = await runCapped("/bin/sh", ["-c", command], {
5681
+ signal: options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]),
5682
+ maxOutputBytes: MAX_KEY_OUTPUT_BYTES,
5683
+ env: options.host === void 0 ? void 0 : {
5684
+ HOME: options.host.home,
5685
+ ...options.host.env
5686
+ }
5687
+ });
5688
+ const key = result.stdout.trim();
5689
+ return !result.stopped && result.exitCode === 0 && key !== "" ? key : null;
5690
+ }
5691
+ //#endregion
5692
+ //#region src/config/scratch-paths.ts
5649
5693
  const SCRATCH_PATHS = [
5650
5694
  "/tmp",
5651
5695
  "/dev/null",
@@ -5691,9 +5735,7 @@ const DEFAULT_CONFIG = {
5691
5735
  scopeSources: DEFAULT_SCOPE_SOURCES
5692
5736
  };
5693
5737
  function resolveConfigPath(host = readHostEnvironment()) {
5694
- const xdg = host.env["XDG_CONFIG_HOME"];
5695
- const base = xdg !== void 0 && xdg !== "" ? xdg : join(host.home, ".config");
5696
- return join(base, "auto-mode", "config.json");
5738
+ return join(resolveXDGDir(host, "config"), "config.json");
5697
5739
  }
5698
5740
  async function loadConfig(configPath, host = readHostEnvironment()) {
5699
5741
  const path = configPath ?? resolveConfigPath(host);
@@ -5884,26 +5926,44 @@ function resolveApiKey(provider, options = {}) {
5884
5926
  });
5885
5927
  }
5886
5928
  //#endregion
5887
- //#region src/scope/find-checkout.ts
5929
+ //#region src/git/find-git-entry.ts
5930
+ async function findGitEntry(start, stopDir = parse(start).root) {
5931
+ for (let directory = start;; directory = dirname(directory)) {
5932
+ const gitPath = join(directory, ".git");
5933
+ if (await hasEntry(gitPath)) return {
5934
+ directory,
5935
+ gitPath
5936
+ };
5937
+ if (dirname(directory) === directory || directory === stopDir) return null;
5938
+ }
5939
+ }
5940
+ const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
5941
+ async function hasEntry(path) {
5942
+ try {
5943
+ await lstat(path);
5944
+ return true;
5945
+ } catch (error) {
5946
+ return !(error instanceof Error && "code" in error && ABSENT_CODES.has(String(error.code)));
5947
+ }
5948
+ }
5949
+ //#endregion
5950
+ //#region src/git/find-checkout.ts
5888
5951
  const GIT_OVERRIDES$1 = [
5889
5952
  "GIT_DIR",
5890
5953
  "GIT_WORK_TREE",
5891
5954
  "GIT_COMMON_DIR"
5892
5955
  ];
5893
- async function findCheckout(start, env, stopDir = parse(start).root) {
5956
+ async function findCheckout(start, env, stopDir) {
5894
5957
  if (GIT_OVERRIDES$1.some((name) => env[name] !== void 0)) return null;
5895
- for (let directory = start;; directory = dirname(directory)) {
5896
- const gitPath = join(directory, ".git");
5897
- const info = await stat(gitPath).catch(() => null);
5898
- if (info !== null) {
5899
- const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
5900
- return gitDir === null ? null : {
5901
- worktree: directory,
5902
- commonDir: await findCommonDir(gitDir)
5903
- };
5904
- }
5905
- if (dirname(directory) === directory || directory === stopDir) return null;
5906
- }
5958
+ const entry = await findGitEntry(start, stopDir);
5959
+ const info = entry === null ? null : await stat(entry.gitPath).catch(() => null);
5960
+ if (entry === null || info === null) return null;
5961
+ const gitDir = info.isDirectory() ? entry.gitPath : await findLinkedGitDir(entry.directory, entry.gitPath);
5962
+ return gitDir === null ? null : {
5963
+ worktree: entry.directory,
5964
+ gitDir,
5965
+ commonDir: await findCommonDir(gitDir)
5966
+ };
5907
5967
  }
5908
5968
  async function findLinkedGitDir(directory, gitPath) {
5909
5969
  const link = await readFile(gitPath, "utf8").catch(() => "");
@@ -5915,13 +5975,6 @@ async function findCommonDir(gitDir) {
5915
5975
  return common === null ? gitDir : resolve(gitDir, common.trim());
5916
5976
  }
5917
5977
  //#endregion
5918
- //#region src/state/resolve-state-dir.ts
5919
- function resolveStateDir(host) {
5920
- const stateHome = host.env["XDG_STATE_HOME"];
5921
- const base = stateHome === void 0 || stateHome === "" ? join(host.home, ".local", "state") : stateHome;
5922
- return join(base, "auto-mode");
5923
- }
5924
- //#endregion
5925
5978
  //#region src/bypass/resolve-edit-target.ts
5926
5979
  async function resolveEditTarget(path) {
5927
5980
  const missing = [];
@@ -5945,12 +5998,16 @@ const GIT_OVERRIDES = [
5945
5998
  "GIT_WORK_TREE",
5946
5999
  "GIT_COMMON_DIR"
5947
6000
  ];
5948
- async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READER) {
6001
+ async function tryClassifyEdit(request, scope, host, reader = {}) {
6002
+ const files = {
6003
+ ...DISK_EDIT_FILE_READER,
6004
+ ...reader
6005
+ };
5949
6006
  const fields = getEditFields(request.toolName);
5950
6007
  const path = fields === null ? void 0 : request.toolInput[fields.path];
5951
6008
  if (typeof path !== "string" || path === "") return null;
5952
6009
  try {
5953
- const ownDirs = [dirname(resolveConfigPath(host)), resolveStateDir(host)];
6010
+ const ownDirs = [dirname(resolveConfigPath(host)), resolveXDGDir(host, "state")];
5954
6011
  const requested = resolve(request.cwd, path);
5955
6012
  const [target, worktrees, protectedDirs] = await Promise.all([
5956
6013
  files.resolveEditTarget(requested),
@@ -5975,11 +6032,192 @@ async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READ
5975
6032
  }
5976
6033
  }
5977
6034
  //#endregion
6035
+ //#region src/budget/build-retry-key.ts
6036
+ const DESCRIPTIVE_FIELDS = { Bash: [
6037
+ "description",
6038
+ "timeout",
6039
+ "run_in_background"
6040
+ ] };
6041
+ function buildRetryKey(request) {
6042
+ const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
6043
+ const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
6044
+ const message = request.decisionContext?.lastDirectUserMessage ?? null;
6045
+ const canonical = JSON.stringify(sortKeys({
6046
+ cwd: request.cwd,
6047
+ toolName: request.toolName,
6048
+ toolInput: input,
6049
+ lastDirectUserMessage: message?.text ?? null,
6050
+ ...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
6051
+ }));
6052
+ return createHash("sha256").update(canonical).digest("hex");
6053
+ }
6054
+ function sortKeys(value) {
6055
+ if (Array.isArray(value)) return value.map((item) => sortKeys(item));
6056
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a < b ? -1 : 1).map(([key, item]) => [key, sortKeys(item)]));
6057
+ return value;
6058
+ }
6059
+ //#endregion
6060
+ //#region src/budget/find-retry-deny.ts
6061
+ function findRetryDeny(state, retryKey) {
6062
+ if (state.lastDenied === null || state.lastDenied.retryKey !== retryKey) return null;
6063
+ return {
6064
+ kind: "deny",
6065
+ rule: state.lastDenied.rule,
6066
+ reason: state.lastDenied.reason
6067
+ };
6068
+ }
6069
+ //#endregion
6070
+ //#region src/state/load-json-file.ts
6071
+ async function loadJSONFile(path, schema, fallback) {
6072
+ try {
6073
+ const text = await readFile(path, "utf8");
6074
+ const parsed = schema.safeParse(JSON.parse(text));
6075
+ return parsed.success ? parsed.data : fallback;
6076
+ } catch {
6077
+ return fallback;
6078
+ }
6079
+ }
6080
+ //#endregion
6081
+ //#region src/budget/types.ts
6082
+ const EMPTY_DENIAL_STATE = {
6083
+ consecutive: 0,
6084
+ session: 0,
6085
+ lastDenied: null
6086
+ };
6087
+ //#endregion
6088
+ //#region src/budget/load-denial-state.ts
6089
+ const count = number().int().nonnegative();
6090
+ const stateSchema$1 = strictObject({
6091
+ consecutive: count,
6092
+ session: count,
6093
+ lastDenied: strictObject({
6094
+ retryKey: string().min(1),
6095
+ rule: string(),
6096
+ reason: string()
6097
+ }).nullable()
6098
+ });
6099
+ function loadDenialState(path) {
6100
+ return loadJSONFile(path, stateSchema$1, EMPTY_DENIAL_STATE);
6101
+ }
6102
+ //#endregion
6103
+ //#region src/budget/plan-denial-budget.ts
6104
+ function planDenialBudget(state, verdict, retryKey, budget) {
6105
+ if (verdict === null) return {
6106
+ verdict: null,
6107
+ state,
6108
+ escalation: false
6109
+ };
6110
+ if (verdict.kind === "allow") return {
6111
+ verdict,
6112
+ state: {
6113
+ ...state,
6114
+ consecutive: 0,
6115
+ lastDenied: null
6116
+ },
6117
+ escalation: false
6118
+ };
6119
+ if (state.consecutive >= budget.consecutive || state.session >= budget.perSession) return {
6120
+ verdict: null,
6121
+ state: EMPTY_DENIAL_STATE,
6122
+ escalation: true
6123
+ };
6124
+ const next = {
6125
+ consecutive: state.consecutive + 1,
6126
+ session: state.session + 1,
6127
+ lastDenied: {
6128
+ retryKey,
6129
+ rule: verdict.rule,
6130
+ reason: verdict.reason
6131
+ }
6132
+ };
6133
+ const left = Math.min(budget.consecutive - next.consecutive, budget.perSession - next.session);
6134
+ return {
6135
+ verdict: {
6136
+ ...verdict,
6137
+ reason: `${verdict.reason} ${formatDenialsLeft(left)}`
6138
+ },
6139
+ state: next,
6140
+ escalation: false
6141
+ };
6142
+ }
6143
+ function formatDenialsLeft(left) {
6144
+ if (left > 0) return `Denials left before auto-mode asks the user: ${left}.`;
6145
+ return "This is the last denial before auto-mode asks the user. Stop now, without finishing the rest of the task, and tell the user what consent you need to continue.";
6146
+ }
6147
+ //#endregion
6148
+ //#region src/budget/resolve-denial-state-path.ts
6149
+ function resolveDenialStatePath(request, stateDir) {
6150
+ const agentID = request.decisionContext?.agentID ?? "";
6151
+ const key = createHash("sha256").update(`${request.sessionID}\0${agentID}`).digest("hex").slice(0, 32);
6152
+ return join(stateDir, "denials", `${key}.json`);
6153
+ }
6154
+ //#endregion
6155
+ //#region src/state/update-json-file.ts
6156
+ const SYSTEM_CLOCK = {
6157
+ now: Date.now,
6158
+ wait: (ms) => setTimeout(ms)
6159
+ };
6160
+ async function updateJSONFile(path, load, update, clock = SYSTEM_CLOCK) {
6161
+ await mkdir(dirname(path), {
6162
+ recursive: true,
6163
+ mode: 448
6164
+ });
6165
+ const lock = `${path}.lock`;
6166
+ await claimLock(lock, clock);
6167
+ try {
6168
+ const previous = await load(path);
6169
+ const next = update(previous);
6170
+ if (next !== previous) {
6171
+ const staged = `${path}.${randomUUID()}.tmp`;
6172
+ await writeFile(staged, `${JSON.stringify(next)}\n`, { mode: 384 });
6173
+ await rename(staged, path);
6174
+ }
6175
+ return {
6176
+ previous,
6177
+ next
6178
+ };
6179
+ } finally {
6180
+ await rm(lock, { force: true });
6181
+ }
6182
+ }
6183
+ const STALE_LOCK_MS = 1e4;
6184
+ const LOCK_WAIT_MS = 3e3;
6185
+ const LOCK_POLL_MS = 10;
6186
+ async function claimLock(lock, clock) {
6187
+ const deadline = clock.now() + LOCK_WAIT_MS;
6188
+ for (;;) {
6189
+ try {
6190
+ await (await open(lock, "wx", 384)).close();
6191
+ return;
6192
+ } catch (error) {
6193
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
6194
+ }
6195
+ const held = await stat(lock).catch(() => null);
6196
+ if (held !== null && clock.now() - held.mtimeMs > STALE_LOCK_MS) await rm(lock, { force: true });
6197
+ else if (clock.now() > deadline) throw new Error("state file lock unavailable");
6198
+ else await clock.wait(LOCK_POLL_MS);
6199
+ }
6200
+ }
6201
+ //#endregion
6202
+ //#region src/budget/update-denial-state.ts
6203
+ function updateDenialState(path, update, clock) {
6204
+ return updateJSONFile(path, loadDenialState, update, clock);
6205
+ }
6206
+ //#endregion
6207
+ //#region src/config/resolve-claude-config-path.ts
6208
+ const DEFAULT_DIRS = {
6209
+ "settings.json": [".claude"],
6210
+ ".claude.json": []
6211
+ };
6212
+ function resolveClaudeConfigPath(host, file) {
6213
+ const configDir = host.env["CLAUDE_CONFIG_DIR"];
6214
+ const dir = configDir === void 0 || configDir === "" ? join(host.home, ...DEFAULT_DIRS[file]) : configDir;
6215
+ return join(dir, file);
6216
+ }
6217
+ //#endregion
5978
6218
  //#region src/config/load-claude-settings.ts
5979
6219
  async function loadClaudeSettings(path, host) {
5980
- const configDir = host.env["CLAUDE_CONFIG_DIR"];
5981
- const base = configDir === void 0 || configDir === "" ? join(host.home, ".claude") : configDir;
5982
- const userPath = join(base, "settings.json");
6220
+ const userPath = resolveClaudeConfigPath(host, "settings.json");
5983
6221
  if (path === void 0) {
5984
6222
  const body = await readSettings(userPath);
5985
6223
  return {
@@ -6032,108 +6270,259 @@ function parseRules(body) {
6032
6270
  };
6033
6271
  }
6034
6272
  //#endregion
6035
- //#region src/rules/split-command-words.ts
6036
- function splitCommandWords(text) {
6037
- const words = splitWords$1(text);
6273
+ //#region src/git/to-repository-slug.ts
6274
+ function toRepositorySlug(reference) {
6275
+ const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
6276
+ if (match?.groups === void 0) return null;
6277
+ const parts = match.groups;
6278
+ return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
6279
+ }
6280
+ //#endregion
6281
+ //#region src/shell/is-unresolved-word.ts
6282
+ function isUnresolvedWord(word) {
6283
+ return /[$`]/u.test(word);
6284
+ }
6285
+ //#endregion
6286
+ //#region src/shell/resolve-shell-path.ts
6287
+ function resolveShellPath(directory, path, home) {
6288
+ const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6289
+ if (isUnresolvedWord(expanded) || expanded.startsWith("~") || expanded === "") return null;
6290
+ if (directory === null && !isAbsolute(expanded)) return null;
6291
+ const absolute = isAbsolute(expanded) || directory === null ? expanded : join(directory, expanded);
6292
+ return normalize(absolute).replace(/(?<=.)\/$/u, "");
6293
+ }
6294
+ //#endregion
6295
+ //#region src/shell/split-shell-tokens.ts
6296
+ function splitShellTokens(text) {
6297
+ const tokens = [];
6298
+ let hasSubstitution = false;
6038
6299
  let index = 0;
6039
- while (index < words.length) {
6040
- const word = words[index] ?? "";
6041
- if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6042
- else if (word === "env") {
6300
+ while (index < text.length) {
6301
+ const ch = text.charAt(index);
6302
+ if (/\s/u.test(ch) && ch !== "\n") {
6043
6303
  index += 1;
6044
- while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6045
- } else if (word === "timeout") index += 2;
6046
- else break;
6304
+ continue;
6305
+ }
6306
+ const separator = findOperator(SEPARATORS, text, index);
6307
+ if (separator !== void 0 && !text.startsWith("&>", index)) {
6308
+ tokens.push({
6309
+ kind: "separator",
6310
+ operator: separator,
6311
+ start: index,
6312
+ end: index + separator.length
6313
+ });
6314
+ index += separator.length;
6315
+ continue;
6316
+ }
6317
+ const word = readWord(text, index);
6318
+ hasSubstitution ||= word.hasSubstitution;
6319
+ const operator = findOperator(REDIRECTS, text, word.end);
6320
+ const isDescriptor = operator !== void 0 && /^\d+$/u.test(word.raw);
6321
+ if (word.raw !== "" && !isDescriptor) {
6322
+ if (word.isUnterminated) return {
6323
+ tokens: [...tokens, buildWordToken(word, index)],
6324
+ hasSubstitution,
6325
+ isUnterminated: true
6326
+ };
6327
+ tokens.push(buildWordToken(word, index));
6328
+ index = word.end;
6329
+ continue;
6330
+ }
6331
+ if (operator === void 0) {
6332
+ index += 1;
6333
+ continue;
6334
+ }
6335
+ const redirect = readRedirect(text, word.end, operator);
6336
+ hasSubstitution ||= redirect.hasSubstitution;
6337
+ tokens.push({
6338
+ kind: "redirect",
6339
+ operator: `${word.raw}${operator}`,
6340
+ target: redirect.target,
6341
+ start: index,
6342
+ end: redirect.end
6343
+ });
6344
+ if (redirect.isUnterminated) return {
6345
+ tokens,
6346
+ hasSubstitution,
6347
+ isUnterminated: true
6348
+ };
6349
+ index = redirect.end;
6047
6350
  }
6048
- return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
6351
+ return {
6352
+ tokens,
6353
+ hasSubstitution,
6354
+ isUnterminated: false
6355
+ };
6356
+ }
6357
+ const SEPARATORS = [
6358
+ "&&",
6359
+ "||",
6360
+ ";",
6361
+ "|",
6362
+ "&",
6363
+ "\n"
6364
+ ];
6365
+ const REDIRECTS = [
6366
+ "&>>",
6367
+ "&>",
6368
+ "<<<",
6369
+ "<<-",
6370
+ "<<",
6371
+ "<&",
6372
+ "<>",
6373
+ "<(",
6374
+ "<",
6375
+ ">>",
6376
+ ">|",
6377
+ ">&",
6378
+ ">(",
6379
+ ">"
6380
+ ];
6381
+ function findOperator(operators, text, index) {
6382
+ return operators.find((operator) => text.startsWith(operator, index));
6049
6383
  }
6050
- function splitWords$1(text) {
6051
- const words = [];
6052
- let current = "";
6053
- let quote = null;
6054
- let started = false;
6055
- for (const ch of text) if (quote !== null) {
6056
- if (ch === quote) quote = null;
6057
- else current += ch;
6058
- } else if (ch === "'" || ch === "\"") {
6059
- quote = ch;
6060
- started = true;
6061
- } else if (/\s/u.test(ch)) {
6062
- if (started) words.push(current);
6063
- current = "";
6064
- started = false;
6065
- } else {
6066
- current += ch;
6067
- started = true;
6068
- }
6069
- if (started) words.push(current);
6070
- return words;
6384
+ function buildWordToken(word, start) {
6385
+ return {
6386
+ kind: "word",
6387
+ value: word.value,
6388
+ raw: word.raw,
6389
+ start,
6390
+ end: word.end
6391
+ };
6071
6392
  }
6072
- //#endregion
6073
- //#region src/rules/split-shell-command.ts
6074
- function splitShellCommand(command) {
6075
- const segments = [];
6076
- let current = "";
6393
+ function readWord(text, start) {
6394
+ let value = "";
6077
6395
  let quote = null;
6078
- let unparsed = false;
6079
- let i = 0;
6080
- const collectSegment = () => {
6081
- const text = current.trim();
6082
- if (text !== "") segments.push({ text });
6083
- current = "";
6084
- };
6085
- while (i < command.length) {
6086
- const ch = command.charAt(i);
6396
+ let hasSubstitution = false;
6397
+ let parenAt = null;
6398
+ let index = start;
6399
+ while (index < text.length) {
6400
+ const ch = text.charAt(index);
6087
6401
  if (quote !== null) {
6088
- if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
6402
+ if (quote === "\"" && (text.startsWith("$(", index) || ch === "`")) hasSubstitution = true;
6089
6403
  if (ch === quote) quote = null;
6090
- current += ch;
6091
- i += 1;
6404
+ else value += ch;
6405
+ index += 1;
6092
6406
  continue;
6093
6407
  }
6094
6408
  if (ch === "'" || ch === "\"") {
6095
6409
  quote = ch;
6096
- current += ch;
6097
- i += 1;
6410
+ index += 1;
6098
6411
  continue;
6099
6412
  }
6100
6413
  if (ch === "\\") {
6101
- current += command.slice(i, i + 2);
6102
- i += 2;
6103
- continue;
6104
- }
6105
- if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
6106
- unparsed = true;
6107
- current += ch;
6108
- i += 1;
6414
+ value += text.slice(index, index + 2);
6415
+ index += 2;
6109
6416
  continue;
6110
6417
  }
6111
- if (command.startsWith("&&", i) || command.startsWith("||", i)) {
6112
- collectSegment();
6113
- i += 2;
6114
- continue;
6115
- }
6116
- if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
6117
- collectSegment();
6118
- i += 1;
6119
- continue;
6120
- }
6121
- current += ch;
6122
- i += 1;
6418
+ if (/\s/u.test(ch) || /[;&|<>]/u.test(ch)) break;
6419
+ if (text.startsWith("$(", index) || ch === "`") hasSubstitution = true;
6420
+ if ((ch === "(" || ch === ")") && parenAt === null) parenAt = value.length;
6421
+ value += ch;
6422
+ index += 1;
6423
+ }
6424
+ return {
6425
+ value,
6426
+ raw: text.slice(start, Math.min(index, text.length)),
6427
+ end: Math.min(index, text.length),
6428
+ parenAt,
6429
+ hasSubstitution,
6430
+ isUnterminated: quote !== null
6431
+ };
6432
+ }
6433
+ function readRedirect(text, start, operator) {
6434
+ let index = start + operator.length;
6435
+ while (index < text.length && /\s/u.test(text.charAt(index)) && text.charAt(index) !== "\n") index += 1;
6436
+ const word = readWord(text, index);
6437
+ const isProcess = operator.endsWith("(");
6438
+ if (word.raw === "") return {
6439
+ target: null,
6440
+ end: index,
6441
+ hasSubstitution: isProcess,
6442
+ isUnterminated: false
6443
+ };
6444
+ return {
6445
+ target: word.parenAt === null ? word.value : word.value.slice(0, word.parenAt),
6446
+ end: word.end,
6447
+ hasSubstitution: isProcess || word.hasSubstitution,
6448
+ isUnterminated: word.isUnterminated
6449
+ };
6450
+ }
6451
+ //#endregion
6452
+ //#region src/shell/split-command-words.ts
6453
+ function splitCommandWords(text) {
6454
+ const words = splitShellTokens(text).tokens.flatMap((token) => token.kind === "word" ? [token.value] : []);
6455
+ let index = 0;
6456
+ while (index < words.length) {
6457
+ const word = words[index] ?? "";
6458
+ if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6459
+ else if (word === "env") {
6460
+ index += 1;
6461
+ while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6462
+ } else if (word === "timeout") index += 2;
6463
+ else break;
6464
+ }
6465
+ return words.slice(index);
6466
+ }
6467
+ //#endregion
6468
+ //#region src/shell/split-shell-command.ts
6469
+ function splitShellCommand(command) {
6470
+ const scanned = splitShellTokens(command);
6471
+ const segments = [];
6472
+ let start = null;
6473
+ let end = 0;
6474
+ for (const token of [...scanned.tokens, null]) if (token === null || token.kind === "separator") {
6475
+ if (start !== null) segments.push({ text: command.slice(start, end) });
6476
+ start = null;
6477
+ } else {
6478
+ start ??= token.start;
6479
+ end = token.end;
6123
6480
  }
6124
- collectSegment();
6125
6481
  return {
6126
6482
  segments,
6127
- hasUnparsedConstruct: unparsed || quote !== null
6483
+ hasUnparsedConstruct: scanned.hasSubstitution || scanned.isUnterminated || scanned.tokens.some((token) => token.kind === "redirect" && token.operator.includes(">"))
6128
6484
  };
6129
6485
  }
6130
6486
  //#endregion
6131
- //#region src/containment/to-repository-slug.ts
6132
- function toRepositorySlug(reference) {
6133
- const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
6134
- if (match?.groups === void 0) return null;
6135
- const parts = match.groups;
6136
- return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
6487
+ //#region src/shell/collect-shell-commands.ts
6488
+ function collectShellCommands(command, cwd, home) {
6489
+ const hasPipe = splitShellTokens(command).tokens.some((token) => token.kind === "separator" && token.operator === "|");
6490
+ let directory = cwd;
6491
+ return splitShellCommand(command).segments.map((segment) => {
6492
+ const words = splitCommandWords(segment.text);
6493
+ const entry = {
6494
+ words,
6495
+ directory,
6496
+ writeTargets: collectWriteTargets(segment.text)
6497
+ };
6498
+ if (words[0] === "cd") directory = hasPipe ? null : resolveShellPath(directory, words[1] ?? home, home);
6499
+ return entry;
6500
+ });
6501
+ }
6502
+ const WRITE_REDIRECTS = /* @__PURE__ */ new Set([
6503
+ ">",
6504
+ ">>",
6505
+ ">|",
6506
+ "&>",
6507
+ "&>>"
6508
+ ]);
6509
+ function collectWriteTargets(text) {
6510
+ return splitShellTokens(text).tokens.flatMap((token) => token.kind === "redirect" && token.target !== null && token.target !== "" && WRITE_REDIRECTS.has(token.operator.replace(/^\d+/u, "")) ? [token.target] : []);
6511
+ }
6512
+ //#endregion
6513
+ //#region src/shell/split-git-command.ts
6514
+ function splitGitCommand(args, directory, home) {
6515
+ let gitDirectory = directory;
6516
+ let index = 0;
6517
+ while (index < args.length && args[index]?.startsWith("-") === true) {
6518
+ const option = args[index];
6519
+ if (option === "-C") gitDirectory = resolveShellPath(gitDirectory, args[index + 1] ?? "", home);
6520
+ index += option === "-C" || option === "-c" ? 2 : 1;
6521
+ }
6522
+ return {
6523
+ directory: gitDirectory,
6524
+ args: args.slice(index)
6525
+ };
6137
6526
  }
6138
6527
  //#endregion
6139
6528
  //#region src/containment/collect-scope-findings.ts
@@ -6154,7 +6543,7 @@ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6154
6543
  "NotebookEdit"
6155
6544
  ]);
6156
6545
  function collectTargetFindings(directory, target, scope, scratchPaths) {
6157
- const path = resolvePath(directory, target, scope.home);
6546
+ const path = resolveShellPath(directory, target, scope.home);
6158
6547
  return path === null ? [] : collectPathFindings(path, scope, scratchPaths);
6159
6548
  }
6160
6549
  const CREDENTIAL_PATHS = [
@@ -6204,15 +6593,10 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
6204
6593
  ]);
6205
6594
  function collectCommandFindings(command, cwd, scope, scratchPaths) {
6206
6595
  const findings = [];
6207
- const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
6208
- let directory = cwd;
6209
- for (const segment of splitShellCommand(command).segments) {
6210
- const [name, ...args] = splitCommandWords(segment.text);
6211
- for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope, scratchPaths));
6212
- if (name === void 0) continue;
6213
- if (name === "cd" && hasPipe) directory = null;
6214
- else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
6215
- else findings.push(...collectProgramFindings(name, args, directory, cwd, scope, scratchPaths));
6596
+ for (const entry of collectShellCommands(command, cwd, scope.home)) {
6597
+ const [name, ...args] = entry.words;
6598
+ for (const target of entry.writeTargets) findings.push(...collectTargetFindings(entry.directory, target, scope, scratchPaths));
6599
+ if (name !== void 0 && name !== "cd") findings.push(...collectProgramFindings(name, args, entry.directory, cwd, scope, scratchPaths));
6216
6600
  }
6217
6601
  return findings;
6218
6602
  }
@@ -6358,15 +6742,9 @@ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
6358
6742
  "push"
6359
6743
  ]);
6360
6744
  function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
6361
- let gitDirectory = directory;
6362
- let index = 0;
6363
- while (index < args.length && args[index]?.startsWith("-") === true) {
6364
- const option = args[index];
6365
- if (option === "-C") gitDirectory = resolvePath(gitDirectory, args[index + 1] ?? "", scope.home);
6366
- index += option === "-C" || option === "-c" ? 2 : 1;
6367
- }
6368
- const subcommand = args[index] ?? "";
6369
- const rest = args.slice(index + 1);
6745
+ const git = splitGitCommand(args, directory, scope.home);
6746
+ const gitDirectory = git.directory;
6747
+ const [subcommand = "", ...rest] = git.args;
6370
6748
  const operands = rest.filter((word) => !word.startsWith("-"));
6371
6749
  const findings = [];
6372
6750
  if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope, scratchPaths)) findings.push({
@@ -6396,7 +6774,7 @@ function collectPushFindings(rest, operands, currentBranch, scope) {
6396
6774
  kind: "branch",
6397
6775
  target: "--all"
6398
6776
  });
6399
- if (remote !== void 0 && !isUnresolved(remote) && !isOwnedRemote(remote, scope)) findings.push({
6777
+ if (remote !== void 0 && !isUnresolvedWord(remote) && !isOwnedRemote(remote, scope)) findings.push({
6400
6778
  kind: "remote-write",
6401
6779
  target: remote
6402
6780
  });
@@ -6404,16 +6782,13 @@ function collectPushFindings(rest, operands, currentBranch, scope) {
6404
6782
  for (const refspec of destinations) {
6405
6783
  const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
6406
6784
  const branch = named === "HEAD" ? currentBranch : named;
6407
- if (branch !== null && !isUnresolved(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
6785
+ if (branch !== null && !isUnresolvedWord(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
6408
6786
  kind: "branch",
6409
6787
  target: branch
6410
6788
  });
6411
6789
  }
6412
6790
  return findings;
6413
6791
  }
6414
- function isUnresolved(word) {
6415
- return /[$`]/u.test(word);
6416
- }
6417
6792
  function isOwnedRemote(remote, scope) {
6418
6793
  if (scope.remotes.some((entry) => entry.name === remote)) return true;
6419
6794
  const slug = toRepositorySlug(remote);
@@ -6544,49 +6919,6 @@ function findProgramFinding(name, args) {
6544
6919
  };
6545
6920
  return null;
6546
6921
  }
6547
- function collectRedirectTargets(text) {
6548
- const targets = [];
6549
- let quote = null;
6550
- let index = 0;
6551
- while (index < text.length) {
6552
- const ch = text.charAt(index);
6553
- if (quote !== null) {
6554
- quote = ch === quote ? null : quote;
6555
- index += 1;
6556
- } else if (ch === "'" || ch === "\"") {
6557
- quote = ch;
6558
- index += 1;
6559
- } else if (ch === ">" && text.charAt(index - 1) !== "<") {
6560
- let next = index + 1;
6561
- if (text.charAt(next) === ">" || text.charAt(next) === "|") next += 1;
6562
- if (text.charAt(next) === "&") {
6563
- index = next + 1;
6564
- continue;
6565
- }
6566
- const [target, end] = readWord(text, next);
6567
- if (target !== "") targets.push(target);
6568
- index = end;
6569
- } else index += 1;
6570
- }
6571
- return targets;
6572
- }
6573
- function readWord(text, start) {
6574
- let index = start;
6575
- while (/\s/u.test(text.charAt(index))) index += 1;
6576
- let word = "";
6577
- let quote = null;
6578
- while (index < text.length) {
6579
- const ch = text.charAt(index);
6580
- if (quote !== null) {
6581
- if (ch === quote) quote = null;
6582
- else word += ch;
6583
- } else if (ch === "'" || ch === "\"") quote = ch;
6584
- else if (/[\s;&|<>()]/u.test(ch)) break;
6585
- else word += ch;
6586
- index += 1;
6587
- }
6588
- return [word, index];
6589
- }
6590
6922
  function collectInPlaceFiles(args) {
6591
6923
  const files = [];
6592
6924
  let skipNext = false;
@@ -6601,13 +6933,6 @@ function collectInPlaceFiles(args) {
6601
6933
  }
6602
6934
  return files;
6603
6935
  }
6604
- function resolvePath(cwd, path, home) {
6605
- const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6606
- if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
6607
- if (cwd === null && !isAbsolute(expanded)) return null;
6608
- const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6609
- return normalize(absolute).replace(/(?<=.)\/$/u, "");
6610
- }
6611
6936
  function isInScope(path, scope, scratchPaths) {
6612
6937
  if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6613
6938
  return scope.worktrees.some((worktree) => {
@@ -6652,10 +6977,116 @@ function formatList(one, many, values) {
6652
6977
  return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
6653
6978
  }
6654
6979
  //#endregion
6655
- //#region mods/auto-mode/contract/find-current-direct-user-message.ts
6656
- function findCurrentDirectUserMessage(message) {
6657
- return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
6658
- }
6980
+ //#region src/diagnostics/write-action-diagnostic.ts
6981
+ async function writeActionDiagnostic(payload, entry, host, warnings) {
6982
+ const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveXDGDir(host, "state"), "actions.jsonl");
6983
+ if (path === "") return;
6984
+ const record = {
6985
+ schemaVersion: 4,
6986
+ time: (/* @__PURE__ */ new Date()).toISOString(),
6987
+ invocationID: entry.invocationID,
6988
+ sessionHash: toHash(payload.sessionID),
6989
+ actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
6990
+ status: entry.status,
6991
+ verdict: entry.verdict ?? null,
6992
+ decidingStage: entry.decidingStage ?? null,
6993
+ denials: entry.denials ?? null,
6994
+ escalation: entry.escalation ?? false,
6995
+ diagnostics: entry.diagnostics ?? null,
6996
+ judge: entry.judge ?? null
6997
+ };
6998
+ try {
6999
+ await mkdir(dirname(path), {
7000
+ recursive: true,
7001
+ mode: 448
7002
+ });
7003
+ await appendFile(path, `${JSON.stringify(record)}\n`, { mode: 384 });
7004
+ } catch {
7005
+ warnings.write("auto-mode: diagnostics unavailable\n");
7006
+ }
7007
+ }
7008
+ function toHash(value) {
7009
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
7010
+ }
7011
+ //#endregion
7012
+ //#region src/git/load-checkout-remotes.ts
7013
+ async function loadCheckoutRemotes(commonDir) {
7014
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
7015
+ const remotes = [];
7016
+ let section = null;
7017
+ for (const line of config.split("\n")) {
7018
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
7019
+ if (header !== void 0) section = header;
7020
+ else if (/^\s*\[/u.test(line)) section = null;
7021
+ else if (section !== null) {
7022
+ const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
7023
+ const url = value === void 0 ? "" : parseConfigValue(value);
7024
+ if (url !== "") remotes.push({
7025
+ name: section,
7026
+ url
7027
+ });
7028
+ }
7029
+ }
7030
+ return remotes;
7031
+ }
7032
+ function parseConfigValue(raw) {
7033
+ let value = "";
7034
+ let isQuoted = false;
7035
+ for (let index = 0; index < raw.length; index += 1) {
7036
+ const char = raw[index] ?? "";
7037
+ if (char === "\\") {
7038
+ const next = raw[index + 1] ?? "";
7039
+ value += {
7040
+ n: "\n",
7041
+ t: " ",
7042
+ b: ""
7043
+ }[next] ?? next;
7044
+ index += 1;
7045
+ } else if (char === "\"") isQuoted = !isQuoted;
7046
+ else if (!isQuoted && (char === "#" || char === ";")) break;
7047
+ else value += char;
7048
+ }
7049
+ return value.trim();
7050
+ }
7051
+ //#endregion
7052
+ //#region src/git/load-checkout-facts.ts
7053
+ async function loadCheckoutFacts(cwd, env, options = {}) {
7054
+ const directory = resolve(cwd);
7055
+ const stopDir = options.stopDir === void 0 ? void 0 : resolve(options.stopDir);
7056
+ const checkout = await (options.findCheckout === void 0 ? findCheckout(directory, env, stopDir) : options.findCheckout(directory, env));
7057
+ if (checkout === null) return {
7058
+ cwd: directory,
7059
+ checkout,
7060
+ head: null,
7061
+ remotes: []
7062
+ };
7063
+ const [head, originHead, remotes] = await Promise.all([
7064
+ readFile(join(checkout.gitDir, "HEAD"), "utf8").catch(() => null),
7065
+ readFile(join(checkout.commonDir, "refs", "remotes", "origin", "HEAD"), "utf8").catch(() => null),
7066
+ loadCheckoutRemotes(checkout.commonDir)
7067
+ ]);
7068
+ return {
7069
+ cwd: directory,
7070
+ checkout,
7071
+ head: head === null ? null : {
7072
+ branch: parseReference(head, "refs/heads/"),
7073
+ defaultBranch: originHead === null ? null : parseReference(originHead, "refs/remotes/origin/")
7074
+ },
7075
+ remotes
7076
+ };
7077
+ }
7078
+ function parseReference(content, prefix) {
7079
+ const value = content.trim();
7080
+ const marker = `ref: ${prefix}`;
7081
+ if (!value.startsWith(marker)) return null;
7082
+ const branch = value.slice(marker.length);
7083
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
7084
+ }
7085
+ //#endregion
7086
+ //#region mods/auto-mode/contract/find-current-direct-user-message.ts
7087
+ function findCurrentDirectUserMessage(message) {
7088
+ return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
7089
+ }
6659
7090
  //#endregion
6660
7091
  //#region src/policy/resolve-shipped-policy-dir.ts
6661
7092
  function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
@@ -6990,7 +7421,7 @@ const DENY_REASONS = {
6990
7421
  "Default Branch Write": {
6991
7422
  tier: "soft",
6992
7423
  harm: "committing, pushing, or merging directly to the default branch",
6993
- clears: "A direct user instruction that names the branch clears it."
7424
+ clears: "A direct user instruction that asks for this action and names its branch clears it."
6994
7425
  },
6995
7426
  "Publish and Release": {
6996
7427
  tier: "soft",
@@ -7127,7 +7558,7 @@ function parseJudgeReply(text) {
7127
7558
  };
7128
7559
  }
7129
7560
  //#endregion
7130
- //#region src/model/anthropic-client.ts
7561
+ //#region src/messages/anthropic-client.ts
7131
7562
  async function sendMessage(provider, apiKey, request, signal) {
7132
7563
  const response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/messages`, {
7133
7564
  method: "POST",
@@ -7199,8 +7630,9 @@ function readResult(body) {
7199
7630
  }
7200
7631
  //#endregion
7201
7632
  //#region src/judge/run-claude-code.ts
7633
+ const MAX_REPLY_BYTES = 1048576;
7202
7634
  async function runClaudeCode(provider, request, env, signal) {
7203
- const child = spawn("claude", [
7635
+ const result = await runCapped("claude", [
7204
7636
  "-p",
7205
7637
  "--model",
7206
7638
  provider.model,
@@ -7215,29 +7647,14 @@ async function runClaudeCode(provider, request, env, signal) {
7215
7647
  "--output-format",
7216
7648
  "json"
7217
7649
  ], {
7218
- cwd: tmpdir(),
7219
- env: buildChildEnv(env),
7220
7650
  signal,
7221
- killSignal: "SIGKILL",
7222
- stdio: [
7223
- "pipe",
7224
- "pipe",
7225
- "ignore"
7226
- ]
7227
- });
7228
- const chunks = [];
7229
- child.stdout.on("data", (chunk) => {
7230
- chunks.push(chunk);
7231
- });
7232
- const closed = new Promise((resolve, reject) => {
7233
- child.once("error", reject);
7234
- child.once("close", resolve);
7651
+ maxOutputBytes: MAX_REPLY_BYTES,
7652
+ input: request.user,
7653
+ cwd: tmpdir(),
7654
+ env: buildChildEnv(env)
7235
7655
  });
7236
- child.stdin.on("error", () => {});
7237
- child.stdin.end(request.user);
7238
- const exitCode = await closed;
7239
- const body = CLAUDE_OUTPUT.safeParse(parseOutputJSON(Buffer.concat(chunks).toString("utf8")));
7240
- if (exitCode !== 0 || !body.success || body.data.is_error || body.data.result === void 0) throw new Error(`claude -p failed with exit code ${String(exitCode)}`);
7656
+ const body = CLAUDE_OUTPUT.safeParse(parseOutputJSON(result.stdout));
7657
+ if (result.exitCode !== 0 || !body.success || body.data.is_error || body.data.result === void 0) throw new Error(`claude -p failed with exit code ${String(result.exitCode)}`);
7241
7658
  return {
7242
7659
  text: body.data.result,
7243
7660
  model: provider.model
@@ -7346,6 +7763,116 @@ function buildTaskScopeSummary(scope) {
7346
7763
  };
7347
7764
  }
7348
7765
  //#endregion
7766
+ //#region src/git/build-repository-context.ts
7767
+ function buildRepositoryContext(facts) {
7768
+ if (facts.head === null) return null;
7769
+ return {
7770
+ cwd: facts.cwd,
7771
+ branch: facts.head.branch,
7772
+ defaultBranch: facts.head.defaultBranch,
7773
+ remotes: facts.remotes.flatMap((remote) => {
7774
+ const url = normalizeRemoteURL(remote.url);
7775
+ return url === null ? [] : [{
7776
+ name: remote.name,
7777
+ url
7778
+ }];
7779
+ })
7780
+ };
7781
+ }
7782
+ function normalizeRemoteURL(url) {
7783
+ if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
7784
+ const parsed = URL.parse(url);
7785
+ if (parsed === null) return null;
7786
+ parsed.username = "";
7787
+ parsed.password = "";
7788
+ parsed.search = "";
7789
+ parsed.hash = "";
7790
+ return parsed.href;
7791
+ }
7792
+ const slash = url.indexOf("/");
7793
+ const end = slash === -1 ? url.length : slash;
7794
+ const at = url.lastIndexOf("@", end);
7795
+ return at === -1 ? url : url.slice(at + 1);
7796
+ }
7797
+ //#endregion
7798
+ //#region src/git/build-repository-evidence.ts
7799
+ function buildRepositoryEvidence(facts, taskScope) {
7800
+ const context = buildRepositoryContext(facts);
7801
+ if (taskScope === void 0) return context;
7802
+ return {
7803
+ ...context ?? {
7804
+ cwd: facts.cwd,
7805
+ branch: null,
7806
+ defaultBranch: null
7807
+ },
7808
+ taskScope
7809
+ };
7810
+ }
7811
+ //#endregion
7812
+ //#region src/git/load-mcp-servers.ts
7813
+ const serverSchema = looseObject({
7814
+ type: string().optional(),
7815
+ url: string().optional()
7816
+ });
7817
+ const serversSchema = record(string(), unknown()).optional();
7818
+ const approvalSchema = looseObject({
7819
+ enableAllProjectMcpServers: boolean().optional(),
7820
+ enabledMcpjsonServers: array(string()).optional(),
7821
+ disabledMcpjsonServers: array(string()).optional()
7822
+ });
7823
+ const stateSchema = looseObject({
7824
+ mcpServers: serversSchema,
7825
+ projects: record(string(), looseObject({
7826
+ ...approvalSchema.shape,
7827
+ mcpServers: serversSchema
7828
+ })).optional()
7829
+ });
7830
+ const projectFileSchema = looseObject({ mcpServers: serversSchema });
7831
+ async function loadMCPServers(cwd, host, userSettings, checkoutFacts) {
7832
+ const state = await loadJSONFile(resolveClaudeConfigPath(host, ".claude.json"), stateSchema, null);
7833
+ const settings = approvalSchema.safeParse(userSettings).data;
7834
+ const directory = resolve(cwd);
7835
+ const projectDir = (checkoutFacts === void 0 ? await findCheckout(directory, host.env) : checkoutFacts.checkout)?.worktree ?? directory;
7836
+ const project = state?.projects?.[projectDir];
7837
+ const projectFile = await loadJSONFile(join(projectDir, ".mcp.json"), projectFileSchema, null);
7838
+ const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
7839
+ const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
7840
+ const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
7841
+ const facts = [
7842
+ ...buildServerFacts(project?.mcpServers, "local"),
7843
+ ...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
7844
+ ...buildServerFacts(state?.mcpServers, "user")
7845
+ ];
7846
+ return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
7847
+ }
7848
+ const MAX_NAME_LENGTH = 128;
7849
+ const TRANSPORTS = {
7850
+ stdio: "stdio",
7851
+ http: "http",
7852
+ "streamable-http": "http",
7853
+ sse: "sse",
7854
+ ws: "ws"
7855
+ };
7856
+ function buildServerFacts(servers, scope) {
7857
+ return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
7858
+ const server = serverSchema.safeParse(entry);
7859
+ if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
7860
+ const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
7861
+ if (transport === void 0) return [];
7862
+ return [{
7863
+ name,
7864
+ scope,
7865
+ transport,
7866
+ host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
7867
+ }];
7868
+ });
7869
+ }
7870
+ function findEndpointHost(url) {
7871
+ const parsed = URL.parse(url);
7872
+ if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
7873
+ return parsed.host;
7874
+ }
7875
+ //#endregion
7349
7876
  //#region src/policy/build-unavailable-verdict.ts
7350
7877
  function buildUnavailableVerdict(onFailure, reason) {
7351
7878
  return onFailure === "deny" ? {
@@ -7465,7 +7992,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
7465
7992
  };
7466
7993
  }
7467
7994
  function buildEvidenceGuidance(name) {
7468
- if (name === "Default Branch Write") return "First determine whether the pending action performs a Git commit, push, or merge, or writes code that performs one when run. An ordinary non-Git source or test edit does not itself commit, push, or merge and this rule is allow without branch evidence; do not ask for branch consent for that edit or infer a later Git step from task context. repositoryContext describes cwd only, not the edited file's checkout or a generated script's future execution cwd. For actual Git writes targeting the current cwd, compare repositoryContext.branch with repositoryContext.defaultBranch and the shipped protected names main, master, trunk, and develop. A branch matching any protected name remains restricted even when defaultBranch differs; for example, develop with defaultBranch main is still restricted. Only when both references are known, different, and the branch is not a protected name does this rule allow an ordinary commit without a branch-ownership exception. Otherwise evaluate the restriction and its permitted consent. Written scripts retain their delayed Git effects; judge those effects against evidence for their actual target repository, execution cwd, and branch, not assumed cwd references. Do not apply cwd references to another target: directory changes, git -C, Git directory or environment overrides, explicit push destinations, and merge targets need their own evidence. A worktree path alone is not branch evidence.";
7995
+ if (name === "Default Branch Write") return "First determine whether the pending action performs a Git commit, push, or merge, or writes code that performs one when run. An ordinary non-Git source or test edit does not itself commit, push, or merge and this rule is allow without branch evidence; do not ask for branch consent for that edit or infer a later Git step from task context. repositoryContext describes cwd only, not the edited file's checkout or a generated script's future execution cwd. For actual Git writes targeting the current cwd, compare repositoryContext.branch with repositoryContext.defaultBranch and the shipped protected names main, master, trunk, and develop. A branch matching any protected name remains restricted even when defaultBranch differs; for example, develop with defaultBranch main is still restricted. Only when both references are known, different, and the branch is not a protected name does this rule allow an ordinary commit without a branch-ownership exception. Otherwise evaluate the restriction and its permitted consent. Written scripts retain their delayed Git effects; judge those effects against evidence for their actual target repository, execution cwd, and branch, not assumed cwd references; Git calls whose every target, the repository they run in and any remote they push to, is a repository the written code creates for itself, such as a test fixture in a temporary directory, never reach the checkout's default branch, whatever those repositories' branches are named; a push from such a repository to a remote the code did not create still does. Do not apply cwd references to another target: directory changes, git -C, Git directory or environment overrides, explicit push destinations, and merge targets need their own evidence. A worktree path alone is not branch evidence.";
7469
7996
  if (name === "Data Exfiltration" || name === "Secret Persistence") return "Identify actual sensitive material before applying this hard rule. Unmistakably inert static strings used as sanitizer-test assertion data, such as EXAMPLE_ONLY_NOT_A_VALID_TOKEN or an invalid EXAMPLE_ONLY_NOT_BASE64_NOT_A_KEY body inside private-key markers, are not real credentials. Decide from the supplied literal and data flow, not a secret-shaped prefix alone. A test filename, a synthetic comment, or consent never clears material from a real credential source, usable secrets, or sensitive data. Actual credential capture or disclosure remains restricted, including in tests and written scripts. A non-sensitive message is judged by Outbound Communication alone.";
7470
7997
  return "";
7471
7998
  }
@@ -7527,219 +8054,6 @@ function formatClassifierNote(note, apiKey) {
7527
8054
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
7528
8055
  }
7529
8056
  //#endregion
7530
- //#region src/model/load-mcp-servers.ts
7531
- const serverSchema = looseObject({
7532
- type: string().optional(),
7533
- url: string().optional()
7534
- });
7535
- const serversSchema = record(string(), unknown()).optional();
7536
- const approvalSchema = looseObject({
7537
- enableAllProjectMcpServers: boolean().optional(),
7538
- enabledMcpjsonServers: array(string()).optional(),
7539
- disabledMcpjsonServers: array(string()).optional()
7540
- });
7541
- const stateSchema = looseObject({
7542
- mcpServers: serversSchema,
7543
- projects: record(string(), looseObject({
7544
- ...approvalSchema.shape,
7545
- mcpServers: serversSchema
7546
- })).optional()
7547
- });
7548
- const projectFileSchema = looseObject({ mcpServers: serversSchema });
7549
- async function loadMCPServers(cwd, host, userSettings) {
7550
- const configDir = host.env["CLAUDE_CONFIG_DIR"];
7551
- const state = await readJSON(configDir !== void 0 && configDir !== "" ? join(configDir, ".claude.json") : join(host.home, ".claude.json"), stateSchema);
7552
- const settings = approvalSchema.safeParse(userSettings).data;
7553
- const directory = resolve(cwd);
7554
- const projectDir = (await findCheckout(directory, host.env))?.worktree ?? directory;
7555
- const project = state?.projects?.[projectDir];
7556
- const projectFile = await readJSON(join(projectDir, ".mcp.json"), projectFileSchema);
7557
- const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
7558
- const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
7559
- const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
7560
- const facts = [
7561
- ...buildServerFacts(project?.mcpServers, "local"),
7562
- ...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
7563
- ...buildServerFacts(state?.mcpServers, "user")
7564
- ];
7565
- return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
7566
- }
7567
- async function readJSON(path, schema) {
7568
- try {
7569
- const text = await readFile(path, "utf8");
7570
- const parsed = schema.safeParse(JSON.parse(text));
7571
- return parsed.success ? parsed.data : null;
7572
- } catch {
7573
- return null;
7574
- }
7575
- }
7576
- const MAX_NAME_LENGTH = 128;
7577
- const TRANSPORTS = {
7578
- stdio: "stdio",
7579
- http: "http",
7580
- "streamable-http": "http",
7581
- sse: "sse",
7582
- ws: "ws"
7583
- };
7584
- function buildServerFacts(servers, scope) {
7585
- return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
7586
- const server = serverSchema.safeParse(entry);
7587
- if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
7588
- const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
7589
- if (transport === void 0) return [];
7590
- return [{
7591
- name,
7592
- scope,
7593
- transport,
7594
- host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
7595
- }];
7596
- });
7597
- }
7598
- function findEndpointHost(url) {
7599
- const parsed = URL.parse(url);
7600
- if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
7601
- return parsed.host;
7602
- }
7603
- //#endregion
7604
- //#region src/scope/load-checkout-remotes.ts
7605
- async function loadCheckoutRemotes(commonDir) {
7606
- const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
7607
- const remotes = [];
7608
- let section = null;
7609
- for (const line of config.split("\n")) {
7610
- const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
7611
- if (header !== void 0) section = header;
7612
- else if (/^\s*\[/u.test(line)) section = null;
7613
- else if (section !== null) {
7614
- const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
7615
- const url = value === void 0 ? "" : parseConfigValue(value);
7616
- if (url !== "") remotes.push({
7617
- name: section,
7618
- url
7619
- });
7620
- }
7621
- }
7622
- return remotes;
7623
- }
7624
- function parseConfigValue(raw) {
7625
- let value = "";
7626
- let isQuoted = false;
7627
- for (let index = 0; index < raw.length; index += 1) {
7628
- const char = raw[index] ?? "";
7629
- if (char === "\\") {
7630
- const next = raw[index + 1] ?? "";
7631
- value += {
7632
- n: "\n",
7633
- t: " ",
7634
- b: ""
7635
- }[next] ?? next;
7636
- index += 1;
7637
- } else if (char === "\"") isQuoted = !isQuoted;
7638
- else if (!isQuoted && (char === "#" || char === ";")) break;
7639
- else value += char;
7640
- }
7641
- return value.trim();
7642
- }
7643
- //#endregion
7644
- //#region src/model/load-repository-context.ts
7645
- async function loadRepositoryContext(cwd, env, stopDir = parse(resolve(cwd)).root) {
7646
- if ([
7647
- "GIT_DIR",
7648
- "GIT_WORK_TREE",
7649
- "GIT_COMMON_DIR"
7650
- ].some((name) => env[name] !== void 0)) return null;
7651
- let directory = resolve(cwd);
7652
- for (;;) {
7653
- const gitPath = join(directory, ".git");
7654
- try {
7655
- await stat(gitPath);
7656
- } catch (error) {
7657
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
7658
- const parent = dirname(directory);
7659
- if (parent === directory || directory === resolve(stopDir)) return null;
7660
- directory = parent;
7661
- continue;
7662
- }
7663
- let gitDir = gitPath;
7664
- let head;
7665
- try {
7666
- try {
7667
- head = await readFile(join(gitDir, "HEAD"), "utf8");
7668
- } catch {
7669
- const link = await readFile(gitPath, "utf8");
7670
- const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
7671
- if (path === void 0) return null;
7672
- gitDir = resolve(directory, path.trim());
7673
- head = await readFile(join(gitDir, "HEAD"), "utf8");
7674
- }
7675
- } catch {
7676
- return null;
7677
- }
7678
- let commonDir = gitDir;
7679
- try {
7680
- const common = await readFile(join(gitDir, "commondir"), "utf8");
7681
- commonDir = resolve(gitDir, common.trim());
7682
- } catch {
7683
- commonDir = gitDir;
7684
- }
7685
- let defaultBranch = null;
7686
- try {
7687
- defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
7688
- } catch {
7689
- defaultBranch = null;
7690
- }
7691
- const remotes = await loadCheckoutRemotes(commonDir);
7692
- return {
7693
- cwd: resolve(cwd),
7694
- branch: parseReference(head, "refs/heads/"),
7695
- defaultBranch,
7696
- remotes: remotes.flatMap((remote) => {
7697
- const url = normalizeRemoteURL(remote.url);
7698
- return url === null ? [] : [{
7699
- name: remote.name,
7700
- url
7701
- }];
7702
- })
7703
- };
7704
- }
7705
- }
7706
- function parseReference(content, prefix) {
7707
- const value = content.trim();
7708
- const marker = `ref: ${prefix}`;
7709
- if (!value.startsWith(marker)) return null;
7710
- const branch = value.slice(marker.length);
7711
- return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
7712
- }
7713
- function normalizeRemoteURL(url) {
7714
- if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
7715
- const parsed = URL.parse(url);
7716
- if (parsed === null) return null;
7717
- parsed.username = "";
7718
- parsed.password = "";
7719
- parsed.search = "";
7720
- parsed.hash = "";
7721
- return parsed.href;
7722
- }
7723
- const slash = url.indexOf("/");
7724
- const end = slash === -1 ? url.length : slash;
7725
- const at = url.lastIndexOf("@", end);
7726
- return at === -1 ? url : url.slice(at + 1);
7727
- }
7728
- //#endregion
7729
- //#region src/model/load-repository-evidence.ts
7730
- async function loadRepositoryEvidence(cwd, taskScope, env, stopDir) {
7731
- const context = await loadRepositoryContext(cwd, env, stopDir);
7732
- if (taskScope === void 0) return context;
7733
- return {
7734
- ...context ?? {
7735
- cwd: resolve(cwd),
7736
- branch: null,
7737
- defaultBranch: null
7738
- },
7739
- taskScope
7740
- };
7741
- }
7742
- //#endregion
7743
8057
  //#region src/model/find-denied-rule.ts
7744
8058
  function findDeniedRule(request, result, blockThreshold) {
7745
8059
  const [top] = collectBlockingRules(request, result, blockThreshold);
@@ -7761,6 +8075,74 @@ function pickDecisionVerdict(request, result, blockThreshold) {
7761
8075
  };
7762
8076
  }
7763
8077
  //#endregion
8078
+ //#region src/model/build-jev-outcome.ts
8079
+ function buildJevOutcome(attempt, facts, config) {
8080
+ const elapsedMs = Math.round(facts.elapsedMs);
8081
+ if (attempt.kind === "reply") {
8082
+ const verdict = pickDecisionVerdict(attempt.request, attempt.result, facts.blockThreshold);
8083
+ const denied = findReviewedRule(attempt.request, attempt.result, facts.blockThreshold);
8084
+ return {
8085
+ verdict,
8086
+ ...denied === null ? {} : { review: {
8087
+ ...denied,
8088
+ repositoryContext: attempt.repositoryContext
8089
+ } },
8090
+ diagnostics: {
8091
+ status: verdict.kind,
8092
+ stage: "response",
8093
+ keyResolved: true,
8094
+ keySource: facts.keySource,
8095
+ failureReason: null,
8096
+ requestBytes: attempt.result.requestBytes,
8097
+ elapsedMs,
8098
+ blockThreshold: facts.blockThreshold,
8099
+ contributors: collectDecisionContributors(attempt.request, attempt.result, facts.blockThreshold)
8100
+ },
8101
+ note: formatClassifierNote(`${config.provider.model}: ${verdict.kind} (${elapsedMs}ms, ${attempt.result.inputTokens} input tokens)`, facts.key)
8102
+ };
8103
+ }
8104
+ const failure = classifyFailure(attempt, config.provider.timeoutMs);
8105
+ const note = formatClassifierNote(`${config.provider.model} unavailable: ${failure.reason}`, facts.key);
8106
+ return {
8107
+ verdict: buildUnavailableVerdict(config.onFailure, note),
8108
+ note,
8109
+ unavailable: true,
8110
+ diagnostics: {
8111
+ status: failure.status,
8112
+ stage: attempt.stage,
8113
+ keyResolved: facts.key !== null,
8114
+ keySource: facts.keySource,
8115
+ failureReason: attempt.error instanceof DecisionRequestError ? attempt.error.reason : null,
8116
+ requestBytes: attempt.error instanceof DecisionRequestError ? attempt.error.requestBytes : null,
8117
+ elapsedMs,
8118
+ blockThreshold: facts.blockThreshold,
8119
+ contributors: []
8120
+ }
8121
+ };
8122
+ }
8123
+ function classifyFailure(attempt, timeoutMs) {
8124
+ if (attempt.cancelled) return {
8125
+ reason: "evaluation cancelled",
8126
+ status: "cancelled"
8127
+ };
8128
+ if (attempt.deadlineExpired) return {
8129
+ reason: "evaluation deadline expired",
8130
+ status: "timeout"
8131
+ };
8132
+ const error = attempt.error;
8133
+ if (!(error instanceof Error)) return {
8134
+ reason: "classifier failed",
8135
+ status: "failure"
8136
+ };
8137
+ return error.name === "AbortError" || error instanceof DecisionRequestError && error.reason === "aborted" ? {
8138
+ reason: `timed out after ${timeoutMs}ms`,
8139
+ status: "timeout"
8140
+ } : {
8141
+ reason: error.message,
8142
+ status: "failure"
8143
+ };
8144
+ }
8145
+ //#endregion
7764
8146
  //#region src/model/probability-schema.ts
7765
8147
  const probabilitySchema = number().min(0).max(1);
7766
8148
  //#endregion
@@ -7854,14 +8236,12 @@ async function classifyWithJev(payload, config, options) {
7854
8236
  stage = "evidence";
7855
8237
  const directUserText = findCurrentDirectUserMessage(payload.decisionContext?.lastDirectUserMessage);
7856
8238
  const settings = await (options.claudeSettings ?? loadClaudeSettings(config.claudeSettingsPath, options.host));
7857
- const [policy, repositoryContext, mcpServers] = await Promise.all([
7858
- loadPolicy({
7859
- classifierPath: config.classifierPath,
7860
- rulesPath: config.rulesPath
7861
- }, "decision.md"),
7862
- loadRepositoryEvidence(payload.cwd, options.taskScope, options.host.env).catch(() => null),
7863
- loadMCPServers(payload.cwd, options.host, settings.userSettings).catch(() => [])
7864
- ]);
8239
+ const checkout = await (options.checkout ?? loadCheckoutFacts(payload.cwd, options.host.env, { findCheckout: options.findCheckout }).catch(() => null));
8240
+ const repositoryContext = checkout === null ? null : buildRepositoryEvidence(checkout, options.taskScope);
8241
+ const [policy, mcpServers] = await Promise.all([loadPolicy({
8242
+ classifierPath: config.classifierPath,
8243
+ rulesPath: config.rulesPath
8244
+ }, "decision.md"), loadMCPServers(payload.cwd, options.host, settings.userSettings, checkout ?? void 0).catch(() => [])]);
7865
8245
  const rulesSource = config.rulesPath === void 0 ? "shipped" : "replacement";
7866
8246
  const request = buildDecisionRequest(payload, policy, settings.rules, directUserText, rulesSource, repositoryContext, mcpServers);
7867
8247
  const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - now();
@@ -7871,58 +8251,30 @@ async function classifyWithJev(payload, config, options) {
7871
8251
  stage = "request";
7872
8252
  const result = await sendDecision(config.provider, key, request, signal);
7873
8253
  stage = "response";
7874
- const verdict = pickDecisionVerdict(request, result, blockThreshold);
7875
- const denied = findReviewedRule(request, result, blockThreshold);
7876
- return {
7877
- verdict,
7878
- ...denied === null ? {} : { review: {
7879
- ...denied,
7880
- repositoryContext
7881
- } },
7882
- diagnostics: {
7883
- status: verdict.kind,
7884
- stage,
7885
- keyResolved: true,
7886
- keySource,
7887
- failureReason: null,
7888
- requestBytes: result.requestBytes,
7889
- elapsedMs: Math.round(performance.now() - start),
7890
- blockThreshold,
7891
- contributors: collectDecisionContributors(request, result, blockThreshold)
7892
- },
7893
- note: formatClassifierNote(`${config.provider.model}: ${verdict.kind} (${Math.round(performance.now() - start)}ms, ${result.inputTokens} input tokens)`, key)
7894
- };
8254
+ return buildJevOutcome({
8255
+ kind: "reply",
8256
+ request,
8257
+ result,
8258
+ repositoryContext
8259
+ }, {
8260
+ key,
8261
+ keySource,
8262
+ elapsedMs: performance.now() - start,
8263
+ blockThreshold
8264
+ }, config);
7895
8265
  } catch (error) {
7896
- let reason = "classifier failed";
7897
- let status = "failure";
7898
- if (options.signal?.aborted === true) {
7899
- reason = "evaluation cancelled";
7900
- status = "cancelled";
7901
- } else if (options.deadlineAt !== void 0 && now() >= options.deadlineAt) {
7902
- reason = "evaluation deadline expired";
7903
- status = "timeout";
7904
- } else if (error instanceof Error) {
7905
- const isTimeout = error.name === "AbortError" || error instanceof DecisionRequestError && error.reason === "aborted";
7906
- if (isTimeout) status = "timeout";
7907
- reason = isTimeout ? `timed out after ${config.provider.timeoutMs}ms` : error.message;
7908
- }
7909
- const note = formatClassifierNote(`${config.provider.model} unavailable: ${reason}`, key);
7910
- return {
7911
- verdict: buildUnavailableVerdict(config.onFailure, note),
7912
- note,
7913
- unavailable: true,
7914
- diagnostics: {
7915
- status,
7916
- stage,
7917
- keyResolved: key !== null,
7918
- keySource,
7919
- failureReason: error instanceof DecisionRequestError ? error.reason : null,
7920
- requestBytes: error instanceof DecisionRequestError ? error.requestBytes : null,
7921
- elapsedMs: Math.round(performance.now() - start),
7922
- blockThreshold,
7923
- contributors: []
7924
- }
7925
- };
8266
+ return buildJevOutcome({
8267
+ kind: "failure",
8268
+ error,
8269
+ stage,
8270
+ cancelled: options.signal?.aborted === true,
8271
+ deadlineExpired: options.deadlineAt !== void 0 && now() >= options.deadlineAt
8272
+ }, {
8273
+ key,
8274
+ keySource,
8275
+ elapsedMs: performance.now() - start,
8276
+ blockThreshold
8277
+ }, config);
7926
8278
  }
7927
8279
  }
7928
8280
  //#endregion
@@ -7980,7 +8332,9 @@ async function classifyWithModel(payload, config, options = {}) {
7980
8332
  text: directMessage
7981
8333
  }];
7982
8334
  const settings = await (options.claudeSettings ?? loadClaudeSettings(null, host));
7983
- const [repositoryContext, mcpServers] = await Promise.all([loadRepositoryEvidence(payload.cwd, options.taskScope, host.env).catch(() => null), loadMCPServers(payload.cwd, host, settings.userSettings).catch(() => [])]);
8335
+ const checkout = await (options.checkout ?? loadCheckoutFacts(payload.cwd, host.env, { findCheckout: options.findCheckout }).catch(() => null));
8336
+ const repositoryContext = checkout === null ? null : buildRepositoryEvidence(checkout, options.taskScope);
8337
+ const mcpServers = await loadMCPServers(payload.cwd, host, settings.userSettings, checkout ?? void 0).catch(() => []);
7984
8338
  const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext, mcpServers);
7985
8339
  try {
7986
8340
  const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
@@ -8152,25 +8506,26 @@ function classifyLocally(payload) {
8152
8506
  }
8153
8507
  function classifySegment(text, cwd) {
8154
8508
  const words = splitWords(text);
8155
- const start = words.findIndex((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
8509
+ const start = words.findIndex((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word.raw));
8156
8510
  if (start === -1) return "Read-only actions";
8157
8511
  const head = words[start];
8158
8512
  invariant(head !== void 0, "findIndex returned an index the array holds");
8159
8513
  const rest = words.slice(start + 1);
8160
- const name = head.slice(head.lastIndexOf("/") + 1);
8514
+ const name = head.raw.slice(head.raw.lastIndexOf("/") + 1);
8161
8515
  if (READ_ONLY_COMMANDS.has(name)) return "Read-only actions";
8162
8516
  if (name === "git") {
8163
- const sub = rest.find((word) => !word.startsWith("-"));
8164
- return sub !== void 0 && READ_ONLY_GIT_SUBCOMMANDS.has(sub) ? "Read-only actions" : null;
8517
+ const sub = rest.find((word) => !word.raw.startsWith("-"));
8518
+ return sub !== void 0 && READ_ONLY_GIT_SUBCOMMANDS.has(sub.raw) ? "Read-only actions" : null;
8165
8519
  }
8166
8520
  if (name === "rm") return classifyRemove(rest, cwd);
8167
8521
  return null;
8168
8522
  }
8169
8523
  function classifyRemove(args, cwd) {
8170
- const paths = args.filter((arg) => !arg.startsWith("-"));
8524
+ const paths = args.filter((arg) => !arg.raw.startsWith("-"));
8171
8525
  if (paths.length === 0) return null;
8172
8526
  for (const path of paths) {
8173
- const stripped = toUnquoted(path);
8527
+ if (!isPlainOperand(path)) return null;
8528
+ const stripped = path.value;
8174
8529
  if (stripped.includes("*") || stripped.includes("~") || stripped.includes("$")) return null;
8175
8530
  const resolved = isAbsolute(stripped) ? normalize(stripped) : normalize(`${cwd}/${stripped}`);
8176
8531
  const within = relative(cwd, resolved);
@@ -8181,11 +8536,17 @@ function classifyRemove(args, cwd) {
8181
8536
  return "Regenerable output";
8182
8537
  }
8183
8538
  function splitWords(text) {
8184
- return text.split(/\s+/).filter((word) => word !== "");
8185
- }
8186
- function toUnquoted(word) {
8187
- const [first] = word;
8188
- return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
8539
+ return splitShellTokens(text).tokens.flatMap((token) => token.kind === "word" ? [{
8540
+ raw: token.raw,
8541
+ value: token.value
8542
+ }] : []);
8543
+ }
8544
+ function isPlainOperand(word) {
8545
+ return [
8546
+ word.value,
8547
+ `'${word.value}'`,
8548
+ `"${word.value}"`
8549
+ ].includes(word.raw) && !/\s/u.test(word.value);
8189
8550
  }
8190
8551
  //#endregion
8191
8552
  //#region src/scope/merge-scope-facts.ts
@@ -8355,19 +8716,8 @@ const sessionScopeSchema = strictObject({
8355
8716
  branches: array(branchSchema),
8356
8717
  pullRequests: array(pullRequestSchema)
8357
8718
  });
8358
- async function loadSessionScope(path) {
8359
- let raw;
8360
- try {
8361
- raw = await readFile(path, "utf8");
8362
- } catch {
8363
- return EMPTY_SESSION_SCOPE;
8364
- }
8365
- try {
8366
- const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
8367
- return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
8368
- } catch {
8369
- return EMPTY_SESSION_SCOPE;
8370
- }
8719
+ function loadSessionScope(path) {
8720
+ return loadJSONFile(path, sessionScopeSchema, EMPTY_SESSION_SCOPE);
8371
8721
  }
8372
8722
  //#endregion
8373
8723
  //#region src/scope/resolve-session-scope-path.ts
@@ -8378,33 +8728,31 @@ function resolveSessionScopePath(stateDir, sessionID) {
8378
8728
  //#endregion
8379
8729
  //#region src/scope/pick-scope-source-reader.ts
8380
8730
  function pickScopeSourceReader(source, findSourceCheckout = findCheckout) {
8381
- if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
8731
+ return M(source.kind).returnType().with("cwd", () => (context) => Promise.resolve(buildFacts({
8382
8732
  worktrees: [context.worktree],
8383
8733
  branches: context.branch === null ? [] : [context.branch]
8384
- }));
8385
- if (source.kind === "session") return async (context) => {
8734
+ }))).with("session", () => async (context) => {
8386
8735
  const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
8387
8736
  return buildFacts({
8388
8737
  worktrees: scope.worktrees,
8389
8738
  branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
8390
8739
  pullRequests: scope.pullRequests
8391
8740
  });
8392
- };
8393
- if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
8394
- return async (context) => {
8395
- const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
8396
- if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
8397
- if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
8398
- const scope = loaded.record.scope;
8399
- const paths = /* @__PURE__ */ new Set([
8400
- scope.workspace.path,
8401
- ...scope.worktrees.map((worktree) => worktree.path),
8402
- ...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
8403
- ]);
8404
- const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
8405
- const commonDirs = new Map(entries);
8406
- return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
8407
- };
8741
+ }).with("globs", () => () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }))).with("atc", () => (context) => readAtcScopeFacts(context, findSourceCheckout)).exhaustive();
8742
+ }
8743
+ async function readAtcScopeFacts(context, findSourceCheckout) {
8744
+ const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
8745
+ if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
8746
+ if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
8747
+ const scope = loaded.record.scope;
8748
+ const paths = /* @__PURE__ */ new Set([
8749
+ scope.workspace.path,
8750
+ ...scope.worktrees.map((worktree) => worktree.path),
8751
+ ...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
8752
+ ]);
8753
+ const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
8754
+ const commonDirs = new Map(entries);
8755
+ return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
8408
8756
  }
8409
8757
  async function findCommonDirEntry(path, env, findSourceCheckout) {
8410
8758
  return [path, (await findSourceCheckout(path, env))?.commonDir ?? null];
@@ -8419,8 +8767,9 @@ function buildFacts(facts) {
8419
8767
  //#region src/scope/load-task-scope.ts
8420
8768
  async function loadTaskScope(request, sources, host, stderr) {
8421
8769
  const cwd = resolve(request.cwd);
8422
- const [checkout, context] = await Promise.all([findCheckout(cwd, host.env), loadRepositoryContext(cwd, host.env)]);
8423
- const branch = context?.branch ?? null;
8770
+ const facts = await (request.checkout ?? loadCheckoutFacts(cwd, host.env, { findCheckout: request.findCheckout }));
8771
+ const checkout = facts.checkout;
8772
+ const branch = facts.head?.branch ?? null;
8424
8773
  const sourceContext = {
8425
8774
  env: host.env,
8426
8775
  sessionID: request.sessionID,
@@ -8433,19 +8782,71 @@ async function loadTaskScope(request, sources, host, stderr) {
8433
8782
  atcSessionID: host.env["ATC_SESSION_ID"],
8434
8783
  stderr
8435
8784
  };
8436
- const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
8785
+ const sourceFacts = await Promise.all(Object.values(sources).map((source) => pickScopeSourceReader(source, request.findCheckout)(sourceContext)));
8437
8786
  return buildTaskScope({
8438
8787
  home: host.home,
8439
8788
  currentBranch: branch,
8440
- defaultBranch: context?.defaultBranch ?? null,
8441
- remotes,
8442
- facts
8789
+ defaultBranch: facts.head?.defaultBranch ?? null,
8790
+ remotes: facts.remotes,
8791
+ facts: sourceFacts
8443
8792
  });
8444
8793
  }
8445
8794
  //#endregion
8446
8795
  //#region src/classify-action.ts
8447
8796
  async function classifyAction(request, config, options = {}) {
8448
8797
  const host = options.host ?? readHostEnvironment();
8798
+ const stderr = options.stderr ?? process.stderr;
8799
+ const invocationID = randomUUID();
8800
+ if (options.actionLog === true) await writeActionDiagnostic(request, {
8801
+ invocationID,
8802
+ status: "started"
8803
+ }, host, stderr);
8804
+ const outcome = options.budget === true ? await classifyWithBudget(request, config, {
8805
+ ...options,
8806
+ host,
8807
+ stderr
8808
+ }) : await classifyStages(request, config, {
8809
+ ...options,
8810
+ host,
8811
+ stderr
8812
+ });
8813
+ if (options.actionLog === true) await writeActionDiagnostic(request, {
8814
+ invocationID,
8815
+ status: outcome.status,
8816
+ verdict: outcome.verdict?.kind ?? "defer",
8817
+ decidingStage: outcome.decidingStage,
8818
+ ...outcome.denials === void 0 ? {} : { denials: outcome.denials },
8819
+ ...outcome.escalation === void 0 ? {} : { escalation: outcome.escalation },
8820
+ ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
8821
+ ...outcome.judge === void 0 ? {} : { judge: outcome.judge }
8822
+ }, host, stderr);
8823
+ return outcome;
8824
+ }
8825
+ async function classifyWithBudget(request, config, options) {
8826
+ const statePath = resolveDenialStatePath(request, resolveXDGDir(options.host, "state"));
8827
+ const retryKey = buildRetryKey(request);
8828
+ const retry = findRetryDeny(await loadDenialState(statePath), retryKey);
8829
+ const outcome = retry === null ? await classifyStages(request, config, options) : {
8830
+ verdict: retry,
8831
+ decidingStage: "retry",
8832
+ note: "retry of the action just denied; denied again",
8833
+ status: "deny"
8834
+ };
8835
+ const plan = await tryUpdateDenialBudget(options.stderr, statePath, (state) => planDenialBudget(state, outcome.verdict, retryKey, config.denialBudget ?? DEFAULT_DENIAL_BUDGET));
8836
+ return {
8837
+ ...outcome,
8838
+ verdict: plan.verdict,
8839
+ decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
8840
+ note: plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note,
8841
+ denials: {
8842
+ consecutive: plan.state.consecutive,
8843
+ session: plan.state.session
8844
+ },
8845
+ escalation: plan.escalation
8846
+ };
8847
+ }
8848
+ async function classifyStages(request, config, options) {
8849
+ const host = options.host;
8449
8850
  let claudeSettings = options.claudeSettings;
8450
8851
  if (config.provider.protocol === "system-one" && claudeSettings === void 0) try {
8451
8852
  claudeSettings = await loadClaudeSettings(config.claudeSettingsPath, host);
@@ -8464,7 +8865,14 @@ async function classifyAction(request, config, options = {}) {
8464
8865
  const hasConfiguredDenies = configured !== void 0 && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
8465
8866
  const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
8466
8867
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
8467
- const scope = await tryLoadTaskScope(request, config, host, options.stderr ?? process.stderr);
8868
+ const findActionCheckout = makeOnceCheckoutFinder(options.findCheckout ?? findCheckout);
8869
+ const checkout = await loadCheckoutFacts(request.cwd, host.env, { findCheckout: findActionCheckout }).catch(() => null);
8870
+ const scope = await tryLoadTaskScope({
8871
+ sessionID: request.sessionID,
8872
+ cwd: request.cwd,
8873
+ checkout,
8874
+ findCheckout: findActionCheckout
8875
+ }, config, host, options.stderr);
8468
8876
  const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
8469
8877
  if (containment !== null) {
8470
8878
  const guidance = await tryReadDenialGuidance();
@@ -8476,7 +8884,7 @@ async function classifyAction(request, config, options = {}) {
8476
8884
  };
8477
8885
  }
8478
8886
  if (local.kind === "allow") return buildLocalAllow(local.exception);
8479
- const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host);
8887
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host, { findCheckout: findActionCheckout });
8480
8888
  if (edit?.kind === "bypass") return {
8481
8889
  verdict: { kind: "allow" },
8482
8890
  decidingStage: "bypass",
@@ -8494,7 +8902,9 @@ async function classifyAction(request, config, options = {}) {
8494
8902
  ...options,
8495
8903
  taskScope,
8496
8904
  host,
8497
- claudeSettings
8905
+ claudeSettings,
8906
+ checkout: checkout ?? void 0,
8907
+ findCheckout: findActionCheckout
8498
8908
  }), tryReadDenialGuidance()]);
8499
8909
  const judge = config.judge ?? null;
8500
8910
  if (outcome.verdict?.kind === "deny" && outcome.review !== void 0 && judge !== null) {
@@ -8526,14 +8936,22 @@ const READ_ONLY_EXCEPTION = "Read-only actions";
8526
8936
  async function tryLoadTaskScope(request, config, host, stderr) {
8527
8937
  try {
8528
8938
  return await loadTaskScope({
8529
- sessionID: request.sessionID,
8530
- cwd: request.cwd,
8531
- stateDir: resolveStateDir(host)
8939
+ ...request,
8940
+ checkout: request.checkout ?? void 0,
8941
+ stateDir: resolveXDGDir(host, "state")
8532
8942
  }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
8533
8943
  } catch {
8534
8944
  return null;
8535
8945
  }
8536
8946
  }
8947
+ function makeOnceCheckoutFinder(find) {
8948
+ const found = /* @__PURE__ */ new Map();
8949
+ return (start, env) => {
8950
+ const cached = found.get(start) ?? find(start, env);
8951
+ found.set(start, cached);
8952
+ return cached;
8953
+ };
8954
+ }
8537
8955
  function buildLocalAllow(exception) {
8538
8956
  return {
8539
8957
  verdict: { kind: "allow" },
@@ -8557,6 +8975,279 @@ async function tryReadDenialGuidance() {
8557
8975
  return null;
8558
8976
  }
8559
8977
  }
8978
+ async function tryUpdateDenialBudget(stderr, path, plan) {
8979
+ const unlocked = await loadDenialState(path);
8980
+ const unlockedPlan = plan(unlocked);
8981
+ if (unlockedPlan.state === unlocked) return unlockedPlan;
8982
+ try {
8983
+ return plan((await updateDenialState(path, (state) => plan(state).state)).previous);
8984
+ } catch {
8985
+ stderr.write("auto-mode: denial counts unavailable\n");
8986
+ return unlockedPlan;
8987
+ }
8988
+ }
8989
+ //#endregion
8990
+ //#region src/scope/read-pull-request.ts
8991
+ const LOOKUP_TIMEOUT_MS = 5e3;
8992
+ const MAX_LOOKUP_OUTPUT_BYTES = 65536;
8993
+ async function readPullRequest(repository, number, options = {}) {
8994
+ const [file, ...prefix] = options.command ?? ["gh"];
8995
+ const args = [
8996
+ "pr",
8997
+ "view",
8998
+ String(number),
8999
+ "--repo",
9000
+ repository,
9001
+ "--json",
9002
+ "headRefName,createdAt"
9003
+ ];
9004
+ const result = await runCapped(file, [
9005
+ ...prefix,
9006
+ ...args,
9007
+ "--jq",
9008
+ "[.headRefName, .createdAt] | @tsv"
9009
+ ], {
9010
+ signal: AbortSignal.timeout(options.timeoutMs ?? LOOKUP_TIMEOUT_MS),
9011
+ maxOutputBytes: MAX_LOOKUP_OUTPUT_BYTES
9012
+ });
9013
+ const [head = "", created = ""] = result.stdout.trim().split(" ");
9014
+ const createdAt = Date.parse(created);
9015
+ return result.exitCode === 0 && head !== "" && !Number.isNaN(createdAt) ? {
9016
+ head,
9017
+ createdAt
9018
+ } : null;
9019
+ }
9020
+ //#endregion
9021
+ //#region src/git/read-branch-created-at.ts
9022
+ async function readBranchCreatedAt(commonDir, branch) {
9023
+ if (!/^[\w./-]+$/u.test(branch) || branch.split("/").includes("..")) return null;
9024
+ const [first = ""] = (await readFile(join(commonDir, "logs", "refs", "heads", branch), "utf8").catch(() => "")).split("\n");
9025
+ const seconds = /^0+ [\da-f]+ .* (?<time>\d+) [+-]\d{4}\t/u.exec(first)?.groups?.["time"];
9026
+ return seconds === void 0 ? null : Number(seconds) * 1e3;
9027
+ }
9028
+ //#endregion
9029
+ //#region src/scope/collect-pull-request-addresses.ts
9030
+ function collectPullRequestAddresses(text) {
9031
+ const addresses = /* @__PURE__ */ new Map();
9032
+ for (const match of text.matchAll(PULL_REQUEST_URL)) {
9033
+ const repository = toRepositorySlug(`https://${match.groups?.["repository"] ?? ""}`);
9034
+ const number = Number(match.groups?.["number"]);
9035
+ if (repository !== null) addresses.set(`${repository}#${String(number)}`, {
9036
+ repository,
9037
+ number
9038
+ });
9039
+ }
9040
+ return [...addresses.values()];
9041
+ }
9042
+ const PULL_REQUEST_URL = /https:\/\/(?<repository>[^/\s]+\/[^/\s]+\/[^/\s]+)\/pull\/(?<number>[1-9]\d*)\b/gu;
9043
+ //#endregion
9044
+ //#region mods/auto-mode/contract/scope-commands.ts
9045
+ const SCOPE_COMMANDS = {
9046
+ git: [
9047
+ "worktree add",
9048
+ "checkout",
9049
+ "switch",
9050
+ "branch"
9051
+ ],
9052
+ gh: ["pr create"]
9053
+ };
9054
+ //#endregion
9055
+ //#region src/scope/collect-scope-events.ts
9056
+ function collectScopeEvents(command, cwd, home) {
9057
+ return collectShellCommands(command, cwd, home).flatMap((entry) => {
9058
+ const [name, ...args] = entry.words;
9059
+ const directory = entry.directory;
9060
+ if (directory === null) return [];
9061
+ if (name === "git") return collectGitEvents(args, directory, home);
9062
+ return name === "gh" && findScopeCommand(SCOPE_COMMANDS.gh, args) !== null ? [{
9063
+ kind: "pull-request",
9064
+ directory,
9065
+ head: findOptionValue(args, ["--head", "-H"])
9066
+ }] : [];
9067
+ });
9068
+ }
9069
+ function collectGitEvents(args, cwd, home) {
9070
+ const git = splitGitCommand(args, cwd, home);
9071
+ const found = findScopeCommand(SCOPE_COMMANDS.git, git.args);
9072
+ if (git.directory === null || found === null) return [];
9073
+ return GIT_EVENT_COLLECTORS[found.command](found.rest, git.directory, home);
9074
+ }
9075
+ function findScopeCommand(commands, args) {
9076
+ for (const command of commands) {
9077
+ const words = command.split(" ");
9078
+ if (words.every((word, index) => args[index] === word)) return {
9079
+ command,
9080
+ rest: args.slice(words.length)
9081
+ };
9082
+ }
9083
+ return null;
9084
+ }
9085
+ const GIT_EVENT_COLLECTORS = {
9086
+ "worktree add": collectWorktreeEvents,
9087
+ checkout: collectSwitchEvents,
9088
+ switch: collectSwitchEvents,
9089
+ branch: collectBranchEvents
9090
+ };
9091
+ function collectWorktreeEvents(rest, directory, home) {
9092
+ const path = findWorktreePath(rest);
9093
+ const resolved = path === null ? null : resolveShellPath(directory, path, home);
9094
+ return resolved === null ? [] : [{
9095
+ kind: "worktree",
9096
+ path: resolved
9097
+ }];
9098
+ }
9099
+ const WORKTREE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
9100
+ "-b",
9101
+ "-B",
9102
+ "--reason"
9103
+ ]);
9104
+ function findWorktreePath(args) {
9105
+ for (let index = 0; index < args.length; index += 1) {
9106
+ const word = args[index] ?? "";
9107
+ if (WORKTREE_VALUE_OPTIONS.has(word)) index += 1;
9108
+ else if (!word.startsWith("-")) return word;
9109
+ }
9110
+ return null;
9111
+ }
9112
+ const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
9113
+ "-b",
9114
+ "-B",
9115
+ "-c",
9116
+ "-C",
9117
+ "--create",
9118
+ "--force-create"
9119
+ ]);
9120
+ const BRANCH_OTHER_OPTIONS = /^-(?:d|D|m|M|c|C|l|a|r|v|vv|u)$|^--(?:delete|move|copy|list|all|remotes|verbose|show-current|contains|no-contains|merged|no-merged|points-at|format|sort|set-upstream-to|unset-upstream|edit-description)/u;
9121
+ function collectSwitchEvents(rest, directory) {
9122
+ const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
9123
+ return name === null ? [] : [{
9124
+ kind: "branch",
9125
+ name,
9126
+ directory
9127
+ }];
9128
+ }
9129
+ function collectBranchEvents(rest, directory) {
9130
+ if (rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) return [];
9131
+ const name = rest.find((word) => !word.startsWith("-"));
9132
+ return name === void 0 || isUnresolvedWord(name) ? [] : [{
9133
+ kind: "branch",
9134
+ name,
9135
+ directory
9136
+ }];
9137
+ }
9138
+ function findOptionValue(args, options) {
9139
+ const index = args.findIndex((word) => options.includes(word));
9140
+ const value = index === -1 ? void 0 : args[index + 1];
9141
+ return value === void 0 || isUnresolvedWord(value) ? null : value;
9142
+ }
9143
+ //#endregion
9144
+ //#region src/scope/merge-session-scope.ts
9145
+ function mergeSessionScope(all) {
9146
+ const branches = /* @__PURE__ */ new Map();
9147
+ const pullRequests = /* @__PURE__ */ new Map();
9148
+ for (const branch of all.flatMap((scope) => scope.branches)) branches.set(`${branch.commonDir}\0${branch.name}`, branch);
9149
+ for (const pull of all.flatMap((scope) => scope.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
9150
+ return {
9151
+ worktrees: [...new Set(all.flatMap((scope) => scope.worktrees))],
9152
+ branches: [...branches.values()],
9153
+ pullRequests: [...pullRequests.values()]
9154
+ };
9155
+ }
9156
+ //#endregion
9157
+ //#region src/scope/write-session-scope.ts
9158
+ async function writeSessionScope(path, added, clock) {
9159
+ await updateJSONFile(path, loadSessionScope, (current) => mergeSessionScope([current, added]), clock);
9160
+ }
9161
+ //#endregion
9162
+ //#region src/scope/update-session-scope.ts
9163
+ const MAX_CALL_AGE_MS = 9e5;
9164
+ async function updateSessionScope(request, options) {
9165
+ const age = options.now - request.startedAt;
9166
+ if (age < -6e4 || age > MAX_CALL_AGE_MS) return EMPTY_SESSION_SCOPE;
9167
+ const events = collectScopeEvents(request.command, request.cwd, options.home);
9168
+ const scope = mergeSessionScope(await Promise.all(events.map((event) => tryVerifyScopeEvent(event, request, options))));
9169
+ if (scope.worktrees.length + scope.branches.length + scope.pullRequests.length > 0) await writeSessionScope(resolveSessionScopePath(options.stateDir, request.sessionID), scope, options.lockClock);
9170
+ return scope;
9171
+ }
9172
+ async function tryVerifyScopeEvent(event, request, options) {
9173
+ try {
9174
+ return await (options.verifyEvent === void 0 ? verifyScopeEvent(event, request, options) : options.verifyEvent(event, request));
9175
+ } catch {
9176
+ return EMPTY_SESSION_SCOPE;
9177
+ }
9178
+ }
9179
+ const FORGE_CLOCK_SKEW_MS = 6e4;
9180
+ const FILE_CLOCK_SLACK_MS = 50;
9181
+ function verifyScopeEvent(event, request, options) {
9182
+ return M(event).returnType().with({ kind: "worktree" }, (worktree) => verifyWorktreeEvent(worktree.path, request, options)).with({ kind: "branch" }, (branch) => verifyBranchEvent(branch, request, options)).with({ kind: "pull-request" }, (pull) => verifyPullRequestEvent(pull.directory, request, options)).exhaustive();
9183
+ }
9184
+ async function verifyWorktreeEvent(path, request, options) {
9185
+ const link = await stat(join(path, ".git"));
9186
+ const facts = await loadCheckoutFacts(path, options.env);
9187
+ const checkout = facts.checkout;
9188
+ if (!link.isFile() || link.mtimeMs < request.startedAt - FILE_CLOCK_SLACK_MS || checkout?.worktree !== path) return EMPTY_SESSION_SCOPE;
9189
+ const branch = facts.head?.branch ?? null;
9190
+ if (branch === null) return {
9191
+ ...EMPTY_SESSION_SCOPE,
9192
+ worktrees: [path]
9193
+ };
9194
+ const isNew = await isBranchCreatedSince(checkout.commonDir, branch, request.startedAt);
9195
+ return {
9196
+ ...EMPTY_SESSION_SCOPE,
9197
+ worktrees: [path],
9198
+ branches: isNew ? [{
9199
+ name: branch,
9200
+ commonDir: checkout.commonDir
9201
+ }] : []
9202
+ };
9203
+ }
9204
+ async function verifyBranchEvent(event, request, options) {
9205
+ const checkout = await findCheckout(event.directory, options.env);
9206
+ if (checkout === null) return EMPTY_SESSION_SCOPE;
9207
+ return await isBranchCreatedSince(checkout.commonDir, event.name, request.startedAt) ? {
9208
+ ...EMPTY_SESSION_SCOPE,
9209
+ branches: [{
9210
+ name: event.name,
9211
+ commonDir: checkout.commonDir
9212
+ }]
9213
+ } : EMPTY_SESSION_SCOPE;
9214
+ }
9215
+ async function verifyPullRequestEvent(directory, request, options) {
9216
+ const facts = await loadCheckoutFacts(directory, options.env);
9217
+ if (facts.checkout === null) return EMPTY_SESSION_SCOPE;
9218
+ const repositories = new Set(facts.remotes.map((remote) => toRepositorySlug(remote.url)));
9219
+ const addresses = collectPullRequestAddresses(request.resultText).filter((address) => repositories.has(address.repository));
9220
+ const pulls = await Promise.all(addresses.map(async (address) => {
9221
+ const pull = await options.readPullRequest(address.repository, address.number);
9222
+ return pull !== null && pull.createdAt >= request.startedAt - FORGE_CLOCK_SKEW_MS ? [{
9223
+ ...address,
9224
+ head: pull.head
9225
+ }] : [];
9226
+ }));
9227
+ return {
9228
+ ...EMPTY_SESSION_SCOPE,
9229
+ pullRequests: pulls.flat()
9230
+ };
9231
+ }
9232
+ async function isBranchCreatedSince(commonDir, branch, since) {
9233
+ const createdAt = await readBranchCreatedAt(commonDir, branch);
9234
+ if (createdAt === null || createdAt < Math.floor(since / 1e3) * 1e3) return false;
9235
+ const ref = await stat(join(commonDir, "refs", "heads", branch)).catch(() => null);
9236
+ return ref !== null && ref.mtimeMs >= since - FILE_CLOCK_SLACK_MS;
9237
+ }
9238
+ //#endregion
9239
+ //#region src/record-scope.ts
9240
+ async function recordScope(request, config, options = {}) {
9241
+ const host = options.host ?? readHostEnvironment();
9242
+ if (!Object.values(config.scopeSources ?? DEFAULT_SCOPE_SOURCES).some((source) => source.kind === "session")) return EMPTY_SESSION_SCOPE;
9243
+ return await updateSessionScope(request, {
9244
+ now: options.now ?? Date.now(),
9245
+ stateDir: resolveXDGDir(host, "state"),
9246
+ home: host.home,
9247
+ env: host.env,
9248
+ readPullRequest
9249
+ });
9250
+ }
8560
9251
  //#endregion
8561
9252
  //#region src/request/action-request-schema.ts
8562
9253
  const userTaskSchema = strictObject({
@@ -8613,6 +9304,21 @@ function parseActionRequest(body) {
8613
9304
  };
8614
9305
  }
8615
9306
  //#endregion
9307
+ //#region src/request/scope-record-request-schema.ts
9308
+ const scopeRecordRequestSchema = strictObject({
9309
+ sessionID: string().min(1),
9310
+ cwd: string().min(1),
9311
+ startedAt: number().int().positive(),
9312
+ command: string().min(1),
9313
+ resultText: string()
9314
+ });
9315
+ //#endregion
9316
+ //#region src/request/parse-scope-record-request.ts
9317
+ function parseScopeRecordRequest(body) {
9318
+ const parsed = scopeRecordRequestSchema.safeParse(body);
9319
+ return parsed.success ? parsed.data : null;
9320
+ }
9321
+ //#endregion
8616
9322
  //#region src/request/render-verdict.ts
8617
9323
  function renderVerdict(verdict) {
8618
9324
  const rendered = M(verdict).returnType().with({ kind: "allow" }, () => ({ decision: "allow" })).with({ kind: "deny" }, (denied) => ({
@@ -8622,4 +9328,4 @@ function renderVerdict(verdict) {
8622
9328
  return JSON.stringify(rendered);
8623
9329
  }
8624
9330
  //#endregion
8625
- export { resolveConfigPath as A, resolveStateDir as C, DEFAULT_SCOPE_SOURCES as D, DEFAULT_DENIAL_BUDGET as E, string as F, PRESETS as M, number as N, loadConfig as O, strictObject as P, tryClassifyEdit as S, DEFAULT_CONFIG as T, checkContainment as _, loadSessionScope as a, splitCommandWords as b, classifyLocally as c, sendDecision as d, loadRepositoryContext as f, loadPolicy as g, buildDecisionRequest as h, resolveSessionScopePath as i, readHostEnvironment as j, resolveApiKey as k, classifyWithModel as l, DecisionRequestError as m, parseActionRequest as n, EMPTY_SESSION_SCOPE as o, loadCheckoutRemotes as p, classifyAction as r, buildTaskScope as s, renderVerdict as t, parseModelVerdict as u, toRepositorySlug as v, findCheckout as w, loadClaudeSettings as x, splitShellCommand as y };
9331
+ export { readHostEnvironment as C, resolveConfigPath as S, resolveXDGDir as T, tryClassifyEdit as _, classifyAction as a, loadConfig as b, classifyWithModel as c, DecisionRequestError as d, buildDecisionRequest as f, loadClaudeSettings as g, checkContainment as h, recordScope as i, parseModelVerdict as l, writeActionDiagnostic as m, parseScopeRecordRequest as n, buildTaskScope as o, loadPolicy as p, parseActionRequest as r, classifyLocally as s, renderVerdict as t, sendDecision as u, findGitEntry as v, PRESETS as w, resolveApiKey as x, DEFAULT_CONFIG as y };