auto-mode 0.17.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 +2 -1
- package/dist/cli.js +82 -39
- package/dist/index.d.ts +30 -14
- package/dist/index.js +1 -1
- package/dist/{render-verdict-Bx9Lw7gO.js → render-verdict-lM31QV2u.js} +12 -15
- package/mods/auto-mode/contract/find-current-direct-user-message.test.ts +26 -0
- package/mods/auto-mode/contract/find-current-direct-user-message.ts +9 -0
- package/mods/auto-mode/contract/is-message-origin.test.ts +10 -0
- package/mods/auto-mode/contract/is-message-origin.ts +6 -0
- package/mods/auto-mode/contract/message-origins.ts +3 -0
- package/mods/auto-mode/contract/scope-commands.ts +6 -0
- package/mods/auto-mode/contract/types.ts +51 -0
- package/mods/auto-mode/hooks/build-action-request.ts +46 -0
- package/mods/auto-mode/hooks/build-prompt-context.ts +4 -2
- package/mods/auto-mode/hooks/is-scope-command.ts +18 -0
- package/mods/auto-mode/hooks/is-tool-input.ts +3 -0
- package/mods/auto-mode/hooks/parse-decision.ts +2 -5
- package/mods/auto-mode/hooks/register.ts +24 -31
- package/mods/auto-mode/hooks/types.ts +2 -9
- package/mods/auto-mode/tsconfig.json +2 -1
- package/package.json +1 -1
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,5 +1,5 @@
|
|
|
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-
|
|
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
3
|
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -274,6 +274,22 @@ function parseScopeRecordRequest(body) {
|
|
|
274
274
|
return parsed.success ? parsed.data : null;
|
|
275
275
|
}
|
|
276
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
|
|
277
293
|
//#region src/scope/read-pull-request.ts
|
|
278
294
|
const LOOKUP_TIMEOUT_MS = 5e3;
|
|
279
295
|
function readPullRequest(repository, number, options = {}) {
|
|
@@ -335,6 +351,17 @@ function collectPullRequestAddresses(text) {
|
|
|
335
351
|
}
|
|
336
352
|
const PULL_REQUEST_URL = /https:\/\/(?<repository>[^/\s]+\/[^/\s]+\/[^/\s]+)\/pull\/(?<number>[1-9]\d*)\b/gu;
|
|
337
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
|
|
338
365
|
//#region src/scope/collect-scope-events.ts
|
|
339
366
|
function collectScopeEvents(command, cwd, home) {
|
|
340
367
|
const events = [];
|
|
@@ -344,7 +371,7 @@ function collectScopeEvents(command, cwd, home) {
|
|
|
344
371
|
const [name, ...args] = splitCommandWords(segment.text);
|
|
345
372
|
if (name === "cd") directory = hasPipe ? null : resolvePath(directory, args[0] ?? home, home);
|
|
346
373
|
else if (name === "git" && directory !== null) events.push(...collectGitEvents(args, directory, home));
|
|
347
|
-
else if (name === "gh" && directory !== null &&
|
|
374
|
+
else if (name === "gh" && directory !== null && findScopeCommand(SCOPE_COMMANDS.gh, args) !== null) events.push({
|
|
348
375
|
kind: "pull-request",
|
|
349
376
|
directory,
|
|
350
377
|
head: findOptionValue(args, ["--head", "-H"])
|
|
@@ -352,15 +379,6 @@ function collectScopeEvents(command, cwd, home) {
|
|
|
352
379
|
}
|
|
353
380
|
return events;
|
|
354
381
|
}
|
|
355
|
-
const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
|
|
356
|
-
"-b",
|
|
357
|
-
"-B",
|
|
358
|
-
"-c",
|
|
359
|
-
"-C",
|
|
360
|
-
"--create",
|
|
361
|
-
"--force-create"
|
|
362
|
-
]);
|
|
363
|
-
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;
|
|
364
382
|
function collectGitEvents(args, cwd, home) {
|
|
365
383
|
let directory = cwd;
|
|
366
384
|
let index = 0;
|
|
@@ -368,34 +386,33 @@ function collectGitEvents(args, cwd, home) {
|
|
|
368
386
|
if (args[index] === "-C") directory = resolvePath(directory, args[index + 1] ?? "", home);
|
|
369
387
|
index += args[index] === "-C" || args[index] === "-c" ? 2 : 1;
|
|
370
388
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (subcommand === "checkout" || subcommand === "switch") {
|
|
383
|
-
const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
|
|
384
|
-
return name === null ? [] : [{
|
|
385
|
-
kind: "branch",
|
|
386
|
-
name,
|
|
387
|
-
directory
|
|
388
|
-
}];
|
|
389
|
-
}
|
|
390
|
-
if (subcommand === "branch" && !rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) {
|
|
391
|
-
const name = rest.find((word) => !word.startsWith("-"));
|
|
392
|
-
return name === void 0 || isUnresolved(name) ? [] : [{
|
|
393
|
-
kind: "branch",
|
|
394
|
-
name,
|
|
395
|
-
directory
|
|
396
|
-
}];
|
|
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
|
+
};
|
|
397
400
|
}
|
|
398
|
-
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
|
+
}];
|
|
399
416
|
}
|
|
400
417
|
const WORKTREE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
401
418
|
"-b",
|
|
@@ -410,6 +427,32 @@ function findWorktreePath(args) {
|
|
|
410
427
|
}
|
|
411
428
|
return null;
|
|
412
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
|
+
}
|
|
413
456
|
function findOptionValue(args, options) {
|
|
414
457
|
const index = args.findIndex((word) => options.includes(word));
|
|
415
458
|
const value = index === -1 ? void 0 : args[index + 1];
|
|
@@ -704,7 +747,7 @@ async function run(io, explain, localOnly, jevOnly, deadline) {
|
|
|
704
747
|
const captured = await tryWriteRequestCapture(resolveCaptureDir(config.capture, host), {
|
|
705
748
|
schemaVersion: 1,
|
|
706
749
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
707
|
-
request:
|
|
750
|
+
request: toModRequest(request),
|
|
708
751
|
verdict: plan.verdict,
|
|
709
752
|
decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
|
|
710
753
|
escalation: plan.escalation
|
package/dist/index.d.ts
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
|
-
//#region
|
|
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:
|
|
8
|
+
readonly origin: MessageOrigin;
|
|
5
9
|
}
|
|
6
10
|
interface DirectUserMessage extends UserTask {
|
|
7
11
|
readonly freshness?: 'stale' | undefined;
|
|
8
12
|
}
|
|
9
|
-
interface
|
|
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 {
|
|
10
22
|
readonly agentID: string | null;
|
|
11
23
|
readonly originalUserTask: UserTask | null;
|
|
12
|
-
readonly delegatedTask:
|
|
13
|
-
readonly text: string;
|
|
14
|
-
readonly origin: 'agent.spawn';
|
|
15
|
-
} | null;
|
|
24
|
+
readonly delegatedTask: DelegatedTask | null;
|
|
16
25
|
readonly lastDirectUserMessage: DirectUserMessage | null;
|
|
17
|
-
readonly omittedTaskContext:
|
|
18
|
-
readonly field: 'originalUserTask' | 'delegatedTask';
|
|
19
|
-
readonly reason: 'unavailable' | 'budget';
|
|
20
|
-
}[];
|
|
26
|
+
readonly omittedTaskContext: TaskOmission[];
|
|
21
27
|
}
|
|
22
|
-
interface
|
|
28
|
+
interface ModRequest {
|
|
23
29
|
readonly sessionID: string;
|
|
24
30
|
readonly toolUseID?: string | undefined;
|
|
25
31
|
readonly cwd: string;
|
|
26
32
|
readonly toolName: string;
|
|
27
33
|
readonly toolInput: Readonly<Record<string, unknown>>;
|
|
28
|
-
readonly
|
|
34
|
+
readonly context: ModContext;
|
|
29
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
|
+
};
|
|
30
46
|
type Verdict = {
|
|
31
47
|
readonly kind: 'allow';
|
|
32
48
|
} | {
|
|
@@ -349,7 +365,7 @@ interface PolicyPaths {
|
|
|
349
365
|
export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md' | 'judge.md'): Promise<string>;
|
|
350
366
|
//#endregion
|
|
351
367
|
//#region src/request/parse-action-request.d.ts
|
|
352
|
-
export declare function parseActionRequest(body: unknown):
|
|
368
|
+
export declare function parseActionRequest(body: unknown): ParsedActionRequest | null;
|
|
353
369
|
//#endregion
|
|
354
370
|
//#region src/request/render-verdict.d.ts
|
|
355
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-
|
|
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;
|
|
@@ -6652,6 +6652,11 @@ function formatList(one, many, values) {
|
|
|
6652
6652
|
return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
|
|
6653
6653
|
}
|
|
6654
6654
|
//#endregion
|
|
6655
|
+
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
6656
|
+
function findCurrentDirectUserMessage(message) {
|
|
6657
|
+
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
6658
|
+
}
|
|
6659
|
+
//#endregion
|
|
6655
6660
|
//#region src/policy/resolve-shipped-policy-dir.ts
|
|
6656
6661
|
function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
|
|
6657
6662
|
let dir = startDir;
|
|
@@ -6674,13 +6679,6 @@ async function loadPolicy(paths = {}, framework = "classifier.md") {
|
|
|
6674
6679
|
return classifier.replace(RULES_MARKER, rules.trim());
|
|
6675
6680
|
}
|
|
6676
6681
|
//#endregion
|
|
6677
|
-
//#region src/request/find-current-direct-user-message.ts
|
|
6678
|
-
function findCurrentDirectUserMessage(context) {
|
|
6679
|
-
if (context === void 0 || context.agentID !== null) return null;
|
|
6680
|
-
const message = context.lastDirectUserMessage;
|
|
6681
|
-
return message === null || message.freshness === "stale" ? null : message.text;
|
|
6682
|
-
}
|
|
6683
|
-
//#endregion
|
|
6684
6682
|
//#region src/judge/build-judge-message.ts
|
|
6685
6683
|
function buildJudgeMessage(input) {
|
|
6686
6684
|
const evidence = {
|
|
@@ -7289,7 +7287,7 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
7289
7287
|
if (judge.protocol !== "claude-code" && apiKey === null) throw new Error("no API key for the judge");
|
|
7290
7288
|
failure = "policy";
|
|
7291
7289
|
const system = await loadPolicy({ rulesPath: options.rulesPath }, "judge.md");
|
|
7292
|
-
const directMessage = findCurrentDirectUserMessage(payload.decisionContext);
|
|
7290
|
+
const directMessage = findCurrentDirectUserMessage(payload.decisionContext?.lastDirectUserMessage);
|
|
7293
7291
|
const user = buildJudgeMessage({
|
|
7294
7292
|
rule: evidence.rule,
|
|
7295
7293
|
basis: evidence.basis,
|
|
@@ -7393,7 +7391,6 @@ function buildBudgetOmittedContext(context, field) {
|
|
|
7393
7391
|
function buildTaskContext(context) {
|
|
7394
7392
|
let result = {
|
|
7395
7393
|
...context,
|
|
7396
|
-
lastDirectUserMessage: context.agentID === null ? context.lastDirectUserMessage : null,
|
|
7397
7394
|
omittedTaskContext: [...context.omittedTaskContext]
|
|
7398
7395
|
};
|
|
7399
7396
|
for (const field of ["originalUserTask", "delegatedTask"]) {
|
|
@@ -7431,8 +7428,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
|
|
|
7431
7428
|
source: "configured",
|
|
7432
7429
|
text
|
|
7433
7430
|
};
|
|
7434
|
-
const
|
|
7435
|
-
const isStale = context?.agentID === null && context.lastDirectUserMessage?.freshness === "stale";
|
|
7431
|
+
const isStale = payload.decisionContext?.lastDirectUserMessage?.freshness === "stale";
|
|
7436
7432
|
const answerGuidance = isStale ? `${BASE_ANSWER_GUIDANCE} ${STALE_ANSWER_GUIDANCE}` : BASE_ANSWER_GUIDANCE;
|
|
7437
7433
|
const questions = Object.fromEntries(Object.entries(rules).map(([id, rule]) => {
|
|
7438
7434
|
const canClarifyFalsePositive = rule.source === "shipped" && rule.tier === "hard" && (rule.name === "Policy Tampering" || rule.name === "Audit Tampering");
|
|
@@ -7456,7 +7452,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
|
|
|
7456
7452
|
configuredRules,
|
|
7457
7453
|
...repositoryContext === null ? {} : { repositoryContext },
|
|
7458
7454
|
...mcpServers.length === 0 ? {} : { mcpServers },
|
|
7459
|
-
lastUserMessage:
|
|
7455
|
+
lastUserMessage: isStale ? null : lastUserMessage,
|
|
7460
7456
|
...payload.decisionContext === void 0 ? {} : { taskContext: buildTaskContext(payload.decisionContext) },
|
|
7461
7457
|
action: {
|
|
7462
7458
|
tool: payload.toolName,
|
|
@@ -7856,7 +7852,7 @@ async function classifyWithJev(payload, config, options) {
|
|
|
7856
7852
|
key = await resolveApiKey(config.provider, options);
|
|
7857
7853
|
if (key === null) throw new Error("no API key: set the configured environment variable or key command");
|
|
7858
7854
|
stage = "evidence";
|
|
7859
|
-
const directUserText = findCurrentDirectUserMessage(payload.decisionContext);
|
|
7855
|
+
const directUserText = findCurrentDirectUserMessage(payload.decisionContext?.lastDirectUserMessage);
|
|
7860
7856
|
const settings = await (options.claudeSettings ?? loadClaudeSettings(config.claudeSettingsPath, options.host));
|
|
7861
7857
|
const [policy, repositoryContext, mcpServers] = await Promise.all([
|
|
7862
7858
|
loadPolicy({
|
|
@@ -7978,7 +7974,7 @@ async function classifyWithModel(payload, config, options = {}) {
|
|
|
7978
7974
|
} catch (error) {
|
|
7979
7975
|
return buildFailure(config, `policy unreadable: ${toMessage(error)}`);
|
|
7980
7976
|
}
|
|
7981
|
-
const directMessage = findCurrentDirectUserMessage(payload.decisionContext);
|
|
7977
|
+
const directMessage = findCurrentDirectUserMessage(payload.decisionContext?.lastDirectUserMessage);
|
|
7982
7978
|
const transcript = directMessage === null ? [] : [{
|
|
7983
7979
|
role: "user",
|
|
7984
7980
|
text: directMessage
|
|
@@ -8619,10 +8615,11 @@ function parseActionRequest(body) {
|
|
|
8619
8615
|
//#endregion
|
|
8620
8616
|
//#region src/request/render-verdict.ts
|
|
8621
8617
|
function renderVerdict(verdict) {
|
|
8622
|
-
|
|
8618
|
+
const rendered = M(verdict).returnType().with({ kind: "allow" }, () => ({ decision: "allow" })).with({ kind: "deny" }, (denied) => ({
|
|
8623
8619
|
decision: "deny",
|
|
8624
8620
|
reason: `[${denied.rule}] ${denied.reason}`
|
|
8625
8621
|
})).exhaustive();
|
|
8622
|
+
return JSON.stringify(rendered);
|
|
8626
8623
|
}
|
|
8627
8624
|
//#endregion
|
|
8628
8625
|
export { resolveConfigPath as A, resolveStateDir as C, DEFAULT_SCOPE_SOURCES as D, DEFAULT_DENIAL_BUDGET as E, string as F, PRESETS as M, number as N, loadConfig as O, strictObject as P, tryClassifyEdit as S, DEFAULT_CONFIG as T, checkContainment as _, loadSessionScope as a, splitCommandWords as b, classifyLocally as c, sendDecision as d, loadRepositoryContext as f, loadPolicy as g, buildDecisionRequest as h, resolveSessionScopePath as i, readHostEnvironment as j, resolveApiKey as k, classifyWithModel as l, DecisionRequestError as m, parseActionRequest as n, EMPTY_SESSION_SCOPE as o, loadCheckoutRemotes as p, classifyAction as r, buildTaskScope as s, renderVerdict as t, parseModelVerdict as u, toRepositorySlug as v, findCheckout as w, loadClaudeSettings as x, splitShellCommand as y };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { findCurrentDirectUserMessage } from './find-current-direct-user-message.ts';
|
|
3
|
+
|
|
4
|
+
test('it returns the text of a current direct message', () => {
|
|
5
|
+
expect(findCurrentDirectUserMessage({ text: 'Push it to main', origin: 'composer' })).toBe(
|
|
6
|
+
'Push it to main',
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('it returns nothing for a direct message marked stale', () => {
|
|
11
|
+
expect(
|
|
12
|
+
findCurrentDirectUserMessage({
|
|
13
|
+
text: 'Push it to main',
|
|
14
|
+
origin: 'composer',
|
|
15
|
+
freshness: 'stale',
|
|
16
|
+
}),
|
|
17
|
+
).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('it returns nothing when no direct message was sent', () => {
|
|
21
|
+
expect(findCurrentDirectUserMessage(null)).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('it returns nothing when the request carries no decision context', () => {
|
|
25
|
+
expect(findCurrentDirectUserMessage(undefined)).toBeNull();
|
|
26
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DirectUserMessage } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function findCurrentDirectUserMessage(
|
|
4
|
+
message: Readonly<DirectUserMessage> | null | undefined,
|
|
5
|
+
): string | null {
|
|
6
|
+
return message === null || message === undefined || message.freshness === 'stale'
|
|
7
|
+
? null
|
|
8
|
+
: message.text;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { isMessageOrigin } from './is-message-origin.ts';
|
|
3
|
+
|
|
4
|
+
test.each(['composer', 'bridge', 'sdk'])('it accepts %s as a direct message origin', (origin) => {
|
|
5
|
+
expect(isMessageOrigin(origin)).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test.each(['plugin', 'agent.spawn', ''])('it rejects %p as a direct message origin', (origin) => {
|
|
9
|
+
expect(isMessageOrigin(origin)).toBe(false);
|
|
10
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The commands that can create a worktree, a branch, or a PR, keyed by program.
|
|
2
|
+
// Each entry is the words that follow the program and its leading options.
|
|
3
|
+
export const SCOPE_COMMANDS = {
|
|
4
|
+
git: ['worktree add', 'checkout', 'switch', 'branch'],
|
|
5
|
+
gh: ['pr create'],
|
|
6
|
+
} as const;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MESSAGE_ORIGINS } from './message-origins.ts';
|
|
2
|
+
|
|
3
|
+
export type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
|
|
4
|
+
|
|
5
|
+
export interface UserTask {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
readonly origin: MessageOrigin;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DirectUserMessage extends UserTask {
|
|
11
|
+
readonly freshness?: 'stale' | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DelegatedTask {
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly origin: 'agent.spawn';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TaskOmission {
|
|
20
|
+
readonly field: 'originalUserTask' | 'delegatedTask';
|
|
21
|
+
readonly reason: 'unavailable' | 'budget';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ModContext {
|
|
25
|
+
readonly agentID: string | null;
|
|
26
|
+
readonly originalUserTask: UserTask | null;
|
|
27
|
+
readonly delegatedTask: DelegatedTask | null;
|
|
28
|
+
readonly lastDirectUserMessage: DirectUserMessage | null;
|
|
29
|
+
readonly omittedTaskContext: TaskOmission[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModRequest {
|
|
33
|
+
readonly sessionID: string;
|
|
34
|
+
readonly toolUseID?: string | undefined;
|
|
35
|
+
readonly cwd: string;
|
|
36
|
+
readonly toolName: string;
|
|
37
|
+
readonly toolInput: Readonly<Record<string, unknown>>;
|
|
38
|
+
readonly context: ModContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ModScopeRecord {
|
|
42
|
+
readonly sessionID: string;
|
|
43
|
+
readonly cwd: string;
|
|
44
|
+
readonly startedAt: number;
|
|
45
|
+
readonly command: string;
|
|
46
|
+
readonly resultText: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ModVerdict =
|
|
50
|
+
| { readonly decision: 'allow' }
|
|
51
|
+
| { readonly decision: 'deny'; readonly reason: string };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ModRequest, TaskOmission } from '../contract/types.ts';
|
|
2
|
+
import type { PromptContext } from './types.ts';
|
|
3
|
+
|
|
4
|
+
interface ActionRequestInput {
|
|
5
|
+
readonly sessionID: string;
|
|
6
|
+
readonly toolUseID: string | undefined;
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly toolName: string;
|
|
9
|
+
readonly toolInput: Readonly<Record<string, unknown>>;
|
|
10
|
+
readonly agentID: string | null;
|
|
11
|
+
readonly delegatedText: string | undefined;
|
|
12
|
+
readonly prompts: PromptContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildActionRequest(input: Readonly<ActionRequestInput>): ModRequest {
|
|
16
|
+
const isChild = input.agentID !== null;
|
|
17
|
+
|
|
18
|
+
const delegatedTask =
|
|
19
|
+
input.delegatedText === undefined
|
|
20
|
+
? null
|
|
21
|
+
: { text: input.delegatedText, origin: 'agent.spawn' as const };
|
|
22
|
+
|
|
23
|
+
const omittedTaskContext: TaskOmission[] = [
|
|
24
|
+
...(input.prompts.originalUserTask === null
|
|
25
|
+
? [{ field: 'originalUserTask' as const, reason: 'unavailable' as const }]
|
|
26
|
+
: []),
|
|
27
|
+
...(isChild && delegatedTask === null
|
|
28
|
+
? [{ field: 'delegatedTask' as const, reason: 'unavailable' as const }]
|
|
29
|
+
: []),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
sessionID: input.sessionID,
|
|
34
|
+
...(input.toolUseID === undefined ? {} : { toolUseID: input.toolUseID }),
|
|
35
|
+
cwd: input.cwd,
|
|
36
|
+
toolName: input.toolName,
|
|
37
|
+
toolInput: input.toolInput,
|
|
38
|
+
context: {
|
|
39
|
+
agentID: input.agentID,
|
|
40
|
+
originalUserTask: input.prompts.originalUserTask,
|
|
41
|
+
delegatedTask,
|
|
42
|
+
lastDirectUserMessage: input.prompts.lastDirectUserMessage,
|
|
43
|
+
omittedTaskContext,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isMessageOrigin } from '../contract/is-message-origin.ts';
|
|
2
|
+
import type { DirectUserMessage, UserTask } from '../contract/types.ts';
|
|
3
|
+
import type { PromptContext } from './types.ts';
|
|
2
4
|
|
|
3
5
|
export function buildPromptContext(
|
|
4
6
|
previous: PromptContext | null,
|
|
@@ -20,7 +22,7 @@ export function buildPromptContext(
|
|
|
20
22
|
|
|
21
23
|
const origin = input.origin.kind;
|
|
22
24
|
|
|
23
|
-
if (origin
|
|
25
|
+
if (!isMessageOrigin(origin)) {
|
|
24
26
|
return {
|
|
25
27
|
originalUserTask: previous?.originalUserTask ?? null,
|
|
26
28
|
lastDirectUserMessage: buildStaleMessage(previous?.lastDirectUserMessage ?? null),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SCOPE_COMMANDS } from '../contract/scope-commands.ts';
|
|
2
|
+
|
|
3
|
+
// A cheap gate before the record subprocess: options may sit between the
|
|
4
|
+
// program and its subcommand, and the CLI parses the words exactly.
|
|
5
|
+
const SCOPE_COMMAND_PATTERN = new RegExp(
|
|
6
|
+
Object.entries(SCOPE_COMMANDS)
|
|
7
|
+
.map(([program, commands]) => {
|
|
8
|
+
const alternatives = commands.map((command) => command.split(' ').join(String.raw`\s+`));
|
|
9
|
+
|
|
10
|
+
return String.raw`\b${program}\s[^\n]*?\b(?:${alternatives.join('|')})\b`;
|
|
11
|
+
})
|
|
12
|
+
.join('|'),
|
|
13
|
+
'u',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export function isScopeCommand(command: string): boolean {
|
|
17
|
+
return SCOPE_COMMAND_PATTERN.test(command);
|
|
18
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
readonly decision: 'allow' | 'deny';
|
|
3
|
-
readonly reason?: string;
|
|
4
|
-
}
|
|
1
|
+
import type { ModVerdict } from '../contract/types.ts';
|
|
5
2
|
|
|
6
3
|
// The CLI ships with this mod, so any other shape is a mismatched or broken CLI,
|
|
7
4
|
// and only an exact verdict may replace the prompt.
|
|
8
|
-
export function parseDecision(stdout: string):
|
|
5
|
+
export function parseDecision(stdout: string): ModVerdict | null {
|
|
9
6
|
let body: unknown;
|
|
10
7
|
|
|
11
8
|
try {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import type { ModScopeRecord } from '../contract/types.ts';
|
|
2
|
+
import { buildActionRequest } from './build-action-request.ts';
|
|
1
3
|
import { buildPromptContext } from './build-prompt-context.ts';
|
|
4
|
+
import { isScopeCommand } from './is-scope-command.ts';
|
|
5
|
+
import { isToolInput } from './is-tool-input.ts';
|
|
2
6
|
import { parseDecision } from './parse-decision.ts';
|
|
3
7
|
import type { ModOn, ModOptions, PromptContext } from './types.ts';
|
|
4
8
|
|
|
5
|
-
// Commands that can create a worktree, a branch, or a PR. Any other call skips
|
|
6
|
-
// the record subprocess, so ordinary calls pay nothing for the session scope.
|
|
7
|
-
const SCOPE_COMMAND =
|
|
8
|
-
/\bgit\s[^\n]*?\b(?:worktree\s+add|checkout|switch|branch)\b|\bgh\s+pr\s+create\b/u;
|
|
9
|
-
|
|
10
9
|
// The record may look up a created PR's head branch with gh, which the CLI
|
|
11
10
|
// bounds at 5 seconds.
|
|
12
11
|
const RECORD_TIMEOUT_MS = 8000;
|
|
@@ -73,7 +72,7 @@ export function register(on: ModOn, options: ModOptions): void {
|
|
|
73
72
|
activeCalls.set(e.tool_use_id, e.agentId ?? null);
|
|
74
73
|
|
|
75
74
|
const command =
|
|
76
|
-
e.tool === 'Bash' && typeof e.command === 'string' &&
|
|
75
|
+
e.tool === 'Bash' && typeof e.command === 'string' && isScopeCommand(e.command)
|
|
77
76
|
? e.command
|
|
78
77
|
: null;
|
|
79
78
|
|
|
@@ -89,7 +88,14 @@ export function register(on: ModOn, options: ModOptions): void {
|
|
|
89
88
|
|
|
90
89
|
if (command !== null && cwd !== null && sessionID !== null && result.deny === undefined) {
|
|
91
90
|
const executable = typeof options.command === 'string' ? options.command : 'auto-mode';
|
|
92
|
-
|
|
91
|
+
|
|
92
|
+
const request: ModScopeRecord = {
|
|
93
|
+
sessionID,
|
|
94
|
+
cwd,
|
|
95
|
+
startedAt,
|
|
96
|
+
command,
|
|
97
|
+
resultText: result.text ?? '',
|
|
98
|
+
};
|
|
93
99
|
|
|
94
100
|
try {
|
|
95
101
|
await $.process.run([executable, 'record'], {
|
|
@@ -137,26 +143,17 @@ export function register(on: ModOn, options: ModOptions): void {
|
|
|
137
143
|
|
|
138
144
|
const agentID = e.tool_use_id === undefined ? null : activeCalls.get(e.tool_use_id);
|
|
139
145
|
|
|
140
|
-
if (
|
|
146
|
+
if (agentID === undefined) {
|
|
141
147
|
$.ui.log(`${logPrefix} evaluation skipped; untracked tool call`, { to: 'debug' });
|
|
142
148
|
|
|
143
149
|
return decided;
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
if (!isToolInput(e.input)) {
|
|
153
|
+
$.ui.log(`${logPrefix} evaluation skipped; tool input is not an object`, { to: 'debug' });
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const omittedTaskContext = [
|
|
153
|
-
...(prompts.originalUserTask === null
|
|
154
|
-
? [{ field: 'originalUserTask', reason: 'unavailable' }]
|
|
155
|
-
: []),
|
|
156
|
-
...(isChild && delegatedTask === null
|
|
157
|
-
? [{ field: 'delegatedTask', reason: 'unavailable' }]
|
|
158
|
-
: []),
|
|
159
|
-
];
|
|
155
|
+
return decided;
|
|
156
|
+
}
|
|
160
157
|
|
|
161
158
|
const command = typeof options.command === 'string' ? options.command : 'auto-mode';
|
|
162
159
|
const childTimeoutMs = EVALUATION_TIMEOUT_MS;
|
|
@@ -164,20 +161,16 @@ export function register(on: ModOn, options: ModOptions): void {
|
|
|
164
161
|
|
|
165
162
|
$.ui.log(`${logPrefix} evaluator invoked`, { to: 'debug' });
|
|
166
163
|
|
|
167
|
-
const request = {
|
|
164
|
+
const request = buildActionRequest({
|
|
168
165
|
sessionID,
|
|
169
|
-
|
|
166
|
+
toolUseID: e.tool_use_id,
|
|
170
167
|
cwd,
|
|
171
168
|
toolName: e.tool,
|
|
172
169
|
toolInput: e.input,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
lastDirectUserMessage: isChild ? null : prompts.lastDirectUserMessage,
|
|
178
|
-
omittedTaskContext,
|
|
179
|
-
},
|
|
180
|
-
};
|
|
170
|
+
agentID,
|
|
171
|
+
delegatedText: agentID === null ? undefined : delegatedTasks.get(agentID),
|
|
172
|
+
prompts,
|
|
173
|
+
});
|
|
181
174
|
|
|
182
175
|
const result = await $.process.run(
|
|
183
176
|
[command, 'run', '--jev-only', '--evaluation-deadline', String(deadlineAt)],
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DirectUserMessage, UserTask } from '../contract/types.ts';
|
|
2
|
+
|
|
1
3
|
export interface PermissionDecision {
|
|
2
4
|
readonly decision: 'allow' | 'ask' | 'deny';
|
|
3
5
|
readonly reason?: string;
|
|
@@ -45,15 +47,6 @@ export interface CallResult {
|
|
|
45
47
|
readonly isError?: true;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export interface UserTask {
|
|
49
|
-
readonly text: string;
|
|
50
|
-
readonly origin: 'composer' | 'bridge' | 'sdk';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface DirectUserMessage extends UserTask {
|
|
54
|
-
readonly freshness?: 'stale';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
50
|
export interface PromptContext {
|
|
58
51
|
readonly originalUserTask: UserTask | null;
|
|
59
52
|
readonly lastDirectUserMessage: DirectUserMessage | null;
|