auto-mode 3.1.3 → 3.1.5

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-Czvdr5DS.js";
3
- import { a as literal, d as string, i as discriminatedUnion, r as boolean, s as number, u as strictObject } from "./schemas-B80Yxv6m.js";
2
+ import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-CHuF6LYV.js";
3
+ import { a as literal, d as string, i as discriminatedUnion, l as record, p as unknown, r as boolean, s as number, u as strictObject } from "./schemas-B80Yxv6m.js";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { chmod, mkdir, open, readFile, realpath, stat } from "node:fs/promises";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
@@ -2665,13 +2665,15 @@ function startDetachedProcess(file, args, env) {
2665
2665
  }
2666
2666
  //#endregion
2667
2667
  //#region src/judge/judge-server-response-schema.ts
2668
+ const modelUsageSchema = record(string(), unknown());
2668
2669
  const judgeServerResponseSchema = discriminatedUnion("kind", [strictObject({
2669
2670
  kind: literal("reply"),
2670
2671
  text: string(),
2671
2672
  model: string(),
2672
2673
  safetyStops: number().nullable(),
2673
2674
  fallback: boolean(),
2674
- warm: boolean().nullable()
2675
+ warm: boolean().nullable(),
2676
+ modelUsage: modelUsageSchema.nullable()
2675
2677
  }), strictObject({
2676
2678
  kind: literal("failure"),
2677
2679
  aborted: boolean(),
@@ -3283,7 +3285,7 @@ async function runJudgeServer(io, socketPath) {
3283
3285
  io.stderr.write(`auto-mode: judge-server needs a socket path\n\n${USAGE}`);
3284
3286
  return 2;
3285
3287
  }
3286
- await (await (await import("./start-judge-server-CDYdRR4I.js")).startJudgeServer({
3288
+ await (await (await import("./start-judge-server-BaeD4mmu.js")).startJudgeServer({
3287
3289
  socketPath,
3288
3290
  env: io.runtime.host.env,
3289
3291
  idleMs: JUDGE_IDLE_MS
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ interface JudgeMessageReply {
20
20
  readonly safetyStops: number | null;
21
21
  readonly fallback: boolean;
22
22
  readonly warm: boolean | null;
23
+ readonly modelUsage: Readonly<Record<string, unknown>> | null;
23
24
  }
24
25
  interface JudgeSession {
25
26
  readonly review: (model: string, request: Readonly<ModelRequest>, signal: Readonly<AbortSignal>) => Promise<JudgeMessageReply>;
@@ -30,6 +31,7 @@ interface JudgeDiagnostics {
30
31
  readonly status: JudgeStatus;
31
32
  readonly failureReason: JudgeFailure | null;
32
33
  readonly overturnBasis: OverturnBasis | null;
34
+ readonly overturnReason: string | null;
33
35
  readonly model: string;
34
36
  readonly answeringModel: string | null;
35
37
  readonly safetyStops: number | null;
@@ -145,10 +147,10 @@ type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
145
147
  interface UserTask {
146
148
  readonly text: string;
147
149
  readonly origin: MessageOrigin;
150
+ readonly position?: 'opening' | undefined;
148
151
  }
149
152
  interface DirectUserMessage extends UserTask {
150
153
  readonly freshness?: 'stale' | undefined;
151
- readonly position?: 'opening' | undefined;
152
154
  }
153
155
  interface DelegatedTask {
154
156
  readonly text: string;
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-Czvdr5DS.js";
1
+ import { b as loadConfig$1, i as classifyAction$1, n as readRuntime, r as readHostEnvironment, t as parseActionRequest } from "./parse-action-request-CHuF6LYV.js";
2
2
  //#region src/index.ts
3
3
  function classifyAction(request, config, options = {}) {
4
4
  return classifyAction$1(request, config, {
@@ -251,6 +251,25 @@ async function tryUpdateDenialBudget(stderr, path, plan) {
251
251
  }
252
252
  }
253
253
  //#endregion
254
+ //#region src/scope/find-reply-channel.ts
255
+ function findReplyChannel(tool, input, channels) {
256
+ return channels.find((channel) => channel.tool === tool && input[channel.field] === channel.recipient) ?? null;
257
+ }
258
+ //#endregion
259
+ //#region src/bypass/classify-reply-call.ts
260
+ async function classifyReplyCall(call, scope, hasConfiguredDenies) {
261
+ const channel = scope === null ? null : findReplyChannel(call.toolName, call.toolInput, scope.replyChannels);
262
+ if (channel === null) return { kind: "none" };
263
+ if ((await import("./classify-reply-message-CdHLlNXA.js")).classifyReplyMessage(call.toolInput).kind !== "bypass") return { kind: "none" };
264
+ return hasConfiguredDenies ? {
265
+ kind: "exempt",
266
+ rules: ["Outbound Communication"]
267
+ } : {
268
+ kind: "bypass",
269
+ channel
270
+ };
271
+ }
272
+ //#endregion
254
273
  //#region src/state/resolve-xdg-dir.ts
255
274
  const BASE_DIRS = {
256
275
  config: {
@@ -1098,11 +1117,6 @@ const SCRATCH_PATHS = [
1098
1117
  "/dev/stderr"
1099
1118
  ];
1100
1119
  //#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
1106
1120
  //#region src/shell/is-unresolved-word.ts
1107
1121
  function isUnresolvedWord(word) {
1108
1122
  return /[$`]/u.test(word);
@@ -3091,7 +3105,7 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3091
3105
  const agentKey = agentID === null ? payload.sessionID : `${payload.sessionID}\0${agentID}`;
3092
3106
  const isReviewed = entry.judge !== void 0 || entry.grant !== void 0;
3093
3107
  const record = {
3094
- schemaVersion: 7,
3108
+ schemaVersion: 9,
3095
3109
  time: (/* @__PURE__ */ new Date()).toISOString(),
3096
3110
  invocationID: entry.invocationID,
3097
3111
  sessionHash: toHash(payload.sessionID),
@@ -3103,9 +3117,10 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3103
3117
  denials: entry.denials ?? null,
3104
3118
  escalation: entry.escalation ?? false,
3105
3119
  diagnostics: entry.diagnostics ?? null,
3106
- judge: entry.judge ?? null,
3107
- grant: entry.grant ?? null,
3108
- action: isReviewed ? buildLoggedAction(payload) : null
3120
+ judge: entry.judge === void 0 ? null : buildLoggedReview(entry.judge),
3121
+ grant: entry.grant === void 0 ? null : buildLoggedReview(entry.grant),
3122
+ action: isReviewed ? buildLoggedAction(payload) : null,
3123
+ taskEvidence: buildTaskEvidence(entry.taskContext ?? payload.decisionContext)
3109
3124
  };
3110
3125
  try {
3111
3126
  await mkdir(dirname(path), {
@@ -3120,6 +3135,17 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3120
3135
  function toHash(value) {
3121
3136
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
3122
3137
  }
3138
+ function buildLoggedReview(review) {
3139
+ if (review.overturnReason === null) return review;
3140
+ const reason = buildSecretFreeText(review.overturnReason, () => "[secret]");
3141
+ return {
3142
+ ...review,
3143
+ overturnReason: findSecret({
3144
+ text: reason,
3145
+ path: null
3146
+ }) === null ? reason : null
3147
+ };
3148
+ }
3123
3149
  const LOGGED_INPUT_MAX_CHARS = 2e3;
3124
3150
  function buildLoggedAction(payload) {
3125
3151
  const redacted = buildSecretFreeValue(payload.toolInput);
@@ -3148,6 +3174,34 @@ function hasSecret(value) {
3148
3174
  if (typeof value === "object" && value !== null) return Object.values(value).some((item) => hasSecret(item));
3149
3175
  return false;
3150
3176
  }
3177
+ function buildTaskEvidence(context) {
3178
+ if (context === void 0) return null;
3179
+ const message = context.lastDirectUserMessage;
3180
+ return {
3181
+ userMessage: message === null ? null : message.freshness ?? "current",
3182
+ originalUserTask: context.originalUserTask?.origin ?? null,
3183
+ delegatedTask: context.delegatedTask !== null
3184
+ };
3185
+ }
3186
+ //#endregion
3187
+ //#region src/diagnostics/write-judge-usage.ts
3188
+ async function writeJudgeUsage(usage, host, warnings) {
3189
+ const path = host.env["AUTO_MODE_DEBUG_PATH"];
3190
+ if (path === void 0 || path === "") return;
3191
+ const line = {
3192
+ time: (/* @__PURE__ */ new Date()).toISOString(),
3193
+ ...usage
3194
+ };
3195
+ try {
3196
+ await mkdir(dirname(path), {
3197
+ recursive: true,
3198
+ mode: 448
3199
+ });
3200
+ await appendFile(path, `${JSON.stringify(line)}\n`, { mode: 384 });
3201
+ } catch {
3202
+ warnings.write("auto-mode: judge debug line unavailable\n");
3203
+ }
3204
+ }
3151
3205
  //#endregion
3152
3206
  //#region src/git/build-repository-context.ts
3153
3207
  function buildRepositoryContext(facts) {
@@ -3571,18 +3625,24 @@ function buildJudgedVerdict(rule, basis, reply, options) {
3571
3625
  if (reply === null || reply.kind === "unreadable") return {
3572
3626
  verdict: template,
3573
3627
  status: "failed",
3574
- overturnBasis: null
3628
+ overturnBasis: null,
3629
+ overturnReason: null
3575
3630
  };
3576
3631
  if (reply.kind === "overturn") {
3577
3632
  const clearsHardRule = reply.basis === "misread" && options.misreadClears === true;
3578
- return rule.tier === "hard" && !clearsHardRule || reply.basis === "misread" && options.overturns === "consent" || reply.basis === "consent" && !options.hasUserMessage ? {
3633
+ const isHeld = rule.tier === "hard" && !clearsHardRule || reply.basis === "misread" && options.overturns === "consent" || reply.basis === "consent" && !options.hasUserMessage;
3634
+ const overturn = {
3635
+ overturnBasis: reply.basis,
3636
+ overturnReason: formatJudgeText(buildSecretFreeText(reply.reason, () => "[secret]"))
3637
+ };
3638
+ return isHeld ? {
3579
3639
  verdict: template,
3580
3640
  status: "held",
3581
- overturnBasis: reply.basis
3641
+ ...overturn
3582
3642
  } : {
3583
3643
  verdict: { kind: "allow" },
3584
3644
  status: "overturned",
3585
- overturnBasis: reply.basis
3645
+ ...overturn
3586
3646
  };
3587
3647
  }
3588
3648
  return {
@@ -3592,7 +3652,8 @@ function buildJudgedVerdict(rule, basis, reply, options) {
3592
3652
  reason: `The reviewer confirmed the ${rule.name} deny: ${formatJudgeText(reply.reason)}`
3593
3653
  },
3594
3654
  status: "confirmed",
3595
- overturnBasis: null
3655
+ overturnBasis: null,
3656
+ overturnReason: null
3596
3657
  };
3597
3658
  }
3598
3659
  function formatJudgeText(text) {
@@ -3808,7 +3869,7 @@ function sendJudgeMessage(provider, request, transport, signal) {
3808
3869
  }
3809
3870
  async function sendToAgentSDK(provider, request, transport, signal) {
3810
3871
  if (transport.session !== void 0) return transport.session.review(provider.model, request, signal);
3811
- const session = (await import("./start-judge-session-C0eRJFlg.js")).startJudgeSession(transport.env);
3872
+ const session = (await import("./start-judge-session-B65PMvQ6.js")).startJudgeSession(transport.env);
3812
3873
  try {
3813
3874
  return await session.review(provider.model, request, signal);
3814
3875
  } finally {
@@ -3822,7 +3883,8 @@ async function sendToMessagesAPI(provider, request, transport, signal) {
3822
3883
  model: provider.model,
3823
3884
  safetyStops: null,
3824
3885
  fallback: false,
3825
- warm: null
3886
+ warm: null,
3887
+ modelUsage: null
3826
3888
  };
3827
3889
  }
3828
3890
  //#endregion
@@ -3872,6 +3934,11 @@ async function classifyWithJudge(payload, evidence, judge, options) {
3872
3934
  throw error;
3873
3935
  });
3874
3936
  replies = parseJudgeReplies(answered.text, evidence.map((entry) => entry.rule.name));
3937
+ if (answered.modelUsage !== null) await options.onUsage?.({
3938
+ model: judge.model,
3939
+ answeringModel: answered.model,
3940
+ modelUsage: answered.modelUsage
3941
+ });
3875
3942
  failure = null;
3876
3943
  } catch {}
3877
3944
  const elapsedMs = Math.round(performance.now() - start);
@@ -3890,6 +3957,7 @@ async function classifyWithJudge(payload, evidence, judge, options) {
3890
3957
  status: verdict.status,
3891
3958
  failureReason: failure ?? (reply?.kind === "unreadable" ? "unreadable" : null),
3892
3959
  overturnBasis: verdict.overturnBasis,
3960
+ overturnReason: verdict.overturnReason,
3893
3961
  model: judge.model,
3894
3962
  answeringModel: answered?.model ?? null,
3895
3963
  safetyStops: answered?.safetyStops ?? null,
@@ -3902,6 +3970,31 @@ async function classifyWithJudge(payload, evidence, judge, options) {
3902
3970
  });
3903
3971
  }
3904
3972
  //#endregion
3973
+ //#region src/model/build-budget-omitted-context.ts
3974
+ function buildBudgetOmittedContext(context, field) {
3975
+ return {
3976
+ ...context,
3977
+ [field]: null,
3978
+ omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
3979
+ field,
3980
+ reason: "budget"
3981
+ }]
3982
+ };
3983
+ }
3984
+ //#endregion
3985
+ //#region src/model/build-task-context.ts
3986
+ function buildTaskContext(context) {
3987
+ let result = {
3988
+ ...context,
3989
+ omittedTaskContext: [...context.omittedTaskContext]
3990
+ };
3991
+ for (const field of ["originalUserTask", "delegatedTask"]) {
3992
+ const task = result[field];
3993
+ if (task !== null && Buffer.byteLength(task.text) > 4096) result = buildBudgetOmittedContext(result, field);
3994
+ }
3995
+ return result;
3996
+ }
3997
+ //#endregion
3905
3998
  //#region src/model/build-task-scope-summary.ts
3906
3999
  function buildTaskScopeSummary(scope) {
3907
4000
  return {
@@ -4186,31 +4279,6 @@ function resolveHooksPath(path, worktree, home) {
4186
4279
  return path.startsWith("~/") ? join(home, path.slice(2)) : resolve(worktree, path);
4187
4280
  }
4188
4281
  //#endregion
4189
- //#region src/model/build-budget-omitted-context.ts
4190
- function buildBudgetOmittedContext(context, field) {
4191
- return {
4192
- ...context,
4193
- [field]: null,
4194
- omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
4195
- field,
4196
- reason: "budget"
4197
- }]
4198
- };
4199
- }
4200
- //#endregion
4201
- //#region src/model/build-task-context.ts
4202
- function buildTaskContext(context) {
4203
- let result = {
4204
- ...context,
4205
- omittedTaskContext: [...context.omittedTaskContext]
4206
- };
4207
- for (const field of ["originalUserTask", "delegatedTask"]) {
4208
- const task = result[field];
4209
- if (task !== null && Buffer.byteLength(task.text) > 4096) result = buildBudgetOmittedContext(result, field);
4210
- }
4211
- return result;
4212
- }
4213
- //#endregion
4214
4282
  //#region src/git/parse-config-parameters.ts
4215
4283
  function parseConfigParameters(text) {
4216
4284
  const parameters = [];
@@ -4754,6 +4822,7 @@ function buildDecisionRequest(payload, policy, options) {
4754
4822
  const rulesSource = options.rulesSource ?? "replacement";
4755
4823
  const repositoryContext = options.repositoryContext ?? null;
4756
4824
  const mcpServers = options.mcpServers ?? [];
4825
+ const exemptRules = options.exemptRules ?? [];
4757
4826
  const rules = {};
4758
4827
  let tier = null;
4759
4828
  for (const section of policy.split(/(?=^## |^### )/mu)) if (section.startsWith("## HARD BLOCK rules")) tier = "hard";
@@ -4764,6 +4833,7 @@ function buildDecisionRequest(payload, policy, options) {
4764
4833
  const end = newline === -1 ? section.length : newline;
4765
4834
  const name = section.slice(4, end).trim();
4766
4835
  if (name === "" || Object.values(rules).some((rule) => rule.name === name)) throw new Error("Policy has an empty or duplicate rule");
4836
+ if (rulesSource === "shipped" && exemptRules.includes(name)) continue;
4767
4837
  rules[`rule_${Object.keys(rules).length}`] = {
4768
4838
  name,
4769
4839
  tier,
@@ -4989,6 +5059,30 @@ function classifyFailure(attempt, timeoutMs) {
4989
5059
  };
4990
5060
  }
4991
5061
  //#endregion
5062
+ //#region src/model/build-sized-decision-input.ts
5063
+ function buildSizedDecisionInput(model, request) {
5064
+ return buildFittedInput(buildFittedInput(buildInput(model, request.state, request.questions), "delegatedTask"), "originalUserTask");
5065
+ }
5066
+ function buildInput(model, state, questions) {
5067
+ const input = {
5068
+ model,
5069
+ state,
5070
+ questions
5071
+ };
5072
+ return {
5073
+ input,
5074
+ body: JSON.stringify(input)
5075
+ };
5076
+ }
5077
+ function buildFittedInput(sized, field) {
5078
+ const context = sized.input.state.taskContext;
5079
+ if (Buffer.byteLength(sized.body) <= 1e5 || context === void 0 || context[field] === null) return sized;
5080
+ return buildInput(sized.input.model, {
5081
+ ...sized.input.state,
5082
+ taskContext: buildBudgetOmittedContext(context, field)
5083
+ }, sized.input.questions);
5084
+ }
5085
+ //#endregion
4992
5086
  //#region src/model/collect-config-file-paths.ts
4993
5087
  function collectConfigFilePaths(command) {
4994
5088
  const values = splitShellCommand(command).segments.flatMap((segment) => splitShellTokens(segment.text).tokens.flatMap((token) => toWordValues(token))).flatMap((word) => {
@@ -5025,18 +5119,7 @@ function buildDecisionResponseSchema(choices) {
5025
5119
  //#endregion
5026
5120
  //#region src/model/send-decision.ts
5027
5121
  async function sendDecision(provider, apiKey, request, options) {
5028
- const input = {
5029
- model: provider.model,
5030
- state: { ...request.state },
5031
- questions: request.questions
5032
- };
5033
- let body = JSON.stringify(input);
5034
- for (const field of ["delegatedTask", "originalUserTask"]) {
5035
- const context = input.state.taskContext;
5036
- if (Buffer.byteLength(body) <= 1e5 || context === void 0 || context[field] === null) continue;
5037
- input.state.taskContext = buildBudgetOmittedContext(context, field);
5038
- body = JSON.stringify(input);
5039
- }
5122
+ const body = buildSizedDecisionInput(provider.model, request).body;
5040
5123
  const requestBytes = Buffer.byteLength(body);
5041
5124
  if (requestBytes > 1e5) throw new DecisionRequestError("request-too-large", requestBytes, "Decision input exceeds 100000 bytes; refusing to truncate the action or user message");
5042
5125
  let response;
@@ -5121,7 +5204,8 @@ async function classifyWithJev(payload, config, options) {
5121
5204
  repositoryContext,
5122
5205
  mcpServers,
5123
5206
  localHookConfig,
5124
- hookFacts
5207
+ hookFacts,
5208
+ exemptRules: options.exemptRules
5125
5209
  });
5126
5210
  const signal = createDeadlineSignal(config.provider.timeoutMs, options);
5127
5211
  if (signal === null) throw new DOMException("Evaluation deadline expired", "AbortError");
@@ -5131,6 +5215,7 @@ async function classifyWithJev(payload, config, options) {
5131
5215
  fetch: runtime.fetch
5132
5216
  });
5133
5217
  stage = "response";
5218
+ options.onTaskContext?.(buildSizedDecisionInput(config.provider.model, request).input.state.taskContext);
5134
5219
  return buildJevOutcome({
5135
5220
  kind: "reply",
5136
5221
  request,
@@ -5937,31 +6022,65 @@ async function probeLauncherSpawner(location, readTextFile) {
5937
6022
  //#region src/scope/resolve-opening-prompt.ts
5938
6023
  async function resolveOpeningPrompt(request, runtime) {
5939
6024
  const context = request.decisionContext;
5940
- const message = context?.lastDirectUserMessage;
5941
- if (context === void 0 || message?.position !== "opening") return request;
5942
- const { position: _, ...unmarked } = message;
6025
+ const task = context?.originalUserTask ?? null;
6026
+ const message = context?.lastDirectUserMessage ?? null;
6027
+ const opening = task?.position === "opening" ? task : message;
6028
+ if (context === void 0 || opening?.position !== "opening") return request;
5943
6029
  const isSpawned = await probeLauncherSpawner(runtime.host.env["AUTO_MODE_SCOPE_FILE"], runtime.readTextFile);
5944
6030
  return {
5945
6031
  ...request,
5946
- decisionContext: {
6032
+ decisionContext: isSpawned ? buildSpawnedContext(context, opening.text) : {
5947
6033
  ...context,
5948
- lastDirectUserMessage: isSpawned ? null : unmarked
6034
+ originalUserTask: task === null ? null : toUnmarked(task),
6035
+ lastDirectUserMessage: message === null ? null : toUnmarked(message)
5949
6036
  }
5950
6037
  };
5951
6038
  }
6039
+ function buildSpawnedContext(context, openingText) {
6040
+ const isTaskOpening = context.originalUserTask?.position === "opening";
6041
+ const message = context.lastDirectUserMessage;
6042
+ const isOmitted = context.omittedTaskContext.some((omission) => omission.field === "originalUserTask");
6043
+ return {
6044
+ ...context,
6045
+ originalUserTask: isTaskOpening ? null : context.originalUserTask,
6046
+ delegatedTask: context.agentID === null ? context.delegatedTask ?? {
6047
+ text: openingText,
6048
+ origin: "agent.spawn"
6049
+ } : context.delegatedTask,
6050
+ lastDirectUserMessage: message?.position === "opening" ? null : message,
6051
+ omittedTaskContext: isTaskOpening && !isOmitted ? [...context.omittedTaskContext, {
6052
+ field: "originalUserTask",
6053
+ reason: "unavailable"
6054
+ }] : context.omittedTaskContext
6055
+ };
6056
+ }
6057
+ function toUnmarked(value) {
6058
+ const { position: _, ...unmarked } = value;
6059
+ return unmarked;
6060
+ }
5952
6061
  //#endregion
5953
6062
  //#region src/classify-action.ts
5954
6063
  async function classifyAction(request, config, options) {
5955
- const stderr = options.stderr;
5956
- const host = options.runtime.host;
5957
6064
  const invocationID = randomUUID();
5958
- if (options.actionLog === true) await writeActionDiagnostic(request, {
5959
- invocationID,
5960
- status: "started"
5961
- }, host, stderr);
5962
6065
  const resolved = await resolveOpeningPrompt(request, options.runtime);
5963
- const outcome = options.budget === true ? await classifyWithBudget(resolved, config, options) : await classifyStages(resolved, config, options);
5964
- if (options.actionLog === true) await writeActionDiagnostic(request, {
6066
+ const offered = resolved.decisionContext === void 0 ? void 0 : buildTaskContext(resolved.decisionContext);
6067
+ if (options.actionLog === true) await writeActionDiagnostic(resolved, {
6068
+ invocationID,
6069
+ status: "started",
6070
+ taskContext: offered
6071
+ }, options.runtime.host, options.stderr);
6072
+ const received = { context: offered };
6073
+ const onTaskContext = (context) => {
6074
+ received.context = context;
6075
+ };
6076
+ const outcome = options.budget === true ? await classifyWithBudget(resolved, config, {
6077
+ ...options,
6078
+ onTaskContext
6079
+ }) : await classifyStages(resolved, config, {
6080
+ ...options,
6081
+ onTaskContext
6082
+ });
6083
+ if (options.actionLog === true) await writeActionDiagnostic(resolved, {
5965
6084
  invocationID,
5966
6085
  status: outcome.status,
5967
6086
  verdict: outcome.verdict?.kind ?? "defer",
@@ -5970,8 +6089,9 @@ async function classifyAction(request, config, options) {
5970
6089
  ...outcome.escalation === void 0 ? {} : { escalation: outcome.escalation },
5971
6090
  ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
5972
6091
  ...outcome.judge === void 0 ? {} : { judge: outcome.judge },
5973
- ...outcome.grant === void 0 ? {} : { grant: outcome.grant }
5974
- }, host, stderr);
6092
+ ...outcome.grant === void 0 ? {} : { grant: outcome.grant },
6093
+ taskContext: received.context
6094
+ }, options.runtime.host, options.stderr);
5975
6095
  return outcome;
5976
6096
  }
5977
6097
  async function classifyWithBudget(request, config, options) {
@@ -6008,7 +6128,8 @@ async function classifyStages(request, config, options) {
6008
6128
  local: { kind: "escalate" },
6009
6129
  checkout: null,
6010
6130
  scope: null,
6011
- grant: null
6131
+ grant: null,
6132
+ exemptRules: []
6012
6133
  };
6013
6134
  const reviewed = await classifyWithGrant(ctx, await runStages(ctx));
6014
6135
  if (reviewed.verdict?.kind !== "deny") return reviewed;
@@ -6192,9 +6313,12 @@ async function classifyEditBypass(ctx) {
6192
6313
  };
6193
6314
  }
6194
6315
  async function classifyReplyChannel(ctx) {
6195
- const channel = ctx.hasConfiguredDenies || ctx.scope === null ? null : findReplyChannel(ctx.request.toolName, ctx.request.toolInput, ctx.scope.replyChannels);
6196
- if (channel === null) return { kind: "pass" };
6197
- if ((await import("./classify-reply-message-CdHLlNXA.js")).classifyReplyMessage(ctx.request.toolInput).kind !== "bypass") return { kind: "pass" };
6316
+ const reply = await classifyReplyCall(ctx.request, ctx.scope, ctx.hasConfiguredDenies);
6317
+ if (reply.kind !== "bypass") return {
6318
+ kind: "pass",
6319
+ facts: { exemptRules: reply.kind === "exempt" ? reply.rules : [] }
6320
+ };
6321
+ const channel = reply.channel;
6198
6322
  return {
6199
6323
  kind: "decided",
6200
6324
  outcome: {
@@ -6223,7 +6347,8 @@ async function classifyJev(ctx) {
6223
6347
  taskScope: ctx.scope === null ? void 0 : buildTaskScopeSummary(ctx.scope),
6224
6348
  claudeSettings: ctx.claudeSettings,
6225
6349
  checkout: ctx.checkout ?? void 0,
6226
- readCheckout: ctx.readCheckout
6350
+ readCheckout: ctx.readCheckout,
6351
+ exemptRules: ctx.exemptRules
6227
6352
  });
6228
6353
  return {
6229
6354
  ...outcome,
@@ -6241,7 +6366,8 @@ async function classifyWithGrant(ctx, decided) {
6241
6366
  const judged = await classifyWithJudge(ctx.request, evidence, judge, {
6242
6367
  ...ctx.options,
6243
6368
  rulesPath: ctx.config.rulesPath,
6244
- overturns: ctx.config.judgeOverturns ?? "any"
6369
+ overturns: ctx.config.judgeOverturns ?? "any",
6370
+ onUsage: (usage) => writeJudgeUsage(usage, ctx.options.runtime.host, ctx.options.stderr)
6245
6371
  });
6246
6372
  const granted = grant === null ? null : judged[0] ?? null;
6247
6373
  const reviewed = denied === null ? null : judged.at(-1) ?? null;
@@ -6314,12 +6440,12 @@ const userTaskSchema = strictObject({
6314
6440
  "composer",
6315
6441
  "bridge",
6316
6442
  "sdk"
6317
- ])
6443
+ ]),
6444
+ position: literal("opening").optional()
6318
6445
  });
6319
6446
  const directUserMessageSchema = strictObject({
6320
6447
  ...userTaskSchema.shape,
6321
- freshness: literal("stale").optional(),
6322
- position: literal("opening").optional()
6448
+ freshness: literal("stale").optional()
6323
6449
  });
6324
6450
  const delegatedTaskSchema = strictObject({
6325
6451
  text: string(),
@@ -1,5 +1,5 @@
1
1
  import { d as string, s as number, u as strictObject } from "./schemas-B80Yxv6m.js";
2
- import { t as startJudgeSession } from "./start-judge-session-vhUxe9-V.js";
2
+ import { t as startJudgeSession } from "./start-judge-session-C_k8lacU.js";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { chmod, mkdir, rename, rm, stat } from "node:fs/promises";
5
5
  import { dirname, join } from "node:path";
@@ -0,0 +1 @@
1
+ export { t as startJudgeSession } from "./start-judge-session-C_k8lacU.js";
@@ -1,4 +1,4 @@
1
- import { c as object, d as string, l as record, s as number } from "./schemas-B80Yxv6m.js";
1
+ import { c as object, d as string, l as record, p as unknown, s as number } from "./schemas-B80Yxv6m.js";
2
2
  import { createRequire } from "node:module";
3
3
  import ut, { delimiter, isAbsolute, join } from "node:path";
4
4
  import { spawn } from "node:child_process";
@@ -39277,7 +39277,7 @@ function startJudgeSession(env) {
39277
39277
  current.reviews += 1;
39278
39278
  if (turn.sessionFallback) stop();
39279
39279
  else current.cleared = sendClear(current.send, current.stop);
39280
- return buildReply(turn.result, turn.fallbacks, model, warm);
39280
+ return buildReply(turn, model, warm);
39281
39281
  } catch (error) {
39282
39282
  if (signal.aborted) throw new DOMException("Judge review aborted", "AbortError");
39283
39283
  throw error;
@@ -39343,13 +39343,18 @@ function startLiveProcess(model, system, key, env) {
39343
39343
  waiter.sessionFallback ||= message.scope !== "local";
39344
39344
  return;
39345
39345
  }
39346
+ if (message.type === "assistant" && message.parent_tool_use_id === null && message.error === void 0) {
39347
+ waiter.replyModel = message.message.model;
39348
+ return;
39349
+ }
39346
39350
  if (message.type === "result") {
39347
39351
  const done = waiter;
39348
39352
  waiter = null;
39349
39353
  done.resolve({
39350
39354
  result: message,
39351
39355
  fallbacks: done.fallbacks,
39352
- sessionFallback: done.sessionFallback
39356
+ sessionFallback: done.sessionFallback,
39357
+ replyModel: done.replyModel
39353
39358
  });
39354
39359
  }
39355
39360
  };
@@ -39371,7 +39376,8 @@ function startLiveProcess(model, system, key, env) {
39371
39376
  resolve,
39372
39377
  reject,
39373
39378
  fallbacks: 0,
39374
- sessionFallback: false
39379
+ sessionFallback: false,
39380
+ replyModel: null
39375
39381
  };
39376
39382
  input.push(text);
39377
39383
  })
@@ -39445,23 +39451,22 @@ async function sendClear(send, stop) {
39445
39451
  }
39446
39452
  }
39447
39453
  const MODEL_USAGE = record(string(), object({ outputTokens: number() }));
39454
+ const RAW_MODEL_USAGE = record(string(), unknown());
39448
39455
  const SAFETY_STOPS = object({ safety_stops: number() });
39449
- function buildReply(result, fallbacks, requested, warm) {
39456
+ function buildReply(turn, requested, warm) {
39457
+ const result = turn.result;
39450
39458
  if (result.subtype !== "success" || result.is_error) throw new Error(`the judge turn ended in ${result.subtype}`);
39451
39459
  const usage = MODEL_USAGE.safeParse(result.modelUsage).data;
39452
39460
  const stops = SAFETY_STOPS.safeParse(result).data?.safety_stops;
39453
39461
  return {
39454
39462
  text: result.result,
39455
- model: pickAnsweringModel(usage, requested),
39463
+ model: turn.replyModel ?? requested,
39456
39464
  safetyStops: stops ?? null,
39457
- fallback: fallbacks > 0 || isFallbackReply(usage, stops),
39458
- warm
39465
+ fallback: turn.fallbacks > 0 || isFallbackReply(usage, stops),
39466
+ warm,
39467
+ modelUsage: RAW_MODEL_USAGE.safeParse(result.modelUsage).data ?? null
39459
39468
  };
39460
39469
  }
39461
- function pickAnsweringModel(usage, requested) {
39462
- const [top] = Object.entries(usage ?? {}).filter(([, entry]) => entry.outputTokens > 0).toSorted(([, left], [, right]) => right.outputTokens - left.outputTokens);
39463
- return top === void 0 ? requested : top[0];
39464
- }
39465
39470
  function isFallbackReply(usage, stops) {
39466
39471
  if (stops !== void 0) return stops > 0;
39467
39472
  const entries = Object.values(usage ?? {});
@@ -5,11 +5,11 @@ export type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
5
5
  export interface UserTask {
6
6
  readonly text: string;
7
7
  readonly origin: MessageOrigin;
8
+ readonly position?: 'opening' | undefined;
8
9
  }
9
10
 
10
11
  export interface DirectUserMessage extends UserTask {
11
12
  readonly freshness?: 'stale' | undefined;
12
- readonly position?: 'opening' | undefined;
13
13
  }
14
14
 
15
15
  interface DelegatedTask {
@@ -36,13 +36,16 @@ export function buildPromptContext(
36
36
  };
37
37
  }
38
38
 
39
- const message: UserTask = { text: input.text, origin };
40
39
  const isOpeningPrompt = previous?.awaitsOpeningPrompt ?? true;
41
40
 
41
+ const message: UserTask = isOpeningPrompt
42
+ ? { text: input.text, origin, position: 'opening' }
43
+ : { text: input.text, origin };
44
+
42
45
  return {
43
46
  originalUserTask:
44
47
  previous?.originalUserTask ?? (previous?.canCaptureOriginal === true ? message : null),
45
- lastDirectUserMessage: isOpeningPrompt ? { ...message, position: 'opening' } : message,
48
+ lastDirectUserMessage: message,
46
49
  canCaptureOriginal: previous?.canCaptureOriginal ?? false,
47
50
  awaitsOpeningPrompt: false,
48
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "A permission classifier library and Claude Code mod, so agents that are not Claude can run unattended",
5
5
  "keywords": [
6
6
  "agent",
@@ -52,6 +52,7 @@
52
52
  "eval:question-shape": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-question-shape-evaluation.ts",
53
53
  "eval:relay-consent": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-relay-consent-evaluation.ts",
54
54
  "eval:second-judge": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-second-judge-evaluation.ts",
55
+ "eval:spawned-task": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-spawned-task-evaluation.ts",
55
56
  "eval:stale-consent": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-stale-consent-evaluation.ts",
56
57
  "format": "oxfmt . && format-codemod --quiet .",
57
58
  "format:check": "oxfmt --check . && format-codemod --check --quiet .",
@@ -1 +0,0 @@
1
- export { t as startJudgeSession } from "./start-judge-session-vhUxe9-V.js";