auto-mode 0.16.0 → 0.17.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/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-Bx9Lw7gO.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), {
@@ -617,8 +697,20 @@ async function run(io, explain, localOnly, jevOnly, deadline) {
617
697
  session: plan.state.session
618
698
  },
619
699
  escalation: plan.escalation,
620
- ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
700
+ ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
701
+ ...outcome.judge === void 0 ? {} : { judge: outcome.judge }
621
702
  }, host, io.stderr);
703
+ if (config.capture?.enabled === true) {
704
+ const captured = await tryWriteRequestCapture(resolveCaptureDir(config.capture, host), {
705
+ schemaVersion: 1,
706
+ time: (/* @__PURE__ */ new Date()).toISOString(),
707
+ request: body,
708
+ verdict: plan.verdict,
709
+ decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
710
+ escalation: plan.escalation
711
+ });
712
+ printCaptureNote(io.stderr, captured);
713
+ }
622
714
  const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
623
715
  return printNote(io.stderr, explain || outcome.unavailable === true, note);
624
716
  } finally {
@@ -642,6 +734,10 @@ async function tryWriteDenialState(stderr, path, state) {
642
734
  function printVerdict(stdout, verdict) {
643
735
  stdout.write(renderVerdict(verdict));
644
736
  }
737
+ function printCaptureNote(stderr, captured) {
738
+ if (captured.kind === "refused") stderr.write(`auto-mode: capture skipped: ${captured.reason}\n`);
739
+ if (captured.kind === "failed") stderr.write("auto-mode: capture unavailable\n");
740
+ }
645
741
  async function runRecord(io) {
646
742
  const raw = await io.stdin();
647
743
  let body;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ interface UserTask {
3
3
  readonly text: string;
4
4
  readonly origin: 'composer' | 'bridge' | 'sdk';
5
5
  }
6
+ interface DirectUserMessage extends UserTask {
7
+ readonly freshness?: 'stale' | undefined;
8
+ }
6
9
  interface DecisionContext {
7
10
  readonly agentID: string | null;
8
11
  readonly originalUserTask: UserTask | null;
@@ -10,7 +13,7 @@ interface DecisionContext {
10
13
  readonly text: string;
11
14
  readonly origin: 'agent.spawn';
12
15
  } | null;
13
- readonly lastDirectUserMessage: UserTask | null;
16
+ readonly lastDirectUserMessage: DirectUserMessage | null;
14
17
  readonly omittedTaskContext: readonly {
15
18
  readonly field: 'originalUserTask' | 'delegatedTask';
16
19
  readonly reason: 'unavailable' | 'budget';
@@ -205,12 +208,27 @@ interface DenialBudget {
205
208
  readonly perSession: number;
206
209
  }
207
210
  //#endregion
211
+ //#region src/judge/types.d.ts
212
+ type OverturnBasis = 'consent' | 'misread';
213
+ type JudgeOverturns = 'consent' | 'any';
214
+ type JudgeStatus = 'confirmed' | 'overturned' | 'held' | 'failed';
215
+ type JudgeFailure = 'credential' | 'policy' | 'request' | 'timeout' | 'unreadable';
216
+ interface JudgeDiagnostics {
217
+ readonly status: JudgeStatus;
218
+ readonly failureReason: JudgeFailure | null;
219
+ readonly overturnBasis: OverturnBasis | null;
220
+ readonly model: string;
221
+ readonly rule: string;
222
+ readonly tier: 'hard' | 'soft';
223
+ readonly elapsedMs: number;
224
+ }
225
+ //#endregion
208
226
  //#region src/config/presets.d.ts
209
227
  export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
210
228
  //#endregion
211
229
  //#region src/config/config.d.ts
212
230
  interface ProviderConfig {
213
- readonly protocol?: 'messages' | 'system-one';
231
+ readonly protocol?: 'messages' | 'system-one' | 'claude-code';
214
232
  readonly baseURL: string;
215
233
  readonly model: string;
216
234
  readonly apiKeyEnv?: string | undefined;
@@ -225,9 +243,14 @@ interface ScopeSource {
225
243
  }
226
244
  type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
227
245
  declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
246
+ interface CaptureConfig {
247
+ readonly enabled: boolean;
248
+ readonly dir?: string | undefined;
249
+ }
228
250
  interface Config {
229
251
  readonly provider: ProviderConfig;
230
252
  readonly judge?: ProviderConfig | null;
253
+ readonly judgeOverturns?: JudgeOverturns;
231
254
  readonly scopeSources?: Readonly<Record<string, ScopeSource>>;
232
255
  readonly classifierPath?: string | undefined;
233
256
  readonly rulesPath?: string | undefined;
@@ -235,6 +258,7 @@ interface Config {
235
258
  readonly claudeSettingsPath?: string | null | undefined;
236
259
  readonly blockThreshold?: number | undefined;
237
260
  readonly denialBudget?: DenialBudget;
261
+ readonly capture?: CaptureConfig;
238
262
  readonly warnings?: readonly string[];
239
263
  }
240
264
  export declare const DEFAULT_CONFIG: Config;
@@ -243,7 +267,7 @@ export declare function loadConfig(configPath?: string, host?: Readonly<HostEnvi
243
267
  export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
244
268
  //#endregion
245
269
  //#region src/classify-action.d.ts
246
- type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'messages' | 'retry';
270
+ type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'judge' | 'messages' | 'retry';
247
271
  interface ActionOutcome {
248
272
  readonly verdict: Verdict | null;
249
273
  readonly decidingStage: DecidingStage;
@@ -251,6 +275,7 @@ interface ActionOutcome {
251
275
  readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
252
276
  readonly unavailable?: boolean;
253
277
  readonly diagnostics?: DecisionDiagnostics;
278
+ readonly judge?: JudgeDiagnostics;
254
279
  }
255
280
  interface ClassifyOptions extends EvaluationOptions {
256
281
  readonly localOnly?: boolean;
@@ -272,12 +297,27 @@ export declare function checkContainment(request: Readonly<ActionRequest>, scope
272
297
  //#region src/model/build-decision-request.d.ts
273
298
  export declare function buildDecisionRequest(payload: ActionRequest, policy: string, configuredRules: ClaudeRules, lastUserMessage: string | null, rulesSource?: 'shipped' | 'replacement', repositoryContext?: RepositoryContext | null, mcpServers?: readonly MCPServerFact[]): DecisionRequest;
274
299
  //#endregion
300
+ //#region src/policy/build-deny-reason.d.ts
301
+ type DenyBasis = 'matched' | 'unresolved';
302
+ //#endregion
303
+ //#region src/model/find-denied-rule.d.ts
304
+ interface DeniedRule {
305
+ readonly rule: DecisionRule;
306
+ readonly basis: DenyBasis;
307
+ }
308
+ //#endregion
309
+ //#region src/judge/classify-with-judge.d.ts
310
+ interface JudgeEvidence extends DeniedRule {
311
+ readonly repositoryContext: RepositoryContext | null;
312
+ }
313
+ //#endregion
275
314
  //#region src/model/classify-with-model.d.ts
276
315
  interface ModelOutcome {
277
316
  readonly verdict: Verdict | null;
278
317
  readonly note: string;
279
318
  readonly unavailable?: boolean;
280
319
  readonly diagnostics?: DecisionDiagnostics;
320
+ readonly review?: JudgeEvidence;
281
321
  }
282
322
  export declare function classifyWithModel(payload: ActionRequest, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
283
323
  //#endregion
@@ -306,7 +346,7 @@ interface PolicyPaths {
306
346
  readonly classifierPath?: string | undefined;
307
347
  readonly rulesPath?: string | undefined;
308
348
  }
309
- export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
349
+ export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md' | 'judge.md'): Promise<string>;
310
350
  //#endregion
311
351
  //#region src/request/parse-action-request.d.ts
312
352
  export declare function parseActionRequest(body: unknown): ActionRequest | null;
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-Bx9Lw7gO.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;