auto-mode 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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"
6005
+ function collectTargetFindings(directory, target, scope) {
6006
+ const path = resolvePath(directory, target, scope.home);
6007
+ return path === null ? [] : collectPathFindings(path, scope);
6008
+ }
6009
+ const CREDENTIAL_PATHS = [
6010
+ ".ssh",
6011
+ ".aws",
6012
+ ".gnupg",
6013
+ ".gitconfig",
6014
+ ".netrc",
6015
+ ".npmrc",
6016
+ ".config/gh"
5984
6017
  ];
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
- }
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
+ }];
5999
6027
  }
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());
6004
- }
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,
@@ -7020,11 +6768,73 @@ var DecisionRequestError = class extends Error {
7020
6768
  this.reason = reason;
7021
6769
  this.requestBytes = requestBytes;
7022
6770
  }
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]");
6771
+ };
6772
+ //#endregion
6773
+ //#region src/model/format-classifier-note.ts
6774
+ function formatClassifierNote(note, apiKey) {
6775
+ return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6776
+ }
6777
+ //#endregion
6778
+ //#region src/model/load-repository-context.ts
6779
+ async function loadRepositoryContext(cwd) {
6780
+ if ([
6781
+ "GIT_DIR",
6782
+ "GIT_WORK_TREE",
6783
+ "GIT_COMMON_DIR"
6784
+ ].some((name) => process.env[name] !== void 0)) return null;
6785
+ let directory = resolve(cwd);
6786
+ for (;;) {
6787
+ const gitPath = join(directory, ".git");
6788
+ try {
6789
+ await stat(gitPath);
6790
+ } catch (error) {
6791
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6792
+ const parent = dirname(directory);
6793
+ if (parent === directory) return null;
6794
+ directory = parent;
6795
+ continue;
6796
+ }
6797
+ let gitDir = gitPath;
6798
+ let head;
6799
+ try {
6800
+ try {
6801
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6802
+ } catch {
6803
+ const link = await readFile(gitPath, "utf8");
6804
+ const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6805
+ if (path === void 0) return null;
6806
+ gitDir = resolve(directory, path.trim());
6807
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6808
+ }
6809
+ } catch {
6810
+ return null;
6811
+ }
6812
+ let commonDir = gitDir;
6813
+ try {
6814
+ const common = await readFile(join(gitDir, "commondir"), "utf8");
6815
+ commonDir = resolve(gitDir, common.trim());
6816
+ } catch {
6817
+ commonDir = gitDir;
6818
+ }
6819
+ let defaultBranch = null;
6820
+ try {
6821
+ defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6822
+ } catch {
6823
+ defaultBranch = null;
6824
+ }
6825
+ return {
6826
+ cwd: resolve(cwd),
6827
+ branch: parseReference(head, "refs/heads/"),
6828
+ defaultBranch
6829
+ };
6830
+ }
6831
+ }
6832
+ function parseReference(content, prefix) {
6833
+ const value = content.trim();
6834
+ const marker = `ref: ${prefix}`;
6835
+ if (!value.startsWith(marker)) return null;
6836
+ const branch = value.slice(marker.length);
6837
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
7028
6838
  }
7029
6839
  //#endregion
7030
6840
  //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
@@ -7871,6 +7681,199 @@ function toUnquoted(word) {
7871
7681
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
7872
7682
  }
7873
7683
  //#endregion
7684
+ //#region src/scope/find-checkout.ts
7685
+ const GIT_OVERRIDES$1 = [
7686
+ "GIT_DIR",
7687
+ "GIT_WORK_TREE",
7688
+ "GIT_COMMON_DIR"
7689
+ ];
7690
+ async function findCheckout(start) {
7691
+ if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
7692
+ for (let directory = start;; directory = dirname(directory)) {
7693
+ const gitPath = join(directory, ".git");
7694
+ const info = await stat(gitPath).catch(() => null);
7695
+ if (info !== null) {
7696
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
7697
+ return gitDir === null ? null : {
7698
+ worktree: directory,
7699
+ commonDir: await findCommonDir(gitDir)
7700
+ };
7701
+ }
7702
+ if (dirname(directory) === directory) return null;
7703
+ }
7704
+ }
7705
+ async function findLinkedGitDir(directory, gitPath) {
7706
+ const link = await readFile(gitPath, "utf8").catch(() => "");
7707
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
7708
+ return path === void 0 ? null : resolve(directory, path.trim());
7709
+ }
7710
+ async function findCommonDir(gitDir) {
7711
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
7712
+ return common === null ? gitDir : resolve(gitDir, common.trim());
7713
+ }
7714
+ //#endregion
7715
+ //#region src/scope/merge-scope-facts.ts
7716
+ function mergeScopeFacts(all) {
7717
+ const pullRequests = /* @__PURE__ */ new Map();
7718
+ for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
7719
+ return {
7720
+ worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
7721
+ branches: [...new Set(all.flatMap((facts) => facts.branches))],
7722
+ pullRequests: [...pullRequests.values()],
7723
+ pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
7724
+ };
7725
+ }
7726
+ //#endregion
7727
+ //#region src/scope/build-task-scope.ts
7728
+ function buildTaskScope(input) {
7729
+ const merged = mergeScopeFacts(input.facts);
7730
+ const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
7731
+ const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
7732
+ const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
7733
+ number: pull.number,
7734
+ repository: pull.repository
7735
+ }));
7736
+ return {
7737
+ home: input.home,
7738
+ worktrees: merged.worktrees,
7739
+ branches,
7740
+ currentBranch: input.currentBranch,
7741
+ remotes: input.remotes,
7742
+ pullRequests,
7743
+ pathGlobs: merged.pathGlobs
7744
+ };
7745
+ }
7746
+ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
7747
+ "main",
7748
+ "master",
7749
+ "trunk",
7750
+ "develop"
7751
+ ]);
7752
+ //#endregion
7753
+ //#region src/scope/load-checkout-remotes.ts
7754
+ async function loadCheckoutRemotes(commonDir) {
7755
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
7756
+ const remotes = [];
7757
+ let section = null;
7758
+ for (const line of config.split("\n")) {
7759
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
7760
+ if (header !== void 0) section = header;
7761
+ else if (/^\s*\[/u.test(line)) section = null;
7762
+ else if (section !== null) {
7763
+ const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
7764
+ if (url !== void 0) remotes.push({
7765
+ name: section,
7766
+ url
7767
+ });
7768
+ }
7769
+ }
7770
+ return remotes;
7771
+ }
7772
+ //#endregion
7773
+ //#region src/scope/types.ts
7774
+ const EMPTY_SCOPE_FACTS = {
7775
+ worktrees: [],
7776
+ branches: [],
7777
+ pullRequests: [],
7778
+ pathGlobs: []
7779
+ };
7780
+ const EMPTY_SESSION_SCOPE = {
7781
+ worktrees: [],
7782
+ branches: [],
7783
+ pullRequests: []
7784
+ };
7785
+ //#endregion
7786
+ //#region src/scope/load-session-scope.ts
7787
+ const name = string().min(1);
7788
+ const pullRequestSchema = strictObject({
7789
+ number: number().int().positive(),
7790
+ head: name,
7791
+ repository: name
7792
+ });
7793
+ const branchSchema = strictObject({
7794
+ name,
7795
+ commonDir: name
7796
+ });
7797
+ const sessionScopeSchema = strictObject({
7798
+ worktrees: array(name),
7799
+ branches: array(branchSchema),
7800
+ pullRequests: array(pullRequestSchema)
7801
+ });
7802
+ async function loadSessionScope(path) {
7803
+ let raw;
7804
+ try {
7805
+ raw = await readFile(path, "utf8");
7806
+ } catch {
7807
+ return EMPTY_SESSION_SCOPE;
7808
+ }
7809
+ try {
7810
+ const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
7811
+ return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
7812
+ } catch {
7813
+ return EMPTY_SESSION_SCOPE;
7814
+ }
7815
+ }
7816
+ //#endregion
7817
+ //#region src/scope/resolve-session-scope-path.ts
7818
+ function resolveSessionScopePath(stateDir, sessionID) {
7819
+ const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
7820
+ return join(stateDir, "session-scope", `${key}.json`);
7821
+ }
7822
+ //#endregion
7823
+ //#region src/scope/pick-scope-source-reader.ts
7824
+ function pickScopeSourceReader(source) {
7825
+ if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
7826
+ worktrees: [context.worktree],
7827
+ branches: context.branch === null ? [] : [context.branch]
7828
+ }));
7829
+ if (source.kind === "session") return async (context) => {
7830
+ const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
7831
+ return buildFacts({
7832
+ worktrees: scope.worktrees,
7833
+ branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
7834
+ pullRequests: scope.pullRequests
7835
+ });
7836
+ };
7837
+ if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
7838
+ return () => Promise.resolve(EMPTY_SCOPE_FACTS);
7839
+ }
7840
+ function buildFacts(facts) {
7841
+ return {
7842
+ ...EMPTY_SCOPE_FACTS,
7843
+ ...facts
7844
+ };
7845
+ }
7846
+ //#endregion
7847
+ //#region src/scope/load-task-scope.ts
7848
+ async function loadTaskScope(request, sources) {
7849
+ const cwd = resolve(request.cwd);
7850
+ const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
7851
+ const branch = context?.branch ?? null;
7852
+ const sourceContext = {
7853
+ sessionID: request.sessionID,
7854
+ cwd,
7855
+ worktree: checkout?.worktree ?? cwd,
7856
+ commonDir: checkout?.commonDir ?? null,
7857
+ branch,
7858
+ stateDir: request.stateDir
7859
+ };
7860
+ const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
7861
+ return buildTaskScope({
7862
+ home: homedir(),
7863
+ currentBranch: branch,
7864
+ defaultBranch: context?.defaultBranch ?? null,
7865
+ remotes,
7866
+ facts
7867
+ });
7868
+ }
7869
+ //#endregion
7870
+ //#region src/state/resolve-state-dir.ts
7871
+ function resolveStateDir() {
7872
+ const stateHome = process.env["XDG_STATE_HOME"];
7873
+ const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
7874
+ return join(base, "auto-mode");
7875
+ }
7876
+ //#endregion
7874
7877
  //#region src/classify-action.ts
7875
7878
  async function classifyAction(request, config, options = {}) {
7876
7879
  let configured = null;
@@ -7886,9 +7889,11 @@ async function classifyAction(request, config, options = {}) {
7886
7889
  unavailable: true
7887
7890
  };
7888
7891
  }
7889
- const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
7892
+ const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
7893
+ const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
7890
7894
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7891
- const containment = await checkContainment(request, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
7895
+ const scope = await tryLoadTaskScope(request, config);
7896
+ const containment = scope === null ? null : checkContainment(request, scope);
7892
7897
  if (containment !== null) {
7893
7898
  const guidance = await tryReadDenialGuidance();
7894
7899
  return {
@@ -7899,6 +7904,13 @@ async function classifyAction(request, config, options = {}) {
7899
7904
  };
7900
7905
  }
7901
7906
  if (local.kind === "allow") return buildLocalAllow(local.exception);
7907
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
7908
+ if (edit?.kind === "bypass") return {
7909
+ verdict: { kind: "allow" },
7910
+ decidingStage: "bypass",
7911
+ note: `allowed by the edit bypass: ${edit.target}`,
7912
+ status: "allow"
7913
+ };
7902
7914
  if (options.localOnly === true) return {
7903
7915
  verdict: null,
7904
7916
  decidingStage: "local",
@@ -7914,6 +7926,51 @@ async function classifyAction(request, config, options = {}) {
7914
7926
  };
7915
7927
  }
7916
7928
  const READ_ONLY_EXCEPTION = "Read-only actions";
7929
+ async function tryLoadTaskScope(request, config) {
7930
+ try {
7931
+ return await loadTaskScope({
7932
+ sessionID: request.sessionID,
7933
+ cwd: request.cwd,
7934
+ stateDir: resolveStateDir()
7935
+ }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
7936
+ } catch {
7937
+ return null;
7938
+ }
7939
+ }
7940
+ const GIT_OVERRIDES = [
7941
+ "GIT_DIR",
7942
+ "GIT_WORK_TREE",
7943
+ "GIT_COMMON_DIR"
7944
+ ];
7945
+ async function tryClassifyEdit(request, scope) {
7946
+ const fields = getEditFields(request.toolName);
7947
+ const path = fields === null ? void 0 : request.toolInput[fields.path];
7948
+ if (typeof path !== "string" || path === "") return null;
7949
+ try {
7950
+ const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
7951
+ const requested = resolve(request.cwd, path);
7952
+ const [target, worktrees, protectedDirs] = await Promise.all([
7953
+ resolveEditTarget(requested),
7954
+ Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
7955
+ Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
7956
+ ]);
7957
+ if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
7958
+ const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
7959
+ return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
7960
+ toolName: request.toolName,
7961
+ toolInput: request.toolInput,
7962
+ requested,
7963
+ target,
7964
+ checkout: checkout?.worktree ?? null,
7965
+ current
7966
+ }, {
7967
+ worktrees: worktrees.filter((worktree) => worktree !== null),
7968
+ protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
7969
+ });
7970
+ } catch {
7971
+ return null;
7972
+ }
7973
+ }
7917
7974
  function buildLocalAllow(exception) {
7918
7975
  return {
7919
7976
  verdict: { kind: "allow" },
@@ -7995,4 +8052,4 @@ function renderVerdict(verdict) {
7995
8052
  })).exhaustive();
7996
8053
  }
7997
8054
  //#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 };
8055
+ export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, splitCommandWords as _, resolveSessionScopePath as a, DEFAULT_SCOPE_SOURCES as b, loadCheckoutRemotes as c, classifyWithModel as d, parseModelVerdict as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, findCheckout as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadRepositoryContext as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyLocally as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };