auto-mode 0.4.2 → 0.5.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.
@@ -14,7 +14,6 @@ test('it keeps the original human task separate from later instructions', () =>
14
14
  expect(later).toStrictEqual({
15
15
  originalUserTask: { text: 'Build the parser', origin: 'composer' },
16
16
  lastDirectUserMessage: { text: 'Do not push', origin: 'bridge' },
17
- hasPrompt: true,
18
17
  canCaptureOriginal: true,
19
18
  });
20
19
  });
@@ -14,7 +14,6 @@ export function buildPromptContext(
14
14
  return {
15
15
  originalUserTask: null,
16
16
  lastDirectUserMessage: null,
17
- hasPrompt: false,
18
17
  canCaptureOriginal: input.source === 'startup' || input.source === 'clear',
19
18
  };
20
19
  }
@@ -31,7 +30,6 @@ export function buildPromptContext(
31
30
  previous?.originalUserTask ??
32
31
  (previous?.canCaptureOriginal === true ? lastDirectUserMessage : null),
33
32
  lastDirectUserMessage,
34
- hasPrompt: true,
35
33
  canCaptureOriginal: previous?.canCaptureOriginal ?? false,
36
34
  };
37
35
  }
@@ -3,6 +3,8 @@ interface Decision {
3
3
  readonly reason?: string;
4
4
  }
5
5
 
6
+ // The CLI ships with this mod, so any other shape is a mismatched or broken CLI,
7
+ // and only an exact verdict may replace the prompt.
6
8
  export function parseDecision(stdout: string): Decision | null {
7
9
  let body: unknown;
8
10
 
@@ -12,39 +14,25 @@ export function parseDecision(stdout: string): Decision | null {
12
14
  return null;
13
15
  }
14
16
 
15
- if (typeof body !== 'object' || body === null || !('hookSpecificOutput' in body)) {
17
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
16
18
  return null;
17
19
  }
18
20
 
19
- const output = body.hookSpecificOutput;
21
+ const keys = Object.keys(body).toSorted().join(',');
20
22
 
21
- if (
22
- typeof output !== 'object' ||
23
- output === null ||
24
- !('hookEventName' in output) ||
25
- output.hookEventName !== 'PermissionRequest' ||
26
- !('decision' in output)
27
- ) {
28
- return null;
29
- }
30
-
31
- const value = output.decision;
32
-
33
- if (typeof value !== 'object' || value === null || !('behavior' in value)) {
34
- return null;
35
- }
36
-
37
- if (value.behavior === 'allow') {
23
+ if (keys === 'decision' && 'decision' in body && body.decision === 'allow') {
38
24
  return { decision: 'allow' };
39
25
  }
40
26
 
41
27
  if (
42
- value.behavior === 'deny' &&
43
- 'message' in value &&
44
- typeof value.message === 'string' &&
45
- value.message.trim() !== ''
28
+ keys === 'decision,reason' &&
29
+ 'decision' in body &&
30
+ body.decision === 'deny' &&
31
+ 'reason' in body &&
32
+ typeof body.reason === 'string' &&
33
+ body.reason.trim() !== ''
46
34
  ) {
47
- return { decision: 'deny', reason: value.message };
35
+ return { decision: 'deny', reason: body.reason };
48
36
  }
49
37
 
50
38
  return null;
@@ -1,5 +1,13 @@
1
1
  import { expect, test } from 'claude-code/testing';
2
2
 
3
+ const NO_PROMPT_CONTEXT = {
4
+ agentID: null,
5
+ originalUserTask: null,
6
+ delegatedTask: null,
7
+ lastDirectUserMessage: null,
8
+ omittedTaskContext: [{ field: 'originalUserTask', reason: 'unavailable' }],
9
+ };
10
+
3
11
  test('it preserves an existing denial and its rule', async ($, on) => {
4
12
  const decided = {
5
13
  decision: 'deny',
@@ -20,8 +28,7 @@ test('it preserves an existing denial and its rule', async ($, on) => {
20
28
  return {
21
29
  value: {
22
30
  exitCode: 0,
23
- stdout:
24
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
31
+ stdout: '{"decision":"allow"}',
25
32
  stderr: 'synthetic-private-fragment',
26
33
  isStdoutTruncated: false,
27
34
  isStderrTruncated: false,
@@ -66,8 +73,7 @@ test('it preserves an existing allowance without a second evaluator', async ($,
66
73
  return {
67
74
  value: {
68
75
  exitCode: 0,
69
- stdout:
70
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"[Data Exfiltration] Refuse the transfer."}}}',
76
+ stdout: '{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer."}',
71
77
  stderr: 'synthetic-private-fragment',
72
78
  isStdoutTruncated: false,
73
79
  isStderrTruncated: false,
@@ -118,8 +124,7 @@ test('it approves an ask after Jev allows the action', async ($, on) => {
118
124
  return {
119
125
  value: {
120
126
  exitCode: 0,
121
- stdout:
122
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
127
+ stdout: '{"decision":"allow"}',
123
128
  stderr: 'synthetic-private-fragment',
124
129
  isStdoutTruncated: false,
125
130
  isStderrTruncated: false,
@@ -146,13 +151,11 @@ test('it approves an ask after Jev allows the action', async ($, on) => {
146
151
  expect(timeoutMs).toBeLessThanOrEqual(8000);
147
152
 
148
153
  expect(JSON.parse(stdin)).toStrictEqual({
149
- hook_event_name: 'PermissionRequest',
150
- prompt_id: 'mod-check',
151
- session_id: 'session-1',
154
+ sessionID: 'session-1',
152
155
  cwd: '/repo',
153
- transcript_path: '/repo/transcript.jsonl',
154
- tool_name: 'Bash',
155
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
156
+ toolName: 'Bash',
157
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
158
+ context: NO_PROMPT_CONTEXT,
156
159
  });
157
160
 
158
161
  expect(invocation).toMatchObject({
@@ -187,8 +190,7 @@ test('it refuses an ask after Jev denies the action', async ($, on) => {
187
190
  return {
188
191
  value: {
189
192
  exitCode: 0,
190
- stdout:
191
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"[Data Exfiltration] Refuse the transfer."}}}',
193
+ stdout: '{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer."}',
192
194
  stderr: 'synthetic-private-fragment',
193
195
  isStdoutTruncated: false,
194
196
  isStderrTruncated: false,
@@ -219,13 +221,11 @@ test('it refuses an ask after Jev denies the action', async ($, on) => {
219
221
  expect(timeoutMs).toBeLessThanOrEqual(8000);
220
222
 
221
223
  expect(JSON.parse(stdin)).toStrictEqual({
222
- hook_event_name: 'PermissionRequest',
223
- prompt_id: 'mod-check',
224
- session_id: 'session-1',
224
+ sessionID: 'session-1',
225
225
  cwd: '/repo',
226
- transcript_path: '/repo/transcript.jsonl',
227
- tool_name: 'Bash',
228
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
226
+ toolName: 'Bash',
227
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
228
+ context: NO_PROMPT_CONTEXT,
229
229
  });
230
230
 
231
231
  expect(invocation).toMatchObject({
@@ -287,13 +287,11 @@ test('it retains manual approval when Jev returns no opinion', async ($, on) =>
287
287
  expect(timeoutMs).toBeLessThanOrEqual(8000);
288
288
 
289
289
  expect(JSON.parse(stdin)).toStrictEqual({
290
- hook_event_name: 'PermissionRequest',
291
- prompt_id: 'mod-check',
292
- session_id: 'session-1',
290
+ sessionID: 'session-1',
293
291
  cwd: '/repo',
294
- transcript_path: '/repo/transcript.jsonl',
295
- tool_name: 'Bash',
296
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
292
+ toolName: 'Bash',
293
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
294
+ context: NO_PROMPT_CONTEXT,
297
295
  });
298
296
 
299
297
  expect(invocation).toMatchObject({
@@ -355,13 +353,11 @@ test('it retains manual approval for malformed JSON without copying diagnostics'
355
353
  expect(timeoutMs).toBeLessThanOrEqual(8000);
356
354
 
357
355
  expect(JSON.parse(stdin)).toStrictEqual({
358
- hook_event_name: 'PermissionRequest',
359
- prompt_id: 'mod-check',
360
- session_id: 'session-1',
356
+ sessionID: 'session-1',
361
357
  cwd: '/repo',
362
- transcript_path: '/repo/transcript.jsonl',
363
- tool_name: 'Bash',
364
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
358
+ toolName: 'Bash',
359
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
360
+ context: NO_PROMPT_CONTEXT,
365
361
  });
366
362
 
367
363
  expect(invocation).toMatchObject({
@@ -370,7 +366,7 @@ test('it retains manual approval for malformed JSON without copying diagnostics'
370
366
  });
371
367
  });
372
368
 
373
- test('it retains manual approval for a verdict from another event', async ($, on) => {
369
+ test('it retains manual approval for a hook-shaped verdict', async ($, on) => {
374
370
  const decided = {
375
371
  decision: 'ask',
376
372
  reason: 'Existing permission decision',
@@ -424,13 +420,11 @@ test('it retains manual approval for a verdict from another event', async ($, on
424
420
  expect(timeoutMs).toBeLessThanOrEqual(8000);
425
421
 
426
422
  expect(JSON.parse(stdin)).toStrictEqual({
427
- hook_event_name: 'PermissionRequest',
428
- prompt_id: 'mod-check',
429
- session_id: 'session-1',
423
+ sessionID: 'session-1',
430
424
  cwd: '/repo',
431
- transcript_path: '/repo/transcript.jsonl',
432
- tool_name: 'Bash',
433
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
425
+ toolName: 'Bash',
426
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
427
+ context: NO_PROMPT_CONTEXT,
434
428
  });
435
429
 
436
430
  expect(invocation).toMatchObject({
@@ -439,6 +433,47 @@ test('it retains manual approval for a verdict from another event', async ($, on
439
433
  });
440
434
  });
441
435
 
436
+ test('it retains manual approval for a verdict with unknown or mistyped fields', async ($, on) => {
437
+ const outputs = [
438
+ '{"decision":"allow","reason":123}',
439
+ '{"decision":"allow","rule":"Read-only actions"}',
440
+ '{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer.","extra":true}',
441
+ '{"decision":"deny","reason":42}',
442
+ '["allow"]',
443
+ ];
444
+
445
+ let stdout = '';
446
+
447
+ on('classic.SessionStart', () => ({}));
448
+ on('session.cwd', () => ({ value: '/repo' }));
449
+ on('tool.check', () => ({ decision: 'ask' }));
450
+
451
+ on('process.run', () => ({
452
+ value: {
453
+ exitCode: 0,
454
+ stdout,
455
+ stderr: '',
456
+ isStdoutTruncated: false,
457
+ isStderrTruncated: false,
458
+ },
459
+ }));
460
+
461
+ // oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
462
+ await $.classic.SessionStart({ source: 'startup', session_id: 'session-1', cwd: '/repo' });
463
+
464
+ const results: unknown[] = [];
465
+
466
+ for (const output of outputs) {
467
+ stdout = output;
468
+
469
+ const result = await $.tool.check({ tool: 'Bash', input: { command: 'rm fixture.txt' } });
470
+
471
+ results.push(result);
472
+ }
473
+
474
+ expect(results).toStrictEqual(outputs.map(() => ({ decision: 'ask' })));
475
+ });
476
+
442
477
  test('it retains manual approval for a malformed denial', async ($, on) => {
443
478
  const decided = {
444
479
  decision: 'ask',
@@ -465,8 +500,7 @@ test('it retains manual approval for a malformed denial', async ($, on) => {
465
500
  return {
466
501
  value: {
467
502
  exitCode: 0,
468
- stdout:
469
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny"}}}',
503
+ stdout: '{"decision":"deny"}',
470
504
  stderr: 'synthetic-private-fragment',
471
505
  isStdoutTruncated: false,
472
506
  isStderrTruncated: false,
@@ -493,13 +527,11 @@ test('it retains manual approval for a malformed denial', async ($, on) => {
493
527
  expect(timeoutMs).toBeLessThanOrEqual(8000);
494
528
 
495
529
  expect(JSON.parse(stdin)).toStrictEqual({
496
- hook_event_name: 'PermissionRequest',
497
- prompt_id: 'mod-check',
498
- session_id: 'session-1',
530
+ sessionID: 'session-1',
499
531
  cwd: '/repo',
500
- transcript_path: '/repo/transcript.jsonl',
501
- tool_name: 'Bash',
502
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
532
+ toolName: 'Bash',
533
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
534
+ context: NO_PROMPT_CONTEXT,
503
535
  });
504
536
 
505
537
  expect(invocation).toMatchObject({
@@ -534,8 +566,7 @@ test('it retains manual approval for a truncated response', async ($, on) => {
534
566
  return {
535
567
  value: {
536
568
  exitCode: 0,
537
- stdout:
538
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
569
+ stdout: '{"decision":"allow"}',
539
570
  stderr: 'synthetic-private-fragment',
540
571
  isStdoutTruncated: true,
541
572
  isStderrTruncated: false,
@@ -562,13 +593,11 @@ test('it retains manual approval for a truncated response', async ($, on) => {
562
593
  expect(timeoutMs).toBeLessThanOrEqual(8000);
563
594
 
564
595
  expect(JSON.parse(stdin)).toStrictEqual({
565
- hook_event_name: 'PermissionRequest',
566
- prompt_id: 'mod-check',
567
- session_id: 'session-1',
596
+ sessionID: 'session-1',
568
597
  cwd: '/repo',
569
- transcript_path: '/repo/transcript.jsonl',
570
- tool_name: 'Bash',
571
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
598
+ toolName: 'Bash',
599
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
600
+ context: NO_PROMPT_CONTEXT,
572
601
  });
573
602
 
574
603
  expect(invocation).toMatchObject({
@@ -603,8 +632,7 @@ test('it retains manual approval when the child exits nonzero', async ($, on) =>
603
632
  return {
604
633
  value: {
605
634
  exitCode: 1,
606
- stdout:
607
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
635
+ stdout: '{"decision":"allow"}',
608
636
  stderr: 'synthetic-private-fragment',
609
637
  isStdoutTruncated: false,
610
638
  isStderrTruncated: false,
@@ -631,13 +659,11 @@ test('it retains manual approval when the child exits nonzero', async ($, on) =>
631
659
  expect(timeoutMs).toBeLessThanOrEqual(8000);
632
660
 
633
661
  expect(JSON.parse(stdin)).toStrictEqual({
634
- hook_event_name: 'PermissionRequest',
635
- prompt_id: 'mod-check',
636
- session_id: 'session-1',
662
+ sessionID: 'session-1',
637
663
  cwd: '/repo',
638
- transcript_path: '/repo/transcript.jsonl',
639
- tool_name: 'Bash',
640
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
664
+ toolName: 'Bash',
665
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
666
+ context: NO_PROMPT_CONTEXT,
641
667
  });
642
668
 
643
669
  expect(invocation).toMatchObject({
@@ -690,13 +716,11 @@ test('it retains manual approval when the child times out', async ($, on) => {
690
716
  expect(timeoutMs).toBeLessThanOrEqual(8000);
691
717
 
692
718
  expect(JSON.parse(stdin)).toStrictEqual({
693
- hook_event_name: 'PermissionRequest',
694
- prompt_id: 'mod-check',
695
- session_id: 'session-1',
719
+ sessionID: 'session-1',
696
720
  cwd: '/repo',
697
- transcript_path: '/repo/transcript.jsonl',
698
- tool_name: 'Bash',
699
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
721
+ toolName: 'Bash',
722
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
723
+ context: NO_PROMPT_CONTEXT,
700
724
  });
701
725
 
702
726
  expect(invocation).toMatchObject({
@@ -724,8 +748,7 @@ test('it retains manual approval before the session context arrives', async ($,
724
748
  return {
725
749
  value: {
726
750
  exitCode: 0,
727
- stdout:
728
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
751
+ stdout: '{"decision":"allow"}',
729
752
  stderr: 'synthetic-private-fragment',
730
753
  isStdoutTruncated: false,
731
754
  isStderrTruncated: false,
@@ -771,8 +794,7 @@ test(
771
794
  return {
772
795
  value: {
773
796
  exitCode: 0,
774
- stdout:
775
- '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}',
797
+ stdout: '{"decision":"allow"}',
776
798
  stderr: 'synthetic-private-fragment',
777
799
  isStdoutTruncated: false,
778
800
  isStderrTruncated: false,
@@ -799,13 +821,11 @@ test(
799
821
  expect(timeoutMs).toBeLessThanOrEqual(8000);
800
822
 
801
823
  expect(JSON.parse(stdin)).toStrictEqual({
802
- hook_event_name: 'PermissionRequest',
803
- prompt_id: 'mod-check',
804
- session_id: 'session-1',
824
+ sessionID: 'session-1',
805
825
  cwd: '/repo',
806
- transcript_path: '/repo/transcript.jsonl',
807
- tool_name: 'Bash',
808
- tool_input: { command: 'git push origin feature', timeout: 120_000 },
826
+ toolName: 'Bash',
827
+ toolInput: { command: 'git push origin feature', timeout: 120_000 },
828
+ context: NO_PROMPT_CONTEXT,
809
829
  });
810
830
 
811
831
  expect(invocation).toMatchObject({
@@ -856,11 +876,45 @@ test('it refreshes the context from the current user prompt after reload', async
856
876
  });
857
877
 
858
878
  expect(result).toStrictEqual({ decision: 'ask' });
879
+ expect(JSON.parse(stdin)).toMatchObject({ sessionID: 'new-session', cwd: '/new-repo' });
880
+ expect(stdin.includes('transcript')).toBe(false);
881
+ });
882
+
883
+ test('it carries the session identity and the direct user message', async ($, on) => {
884
+ let stdin = '';
885
+
886
+ on('classic.SessionStart', () => ({}));
887
+ on('prompt.submit', (_api, e) => ({ text: e.text }));
888
+ on('session.cwd', () => ({ value: '/repo' }));
889
+ on('tool.check', () => ({ decision: 'ask' }));
890
+
891
+ on('process.run', (_api, e) => {
892
+ stdin = e.init?.stdin ?? '';
893
+
894
+ return {
895
+ value: {
896
+ exitCode: 0,
897
+ stdout: '',
898
+ stderr: '',
899
+ isStdoutTruncated: false,
900
+ isStderrTruncated: false,
901
+ },
902
+ };
903
+ });
904
+
905
+ // oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
906
+ await $.classic.SessionStart({ source: 'startup', session_id: 'session-1', cwd: '/repo' });
907
+ await $.prompt.submit({ text: 'Clean the build output.', origin: { kind: 'composer' } });
908
+ await $.tool.check({ tool: 'Bash', input: { command: 'rm -rf dist' } });
859
909
 
860
910
  expect(JSON.parse(stdin)).toMatchObject({
861
- session_id: 'new-session',
862
- cwd: '/new-repo',
863
- transcript_path: '/new-repo/current.jsonl',
911
+ sessionID: 'session-1',
912
+ context: {
913
+ agentID: null,
914
+ originalUserTask: { text: 'Clean the build output.' },
915
+ lastDirectUserMessage: { text: 'Clean the build output.' },
916
+ omittedTaskContext: [],
917
+ },
864
918
  });
865
919
  });
866
920
 
@@ -892,7 +946,7 @@ test('it preserves the complete action without truncation', async ($, on) => {
892
946
  const result = await $.tool.check({ tool: 'Write', input: { file_path: '/repo/file', content } });
893
947
 
894
948
  expect(result).toStrictEqual({ decision: 'ask' });
895
- expect(JSON.parse(stdin)).toMatchObject({ tool_input: { file_path: '/repo/file', content } });
949
+ expect(JSON.parse(stdin)).toMatchObject({ toolInput: { file_path: '/repo/file', content } });
896
950
  });
897
951
 
898
952
  test('it does not replace the main context with a subagent prompt', async ($, on) => {
@@ -940,12 +994,7 @@ test('it does not replace the main context with a subagent prompt', async ($, on
940
994
  });
941
995
 
942
996
  expect(result).toStrictEqual({ decision: 'ask' });
943
-
944
- expect(JSON.parse(stdin)).toMatchObject({
945
- cwd: '/main',
946
- session_id: 'main-session',
947
- transcript_path: '/main/user.jsonl',
948
- });
997
+ expect(JSON.parse(stdin)).toMatchObject({ cwd: '/main', sessionID: 'main-session' });
949
998
  });
950
999
 
951
1000
  test('it honors a configured fail-closed classifier verdict', async ($, on) => {
@@ -957,10 +1006,8 @@ test('it honors a configured fail-closed classifier verdict', async ($, on) => {
957
1006
  value: {
958
1007
  exitCode: 0,
959
1008
  stdout: JSON.stringify({
960
- hookSpecificOutput: {
961
- hookEventName: 'PermissionRequest',
962
- decision: { behavior: 'deny', message: '[Classifier Unavailable] Jev unavailable.' },
963
- },
1009
+ decision: 'deny',
1010
+ reason: '[Classifier Unavailable] Jev unavailable.',
964
1011
  }),
965
1012
  stderr: '',
966
1013
  isStdoutTruncated: false,
@@ -3,10 +3,7 @@ import { parseDecision } from './parse-decision.ts';
3
3
  import type { ModOn, ModOptions, PromptContext } from './types.ts';
4
4
 
5
5
  export function register(on: ModOn, options: ModOptions): void {
6
- let context: {
7
- readonly sessionID: string;
8
- readonly transcriptPath: string;
9
- } | null = null;
6
+ let sessionID: string | null = null;
10
7
 
11
8
  const activeCalls = new Map<string, string | null>();
12
9
  const delegatedTasks = new Map<string, string>();
@@ -15,9 +12,9 @@ export function register(on: ModOn, options: ModOptions): void {
15
12
 
16
13
  on('classic.SessionStart', (_api, e, next) => {
17
14
  if (e.agent_id === undefined) {
18
- const isCompact = e.source === 'compact' && context?.sessionID === e.session_id;
15
+ const isCompact = e.source === 'compact' && sessionID === e.session_id;
19
16
 
20
- context = { sessionID: e.session_id, transcriptPath: e.transcript_path };
17
+ sessionID = e.session_id;
21
18
 
22
19
  const previousPrompts = isCompact ? prompts : null;
23
20
 
@@ -33,7 +30,7 @@ export function register(on: ModOn, options: ModOptions): void {
33
30
 
34
31
  on('classic.UserPromptSubmit', (_api, e, next) => {
35
32
  if (e.agent_id === undefined) {
36
- context = { sessionID: e.session_id, transcriptPath: e.transcript_path };
33
+ sessionID = e.session_id;
37
34
  }
38
35
 
39
36
  return next(e);
@@ -83,7 +80,7 @@ export function register(on: ModOn, options: ModOptions): void {
83
80
  return decided;
84
81
  }
85
82
 
86
- if (next.signal.aborted || context === null) {
83
+ if (next.signal.aborted || sessionID === null) {
87
84
  const status = next.signal.aborted ? 'cancelled' : 'missing session context';
88
85
 
89
86
  $.ui.log(`${logPrefix} evaluation skipped; ${status}`, { to: 'debug' });
@@ -140,31 +137,24 @@ export function register(on: ModOn, options: ModOptions): void {
140
137
 
141
138
  $.ui.log(`${logPrefix} evaluator invoked`, { to: 'debug' });
142
139
 
140
+ const request = {
141
+ sessionID,
142
+ ...(e.tool_use_id === undefined ? {} : { toolUseID: e.tool_use_id }),
143
+ cwd,
144
+ toolName: e.tool,
145
+ toolInput: e.input,
146
+ context: {
147
+ agentID: agentID ?? null,
148
+ originalUserTask: prompts.originalUserTask,
149
+ delegatedTask,
150
+ lastDirectUserMessage: isChild ? null : prompts.lastDirectUserMessage,
151
+ omittedTaskContext,
152
+ },
153
+ };
154
+
143
155
  const result = await $.process.run(
144
156
  [command, 'run', '--jev-only', '--evaluation-deadline', String(deadlineAt)],
145
- {
146
- timeoutMs: childTimeoutMs,
147
- stdin: JSON.stringify({
148
- hook_event_name: 'PermissionRequest',
149
- prompt_id: e.tool_use_id ?? 'mod-check',
150
- session_id: context.sessionID,
151
- cwd,
152
- ...(isChild ? {} : { transcript_path: context.transcriptPath }),
153
- ...(prompts.hasPrompt || isChild
154
- ? {
155
- auto_mode_context: {
156
- agentID: agentID ?? null,
157
- originalUserTask: prompts.originalUserTask,
158
- delegatedTask,
159
- lastDirectUserMessage: isChild ? null : prompts.lastDirectUserMessage,
160
- omittedTaskContext,
161
- },
162
- }
163
- : {}),
164
- tool_name: e.tool,
165
- tool_input: e.input,
166
- }),
167
- },
157
+ { timeoutMs: childTimeoutMs, stdin: JSON.stringify(request) },
168
158
  );
169
159
 
170
160
  if (result.exitCode !== 0 || result.isStdoutTruncated || next.signal.aborted) {
@@ -45,7 +45,6 @@ export interface UserTask {
45
45
  export interface PromptContext {
46
46
  readonly originalUserTask: UserTask | null;
47
47
  readonly lastDirectUserMessage: UserTask | null;
48
- readonly hasPrompt: boolean;
49
48
  readonly canCaptureOriginal: boolean;
50
49
  }
51
50
 
@@ -100,7 +99,7 @@ export interface ModAPI {
100
99
  readonly process: {
101
100
  readonly run: (argv: readonly string[], init?: ProcessInput['init']) => Promise<ProcessResult>;
102
101
  };
103
- readonly prompt: { readonly submit: (input: Pick<PromptInput, 'text'>) => Promise<object> };
102
+ readonly prompt: { readonly submit: (input: PromptInput) => Promise<object> };
104
103
  readonly tool: { readonly check: (input: CheckInput) => Promise<PermissionDecision> };
105
104
  readonly classic: {
106
105
  readonly SessionStart: (
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.4.2",
4
- "description": "A permission classifier that runs as a hook, so agents that are not Claude can run unattended",
3
+ "version": "0.5.0",
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",
7
7
  "claude-code",
8
- "codex",
9
- "hook",
10
- "muse",
8
+ "mod",
11
9
  "permissions"
12
10
  ],
13
11
  "homepage": "https://github.com/zgeoff/auto-mode#readme",
@@ -41,7 +39,13 @@
41
39
  "build:agents": "bash scripts/build-agents-md.sh agents/shared.md agents/project.md AGENTS.md",
42
40
  "check": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test && bun run build && bun run check:artifact && bun run publint",
43
41
  "deadcode": "knip",
42
+ "eval:answer-guidance": "bun --no-env-file scripts/run-answer-guidance-evaluation.ts",
44
43
  "eval:applicability": "bun --no-env-file scripts/run-applicability-evaluation.ts",
44
+ "eval:containment": "bun --no-env-file scripts/run-containment-evaluation.ts",
45
+ "eval:question-shape": "bun --no-env-file scripts/run-question-shape-evaluation.ts",
46
+ "eval:relay-consent": "bun --no-env-file scripts/run-relay-consent-evaluation.ts",
47
+ "eval:second-judge": "bun --no-env-file scripts/run-second-judge-evaluation.ts",
48
+ "eval:stale-consent": "bun --no-env-file scripts/run-stale-consent-evaluation.ts",
45
49
  "first-publish": "scripts/first-publish.sh",
46
50
  "format": "oxfmt . && format-codemod --quiet .",
47
51
  "format:check": "oxfmt --check . && format-codemod --check --quiet .",
@@ -77,6 +81,9 @@
77
81
  "typescript": "7.0.2",
78
82
  "zod": "4.6.2"
79
83
  },
84
+ "overrides": {
85
+ "smol-toml": "1.9.0"
86
+ },
80
87
  "engines": {
81
88
  "node": ">=24"
82
89
  },