auto-mode 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,22 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
2
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
2
4
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
4
5
  import { spawn } from "node:child_process";
5
- import { createHash } from "node:crypto";
6
6
  import { existsSync } from "node:fs";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/bypass/resolve-edit-target.ts
9
+ async function resolveEditTarget(path) {
10
+ const missing = [];
11
+ for (let current = path;; current = dirname(current)) try {
12
+ const real = await realpath(current);
13
+ return join(real, ...missing.toReversed());
14
+ } catch {
15
+ if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
16
+ missing.push(basename(current));
17
+ }
18
+ }
19
+ //#endregion
7
20
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
8
21
  function getEnumValues(entries) {
9
22
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5868,66 +5881,100 @@ async function loadClaudeRules(path) {
5868
5881
  };
5869
5882
  }
5870
5883
  //#endregion
5871
- //#region src/model/load-repository-context.ts
5872
- async function loadRepositoryContext(cwd) {
5873
- if ([
5874
- "GIT_DIR",
5875
- "GIT_WORK_TREE",
5876
- "GIT_COMMON_DIR"
5877
- ].some((name) => process.env[name] !== void 0)) return null;
5878
- let directory = resolve(cwd);
5879
- for (;;) {
5880
- const gitPath = join(directory, ".git");
5881
- try {
5882
- await stat(gitPath);
5883
- } catch (error) {
5884
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
5885
- const parent = dirname(directory);
5886
- if (parent === directory) return null;
5887
- directory = parent;
5884
+ //#region src/rules/split-command-words.ts
5885
+ function splitCommandWords(text) {
5886
+ const words = splitWords$1(text);
5887
+ let index = 0;
5888
+ while (index < words.length) {
5889
+ const word = words[index] ?? "";
5890
+ if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
5891
+ else if (word === "env") {
5892
+ index += 1;
5893
+ while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
5894
+ } else if (word === "timeout") index += 2;
5895
+ else break;
5896
+ }
5897
+ return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
5898
+ }
5899
+ function splitWords$1(text) {
5900
+ const words = [];
5901
+ let current = "";
5902
+ let quote = null;
5903
+ let started = false;
5904
+ for (const ch of text) if (quote !== null) {
5905
+ if (ch === quote) quote = null;
5906
+ else current += ch;
5907
+ } else if (ch === "'" || ch === "\"") {
5908
+ quote = ch;
5909
+ started = true;
5910
+ } else if (/\s/u.test(ch)) {
5911
+ if (started) words.push(current);
5912
+ current = "";
5913
+ started = false;
5914
+ } else {
5915
+ current += ch;
5916
+ started = true;
5917
+ }
5918
+ if (started) words.push(current);
5919
+ return words;
5920
+ }
5921
+ //#endregion
5922
+ //#region src/rules/split-shell-command.ts
5923
+ function splitShellCommand(command) {
5924
+ const segments = [];
5925
+ let current = "";
5926
+ let quote = null;
5927
+ let unparsed = false;
5928
+ let i = 0;
5929
+ const collectSegment = () => {
5930
+ const text = current.trim();
5931
+ if (text !== "") segments.push({ text });
5932
+ current = "";
5933
+ };
5934
+ while (i < command.length) {
5935
+ const ch = command.charAt(i);
5936
+ if (quote !== null) {
5937
+ if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
5938
+ if (ch === quote) quote = null;
5939
+ current += ch;
5940
+ i += 1;
5888
5941
  continue;
5889
5942
  }
5890
- let gitDir = gitPath;
5891
- let head;
5892
- try {
5893
- try {
5894
- head = await readFile(join(gitDir, "HEAD"), "utf8");
5895
- } catch {
5896
- const link = await readFile(gitPath, "utf8");
5897
- const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
5898
- if (path === void 0) return null;
5899
- gitDir = resolve(directory, path.trim());
5900
- head = await readFile(join(gitDir, "HEAD"), "utf8");
5901
- }
5902
- } catch {
5903
- return null;
5943
+ if (ch === "'" || ch === "\"") {
5944
+ quote = ch;
5945
+ current += ch;
5946
+ i += 1;
5947
+ continue;
5904
5948
  }
5905
- let commonDir = gitDir;
5906
- try {
5907
- const common = await readFile(join(gitDir, "commondir"), "utf8");
5908
- commonDir = resolve(gitDir, common.trim());
5909
- } catch {
5910
- commonDir = gitDir;
5949
+ if (ch === "\\") {
5950
+ current += command.slice(i, i + 2);
5951
+ i += 2;
5952
+ continue;
5911
5953
  }
5912
- let defaultBranch = null;
5913
- try {
5914
- defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
5915
- } catch {
5916
- defaultBranch = null;
5954
+ if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
5955
+ unparsed = true;
5956
+ current += ch;
5957
+ i += 1;
5958
+ continue;
5917
5959
  }
5918
- return {
5919
- cwd: resolve(cwd),
5920
- branch: parseReference(head, "refs/heads/"),
5921
- defaultBranch
5922
- };
5960
+ if (command.startsWith("&&", i) || command.startsWith("||", i)) {
5961
+ collectSegment();
5962
+ i += 2;
5963
+ continue;
5964
+ }
5965
+ if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
5966
+ collectSegment();
5967
+ i += 1;
5968
+ continue;
5969
+ }
5970
+ current += ch;
5971
+ i += 1;
5923
5972
  }
5924
- }
5925
- function parseReference(content, prefix) {
5926
- const value = content.trim();
5927
- const marker = `ref: ${prefix}`;
5928
- if (!value.startsWith(marker)) return null;
5929
- const branch = value.slice(marker.length);
5930
- return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
5973
+ collectSegment();
5974
+ return {
5975
+ segments,
5976
+ hasUnparsedConstruct: unparsed || quote !== null
5977
+ };
5931
5978
  }
5932
5979
  //#endregion
5933
5980
  //#region src/containment/to-repository-slug.ts
@@ -5938,346 +5985,57 @@ function toRepositorySlug(reference) {
5938
5985
  return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
5939
5986
  }
5940
5987
  //#endregion
5941
- //#region src/scope/merge-scope-facts.ts
5942
- function mergeScopeFacts(all) {
5943
- const pullRequests = /* @__PURE__ */ new Map();
5944
- for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
5945
- return {
5946
- worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
5947
- branches: [...new Set(all.flatMap((facts) => facts.branches))],
5948
- pullRequests: [...pullRequests.values()],
5949
- pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
5950
- };
5951
- }
5952
- //#endregion
5953
- //#region src/scope/build-task-scope.ts
5954
- function buildTaskScope(input) {
5955
- const merged = mergeScopeFacts(input.facts);
5956
- const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
5957
- const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
5958
- const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
5959
- number: pull.number,
5960
- repository: pull.repository
5961
- }));
5962
- return {
5963
- home: input.home,
5964
- worktrees: merged.worktrees,
5965
- branches,
5966
- currentBranch: input.currentBranch,
5967
- remotes: input.remotes,
5968
- pullRequests,
5969
- pathGlobs: merged.pathGlobs
5970
- };
5988
+ //#region src/containment/collect-scope-findings.ts
5989
+ function collectScopeFindings(action, scope) {
5990
+ const filePath = action.input["file_path"] ?? action.input["notebook_path"];
5991
+ const command = action.input["command"];
5992
+ const findings = [
5993
+ ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
5994
+ ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
5995
+ ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
5996
+ ];
5997
+ return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
5971
5998
  }
5972
- const DEFAULT_NAMES = /* @__PURE__ */ new Set([
5973
- "main",
5974
- "master",
5975
- "trunk",
5976
- "develop"
5999
+ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6000
+ "Edit",
6001
+ "MultiEdit",
6002
+ "Write",
6003
+ "NotebookEdit"
5977
6004
  ]);
5978
- //#endregion
5979
- //#region src/scope/find-checkout.ts
5980
- const GIT_OVERRIDES = [
5981
- "GIT_DIR",
5982
- "GIT_WORK_TREE",
5983
- "GIT_COMMON_DIR"
5984
- ];
5985
- async function findCheckout(start) {
5986
- if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
5987
- for (let directory = start;; directory = dirname(directory)) {
5988
- const gitPath = join(directory, ".git");
5989
- const info = await stat(gitPath).catch(() => null);
5990
- if (info !== null) {
5991
- const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
5992
- return gitDir === null ? null : {
5993
- worktree: directory,
5994
- commonDir: await findCommonDir(gitDir)
5995
- };
5996
- }
5997
- if (dirname(directory) === directory) return null;
5998
- }
6005
+ function collectTargetFindings(directory, target, scope) {
6006
+ const path = resolvePath(directory, target, scope.home);
6007
+ return path === null ? [] : collectPathFindings(path, scope);
5999
6008
  }
6000
- async function findLinkedGitDir(directory, gitPath) {
6001
- const link = await readFile(gitPath, "utf8").catch(() => "");
6002
- const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
6003
- return path === void 0 ? null : resolve(directory, path.trim());
6009
+ const CREDENTIAL_PATHS = [
6010
+ ".ssh",
6011
+ ".aws",
6012
+ ".gnupg",
6013
+ ".gitconfig",
6014
+ ".netrc",
6015
+ ".npmrc",
6016
+ ".config/gh"
6017
+ ];
6018
+ function collectPathFindings(path, scope) {
6019
+ if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
6020
+ kind: "credential",
6021
+ target: path
6022
+ }];
6023
+ return isInScope(path, scope) ? [] : [{
6024
+ kind: "path",
6025
+ target: path
6026
+ }];
6004
6027
  }
6005
- async function findCommonDir(gitDir) {
6006
- const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
6007
- return common === null ? gitDir : resolve(gitDir, common.trim());
6008
- }
6009
- //#endregion
6010
- //#region src/scope/load-checkout-remotes.ts
6011
- async function loadCheckoutRemotes(commonDir) {
6012
- const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
6013
- const remotes = [];
6014
- let section = null;
6015
- for (const line of config.split("\n")) {
6016
- const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
6017
- if (header !== void 0) section = header;
6018
- else if (/^\s*\[/u.test(line)) section = null;
6019
- else if (section !== null) {
6020
- const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
6021
- if (url !== void 0) remotes.push({
6022
- name: section,
6023
- url
6024
- });
6025
- }
6026
- }
6027
- return remotes;
6028
- }
6029
- //#endregion
6030
- //#region src/scope/types.ts
6031
- const EMPTY_SCOPE_FACTS = {
6032
- worktrees: [],
6033
- branches: [],
6034
- pullRequests: [],
6035
- pathGlobs: []
6036
- };
6037
- const EMPTY_SESSION_SCOPE = {
6038
- worktrees: [],
6039
- branches: [],
6040
- pullRequests: []
6041
- };
6042
- //#endregion
6043
- //#region src/scope/load-session-scope.ts
6044
- const name = string().min(1);
6045
- const pullRequestSchema = strictObject({
6046
- number: number().int().positive(),
6047
- head: name,
6048
- repository: name
6049
- });
6050
- const branchSchema = strictObject({
6051
- name,
6052
- commonDir: name
6053
- });
6054
- const sessionScopeSchema = strictObject({
6055
- worktrees: array(name),
6056
- branches: array(branchSchema),
6057
- pullRequests: array(pullRequestSchema)
6058
- });
6059
- async function loadSessionScope(path) {
6060
- let raw;
6061
- try {
6062
- raw = await readFile(path, "utf8");
6063
- } catch {
6064
- return EMPTY_SESSION_SCOPE;
6065
- }
6066
- try {
6067
- const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
6068
- return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
6069
- } catch {
6070
- return EMPTY_SESSION_SCOPE;
6071
- }
6072
- }
6073
- //#endregion
6074
- //#region src/scope/resolve-session-scope-path.ts
6075
- function resolveSessionScopePath(stateDir, sessionID) {
6076
- const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
6077
- return join(stateDir, "session-scope", `${key}.json`);
6078
- }
6079
- //#endregion
6080
- //#region src/scope/pick-scope-source-reader.ts
6081
- function pickScopeSourceReader(source) {
6082
- if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
6083
- worktrees: [context.worktree],
6084
- branches: context.branch === null ? [] : [context.branch]
6085
- }));
6086
- if (source.kind === "session") return async (context) => {
6087
- const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
6088
- return buildFacts({
6089
- worktrees: scope.worktrees,
6090
- branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
6091
- pullRequests: scope.pullRequests
6092
- });
6093
- };
6094
- if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
6095
- return () => Promise.resolve(EMPTY_SCOPE_FACTS);
6096
- }
6097
- function buildFacts(facts) {
6098
- return {
6099
- ...EMPTY_SCOPE_FACTS,
6100
- ...facts
6101
- };
6102
- }
6103
- //#endregion
6104
- //#region src/scope/load-task-scope.ts
6105
- async function loadTaskScope(request, sources) {
6106
- const cwd = resolve(request.cwd);
6107
- const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
6108
- const branch = context?.branch ?? null;
6109
- const sourceContext = {
6110
- sessionID: request.sessionID,
6111
- cwd,
6112
- worktree: checkout?.worktree ?? cwd,
6113
- commonDir: checkout?.commonDir ?? null,
6114
- branch,
6115
- stateDir: request.stateDir
6116
- };
6117
- const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
6118
- return buildTaskScope({
6119
- home: homedir(),
6120
- currentBranch: branch,
6121
- defaultBranch: context?.defaultBranch ?? null,
6122
- remotes,
6123
- facts
6124
- });
6125
- }
6126
- //#endregion
6127
- //#region src/state/resolve-state-dir.ts
6128
- function resolveStateDir() {
6129
- const stateHome = process.env["XDG_STATE_HOME"];
6130
- const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
6131
- return join(base, "auto-mode");
6132
- }
6133
- //#endregion
6134
- //#region src/rules/split-command-words.ts
6135
- function splitCommandWords(text) {
6136
- const words = splitWords$1(text);
6137
- let index = 0;
6138
- while (index < words.length) {
6139
- const word = words[index] ?? "";
6140
- if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6141
- else if (word === "env") {
6142
- index += 1;
6143
- while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6144
- } else if (word === "timeout") index += 2;
6145
- else break;
6146
- }
6147
- return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
6148
- }
6149
- function splitWords$1(text) {
6150
- const words = [];
6151
- let current = "";
6152
- let quote = null;
6153
- let started = false;
6154
- for (const ch of text) if (quote !== null) {
6155
- if (ch === quote) quote = null;
6156
- else current += ch;
6157
- } else if (ch === "'" || ch === "\"") {
6158
- quote = ch;
6159
- started = true;
6160
- } else if (/\s/u.test(ch)) {
6161
- if (started) words.push(current);
6162
- current = "";
6163
- started = false;
6164
- } else {
6165
- current += ch;
6166
- started = true;
6167
- }
6168
- if (started) words.push(current);
6169
- return words;
6170
- }
6171
- //#endregion
6172
- //#region src/rules/split-shell-command.ts
6173
- function splitShellCommand(command) {
6174
- const segments = [];
6175
- let current = "";
6176
- let quote = null;
6177
- let unparsed = false;
6178
- let i = 0;
6179
- const collectSegment = () => {
6180
- const text = current.trim();
6181
- if (text !== "") segments.push({ text });
6182
- current = "";
6183
- };
6184
- while (i < command.length) {
6185
- const ch = command.charAt(i);
6186
- if (quote !== null) {
6187
- if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
6188
- if (ch === quote) quote = null;
6189
- current += ch;
6190
- i += 1;
6191
- continue;
6192
- }
6193
- if (ch === "'" || ch === "\"") {
6194
- quote = ch;
6195
- current += ch;
6196
- i += 1;
6197
- continue;
6198
- }
6199
- if (ch === "\\") {
6200
- current += command.slice(i, i + 2);
6201
- i += 2;
6202
- continue;
6203
- }
6204
- if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
6205
- unparsed = true;
6206
- current += ch;
6207
- i += 1;
6208
- continue;
6209
- }
6210
- if (command.startsWith("&&", i) || command.startsWith("||", i)) {
6211
- collectSegment();
6212
- i += 2;
6213
- continue;
6214
- }
6215
- if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
6216
- collectSegment();
6217
- i += 1;
6218
- continue;
6219
- }
6220
- current += ch;
6221
- i += 1;
6222
- }
6223
- collectSegment();
6224
- return {
6225
- segments,
6226
- hasUnparsedConstruct: unparsed || quote !== null
6227
- };
6228
- }
6229
- //#endregion
6230
- //#region src/containment/collect-scope-findings.ts
6231
- function collectScopeFindings(action, scope) {
6232
- const filePath = action.input["file_path"] ?? action.input["notebook_path"];
6233
- const command = action.input["command"];
6234
- const findings = [
6235
- ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
6236
- ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
6237
- ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
6238
- ];
6239
- return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
6240
- }
6241
- const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6242
- "Edit",
6243
- "MultiEdit",
6244
- "Write",
6245
- "NotebookEdit"
6246
- ]);
6247
- function collectTargetFindings(directory, target, scope) {
6248
- const path = resolvePath(directory, target, scope.home);
6249
- return path === null ? [] : collectPathFindings(path, scope);
6250
- }
6251
- const CREDENTIAL_PATHS = [
6252
- ".ssh",
6253
- ".aws",
6254
- ".gnupg",
6255
- ".gitconfig",
6256
- ".netrc",
6257
- ".npmrc",
6258
- ".config/gh"
6259
- ];
6260
- function collectPathFindings(path, scope) {
6261
- if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
6262
- kind: "credential",
6263
- target: path
6264
- }];
6265
- return isInScope(path, scope) ? [] : [{
6266
- kind: "path",
6267
- target: path
6268
- }];
6269
- }
6270
- function collectToolFindings(action, scope) {
6271
- const verb = action.tool.split("__").at(-1) ?? "";
6272
- if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
6273
- const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
6274
- const owner = action.input["owner"];
6275
- const name = action.input["repo"];
6276
- const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
6277
- return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
6278
- kind: "remote-write",
6279
- target: action.tool
6280
- }];
6028
+ function collectToolFindings(action, scope) {
6029
+ const verb = action.tool.split("__").at(-1) ?? "";
6030
+ if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
6031
+ const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
6032
+ const owner = action.input["owner"];
6033
+ const name = action.input["repo"];
6034
+ const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
6035
+ return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
6036
+ kind: "remote-write",
6037
+ target: action.tool
6038
+ }];
6281
6039
  }
6282
6040
  const PATH_WRITERS = /* @__PURE__ */ new Set([
6283
6041
  "rm",
@@ -6719,17 +6477,7 @@ function isUnder(path, root) {
6719
6477
  //#endregion
6720
6478
  //#region src/containment/check-containment.ts
6721
6479
  const CONTAINMENT_RULE = "Outside Task Scope";
6722
- async function checkContainment(request, sources) {
6723
- let scope;
6724
- try {
6725
- scope = await loadTaskScope({
6726
- sessionID: request.sessionID,
6727
- cwd: request.cwd,
6728
- stateDir: resolveStateDir()
6729
- }, sources);
6730
- } catch {
6731
- return null;
6732
- }
6480
+ function checkContainment(request, scope) {
6733
6481
  const findings = collectScopeFindings({
6734
6482
  tool: request.toolName,
6735
6483
  cwd: request.cwd,
@@ -6759,6 +6507,18 @@ function formatList(one, many, values) {
6759
6507
  return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
6760
6508
  }
6761
6509
  //#endregion
6510
+ //#region src/model/build-task-scope-summary.ts
6511
+ function buildTaskScopeSummary(scope) {
6512
+ return {
6513
+ worktrees: scope.worktrees,
6514
+ branches: scope.branches,
6515
+ pullRequests: scope.pullRequests.map((pull) => ({
6516
+ repository: pull.repository,
6517
+ number: pull.number
6518
+ }))
6519
+ };
6520
+ }
6521
+ //#endregion
6762
6522
  //#region src/policy/resolve-shipped-policy-dir.ts
6763
6523
  function resolveShippedPolicyDir() {
6764
6524
  let dir = import.meta.dirname;
@@ -6862,7 +6622,7 @@ function readResult(body) {
6862
6622
  //#endregion
6863
6623
  //#region src/model/build-request.ts
6864
6624
  const MAX_INPUT_CHARS = 4e3;
6865
- function buildUserMessage(payload, transcript, reasoning) {
6625
+ function buildUserMessage(payload, transcript, reasoning, repositoryContext = null) {
6866
6626
  const history = transcript.length === 0 ? "(no transcript available from this harness)" : transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n\n");
6867
6627
  const input = JSON.stringify(payload.toolInput, null, 2).slice(0, MAX_INPUT_CHARS);
6868
6628
  const closing = reasoning ? "Work through the classification process, then end your reply with the output contract tags." : "Reply with the output contract tags and nothing else.";
@@ -6870,7 +6630,7 @@ function buildUserMessage(payload, transcript, reasoning) {
6870
6630
  ${history}
6871
6631
  </transcript>
6872
6632
 
6873
- <action>
6633
+ ${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}<action>
6874
6634
  tool: ${payload.toolName}
6875
6635
  cwd: ${payload.cwd}
6876
6636
  input:
@@ -6974,57 +6734,196 @@ function classifyDecisionAnswers(request, result, minConfidence) {
6974
6734
  const answer = result.answers[id];
6975
6735
  if (answer === void 0) throw new Error("Decision answer missing");
6976
6736
  return {
6977
- rule,
6978
- answer
6737
+ rule,
6738
+ answer
6739
+ };
6740
+ });
6741
+ for (const tier of ["hard", "soft"]) for (const entry of entries) if (entry.rule.tier === tier && entry.answer.choice === "block" && entry.answer.confidence >= minConfidence && entry.answer.probabilities.block >= minConfidence) return {
6742
+ kind: "block",
6743
+ rule: entry.rule
6744
+ };
6745
+ let uncertain = null;
6746
+ for (const entry of entries) {
6747
+ const answer = entry.answer;
6748
+ if (!(answer.choice === "allow" && answer.confidence >= minConfidence && answer.probabilities.allow >= minConfidence) && (uncertain === null || answer.probabilities.block > uncertain.answer.probabilities.block)) uncertain = entry;
6749
+ }
6750
+ return uncertain === null ? { kind: "allow" } : {
6751
+ kind: "uncertain",
6752
+ rule: uncertain.rule
6753
+ };
6754
+ }
6755
+ //#endregion
6756
+ //#region src/model/collect-decision-contributors.ts
6757
+ function collectDecisionContributors(request, result, minConfidence) {
6758
+ const decision = classifyDecisionAnswers(request, result, minConfidence);
6759
+ return Object.entries(request.rules).flatMap(([id, rule]) => {
6760
+ const answer = result.answers[id];
6761
+ if (answer === void 0) throw new Error("Decision answer missing");
6762
+ return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
6763
+ rule: rule.source === "shipped" ? rule.name : id,
6764
+ source: rule.source,
6765
+ tier: rule.tier,
6766
+ choice: answer.choice,
6767
+ confidence: answer.confidence,
6768
+ probability: answer.probabilities[answer.choice]
6769
+ }] : [];
6770
+ });
6771
+ }
6772
+ //#endregion
6773
+ //#region src/model/decision-request-error.ts
6774
+ var DecisionRequestError = class extends Error {
6775
+ reason;
6776
+ requestBytes;
6777
+ constructor(reason, requestBytes, message, options) {
6778
+ super(message, options);
6779
+ this.name = "DecisionRequestError";
6780
+ this.reason = reason;
6781
+ this.requestBytes = requestBytes;
6782
+ }
6783
+ };
6784
+ //#endregion
6785
+ //#region src/model/format-classifier-note.ts
6786
+ function formatClassifierNote(note, apiKey) {
6787
+ return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6788
+ }
6789
+ //#endregion
6790
+ //#region src/scope/load-checkout-remotes.ts
6791
+ async function loadCheckoutRemotes(commonDir) {
6792
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
6793
+ const remotes = [];
6794
+ let section = null;
6795
+ for (const line of config.split("\n")) {
6796
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
6797
+ if (header !== void 0) section = header;
6798
+ else if (/^\s*\[/u.test(line)) section = null;
6799
+ else if (section !== null) {
6800
+ const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
6801
+ const url = value === void 0 ? "" : parseConfigValue(value);
6802
+ if (url !== "") remotes.push({
6803
+ name: section,
6804
+ url
6805
+ });
6806
+ }
6807
+ }
6808
+ return remotes;
6809
+ }
6810
+ function parseConfigValue(raw) {
6811
+ let value = "";
6812
+ let isQuoted = false;
6813
+ for (let index = 0; index < raw.length; index += 1) {
6814
+ const char = raw[index] ?? "";
6815
+ if (char === "\\") {
6816
+ const next = raw[index + 1] ?? "";
6817
+ value += {
6818
+ n: "\n",
6819
+ t: " ",
6820
+ b: ""
6821
+ }[next] ?? next;
6822
+ index += 1;
6823
+ } else if (char === "\"") isQuoted = !isQuoted;
6824
+ else if (!isQuoted && (char === "#" || char === ";")) break;
6825
+ else value += char;
6826
+ }
6827
+ return value.trim();
6828
+ }
6829
+ //#endregion
6830
+ //#region src/model/load-repository-context.ts
6831
+ async function loadRepositoryContext(cwd) {
6832
+ if ([
6833
+ "GIT_DIR",
6834
+ "GIT_WORK_TREE",
6835
+ "GIT_COMMON_DIR"
6836
+ ].some((name) => process.env[name] !== void 0)) return null;
6837
+ let directory = resolve(cwd);
6838
+ for (;;) {
6839
+ const gitPath = join(directory, ".git");
6840
+ try {
6841
+ await stat(gitPath);
6842
+ } catch (error) {
6843
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6844
+ const parent = dirname(directory);
6845
+ if (parent === directory) return null;
6846
+ directory = parent;
6847
+ continue;
6848
+ }
6849
+ let gitDir = gitPath;
6850
+ let head;
6851
+ try {
6852
+ try {
6853
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6854
+ } catch {
6855
+ const link = await readFile(gitPath, "utf8");
6856
+ const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6857
+ if (path === void 0) return null;
6858
+ gitDir = resolve(directory, path.trim());
6859
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6860
+ }
6861
+ } catch {
6862
+ return null;
6863
+ }
6864
+ let commonDir = gitDir;
6865
+ try {
6866
+ const common = await readFile(join(gitDir, "commondir"), "utf8");
6867
+ commonDir = resolve(gitDir, common.trim());
6868
+ } catch {
6869
+ commonDir = gitDir;
6870
+ }
6871
+ let defaultBranch = null;
6872
+ try {
6873
+ defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6874
+ } catch {
6875
+ defaultBranch = null;
6876
+ }
6877
+ const remotes = await loadCheckoutRemotes(commonDir);
6878
+ return {
6879
+ cwd: resolve(cwd),
6880
+ branch: parseReference(head, "refs/heads/"),
6881
+ defaultBranch,
6882
+ remotes: remotes.flatMap((remote) => {
6883
+ const url = normalizeRemoteURL(remote.url);
6884
+ return url === null ? [] : [{
6885
+ name: remote.name,
6886
+ url
6887
+ }];
6888
+ })
6979
6889
  };
6980
- });
6981
- for (const tier of ["hard", "soft"]) for (const entry of entries) if (entry.rule.tier === tier && entry.answer.choice === "block" && entry.answer.confidence >= minConfidence && entry.answer.probabilities.block >= minConfidence) return {
6982
- kind: "block",
6983
- rule: entry.rule
6984
- };
6985
- let uncertain = null;
6986
- for (const entry of entries) {
6987
- const answer = entry.answer;
6988
- if (!(answer.choice === "allow" && answer.confidence >= minConfidence && answer.probabilities.allow >= minConfidence) && (uncertain === null || answer.probabilities.block > uncertain.answer.probabilities.block)) uncertain = entry;
6989
6890
  }
6990
- return uncertain === null ? { kind: "allow" } : {
6991
- kind: "uncertain",
6992
- rule: uncertain.rule
6993
- };
6994
6891
  }
6995
- //#endregion
6996
- //#region src/model/collect-decision-contributors.ts
6997
- function collectDecisionContributors(request, result, minConfidence) {
6998
- const decision = classifyDecisionAnswers(request, result, minConfidence);
6999
- return Object.entries(request.rules).flatMap(([id, rule]) => {
7000
- const answer = result.answers[id];
7001
- if (answer === void 0) throw new Error("Decision answer missing");
7002
- return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
7003
- rule: rule.source === "shipped" ? rule.name : id,
7004
- source: rule.source,
7005
- tier: rule.tier,
7006
- choice: answer.choice,
7007
- confidence: answer.confidence,
7008
- probability: answer.probabilities[answer.choice]
7009
- }] : [];
7010
- });
6892
+ function parseReference(content, prefix) {
6893
+ const value = content.trim();
6894
+ const marker = `ref: ${prefix}`;
6895
+ if (!value.startsWith(marker)) return null;
6896
+ const branch = value.slice(marker.length);
6897
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6898
+ }
6899
+ function normalizeRemoteURL(url) {
6900
+ if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
6901
+ const parsed = URL.parse(url);
6902
+ if (parsed === null) return null;
6903
+ parsed.username = "";
6904
+ parsed.password = "";
6905
+ parsed.search = "";
6906
+ parsed.hash = "";
6907
+ return parsed.href;
6908
+ }
6909
+ const slash = url.indexOf("/");
6910
+ const end = slash === -1 ? url.length : slash;
6911
+ const at = url.lastIndexOf("@", end);
6912
+ return at === -1 ? url : url.slice(at + 1);
7011
6913
  }
7012
6914
  //#endregion
7013
- //#region src/model/decision-request-error.ts
7014
- var DecisionRequestError = class extends Error {
7015
- reason;
7016
- requestBytes;
7017
- constructor(reason, requestBytes, message, options) {
7018
- super(message, options);
7019
- this.name = "DecisionRequestError";
7020
- this.reason = reason;
7021
- this.requestBytes = requestBytes;
7022
- }
7023
- };
7024
- //#endregion
7025
- //#region src/model/format-classifier-note.ts
7026
- function formatClassifierNote(note, apiKey) {
7027
- return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6915
+ //#region src/model/load-repository-evidence.ts
6916
+ async function loadRepositoryEvidence(cwd, taskScope) {
6917
+ const context = await loadRepositoryContext(cwd);
6918
+ if (taskScope === void 0) return context;
6919
+ return {
6920
+ ...context ?? {
6921
+ cwd: resolve(cwd),
6922
+ branch: null,
6923
+ defaultBranch: null
6924
+ },
6925
+ taskScope
6926
+ };
7028
6927
  }
7029
6928
  //#endregion
7030
6929
  //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
@@ -7537,17 +7436,13 @@ async function classifyWithJev(payload, config, options = {}) {
7537
7436
  if (key === null) throw new Error("no API key: set the configured environment variable or key command");
7538
7437
  stage = "evidence";
7539
7438
  const directUserText = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage?.text ?? null : null;
7540
- const command = payload.toolInput["command"] ?? payload.toolInput["cmd"];
7541
- const hasGitCommand = typeof command === "string" && /\bgit\s/u.test(command);
7542
- const isFileEdit = payload.toolName === "Write" || payload.toolName === "Edit";
7543
- const needsRepositoryContext = hasGitCommand || isFileEdit;
7544
7439
  const [policy, rules, repositoryContext] = await Promise.all([
7545
7440
  loadPolicy({
7546
7441
  classifierPath: config.classifierPath,
7547
7442
  rulesPath: config.rulesPath
7548
7443
  }, "decision.md"),
7549
7444
  loadClaudeRules(config.claudeSettingsPath),
7550
- needsRepositoryContext ? loadRepositoryContext(payload.cwd) : Promise.resolve(null)
7445
+ loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null)
7551
7446
  ]);
7552
7447
  const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext);
7553
7448
  const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - Date.now();
@@ -7655,10 +7550,12 @@ async function classifyWithModel(payload, config, options = {}) {
7655
7550
  return buildFailure(config, `policy unreadable: ${toMessage(error)}`);
7656
7551
  }
7657
7552
  const directMessage = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage : null;
7658
- const user = buildUserMessage(payload, directMessage === null ? [] : [{
7553
+ const transcript = directMessage === null ? [] : [{
7659
7554
  role: "user",
7660
7555
  text: directMessage.text
7661
- }], config.provider.reasoning);
7556
+ }];
7557
+ const repositoryContext = await loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null);
7558
+ const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext);
7662
7559
  try {
7663
7560
  const result = await sendMessage(config.provider, apiKey, {
7664
7561
  system,
@@ -7871,6 +7768,179 @@ function toUnquoted(word) {
7871
7768
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
7872
7769
  }
7873
7770
  //#endregion
7771
+ //#region src/scope/find-checkout.ts
7772
+ const GIT_OVERRIDES$1 = [
7773
+ "GIT_DIR",
7774
+ "GIT_WORK_TREE",
7775
+ "GIT_COMMON_DIR"
7776
+ ];
7777
+ async function findCheckout(start) {
7778
+ if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
7779
+ for (let directory = start;; directory = dirname(directory)) {
7780
+ const gitPath = join(directory, ".git");
7781
+ const info = await stat(gitPath).catch(() => null);
7782
+ if (info !== null) {
7783
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
7784
+ return gitDir === null ? null : {
7785
+ worktree: directory,
7786
+ commonDir: await findCommonDir(gitDir)
7787
+ };
7788
+ }
7789
+ if (dirname(directory) === directory) return null;
7790
+ }
7791
+ }
7792
+ async function findLinkedGitDir(directory, gitPath) {
7793
+ const link = await readFile(gitPath, "utf8").catch(() => "");
7794
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
7795
+ return path === void 0 ? null : resolve(directory, path.trim());
7796
+ }
7797
+ async function findCommonDir(gitDir) {
7798
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
7799
+ return common === null ? gitDir : resolve(gitDir, common.trim());
7800
+ }
7801
+ //#endregion
7802
+ //#region src/scope/merge-scope-facts.ts
7803
+ function mergeScopeFacts(all) {
7804
+ const pullRequests = /* @__PURE__ */ new Map();
7805
+ for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
7806
+ return {
7807
+ worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
7808
+ branches: [...new Set(all.flatMap((facts) => facts.branches))],
7809
+ pullRequests: [...pullRequests.values()],
7810
+ pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
7811
+ };
7812
+ }
7813
+ //#endregion
7814
+ //#region src/scope/build-task-scope.ts
7815
+ function buildTaskScope(input) {
7816
+ const merged = mergeScopeFacts(input.facts);
7817
+ const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
7818
+ const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
7819
+ const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
7820
+ number: pull.number,
7821
+ repository: pull.repository
7822
+ }));
7823
+ return {
7824
+ home: input.home,
7825
+ worktrees: merged.worktrees,
7826
+ branches,
7827
+ currentBranch: input.currentBranch,
7828
+ remotes: input.remotes,
7829
+ pullRequests,
7830
+ pathGlobs: merged.pathGlobs
7831
+ };
7832
+ }
7833
+ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
7834
+ "main",
7835
+ "master",
7836
+ "trunk",
7837
+ "develop"
7838
+ ]);
7839
+ //#endregion
7840
+ //#region src/scope/types.ts
7841
+ const EMPTY_SCOPE_FACTS = {
7842
+ worktrees: [],
7843
+ branches: [],
7844
+ pullRequests: [],
7845
+ pathGlobs: []
7846
+ };
7847
+ const EMPTY_SESSION_SCOPE = {
7848
+ worktrees: [],
7849
+ branches: [],
7850
+ pullRequests: []
7851
+ };
7852
+ //#endregion
7853
+ //#region src/scope/load-session-scope.ts
7854
+ const name = string().min(1);
7855
+ const pullRequestSchema = strictObject({
7856
+ number: number().int().positive(),
7857
+ head: name,
7858
+ repository: name
7859
+ });
7860
+ const branchSchema = strictObject({
7861
+ name,
7862
+ commonDir: name
7863
+ });
7864
+ const sessionScopeSchema = strictObject({
7865
+ worktrees: array(name),
7866
+ branches: array(branchSchema),
7867
+ pullRequests: array(pullRequestSchema)
7868
+ });
7869
+ async function loadSessionScope(path) {
7870
+ let raw;
7871
+ try {
7872
+ raw = await readFile(path, "utf8");
7873
+ } catch {
7874
+ return EMPTY_SESSION_SCOPE;
7875
+ }
7876
+ try {
7877
+ const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
7878
+ return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
7879
+ } catch {
7880
+ return EMPTY_SESSION_SCOPE;
7881
+ }
7882
+ }
7883
+ //#endregion
7884
+ //#region src/scope/resolve-session-scope-path.ts
7885
+ function resolveSessionScopePath(stateDir, sessionID) {
7886
+ const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
7887
+ return join(stateDir, "session-scope", `${key}.json`);
7888
+ }
7889
+ //#endregion
7890
+ //#region src/scope/pick-scope-source-reader.ts
7891
+ function pickScopeSourceReader(source) {
7892
+ if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
7893
+ worktrees: [context.worktree],
7894
+ branches: context.branch === null ? [] : [context.branch]
7895
+ }));
7896
+ if (source.kind === "session") return async (context) => {
7897
+ const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
7898
+ return buildFacts({
7899
+ worktrees: scope.worktrees,
7900
+ branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
7901
+ pullRequests: scope.pullRequests
7902
+ });
7903
+ };
7904
+ if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
7905
+ return () => Promise.resolve(EMPTY_SCOPE_FACTS);
7906
+ }
7907
+ function buildFacts(facts) {
7908
+ return {
7909
+ ...EMPTY_SCOPE_FACTS,
7910
+ ...facts
7911
+ };
7912
+ }
7913
+ //#endregion
7914
+ //#region src/scope/load-task-scope.ts
7915
+ async function loadTaskScope(request, sources) {
7916
+ const cwd = resolve(request.cwd);
7917
+ const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
7918
+ const branch = context?.branch ?? null;
7919
+ const sourceContext = {
7920
+ sessionID: request.sessionID,
7921
+ cwd,
7922
+ worktree: checkout?.worktree ?? cwd,
7923
+ commonDir: checkout?.commonDir ?? null,
7924
+ branch,
7925
+ stateDir: request.stateDir
7926
+ };
7927
+ const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
7928
+ return buildTaskScope({
7929
+ home: homedir(),
7930
+ currentBranch: branch,
7931
+ defaultBranch: context?.defaultBranch ?? null,
7932
+ remotes,
7933
+ facts
7934
+ });
7935
+ }
7936
+ //#endregion
7937
+ //#region src/state/resolve-state-dir.ts
7938
+ function resolveStateDir() {
7939
+ const stateHome = process.env["XDG_STATE_HOME"];
7940
+ const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
7941
+ return join(base, "auto-mode");
7942
+ }
7943
+ //#endregion
7874
7944
  //#region src/classify-action.ts
7875
7945
  async function classifyAction(request, config, options = {}) {
7876
7946
  let configured = null;
@@ -7886,9 +7956,11 @@ async function classifyAction(request, config, options = {}) {
7886
7956
  unavailable: true
7887
7957
  };
7888
7958
  }
7889
- const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
7959
+ const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
7960
+ const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
7890
7961
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7891
- const containment = await checkContainment(request, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
7962
+ const scope = await tryLoadTaskScope(request, config);
7963
+ const containment = scope === null ? null : checkContainment(request, scope);
7892
7964
  if (containment !== null) {
7893
7965
  const guidance = await tryReadDenialGuidance();
7894
7966
  return {
@@ -7899,13 +7971,24 @@ async function classifyAction(request, config, options = {}) {
7899
7971
  };
7900
7972
  }
7901
7973
  if (local.kind === "allow") return buildLocalAllow(local.exception);
7974
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
7975
+ if (edit?.kind === "bypass") return {
7976
+ verdict: { kind: "allow" },
7977
+ decidingStage: "bypass",
7978
+ note: `allowed by the edit bypass: ${edit.target}`,
7979
+ status: "allow"
7980
+ };
7902
7981
  if (options.localOnly === true) return {
7903
7982
  verdict: null,
7904
7983
  decidingStage: "local",
7905
7984
  note: `${request.toolName} needs the model tier, which this run skipped`,
7906
7985
  status: "skipped"
7907
7986
  };
7908
- const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, options), tryReadDenialGuidance()]);
7987
+ const taskScope = scope === null ? void 0 : buildTaskScopeSummary(scope);
7988
+ const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, {
7989
+ ...options,
7990
+ taskScope
7991
+ }), tryReadDenialGuidance()]);
7909
7992
  return {
7910
7993
  ...outcome,
7911
7994
  decidingStage: config.provider.protocol === "system-one" ? "jev" : "messages",
@@ -7914,6 +7997,51 @@ async function classifyAction(request, config, options = {}) {
7914
7997
  };
7915
7998
  }
7916
7999
  const READ_ONLY_EXCEPTION = "Read-only actions";
8000
+ async function tryLoadTaskScope(request, config) {
8001
+ try {
8002
+ return await loadTaskScope({
8003
+ sessionID: request.sessionID,
8004
+ cwd: request.cwd,
8005
+ stateDir: resolveStateDir()
8006
+ }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
8007
+ } catch {
8008
+ return null;
8009
+ }
8010
+ }
8011
+ const GIT_OVERRIDES = [
8012
+ "GIT_DIR",
8013
+ "GIT_WORK_TREE",
8014
+ "GIT_COMMON_DIR"
8015
+ ];
8016
+ async function tryClassifyEdit(request, scope) {
8017
+ const fields = getEditFields(request.toolName);
8018
+ const path = fields === null ? void 0 : request.toolInput[fields.path];
8019
+ if (typeof path !== "string" || path === "") return null;
8020
+ try {
8021
+ const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
8022
+ const requested = resolve(request.cwd, path);
8023
+ const [target, worktrees, protectedDirs] = await Promise.all([
8024
+ resolveEditTarget(requested),
8025
+ Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
8026
+ Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
8027
+ ]);
8028
+ if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
8029
+ const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
8030
+ return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
8031
+ toolName: request.toolName,
8032
+ toolInput: request.toolInput,
8033
+ requested,
8034
+ target,
8035
+ checkout: checkout?.worktree ?? null,
8036
+ current
8037
+ }, {
8038
+ worktrees: worktrees.filter((worktree) => worktree !== null),
8039
+ protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
8040
+ });
8041
+ } catch {
8042
+ return null;
8043
+ }
8044
+ }
7917
8045
  function buildLocalAllow(exception) {
7918
8046
  return {
7919
8047
  verdict: { kind: "allow" },
@@ -7995,4 +8123,4 @@ function renderVerdict(verdict) {
7995
8123
  })).exhaustive();
7996
8124
  }
7997
8125
  //#endregion
7998
- export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, loadRepositoryContext as _, classifyWithModel as a, DEFAULT_SCOPE_SOURCES as b, splitShellCommand as c, resolveSessionScopePath as d, loadSessionScope as f, toRepositorySlug as g, findCheckout as h, classifyLocally as i, splitCommandWords as l, loadCheckoutRemotes as m, parseActionRequest as n, parseModelVerdict as o, EMPTY_SESSION_SCOPE as p, classifyAction as r, loadPolicy as s, renderVerdict as t, resolveStateDir as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };
8126
+ export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, splitCommandWords as _, resolveSessionScopePath as a, DEFAULT_SCOPE_SOURCES as b, findCheckout as c, parseModelVerdict as d, loadRepositoryContext as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, classifyLocally as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadCheckoutRemotes as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyWithModel as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };