auto-mode 0.16.0 → 0.18.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/README.md CHANGED
@@ -96,7 +96,8 @@ if (request !== null) {
96
96
 
97
97
  `classifyAction` runs both tiers and returns the verdict, a note, and diagnostics. A request carries
98
98
  the session identity, the action, and the task context; `parseActionRequest` builds one from the
99
- mod's JSON.
99
+ mod's JSON. Build every request with `parseActionRequest`: it drops a child agent's direct user
100
+ message, and the classifiers trust the request they receive.
100
101
 
101
102
  ## Documentation
102
103
 
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { C as resolveStateDir, D as DEFAULT_SCOPE_SOURCES, E as DEFAULT_DENIAL_BUDGET, F as string, N as number, O as loadConfig, P as strictObject, a as loadSessionScope, b as splitCommandWords, f as loadRepositoryContext, g as loadPolicy, i as resolveSessionScopePath, j as readHostEnvironment, n as parseActionRequest, o as EMPTY_SESSION_SCOPE, p as loadCheckoutRemotes, r as classifyAction, t as renderVerdict, v as toRepositorySlug, w as findCheckout, y as splitShellCommand } from "./render-verdict-u3_ftSEp.js";
3
- import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, normalize } from "node:path";
2
+ import { C as resolveStateDir, D as DEFAULT_SCOPE_SOURCES, E as DEFAULT_DENIAL_BUDGET, F as string, N as number, O as loadConfig, P as strictObject, a as loadSessionScope, b as splitCommandWords, f as loadRepositoryContext, g as loadPolicy, i as resolveSessionScopePath, j as readHostEnvironment, n as parseActionRequest, o as EMPTY_SESSION_SCOPE, p as loadCheckoutRemotes, r as classifyAction, t as renderVerdict, v as toRepositorySlug, w as findCheckout, y as splitShellCommand } from "./render-verdict-lM31QV2u.js";
3
+ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
+ import { constants } from "node:fs";
6
7
  import { createHash, randomUUID } from "node:crypto";
7
8
  import { text } from "node:stream/consumers";
8
9
  import { parseArgs } from "node:util";
@@ -16,11 +17,13 @@ const DESCRIPTIVE_FIELDS = { Bash: [
16
17
  function buildRetryKey(request) {
17
18
  const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
18
19
  const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
20
+ const message = request.decisionContext?.lastDirectUserMessage ?? null;
19
21
  const canonical = JSON.stringify(sortKeys({
20
22
  cwd: request.cwd,
21
23
  toolName: request.toolName,
22
24
  toolInput: input,
23
- lastDirectUserMessage: request.decisionContext?.lastDirectUserMessage?.text ?? null
25
+ lastDirectUserMessage: message?.text ?? null,
26
+ ...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
24
27
  }));
25
28
  return createHash("sha256").update(canonical).digest("hex");
26
29
  }
@@ -136,6 +139,82 @@ async function writeDenialState(path, state) {
136
139
  await rename(staged, path);
137
140
  }
138
141
  //#endregion
142
+ //#region src/capture/resolve-capture-dir.ts
143
+ function resolveCaptureDir(capture, host) {
144
+ return capture.dir ?? join(resolveStateDir(host), "captures");
145
+ }
146
+ //#endregion
147
+ //#region src/capture/find-enclosing-work-tree.ts
148
+ async function findEnclosingWorkTree(path) {
149
+ const real = await resolveExistingPrefix(resolve(path));
150
+ for (let directory = real;; directory = dirname(directory)) {
151
+ if (await hasGitEntry(directory)) return directory;
152
+ if (dirname(directory) === directory) return null;
153
+ }
154
+ }
155
+ async function resolveExistingPrefix(path) {
156
+ const missing = [];
157
+ for (let current = path;; current = dirname(current)) {
158
+ const real = await realpath(current).catch(() => null);
159
+ if (real !== null) return join(real, ...missing.toReversed());
160
+ if (dirname(current) === current) return path;
161
+ missing.push(basename(current));
162
+ }
163
+ }
164
+ const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
165
+ async function hasGitEntry(directory) {
166
+ try {
167
+ await lstat(join(directory, ".git"));
168
+ return true;
169
+ } catch (error) {
170
+ return !(error instanceof Error && "code" in error && ABSENT_CODES.has(String(error.code)));
171
+ }
172
+ }
173
+ //#endregion
174
+ //#region src/capture/try-write-request-capture.ts
175
+ async function tryWriteRequestCapture(dir, record) {
176
+ if (!isAbsolute(dir)) return {
177
+ kind: "refused",
178
+ reason: `the capture dir ${dir} is not an absolute path`
179
+ };
180
+ try {
181
+ const before = await findEnclosingWorkTree(dir);
182
+ if (before !== null) return {
183
+ kind: "refused",
184
+ reason: `the capture dir is inside the git work tree ${before}`
185
+ };
186
+ await mkdir(dir, {
187
+ recursive: true,
188
+ mode: 448
189
+ });
190
+ const real = await realpath(dir);
191
+ const after = await findEnclosingWorkTree(real);
192
+ if (after !== null) return {
193
+ kind: "refused",
194
+ reason: `the capture dir is inside the git work tree ${after}`
195
+ };
196
+ if (((await stat(real)).mode & 63) !== 0) await chmod(real, 448);
197
+ await writeOwnerFile(join(real, ".gitignore"), "*\n", constants.O_TRUNC);
198
+ const path = join(real, `requests-${record.time.slice(0, 10)}.jsonl`);
199
+ await writeOwnerFile(path, `${JSON.stringify(record)}\n`, constants.O_APPEND);
200
+ return {
201
+ kind: "written",
202
+ path
203
+ };
204
+ } catch {
205
+ return { kind: "failed" };
206
+ }
207
+ }
208
+ async function writeOwnerFile(path, text, flag) {
209
+ const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW | flag, 384);
210
+ try {
211
+ if (((await handle.stat()).mode & 63) !== 0) await handle.chmod(384);
212
+ await handle.writeFile(text);
213
+ } finally {
214
+ await handle.close();
215
+ }
216
+ }
217
+ //#endregion
139
218
  //#region src/config/build-jev-only-config.ts
140
219
  function buildJevOnlyConfig(config) {
141
220
  if (config.provider.protocol !== "system-one") return null;
@@ -153,7 +232,7 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
153
232
  const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(host), "actions.jsonl");
154
233
  if (path === "") return;
155
234
  const record = {
156
- schemaVersion: 3,
235
+ schemaVersion: 4,
157
236
  time: (/* @__PURE__ */ new Date()).toISOString(),
158
237
  invocationID: entry.invocationID,
159
238
  sessionHash: toHash(payload.sessionID),
@@ -163,7 +242,8 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
163
242
  decidingStage: entry.decidingStage ?? null,
164
243
  denials: entry.denials ?? null,
165
244
  escalation: entry.escalation ?? false,
166
- diagnostics: entry.diagnostics ?? null
245
+ diagnostics: entry.diagnostics ?? null,
246
+ judge: entry.judge ?? null
167
247
  };
168
248
  try {
169
249
  await mkdir(dirname(path), {
@@ -194,6 +274,22 @@ function parseScopeRecordRequest(body) {
194
274
  return parsed.success ? parsed.data : null;
195
275
  }
196
276
  //#endregion
277
+ //#region src/request/to-mod-request.ts
278
+ function toModRequest(request) {
279
+ const context = request.decisionContext;
280
+ return {
281
+ sessionID: request.sessionID,
282
+ ...request.toolUseID === void 0 ? {} : { toolUseID: request.toolUseID },
283
+ cwd: request.cwd,
284
+ toolName: request.toolName,
285
+ toolInput: request.toolInput,
286
+ context: {
287
+ ...context,
288
+ omittedTaskContext: [...context.omittedTaskContext]
289
+ }
290
+ };
291
+ }
292
+ //#endregion
197
293
  //#region src/scope/read-pull-request.ts
198
294
  const LOOKUP_TIMEOUT_MS = 5e3;
199
295
  function readPullRequest(repository, number, options = {}) {
@@ -255,6 +351,17 @@ function collectPullRequestAddresses(text) {
255
351
  }
256
352
  const PULL_REQUEST_URL = /https:\/\/(?<repository>[^/\s]+\/[^/\s]+\/[^/\s]+)\/pull\/(?<number>[1-9]\d*)\b/gu;
257
353
  //#endregion
354
+ //#region mods/auto-mode/contract/scope-commands.ts
355
+ const SCOPE_COMMANDS = {
356
+ git: [
357
+ "worktree add",
358
+ "checkout",
359
+ "switch",
360
+ "branch"
361
+ ],
362
+ gh: ["pr create"]
363
+ };
364
+ //#endregion
258
365
  //#region src/scope/collect-scope-events.ts
259
366
  function collectScopeEvents(command, cwd, home) {
260
367
  const events = [];
@@ -264,7 +371,7 @@ function collectScopeEvents(command, cwd, home) {
264
371
  const [name, ...args] = splitCommandWords(segment.text);
265
372
  if (name === "cd") directory = hasPipe ? null : resolvePath(directory, args[0] ?? home, home);
266
373
  else if (name === "git" && directory !== null) events.push(...collectGitEvents(args, directory, home));
267
- else if (name === "gh" && directory !== null && args[0] === "pr" && args[1] === "create") events.push({
374
+ else if (name === "gh" && directory !== null && findScopeCommand(SCOPE_COMMANDS.gh, args) !== null) events.push({
268
375
  kind: "pull-request",
269
376
  directory,
270
377
  head: findOptionValue(args, ["--head", "-H"])
@@ -272,15 +379,6 @@ function collectScopeEvents(command, cwd, home) {
272
379
  }
273
380
  return events;
274
381
  }
275
- const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
276
- "-b",
277
- "-B",
278
- "-c",
279
- "-C",
280
- "--create",
281
- "--force-create"
282
- ]);
283
- const BRANCH_OTHER_OPTIONS = /^-(?:d|D|m|M|c|C|l|a|r|v|vv|u)$|^--(?:delete|move|copy|list|all|remotes|verbose|show-current|contains|no-contains|merged|no-merged|points-at|format|sort|set-upstream-to|unset-upstream|edit-description)/u;
284
382
  function collectGitEvents(args, cwd, home) {
285
383
  let directory = cwd;
286
384
  let index = 0;
@@ -288,34 +386,33 @@ function collectGitEvents(args, cwd, home) {
288
386
  if (args[index] === "-C") directory = resolvePath(directory, args[index + 1] ?? "", home);
289
387
  index += args[index] === "-C" || args[index] === "-c" ? 2 : 1;
290
388
  }
291
- const subcommand = args[index];
292
- const rest = args.slice(index + 1);
293
- if (directory === null) return [];
294
- if (subcommand === "worktree" && rest[0] === "add") {
295
- const path = findWorktreePath(rest.slice(1));
296
- const resolved = path === null ? null : resolvePath(directory, path, home);
297
- return resolved === null ? [] : [{
298
- kind: "worktree",
299
- path: resolved
300
- }];
301
- }
302
- if (subcommand === "checkout" || subcommand === "switch") {
303
- const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
304
- return name === null ? [] : [{
305
- kind: "branch",
306
- name,
307
- directory
308
- }];
309
- }
310
- if (subcommand === "branch" && !rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) {
311
- const name = rest.find((word) => !word.startsWith("-"));
312
- return name === void 0 || isUnresolved(name) ? [] : [{
313
- kind: "branch",
314
- name,
315
- directory
316
- }];
389
+ const found = findScopeCommand(SCOPE_COMMANDS.git, args.slice(index));
390
+ if (directory === null || found === null) return [];
391
+ return GIT_EVENT_COLLECTORS[found.command](found.rest, directory, home);
392
+ }
393
+ function findScopeCommand(commands, args) {
394
+ for (const command of commands) {
395
+ const words = command.split(" ");
396
+ if (words.every((word, index) => args[index] === word)) return {
397
+ command,
398
+ rest: args.slice(words.length)
399
+ };
317
400
  }
318
- return [];
401
+ return null;
402
+ }
403
+ const GIT_EVENT_COLLECTORS = {
404
+ "worktree add": collectWorktreeEvents,
405
+ checkout: collectSwitchEvents,
406
+ switch: collectSwitchEvents,
407
+ branch: collectBranchEvents
408
+ };
409
+ function collectWorktreeEvents(rest, directory, home) {
410
+ const path = findWorktreePath(rest);
411
+ const resolved = path === null ? null : resolvePath(directory, path, home);
412
+ return resolved === null ? [] : [{
413
+ kind: "worktree",
414
+ path: resolved
415
+ }];
319
416
  }
320
417
  const WORKTREE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
321
418
  "-b",
@@ -330,6 +427,32 @@ function findWorktreePath(args) {
330
427
  }
331
428
  return null;
332
429
  }
430
+ const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
431
+ "-b",
432
+ "-B",
433
+ "-c",
434
+ "-C",
435
+ "--create",
436
+ "--force-create"
437
+ ]);
438
+ const BRANCH_OTHER_OPTIONS = /^-(?:d|D|m|M|c|C|l|a|r|v|vv|u)$|^--(?:delete|move|copy|list|all|remotes|verbose|show-current|contains|no-contains|merged|no-merged|points-at|format|sort|set-upstream-to|unset-upstream|edit-description)/u;
439
+ function collectSwitchEvents(rest, directory) {
440
+ const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
441
+ return name === null ? [] : [{
442
+ kind: "branch",
443
+ name,
444
+ directory
445
+ }];
446
+ }
447
+ function collectBranchEvents(rest, directory) {
448
+ if (rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) return [];
449
+ const name = rest.find((word) => !word.startsWith("-"));
450
+ return name === void 0 || isUnresolved(name) ? [] : [{
451
+ kind: "branch",
452
+ name,
453
+ directory
454
+ }];
455
+ }
333
456
  function findOptionValue(args, options) {
334
457
  const index = args.findIndex((word) => options.includes(word));
335
458
  const value = index === -1 ? void 0 : args[index + 1];
@@ -617,8 +740,20 @@ async function run(io, explain, localOnly, jevOnly, deadline) {
617
740
  session: plan.state.session
618
741
  },
619
742
  escalation: plan.escalation,
620
- ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
743
+ ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
744
+ ...outcome.judge === void 0 ? {} : { judge: outcome.judge }
621
745
  }, host, io.stderr);
746
+ if (config.capture?.enabled === true) {
747
+ const captured = await tryWriteRequestCapture(resolveCaptureDir(config.capture, host), {
748
+ schemaVersion: 1,
749
+ time: (/* @__PURE__ */ new Date()).toISOString(),
750
+ request: toModRequest(request),
751
+ verdict: plan.verdict,
752
+ decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
753
+ escalation: plan.escalation
754
+ });
755
+ printCaptureNote(io.stderr, captured);
756
+ }
622
757
  const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
623
758
  return printNote(io.stderr, explain || outcome.unavailable === true, note);
624
759
  } finally {
@@ -642,6 +777,10 @@ async function tryWriteDenialState(stderr, path, state) {
642
777
  function printVerdict(stdout, verdict) {
643
778
  stdout.write(renderVerdict(verdict));
644
779
  }
780
+ function printCaptureNote(stderr, captured) {
781
+ if (captured.kind === "refused") stderr.write(`auto-mode: capture skipped: ${captured.reason}\n`);
782
+ if (captured.kind === "failed") stderr.write("auto-mode: capture unavailable\n");
783
+ }
645
784
  async function runRecord(io) {
646
785
  const raw = await io.stdin();
647
786
  let body;
package/dist/index.d.ts CHANGED
@@ -1,29 +1,48 @@
1
- //#region src/request/types.d.ts
1
+ //#region mods/auto-mode/contract/message-origins.d.ts
2
+ declare const MESSAGE_ORIGINS: readonly ['composer', 'bridge', 'sdk'];
3
+ //#endregion
4
+ //#region mods/auto-mode/contract/types.d.ts
5
+ type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
2
6
  interface UserTask {
3
7
  readonly text: string;
4
- readonly origin: 'composer' | 'bridge' | 'sdk';
8
+ readonly origin: MessageOrigin;
5
9
  }
6
- interface DecisionContext {
10
+ interface DirectUserMessage extends UserTask {
11
+ readonly freshness?: 'stale' | undefined;
12
+ }
13
+ interface DelegatedTask {
14
+ readonly text: string;
15
+ readonly origin: 'agent.spawn';
16
+ }
17
+ interface TaskOmission {
18
+ readonly field: 'originalUserTask' | 'delegatedTask';
19
+ readonly reason: 'unavailable' | 'budget';
20
+ }
21
+ interface ModContext {
7
22
  readonly agentID: string | null;
8
23
  readonly originalUserTask: UserTask | null;
9
- readonly delegatedTask: {
10
- readonly text: string;
11
- readonly origin: 'agent.spawn';
12
- } | null;
13
- readonly lastDirectUserMessage: UserTask | null;
14
- readonly omittedTaskContext: readonly {
15
- readonly field: 'originalUserTask' | 'delegatedTask';
16
- readonly reason: 'unavailable' | 'budget';
17
- }[];
24
+ readonly delegatedTask: DelegatedTask | null;
25
+ readonly lastDirectUserMessage: DirectUserMessage | null;
26
+ readonly omittedTaskContext: TaskOmission[];
18
27
  }
19
- interface ActionRequest {
28
+ interface ModRequest {
20
29
  readonly sessionID: string;
21
30
  readonly toolUseID?: string | undefined;
22
31
  readonly cwd: string;
23
32
  readonly toolName: string;
24
33
  readonly toolInput: Readonly<Record<string, unknown>>;
25
- readonly decisionContext?: DecisionContext | undefined;
34
+ readonly context: ModContext;
26
35
  }
36
+ //#endregion
37
+ //#region src/request/types.d.ts
38
+ type DeepReadonly<T> = T extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : T extends object ? { readonly [Key in keyof T]: DeepReadonly<T[Key]>; } : T;
39
+ type DecisionContext = DeepReadonly<ModContext>;
40
+ type ActionRequest = DeepReadonly<Omit<ModRequest, 'context'>> & {
41
+ readonly decisionContext?: DecisionContext | undefined;
42
+ };
43
+ type ParsedActionRequest = ActionRequest & {
44
+ readonly decisionContext: DecisionContext;
45
+ };
27
46
  type Verdict = {
28
47
  readonly kind: 'allow';
29
48
  } | {
@@ -205,12 +224,27 @@ interface DenialBudget {
205
224
  readonly perSession: number;
206
225
  }
207
226
  //#endregion
227
+ //#region src/judge/types.d.ts
228
+ type OverturnBasis = 'consent' | 'misread';
229
+ type JudgeOverturns = 'consent' | 'any';
230
+ type JudgeStatus = 'confirmed' | 'overturned' | 'held' | 'failed';
231
+ type JudgeFailure = 'credential' | 'policy' | 'request' | 'timeout' | 'unreadable';
232
+ interface JudgeDiagnostics {
233
+ readonly status: JudgeStatus;
234
+ readonly failureReason: JudgeFailure | null;
235
+ readonly overturnBasis: OverturnBasis | null;
236
+ readonly model: string;
237
+ readonly rule: string;
238
+ readonly tier: 'hard' | 'soft';
239
+ readonly elapsedMs: number;
240
+ }
241
+ //#endregion
208
242
  //#region src/config/presets.d.ts
209
243
  export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
210
244
  //#endregion
211
245
  //#region src/config/config.d.ts
212
246
  interface ProviderConfig {
213
- readonly protocol?: 'messages' | 'system-one';
247
+ readonly protocol?: 'messages' | 'system-one' | 'claude-code';
214
248
  readonly baseURL: string;
215
249
  readonly model: string;
216
250
  readonly apiKeyEnv?: string | undefined;
@@ -225,9 +259,14 @@ interface ScopeSource {
225
259
  }
226
260
  type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
227
261
  declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
262
+ interface CaptureConfig {
263
+ readonly enabled: boolean;
264
+ readonly dir?: string | undefined;
265
+ }
228
266
  interface Config {
229
267
  readonly provider: ProviderConfig;
230
268
  readonly judge?: ProviderConfig | null;
269
+ readonly judgeOverturns?: JudgeOverturns;
231
270
  readonly scopeSources?: Readonly<Record<string, ScopeSource>>;
232
271
  readonly classifierPath?: string | undefined;
233
272
  readonly rulesPath?: string | undefined;
@@ -235,6 +274,7 @@ interface Config {
235
274
  readonly claudeSettingsPath?: string | null | undefined;
236
275
  readonly blockThreshold?: number | undefined;
237
276
  readonly denialBudget?: DenialBudget;
277
+ readonly capture?: CaptureConfig;
238
278
  readonly warnings?: readonly string[];
239
279
  }
240
280
  export declare const DEFAULT_CONFIG: Config;
@@ -243,7 +283,7 @@ export declare function loadConfig(configPath?: string, host?: Readonly<HostEnvi
243
283
  export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
244
284
  //#endregion
245
285
  //#region src/classify-action.d.ts
246
- type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'messages' | 'retry';
286
+ type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'judge' | 'messages' | 'retry';
247
287
  interface ActionOutcome {
248
288
  readonly verdict: Verdict | null;
249
289
  readonly decidingStage: DecidingStage;
@@ -251,6 +291,7 @@ interface ActionOutcome {
251
291
  readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
252
292
  readonly unavailable?: boolean;
253
293
  readonly diagnostics?: DecisionDiagnostics;
294
+ readonly judge?: JudgeDiagnostics;
254
295
  }
255
296
  interface ClassifyOptions extends EvaluationOptions {
256
297
  readonly localOnly?: boolean;
@@ -272,12 +313,27 @@ export declare function checkContainment(request: Readonly<ActionRequest>, scope
272
313
  //#region src/model/build-decision-request.d.ts
273
314
  export declare function buildDecisionRequest(payload: ActionRequest, policy: string, configuredRules: ClaudeRules, lastUserMessage: string | null, rulesSource?: 'shipped' | 'replacement', repositoryContext?: RepositoryContext | null, mcpServers?: readonly MCPServerFact[]): DecisionRequest;
274
315
  //#endregion
316
+ //#region src/policy/build-deny-reason.d.ts
317
+ type DenyBasis = 'matched' | 'unresolved';
318
+ //#endregion
319
+ //#region src/model/find-denied-rule.d.ts
320
+ interface DeniedRule {
321
+ readonly rule: DecisionRule;
322
+ readonly basis: DenyBasis;
323
+ }
324
+ //#endregion
325
+ //#region src/judge/classify-with-judge.d.ts
326
+ interface JudgeEvidence extends DeniedRule {
327
+ readonly repositoryContext: RepositoryContext | null;
328
+ }
329
+ //#endregion
275
330
  //#region src/model/classify-with-model.d.ts
276
331
  interface ModelOutcome {
277
332
  readonly verdict: Verdict | null;
278
333
  readonly note: string;
279
334
  readonly unavailable?: boolean;
280
335
  readonly diagnostics?: DecisionDiagnostics;
336
+ readonly review?: JudgeEvidence;
281
337
  }
282
338
  export declare function classifyWithModel(payload: ActionRequest, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
283
339
  //#endregion
@@ -306,10 +362,10 @@ interface PolicyPaths {
306
362
  readonly classifierPath?: string | undefined;
307
363
  readonly rulesPath?: string | undefined;
308
364
  }
309
- export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
365
+ export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md' | 'judge.md'): Promise<string>;
310
366
  //#endregion
311
367
  //#region src/request/parse-action-request.d.ts
312
- export declare function parseActionRequest(body: unknown): ActionRequest | null;
368
+ export declare function parseActionRequest(body: unknown): ParsedActionRequest | null;
313
369
  //#endregion
314
370
  //#region src/request/render-verdict.d.ts
315
371
  export declare function renderVerdict(verdict: Verdict): string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as resolveConfigPath, M as PRESETS, O as loadConfig, S as tryClassifyEdit, T as DEFAULT_CONFIG, _ as checkContainment, c as classifyLocally, d as sendDecision, g as loadPolicy, h as buildDecisionRequest, k as resolveApiKey, l as classifyWithModel, m as DecisionRequestError, n as parseActionRequest, r as classifyAction, s as buildTaskScope, t as renderVerdict, u as parseModelVerdict, x as loadClaudeSettings } from "./render-verdict-u3_ftSEp.js";
1
+ import { A as resolveConfigPath, M as PRESETS, O as loadConfig, S as tryClassifyEdit, T as DEFAULT_CONFIG, _ as checkContainment, c as classifyLocally, d as sendDecision, g as loadPolicy, h as buildDecisionRequest, k as resolveApiKey, l as classifyWithModel, m as DecisionRequestError, n as parseActionRequest, r as classifyAction, s as buildTaskScope, t as renderVerdict, u as parseModelVerdict, x as loadClaudeSettings } from "./render-verdict-lM31QV2u.js";
2
2
  //#region src/config/load-claude-rules.ts
3
3
  async function loadClaudeRules(path, host) {
4
4
  return (await loadClaudeSettings(path, host)).rules;