auto-mode 3.0.1 → 3.1.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.
package/dist/index.d.ts CHANGED
@@ -68,6 +68,7 @@ interface TaskScopeSummary {
68
68
  interface OutputStream {
69
69
  readonly write: (text: string) => unknown;
70
70
  }
71
+ type TextFileReader = (path: string) => Promise<string>;
71
72
  interface HostEnvironment {
72
73
  readonly env: Readonly<Record<string, string | undefined>>;
73
74
  readonly home: string;
@@ -79,6 +80,7 @@ interface Runtime {
79
80
  readonly timeout: (timeoutMs: number) => AbortSignal;
80
81
  readonly fetch: (url: string, init: Readonly<RequestInit>) => Promise<Response>;
81
82
  readonly readCheckout: CheckoutReader;
83
+ readonly readTextFile: TextFileReader;
82
84
  }
83
85
  interface ClaudeRules {
84
86
  readonly environment: readonly string[];
@@ -114,7 +116,7 @@ interface ScopeSource {
114
116
  readonly paths?: readonly string[];
115
117
  }
116
118
  type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
117
- declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
119
+ declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'launcher', 'atc'];
118
120
  interface CaptureConfig {
119
121
  readonly enabled: boolean;
120
122
  readonly directory?: string | undefined;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b as loadConfig$1, i as classifyAction$1, n as readRuntime, r as readHostEnvironment, t as parseActionRequest } from "./parse-action-request-CaQy7FA3.js";
1
+ import { b as loadConfig$1, i as classifyAction$1, n as readRuntime, r as readHostEnvironment, t as parseActionRequest } from "./parse-action-request-DySYvZxl.js";
2
2
  //#region src/index.ts
3
3
  function classifyAction(request, config, options = {}) {
4
4
  return classifyAction$1(request, config, {
@@ -1,5 +1,6 @@
1
- import { a as literal, c as object, d as string, f as union, l as record, m as prettifyError, n as array, o as looseObject, p as unknown, r as boolean, s as number, t as _enum, u as strictObject } from "./schemas-B_w83Dze.js";
1
+ import { a as literal, c as object, d as string, f as union, l as record, m as prettifyError, n as array, o as looseObject, p as unknown, r as boolean, s as number, t as _enum, u as strictObject } from "./schemas-B80Yxv6m.js";
2
2
  import { t as findEditFields } from "./find-edit-fields-CzkhPA4j.js";
3
+ import { t as findSecret } from "./find-secret-DUEWxfkc.js";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { access, appendFile, link, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
5
6
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
@@ -268,7 +269,7 @@ function resolveXDGDir(host, kind) {
268
269
  return join(root, "auto-mode");
269
270
  }
270
271
  //#endregion
271
- //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
272
+ //#region node_modules/.bun/ts-pattern@5.9.2/node_modules/ts-pattern/dist/index.js
272
273
  const t = Symbol.for("@ts-pattern/matcher");
273
274
  const e = Symbol.for("@ts-pattern/isVariadic");
274
275
  const n = "@ts-pattern/anonymous-select-key";
@@ -692,6 +693,7 @@ const SCOPE_SOURCE_KINDS = [
692
693
  "cwd",
693
694
  "session",
694
695
  "globs",
696
+ "launcher",
695
697
  "atc"
696
698
  ];
697
699
  const DEFAULT_PROVIDER = findPreset("jev");
@@ -704,6 +706,7 @@ const DEFAULT_DENIAL_BUDGET = {
704
706
  const DEFAULT_SCOPE_SOURCES = {
705
707
  cwd: { kind: "cwd" },
706
708
  session: { kind: "session" },
709
+ launcher: { kind: "launcher" },
707
710
  atc: { kind: "atc" }
708
711
  };
709
712
  const DEFAULT_CONFIG = {
@@ -1005,7 +1008,7 @@ async function tryClassifyEdit(request, scope, host, reader = {}) {
1005
1008
  ]);
1006
1009
  if (target === null || GIT_OVERRIDES.some((name) => host.env[name] !== void 0)) return null;
1007
1010
  const [checkout, current] = await Promise.all([files.readCheckout(dirname(target), host.env), request.toolName === "Edit" ? files.readFile(target).catch(() => null) : null]);
1008
- return (await import("./classify-edit-B660kX2I.js")).classifyEdit({
1011
+ return (await import("./classify-edit-O8JNCVde.js")).classifyEdit({
1009
1012
  toolName: request.toolName,
1010
1013
  toolInput: request.toolInput,
1011
1014
  requested,
@@ -1095,6 +1098,11 @@ const SCRATCH_PATHS = [
1095
1098
  "/dev/stderr"
1096
1099
  ];
1097
1100
  //#endregion
1101
+ //#region src/scope/find-reply-channel.ts
1102
+ function findReplyChannel(tool, input, channels) {
1103
+ return channels.find((channel) => channel.tool === tool && input[channel.field] === channel.recipient) ?? null;
1104
+ }
1105
+ //#endregion
1098
1106
  //#region src/shell/is-unresolved-word.ts
1099
1107
  function isUnresolvedWord(word) {
1100
1108
  return /[$`]/u.test(word);
@@ -1834,7 +1842,7 @@ function toRepositorySlug(reference) {
1834
1842
  //#endregion
1835
1843
  //#region src/containment/is-in-scope.ts
1836
1844
  function isInScope(path, scope, scratchPaths) {
1837
- if (scratchPaths.some((entry) => isUnder$2(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
1845
+ if (scratchPaths.some((entry) => isUnder$2(path, entry)) || scope.paths.some((owned) => isUnder$2(path, owned)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
1838
1846
  return scope.worktrees.some((worktree) => {
1839
1847
  const isNested = isUnder$2(path, join(worktree, ".worktrees"));
1840
1848
  const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder$2(path, other));
@@ -1842,7 +1850,8 @@ function isInScope(path, scope, scratchPaths) {
1842
1850
  });
1843
1851
  }
1844
1852
  function isUnder$2(path, root) {
1845
- return path === root || path.startsWith(`${root}/`);
1853
+ const prefix = root.endsWith("/") ? root : `${root}/`;
1854
+ return path === root || path.startsWith(prefix);
1846
1855
  }
1847
1856
  //#endregion
1848
1857
  //#region src/containment/collect-target-findings.ts
@@ -2688,7 +2697,7 @@ function collectScopeFindings(action, scope, scratchPaths = SCRATCH_PATHS) {
2688
2697
  }
2689
2698
  function collectToolFindings(action, scope) {
2690
2699
  const verb = action.tool.split("__").at(-1) ?? "";
2691
- if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
2700
+ if (!/merge|create|comment|update|delete|push|close|review/u.test(verb) || findReplyChannel(action.tool, action.input, scope.replyChannels) !== null) return [];
2692
2701
  const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
2693
2702
  const owner = action.input["owner"];
2694
2703
  const name = action.input["repo"];
@@ -3059,14 +3068,30 @@ function isUnder(path, root) {
3059
3068
  return path === root || root === "/" || path.startsWith(`${root}/`);
3060
3069
  }
3061
3070
  //#endregion
3071
+ //#region src/secrets/build-secret-free-text.ts
3072
+ const MAX_SECRET_PASSES = 50;
3073
+ function buildSecretFreeText(text, toReplacement) {
3074
+ let current = text;
3075
+ for (let pass = 0; pass < MAX_SECRET_PASSES; pass += 1) {
3076
+ const finding = findSecret({
3077
+ text: current,
3078
+ path: null
3079
+ });
3080
+ if (finding === null || finding.secret === "" || !current.includes(finding.secret)) return current;
3081
+ current = current.replaceAll(finding.secret, toReplacement(finding.secret));
3082
+ }
3083
+ return current;
3084
+ }
3085
+ //#endregion
3062
3086
  //#region src/diagnostics/write-action-diagnostic.ts
3063
3087
  async function writeActionDiagnostic(payload, entry, host, warnings) {
3064
3088
  const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveXDGDir(host, "state"), "actions.jsonl");
3065
3089
  if (path === "") return;
3066
3090
  const agentID = payload.decisionContext?.agentID ?? null;
3067
3091
  const agentKey = agentID === null ? payload.sessionID : `${payload.sessionID}\0${agentID}`;
3092
+ const isReviewed = entry.judge !== void 0 || entry.grant !== void 0;
3068
3093
  const record = {
3069
- schemaVersion: 6,
3094
+ schemaVersion: 7,
3070
3095
  time: (/* @__PURE__ */ new Date()).toISOString(),
3071
3096
  invocationID: entry.invocationID,
3072
3097
  sessionHash: toHash(payload.sessionID),
@@ -3079,7 +3104,8 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3079
3104
  escalation: entry.escalation ?? false,
3080
3105
  diagnostics: entry.diagnostics ?? null,
3081
3106
  judge: entry.judge ?? null,
3082
- grant: entry.grant ?? null
3107
+ grant: entry.grant ?? null,
3108
+ action: isReviewed ? buildLoggedAction(payload) : null
3083
3109
  };
3084
3110
  try {
3085
3111
  await mkdir(dirname(path), {
@@ -3094,6 +3120,34 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3094
3120
  function toHash(value) {
3095
3121
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
3096
3122
  }
3123
+ const LOGGED_INPUT_MAX_CHARS = 2e3;
3124
+ function buildLoggedAction(payload) {
3125
+ const redacted = buildSecretFreeValue(payload.toolInput);
3126
+ const input = JSON.stringify(redacted);
3127
+ return {
3128
+ tool: payload.toolName,
3129
+ cwd: payload.cwd,
3130
+ input: hasSecret(redacted) || findSecret({
3131
+ text: input,
3132
+ path: null
3133
+ }) !== null ? null : input.slice(0, LOGGED_INPUT_MAX_CHARS)
3134
+ };
3135
+ }
3136
+ function buildSecretFreeValue(value) {
3137
+ if (typeof value === "string") return buildSecretFreeText(value, () => "[secret]");
3138
+ if (Array.isArray(value)) return value.map((item) => buildSecretFreeValue(item));
3139
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, buildSecretFreeValue(item)]));
3140
+ return value;
3141
+ }
3142
+ function hasSecret(value) {
3143
+ if (typeof value === "string") return findSecret({
3144
+ text: value,
3145
+ path: null
3146
+ }) !== null;
3147
+ if (Array.isArray(value)) return value.some((item) => hasSecret(item));
3148
+ if (typeof value === "object" && value !== null) return Object.values(value).some((item) => hasSecret(item));
3149
+ return false;
3150
+ }
3097
3151
  //#endregion
3098
3152
  //#region src/git/build-repository-context.ts
3099
3153
  function buildRepositoryContext(facts) {
@@ -3754,7 +3808,7 @@ function sendJudgeMessage(provider, request, transport, signal) {
3754
3808
  }
3755
3809
  async function sendToAgentSDK(provider, request, transport, signal) {
3756
3810
  if (transport.session !== void 0) return transport.session.review(provider.model, request, signal);
3757
- const session = (await import("./start-judge-session-B-KYQG13.js")).startJudgeSession(transport.env);
3811
+ const session = (await import("./start-judge-session-C0eRJFlg.js")).startJudgeSession(transport.env);
3758
3812
  try {
3759
3813
  return await session.review(provider.model, request, signal);
3760
3814
  } finally {
@@ -4832,7 +4886,7 @@ function findReviewedRule(request, result, blockThreshold) {
4832
4886
  const blocking = collectBlockingRules(request, result, blockThreshold);
4833
4887
  const chosen = blocking.find((entry) => entry.rule.tier === "hard") ?? blocking[0];
4834
4888
  if (chosen === void 0) return null;
4835
- const misreadClears = chosen.rule.tier === "hard" && chosen.rule.source !== "configured" && chosen.answer.choice === "allow" && chosen.answer.probabilities.block <= .35 && blocking.length === 1;
4889
+ const misreadClears = chosen.rule.tier === "hard" && blocking.every((entry) => entry.rule.source !== "configured" && entry.answer.choice === "allow" && entry.answer.probabilities.block <= .35 && (entry === chosen || entry.rule.tier === "soft"));
4836
4890
  return {
4837
4891
  rule: chosen.rule,
4838
4892
  basis: chosen.answer.choice === "block" ? "matched" : "unresolved",
@@ -5521,11 +5575,19 @@ function isPlainOperand(word) {
5521
5575
  function mergeScopeFacts(all) {
5522
5576
  const pullRequests = /* @__PURE__ */ new Map();
5523
5577
  for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
5578
+ const replyChannels = /* @__PURE__ */ new Map();
5579
+ for (const channel of all.flatMap((facts) => facts.replyChannels)) replyChannels.set(JSON.stringify([
5580
+ channel.tool,
5581
+ channel.field,
5582
+ channel.recipient
5583
+ ]), channel);
5524
5584
  return {
5525
5585
  worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
5526
5586
  branches: [...new Set(all.flatMap((facts) => facts.branches))],
5527
5587
  pullRequests: [...pullRequests.values()],
5528
- pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
5588
+ pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))],
5589
+ paths: [...new Set(all.flatMap((facts) => facts.paths))],
5590
+ replyChannels: [...replyChannels.values()]
5529
5591
  };
5530
5592
  }
5531
5593
  //#endregion
@@ -5545,17 +5607,37 @@ function buildTaskScope(input) {
5545
5607
  currentBranch: input.currentBranch,
5546
5608
  remotes: input.remotes,
5547
5609
  pullRequests,
5548
- pathGlobs: merged.pathGlobs
5610
+ pathGlobs: merged.pathGlobs,
5611
+ paths: merged.paths,
5612
+ replyChannels: merged.replyChannels
5549
5613
  };
5550
5614
  }
5551
5615
  //#endregion
5552
- //#region src/scope/build-atc-scope-facts.ts
5553
- function buildAtcScopeFacts(record, isInRepository) {
5554
- const scope = record.scope;
5555
- const checkouts = [scope.workspace, ...scope.worktrees];
5616
+ //#region src/scope/types.ts
5617
+ const EMPTY_SCOPE_FACTS = {
5618
+ worktrees: [],
5619
+ branches: [],
5620
+ pullRequests: [],
5621
+ pathGlobs: [],
5622
+ paths: [],
5623
+ replyChannels: []
5624
+ };
5625
+ const EMPTY_SESSION_SCOPE = {
5626
+ worktrees: [],
5627
+ branches: [],
5628
+ pullRequests: []
5629
+ };
5630
+ //#endregion
5631
+ //#region src/scope/build-declared-scope-facts.ts
5632
+ function buildDeclaredScopeFacts(scope, isInRepository) {
5633
+ const checkouts = scope.workspace === void 0 ? scope.worktrees : [scope.workspace, ...scope.worktrees];
5556
5634
  return {
5635
+ ...EMPTY_SCOPE_FACTS,
5557
5636
  worktrees: checkouts.map((checkout) => checkout.path),
5558
- branches: [...checkouts.flatMap((checkout) => checkout.branch !== null && isInRepository(checkout.path) ? [checkout.branch] : []), ...scope.branches.flatMap((branch) => isInRepository(branch.repo ?? scope.workspace.path) ? [branch.name] : [])],
5637
+ branches: [...checkouts.flatMap((checkout) => checkout.branch !== null && isInRepository(checkout.path) ? [checkout.branch] : []), ...scope.branches.flatMap((branch) => {
5638
+ const repo = branch.repo ?? scope.workspace?.path;
5639
+ return repo !== void 0 && isInRepository(repo) ? [branch.name] : [];
5640
+ })],
5559
5641
  pullRequests: scope.pullRequests.flatMap((pull) => {
5560
5642
  const repository = toRepositorySlug(`https://${getHost(pull.url)}/${pull.repo}`);
5561
5643
  return pull.branch === null || repository === null ? [] : [{
@@ -5563,8 +5645,7 @@ function buildAtcScopeFacts(record, isInRepository) {
5563
5645
  head: pull.branch,
5564
5646
  repository
5565
5647
  }];
5566
- }),
5567
- pathGlobs: []
5648
+ })
5568
5649
  };
5569
5650
  }
5570
5651
  function getHost(url) {
@@ -5572,33 +5653,33 @@ function getHost(url) {
5572
5653
  }
5573
5654
  //#endregion
5574
5655
  //#region src/scope/load-atc-session-record.ts
5575
- const name$1 = string().min(1);
5576
- const path = name$1.refine(isAbsolute, "an absolute path");
5577
- const checkoutSchema = object({
5578
- path,
5579
- branch: name$1.nullable()
5656
+ const name$2 = string().min(1);
5657
+ const path$1 = name$2.refine(isAbsolute, "an absolute path");
5658
+ const checkoutSchema$1 = object({
5659
+ path: path$1,
5660
+ branch: name$2.nullable()
5580
5661
  }).readonly();
5581
- const branchSchema$1 = object({
5582
- name: name$1,
5583
- repo: path.optional()
5662
+ const branchSchema$2 = object({
5663
+ name: name$2,
5664
+ repo: path$1.optional()
5584
5665
  }).readonly();
5585
5666
  const pullNumber = number().int().positive();
5586
- const pullRequestSchema$1 = object({
5587
- repo: name$1,
5667
+ const pullRequestSchema$2 = object({
5668
+ repo: name$2,
5588
5669
  number: pullNumber,
5589
- url: name$1.optional(),
5590
- branch: name$1.nullable()
5670
+ url: name$2.optional(),
5671
+ branch: name$2.nullable()
5591
5672
  }).readonly();
5592
5673
  const scopeSchema = object({
5593
- workspace: checkoutSchema,
5594
- worktrees: array(checkoutSchema).readonly(),
5595
- branches: array(branchSchema$1).readonly(),
5596
- pullRequests: array(pullRequestSchema$1).readonly()
5674
+ workspace: checkoutSchema$1,
5675
+ worktrees: array(checkoutSchema$1).readonly(),
5676
+ branches: array(branchSchema$2).readonly(),
5677
+ pullRequests: array(pullRequestSchema$2).readonly()
5597
5678
  }).readonly();
5598
5679
  const recordSchema = object({
5599
5680
  format: literal("atc.session-record"),
5600
5681
  version: literal(1),
5601
- session: name$1,
5682
+ session: name$2,
5602
5683
  scope: scopeSchema
5603
5684
  }).readonly();
5604
5685
  async function loadAtcSessionRecord(location, sessionID) {
@@ -5611,7 +5692,7 @@ async function loadAtcSessionRecord(location, sessionID) {
5611
5692
  try {
5612
5693
  raw = await readFile(location, "utf8");
5613
5694
  } catch (error) {
5614
- return isMissing(error) ? { kind: "absent" } : {
5695
+ return isMissing$1(error) ? { kind: "absent" } : {
5615
5696
  kind: "malformed",
5616
5697
  diagnostic: `atc session record unreadable: ${location}`
5617
5698
  };
@@ -5646,22 +5727,94 @@ async function loadAtcSessionRecord(location, sessionID) {
5646
5727
  record: result.data
5647
5728
  };
5648
5729
  }
5649
- function isMissing(error) {
5730
+ function isMissing$1(error) {
5650
5731
  return error instanceof Error && "code" in error && error.code === "ENOENT";
5651
5732
  }
5652
5733
  //#endregion
5653
- //#region src/scope/types.ts
5654
- const EMPTY_SCOPE_FACTS = {
5655
- worktrees: [],
5656
- branches: [],
5657
- pullRequests: [],
5658
- pathGlobs: []
5659
- };
5660
- const EMPTY_SESSION_SCOPE = {
5661
- worktrees: [],
5662
- branches: [],
5663
- pullRequests: []
5664
- };
5734
+ //#region src/scope/load-scope-file.ts
5735
+ const name$1 = string().min(1);
5736
+ const path = name$1.refine(isAbsolute, "an absolute path").transform((value) => resolve(value));
5737
+ const checkoutSchema = strictObject({
5738
+ path,
5739
+ branch: name$1.nullable()
5740
+ }).readonly();
5741
+ const branchSchema$1 = strictObject({
5742
+ name: name$1,
5743
+ repo: path.optional()
5744
+ }).readonly();
5745
+ const pullRequestSchema$1 = strictObject({
5746
+ repo: name$1,
5747
+ number: number().int().positive(),
5748
+ url: name$1.optional(),
5749
+ branch: name$1
5750
+ }).readonly();
5751
+ const replyChannelSchema = strictObject({
5752
+ tool: name$1,
5753
+ field: name$1,
5754
+ recipient: name$1
5755
+ }).readonly();
5756
+ const scopeFileSchema = strictObject({
5757
+ format: literal("auto-mode.scope"),
5758
+ version: literal(1),
5759
+ paths: array(path).readonly().default([]),
5760
+ workspace: checkoutSchema.optional(),
5761
+ worktrees: array(checkoutSchema).readonly().default([]),
5762
+ branches: array(branchSchema$1).readonly().default([]),
5763
+ pullRequests: array(pullRequestSchema$1).readonly().default([]),
5764
+ replyChannels: array(replyChannelSchema).readonly().default([])
5765
+ }).superRefine((file, context) => {
5766
+ if (file.workspace !== void 0) return;
5767
+ for (const [index, branch] of file.branches.entries()) if (branch.repo === void 0) context.addIssue({
5768
+ code: "custom",
5769
+ path: [
5770
+ "branches",
5771
+ index,
5772
+ "repo"
5773
+ ],
5774
+ message: "a branch needs a repo when the file declares no workspace"
5775
+ });
5776
+ }).readonly();
5777
+ async function loadScopeFile(location, readTextFile) {
5778
+ if (location === void 0 || location === "") return { kind: "absent" };
5779
+ if (!isAbsolute(location)) return {
5780
+ kind: "malformed",
5781
+ diagnostic: `scope file path is not absolute: ${location}`
5782
+ };
5783
+ let raw;
5784
+ try {
5785
+ raw = await readTextFile(location);
5786
+ } catch (error) {
5787
+ return isMissing(error) ? { kind: "absent" } : {
5788
+ kind: "malformed",
5789
+ diagnostic: `scope file unreadable: ${location}`
5790
+ };
5791
+ }
5792
+ let parsed;
5793
+ try {
5794
+ parsed = JSON.parse(raw);
5795
+ } catch {
5796
+ return {
5797
+ kind: "malformed",
5798
+ diagnostic: `scope file is not JSON: ${location}`
5799
+ };
5800
+ }
5801
+ const result = scopeFileSchema.safeParse(parsed);
5802
+ if (!result.success) {
5803
+ const [issue] = result.error.issues;
5804
+ const refused = issue?.code === "unrecognized_keys" ? [...issue.path, ...issue.keys.slice(0, 1)] : issue?.path;
5805
+ return {
5806
+ kind: "malformed",
5807
+ diagnostic: `scope file ignored: ${refused === void 0 || refused.length === 0 ? "file" : refused.join(".")} does not match version 1: ${location}`
5808
+ };
5809
+ }
5810
+ return {
5811
+ kind: "scope",
5812
+ scope: result.data
5813
+ };
5814
+ }
5815
+ function isMissing(error) {
5816
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
5817
+ }
5665
5818
  //#endregion
5666
5819
  //#region src/scope/load-session-scope.ts
5667
5820
  const name = string().min(1);
@@ -5701,7 +5854,7 @@ function makeScopeSourceReader(source, readSourceCheckout = readCheckout) {
5701
5854
  branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
5702
5855
  pullRequests: scope.pullRequests
5703
5856
  });
5704
- }).with("globs", () => () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }))).with("atc", () => (context) => readAtcScopeFacts(context, readSourceCheckout)).exhaustive();
5857
+ }).with("globs", () => () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }))).with("launcher", () => (context) => readLauncherScopeFacts(context, readSourceCheckout)).with("atc", () => (context) => readAtcScopeFacts(context, readSourceCheckout)).exhaustive();
5705
5858
  }
5706
5859
  function buildFacts(facts) {
5707
5860
  return {
@@ -5709,23 +5862,39 @@ function buildFacts(facts) {
5709
5862
  ...facts
5710
5863
  };
5711
5864
  }
5712
- async function readAtcScopeFacts(context, readSourceCheckout) {
5713
- const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
5865
+ async function readLauncherScopeFacts(context, readSourceCheckout) {
5866
+ const loaded = await loadScopeFile(context.scopeFilePath, context.readTextFile);
5714
5867
  if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
5715
- if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
5716
- const scope = loaded.record.scope;
5868
+ if (loaded.kind !== "scope") return EMPTY_SCOPE_FACTS;
5869
+ return {
5870
+ ...await readDeclaredScopeFacts(loaded.scope, context, readSourceCheckout),
5871
+ paths: loaded.scope.paths,
5872
+ replyChannels: loaded.scope.replyChannels
5873
+ };
5874
+ }
5875
+ async function readDeclaredScopeFacts(scope, context, readSourceCheckout) {
5876
+ const workspace = scope.workspace?.path;
5717
5877
  const paths = /* @__PURE__ */ new Set([
5718
- scope.workspace.path,
5878
+ ...workspace === void 0 ? [] : [workspace],
5719
5879
  ...scope.worktrees.map((worktree) => worktree.path),
5720
- ...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
5880
+ ...scope.branches.flatMap((branch) => {
5881
+ const repo = branch.repo ?? workspace;
5882
+ return repo === void 0 ? [] : [repo];
5883
+ })
5721
5884
  ]);
5722
5885
  const entries = await Promise.all([...paths].map((path) => readCommonDirEntry(path, context.env, readSourceCheckout)));
5723
5886
  const commonDirs = new Map(entries);
5724
- return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
5887
+ return buildDeclaredScopeFacts(scope, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
5725
5888
  }
5726
5889
  async function readCommonDirEntry(path, env, readSourceCheckout) {
5727
5890
  return [path, (await readSourceCheckout(path, env))?.commonDir ?? null];
5728
5891
  }
5892
+ async function readAtcScopeFacts(context, readSourceCheckout) {
5893
+ const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
5894
+ if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
5895
+ if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
5896
+ return readDeclaredScopeFacts(loaded.record.scope, context, readSourceCheckout);
5897
+ }
5729
5898
  //#endregion
5730
5899
  //#region src/scope/load-task-scope.ts
5731
5900
  async function loadTaskScope(request, sources, host, stderr) {
@@ -5743,6 +5912,8 @@ async function loadTaskScope(request, sources, host, stderr) {
5743
5912
  stateDir: request.stateDir,
5744
5913
  atcRecordPath: host.env["ATC_SESSION_RECORD"],
5745
5914
  atcSessionID: host.env["ATC_SESSION_ID"],
5915
+ scopeFilePath: host.env["AUTO_MODE_SCOPE_FILE"],
5916
+ readTextFile: request.readTextFile,
5746
5917
  stderr
5747
5918
  };
5748
5919
  const sourceFacts = await Promise.all(Object.values(sources).map((source) => makeScopeSourceReader(source, request.readCheckout)(sourceContext)));
@@ -5829,6 +6000,7 @@ const STAGES = [
5829
6000
  { decide: classifyContainment },
5830
6001
  { decide: classifyRegenerable },
5831
6002
  { decide: classifyEditBypass },
6003
+ { decide: classifyReplyChannel },
5832
6004
  { decide: classifyLocalOnly }
5833
6005
  ];
5834
6006
  const FINAL_STAGE = { decide: classifyJev };
@@ -5892,7 +6064,8 @@ async function classifyContainment(ctx) {
5892
6064
  sessionID: request.sessionID,
5893
6065
  cwd: request.cwd,
5894
6066
  checkout,
5895
- readCheckout: ctx.readCheckout
6067
+ readCheckout: ctx.readCheckout,
6068
+ readTextFile: ctx.options.runtime.readTextFile
5896
6069
  }, ctx.config, host, ctx.options.stderr);
5897
6070
  const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
5898
6071
  if (scope === null || containment === null) return {
@@ -5994,6 +6167,20 @@ async function classifyEditBypass(ctx) {
5994
6167
  }
5995
6168
  };
5996
6169
  }
6170
+ async function classifyReplyChannel(ctx) {
6171
+ const channel = ctx.hasConfiguredDenies || ctx.scope === null ? null : findReplyChannel(ctx.request.toolName, ctx.request.toolInput, ctx.scope.replyChannels);
6172
+ if (channel === null) return { kind: "pass" };
6173
+ if ((await import("./classify-reply-message-CrZ6S-AR.js")).classifyReplyMessage(ctx.request.toolInput).kind !== "bypass") return { kind: "pass" };
6174
+ return {
6175
+ kind: "decided",
6176
+ outcome: {
6177
+ verdict: { kind: "allow" },
6178
+ decidingStage: "bypass",
6179
+ note: `allowed by the reply channel: ${channel.tool} to ${channel.recipient}`,
6180
+ status: "allow"
6181
+ }
6182
+ };
6183
+ }
5997
6184
  function classifyLocalOnly(ctx) {
5998
6185
  if (ctx.options.localOnly !== true) return { kind: "pass" };
5999
6186
  return {
@@ -6076,6 +6263,11 @@ function readHostEnvironment() {
6076
6263
  };
6077
6264
  }
6078
6265
  //#endregion
6266
+ //#region src/config/read-text-file.ts
6267
+ function readTextFile(path) {
6268
+ return readFile(path, "utf8");
6269
+ }
6270
+ //#endregion
6079
6271
  //#region src/config/read-runtime.ts
6080
6272
  function readRuntime() {
6081
6273
  return {
@@ -6083,7 +6275,8 @@ function readRuntime() {
6083
6275
  now: Date.now,
6084
6276
  timeout: (timeoutMs) => AbortSignal.timeout(timeoutMs),
6085
6277
  fetch: sendThroughGlobalFetch,
6086
- readCheckout: (start, env) => readCheckout(start, env)
6278
+ readCheckout: (start, env) => readCheckout(start, env),
6279
+ readTextFile
6087
6280
  };
6088
6281
  }
6089
6282
  function sendThroughGlobalFetch(url, init) {