@code-yeongyu/senpi 2026.10.5 → 2026.10.7

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.
Files changed (151) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-SO5RJ2N7.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
  8. package/dist/bundle/chunks/chunk-7SMFF2QJ.js +14 -0
  9. package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-6JXPZACL.js → chunk-CM5QTRC2.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
  12. package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-EUU6W5B3.js} +180 -176
  15. package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-HKG7XL4H.js → chunk-HY4AOLON.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-JJNIU2A2.js} +8 -4
  18. package/dist/bundle/chunks/{chunk-22DAHAHO.js → chunk-LUWWM7GA.js} +2 -2
  19. package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
  23. package/dist/bundle/chunks/chunk-VJJ53ORU.js +2 -0
  24. package/dist/bundle/chunks/{chunk-XYDZ3LIE.js → chunk-YI7SPFDN.js} +2 -2
  25. package/dist/bundle/chunks/chunk-ZJHRDQCH.js +2 -0
  26. package/dist/bundle/chunks/cli-main-XBQN73O2.js +3 -0
  27. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
  28. package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
  29. package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
  30. package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
  31. package/dist/bundle/chunks/host-command-YXQAXNKK.js +2 -0
  32. package/dist/bundle/chunks/{host-lifecycle-7OVWGVIZ.js → host-lifecycle-L4XQIATP.js} +1 -1
  33. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  34. package/dist/bundle/chunks/interactive-mode-ROO5W56P.js +2 -0
  35. package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
  36. package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-5WTKAQJM.js} +5 -5
  37. package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
  38. package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
  39. package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
  40. package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
  41. package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
  42. package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-NCD5FQGV.js} +1 -1
  43. package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
  44. package/dist/bundle/chunks/rpc-mode-MWURKCP2.js +2 -0
  45. package/dist/bundle/chunks/{schedule-command-QTYSUYO2.js → schedule-command-UTXB465F.js} +1 -1
  46. package/dist/bundle/chunks/session-control-endpoint-J6RHZXWB.js +3 -0
  47. package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
  48. package/dist/bundle/chunks/session-worker.js +376 -368
  49. package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
  50. package/dist/bundle/cli.js +1 -1
  51. package/dist/bundle/index.js +1 -1
  52. package/dist/bundle/rpc-entry.js +1 -1
  53. package/dist/bundle/runtime-manifest.json +1 -1
  54. package/dist/core/agent-session-runtime.d.ts +12 -0
  55. package/dist/core/agent-session.d.ts +18 -0
  56. package/dist/core/agent-session.js +55 -19
  57. package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
  58. package/dist/core/dynamic-prompt/handoff.js +1 -1
  59. package/dist/core/extensions/builtin/diff.js +14 -9
  60. package/dist/core/extensions/builtin/files.js +6 -2
  61. package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
  62. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
  63. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
  64. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
  65. package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
  66. package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
  67. package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
  68. package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
  69. package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
  70. package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
  71. package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
  72. package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
  73. package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
  74. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
  75. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
  76. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
  77. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
  78. package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
  79. package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
  80. package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
  81. package/dist/core/extensions/builtin/permission-system/config.js +13 -1
  82. package/dist/core/extensions/builtin/permission-system/index.js +20 -12
  83. package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
  84. package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
  85. package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
  86. package/dist/core/extensions/builtin/permission-system/service.js +43 -7
  87. package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
  88. package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
  89. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  90. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
  91. package/dist/core/extensions/eval-handle-host.d.ts +82 -0
  92. package/dist/core/extensions/eval-handle-host.js +28 -0
  93. package/dist/core/extensions/loader.js +5 -0
  94. package/dist/core/extensions/runner.js +4 -0
  95. package/dist/core/extensions/types.d.ts +15 -0
  96. package/dist/core/retry-fallback/usage-limit.js +5 -1
  97. package/dist/core/tools/diff-render.d.ts +4 -0
  98. package/dist/core/tools/diff-render.js +13 -0
  99. package/dist/core/tools/renderers/edit.js +6 -4
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/main.js +12 -0
  103. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  104. package/dist/modes/interactive/interactive-mode.js +55 -11
  105. package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
  106. package/dist/modes/interactive/provider-error-presentation.js +42 -0
  107. package/dist/modes/interactive/session-control-registry.js +2 -0
  108. package/dist/modes/interactive/theme/dark.json +5 -5
  109. package/dist/modes/interactive/theme/light.json +5 -5
  110. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  111. package/dist/modes/rpc/custom-capability.js +6 -0
  112. package/dist/modes/rpc/host-daemon-registration.d.ts +9 -0
  113. package/dist/modes/rpc/host-daemon-registration.js +2 -4
  114. package/dist/modes/rpc/host-ensure-start.js +2 -0
  115. package/dist/modes/rpc/host-handoff-unregistered.d.ts +24 -0
  116. package/dist/modes/rpc/host-handoff-unregistered.js +50 -0
  117. package/dist/modes/rpc/host-handoff.d.ts +7 -2
  118. package/dist/modes/rpc/host-handoff.js +7 -3
  119. package/dist/modes/rpc/host-legacy.d.ts +3 -0
  120. package/dist/modes/rpc/host-legacy.js +10 -5
  121. package/dist/modes/rpc/host-probe.d.ts +12 -0
  122. package/dist/modes/rpc/host-probe.js +53 -5
  123. package/dist/modes/rpc/host-runner.js +1 -1
  124. package/dist/modes/rpc/host-successor.d.ts +5 -4
  125. package/dist/modes/rpc/host-successor.js +11 -4
  126. package/dist/modes/rpc/rpc-client.d.ts +3 -0
  127. package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
  128. package/dist/modes/rpc/rpc-input-validation.js +37 -0
  129. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  130. package/dist/modes/rpc/session-command-activity.d.ts +2 -0
  131. package/dist/modes/rpc/session-command-activity.js +25 -0
  132. package/dist/modes/rpc/session-command-router.js +11 -4
  133. package/dist/modes/rpc/session-registry-attach.js +1 -0
  134. package/dist/modes/rpc/session-registry-types.d.ts +7 -0
  135. package/dist/modes/rpc/session-registry-types.js +11 -0
  136. package/dist/modes/rpc/session-registry.js +6 -3
  137. package/dist/modes/rpc/session-sweep.d.ts +12 -2
  138. package/dist/modes/rpc/session-sweep.js +36 -3
  139. package/dist/modes/rpc/session-teardown.d.ts +2 -0
  140. package/dist/modes/rpc/session-teardown.js +1 -0
  141. package/dist/modes/rpc/worker-session-registry.js +5 -1
  142. package/docs/rpc.md +49 -7
  143. package/docs/settings.md +3 -1
  144. package/package.json +6 -6
  145. package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
  146. package/dist/bundle/chunks/chunk-UUAKEDVS.js +0 -14
  147. package/dist/bundle/chunks/cli-main-VZWSUHFB.js +0 -3
  148. package/dist/bundle/chunks/host-command-7JIGGOWR.js +0 -2
  149. package/dist/bundle/chunks/interactive-mode-JA7AHYYL.js +0 -2
  150. package/dist/bundle/chunks/rpc-mode-QEBSN2OH.js +0 -2
  151. package/dist/bundle/chunks/session-control-endpoint-UGQIP6ZN.js +0 -3
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Classifies every word of one simple command against `spec`, or returns undefined when any word
3
+ * is not understood: an unknown flag (short or long), a flag in a form the spec does not list, a
4
+ * value-taking flag with no value, or an operand count out of range. Short flags may be
5
+ * clustered (`-la`), and a value-taking short flag takes the rest of its word as the value
6
+ * (`-o/x`, `-ro/x`) or else the next word; a long flag takes `--name=value` or `--name value`.
7
+ */
8
+ export function classifyWords(words, spec) {
9
+ const classified = [];
10
+ const operands = [];
11
+ let optionsEnded = false;
12
+ for (let index = 0; index < words.length; index += 1) {
13
+ const word = words[index];
14
+ const text = word.text;
15
+ if (optionsEnded || text === "-" || !text.startsWith("-")) {
16
+ operands.push(word);
17
+ continue;
18
+ }
19
+ if (text === "--") {
20
+ optionsEnded = true;
21
+ continue;
22
+ }
23
+ if (text.startsWith("--")) {
24
+ const separator = text.indexOf("=");
25
+ const name = separator < 0 ? text : text.slice(0, separator);
26
+ const flag = spec.flags[name];
27
+ if (flag === undefined)
28
+ return undefined;
29
+ if (flag === true) {
30
+ if (separator >= 0)
31
+ return undefined;
32
+ continue;
33
+ }
34
+ if (separator >= 0) {
35
+ classified.push({ role: flag, word: { text: text.slice(separator + 1) } });
36
+ continue;
37
+ }
38
+ const value = words[index + 1];
39
+ if (value === undefined)
40
+ return undefined;
41
+ classified.push({ role: flag, word: value });
42
+ index += 1;
43
+ continue;
44
+ }
45
+ for (let position = 1; position < text.length; position += 1) {
46
+ const flag = spec.flags[`-${text[position]}`];
47
+ if (flag === undefined)
48
+ return undefined;
49
+ if (flag === true)
50
+ continue;
51
+ const attached = text.slice(position + 1);
52
+ if (attached !== "") {
53
+ classified.push({ role: flag, word: { text: attached } });
54
+ }
55
+ else {
56
+ const value = words[index + 1];
57
+ if (value === undefined)
58
+ return undefined;
59
+ classified.push({ role: flag, word: value });
60
+ index += 1;
61
+ }
62
+ break;
63
+ }
64
+ }
65
+ if (operands.length < (spec.minOperands ?? 0))
66
+ return undefined;
67
+ if (spec.maxOperands !== undefined && operands.length > spec.maxOperands)
68
+ return undefined;
69
+ operands.forEach((word, index) => {
70
+ classified.push({ role: spec.operand(index, operands.length, word.text), word });
71
+ });
72
+ return classified;
73
+ }
74
+ //# sourceMappingURL=auto-shell-grammar.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Splits a shell command into the simple commands the `auto` preset can judge one by one.
3
+ *
4
+ * Only plain words joined by `;` or `&&` are understood. Quotes, escapes, expansions (`$`, `~`,
5
+ * globs), redirects, pipes, background jobs, subshells, comments and line breaks return undefined,
6
+ * and the caller asks the user.
7
+ */
8
+ export interface ShellWord {
9
+ readonly text: string;
10
+ }
11
+ export type ShellSegment = readonly ShellWord[];
12
+ export declare function splitShellSegments(command: string): ShellSegment[] | undefined;
13
+ //# sourceMappingURL=auto-shell-segments.d.ts.map
@@ -0,0 +1,23 @@
1
+ const PLAIN_COMMAND = /^[A-Za-z0-9_\-.,/=+ \t;&]*$/;
2
+ const words = (part) => part
3
+ .split(/[ \t]+/)
4
+ .filter((text) => text !== "")
5
+ .map((text) => ({ text }));
6
+ export function splitShellSegments(command) {
7
+ if (!PLAIN_COMMAND.test(command))
8
+ return undefined;
9
+ const chains = command.split("&&");
10
+ if (chains.some((chain) => chain.includes("&")))
11
+ return undefined;
12
+ const segments = [];
13
+ for (const [index, chain] of chains.entries()) {
14
+ const parts = chain.split(";").map(words);
15
+ if (index > 0 && parts[0]?.length === 0)
16
+ return undefined;
17
+ if (index < chains.length - 1 && parts[parts.length - 1]?.length === 0)
18
+ return undefined;
19
+ segments.push(...parts.filter((part) => part.length > 0));
20
+ }
21
+ return segments;
22
+ }
23
+ //# sourceMappingURL=auto-shell-segments.js.map
@@ -1,5 +1,6 @@
1
1
  import type { PermissionPresetName, Ruleset } from "./types.ts";
2
2
  export declare function parsePermissionFlag(value: string): Ruleset;
3
3
  export declare function parsePermissionPresetFlag(value: string): PermissionPresetName | undefined;
4
+ export declare const PERMISSION_PRESET_NAMES: readonly PermissionPresetName[];
4
5
  export declare function parsePermissionPresetName(value: string): PermissionPresetName | undefined;
5
6
  //# sourceMappingURL=cli.d.ts.map
@@ -20,16 +20,15 @@ export function parsePermissionFlag(value) {
20
20
  export function parsePermissionPresetFlag(value) {
21
21
  return parsePermissionPresetName(value.trim());
22
22
  }
23
+ export const PERMISSION_PRESET_NAMES = [
24
+ "full-access",
25
+ "workspace",
26
+ "accept-edits",
27
+ "auto",
28
+ "read-only",
29
+ "ask",
30
+ ];
23
31
  export function parsePermissionPresetName(value) {
24
- switch (value) {
25
- case "full-access":
26
- case "workspace":
27
- case "accept-edits":
28
- case "read-only":
29
- case "ask":
30
- return value;
31
- default:
32
- return undefined;
33
- }
32
+ return PERMISSION_PRESET_NAMES.find((name) => name === value);
34
33
  }
35
34
  //# sourceMappingURL=cli.js.map
@@ -1,10 +1,13 @@
1
- import type { PermissionConfig, PermissionPresetName, Ruleset } from "./types.ts";
1
+ import type { PermissionConfig, PermissionPresetName, Rule, Ruleset } from "./types.ts";
2
2
  export declare const EDIT_TOOLS: string[];
3
3
  export declare const DEFAULT_PERMISSION_PRESET: PermissionPresetName;
4
4
  export declare const ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY = "permission_preset_accept_edits";
5
+ export declare const AUTO_PERMISSION_PRESET_CAPABILITY = "permission_preset_auto";
5
6
  export declare function expand(path: string): string;
6
7
  export declare function fromConfig(config: PermissionConfig): Ruleset;
7
8
  export declare function rulesForPreset(preset: PermissionPresetName): Ruleset;
9
+ /** True only for a rule object produced by `rulesForPreset`, never for a user's identical rule. */
10
+ export declare function isPresetRule(rule: Rule): boolean;
8
11
  export declare function merge(...rulesets: Ruleset[]): Ruleset;
9
12
  export declare function disabled(tools: string[], ruleset: Ruleset): Set<string>;
10
13
  //# sourceMappingURL=config.d.ts.map
@@ -3,6 +3,7 @@ import { Wildcard } from "./wildcard.js";
3
3
  export const EDIT_TOOLS = ["edit", "write", "apply_patch", "multiedit"];
4
4
  export const DEFAULT_PERMISSION_PRESET = "full-access";
5
5
  export const ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY = "permission_preset_accept_edits";
6
+ export const AUTO_PERMISSION_PRESET_CAPABILITY = "permission_preset_auto";
6
7
  const PERMISSION_PRESET_RULES = {
7
8
  "full-access": [{ permission: "*", pattern: "*", action: "allow" }],
8
9
  workspace: [
@@ -23,6 +24,8 @@ const PERMISSION_PRESET_RULES = {
23
24
  { permission: "bash", pattern: "*", action: "ask" },
24
25
  { permission: "external_directory", pattern: "*", action: "ask" },
25
26
  ],
27
+ // Ask for everything; auto-policy.ts approves, over this rule only, the calls on its allowlist.
28
+ auto: [{ permission: "*", pattern: "*", action: "ask" }],
26
29
  "read-only": [
27
30
  { permission: "*", pattern: "*", action: "ask" },
28
31
  { permission: "read", pattern: "*", action: "allow" },
@@ -63,8 +66,17 @@ export function fromConfig(config) {
63
66
  }
64
67
  return rules;
65
68
  }
69
+ const presetRules = new WeakSet();
66
70
  export function rulesForPreset(preset) {
67
- return PERMISSION_PRESET_RULES[preset].map((rule) => ({ ...rule }));
71
+ return PERMISSION_PRESET_RULES[preset].map((rule) => {
72
+ const copy = { ...rule };
73
+ presetRules.add(copy);
74
+ return copy;
75
+ });
76
+ }
77
+ /** True only for a rule object produced by `rulesForPreset`, never for a user's identical rule. */
78
+ export function isPresetRule(rule) {
79
+ return presetRules.has(rule);
68
80
  }
69
81
  export function merge(...rulesets) {
70
82
  return rulesets.flat();
@@ -1,6 +1,7 @@
1
1
  import { SettingsManager } from "../../../settings-manager.js";
2
2
  import { extractPatchedPaths } from "../gpt-apply-patch/index.js";
3
- import { parsePermissionFlag, parsePermissionPresetFlag } from "./cli.js";
3
+ import { decideAuto } from "./auto-policy.js";
4
+ import { PERMISSION_PRESET_NAMES, parsePermissionFlag, parsePermissionPresetFlag } from "./cli.js";
4
5
  import { disabled } from "./config.js";
5
6
  import { createEventEmitter } from "./events.js";
6
7
  import { INTERNAL_PERMISSION_TOOLS } from "./internal-tools.js";
@@ -10,7 +11,7 @@ import { showPermissionPrompt } from "./prompt.js";
10
11
  import { PermissionService } from "./service.js";
11
12
  import { loadPermissionSettings } from "./settings.js";
12
13
  import { appendApproved } from "./storage.js";
13
- import { CorrectedError, DeniedError, RejectedError } from "./types.js";
14
+ import { CorrectedError, DeniedError, RejectedError, } from "./types.js";
14
15
  function createRequestIDFactory() {
15
16
  let counter = 0;
16
17
  return () => {
@@ -48,6 +49,7 @@ export default function permissionSystemExtension(pi) {
48
49
  let parserRegistry = null;
49
50
  let cliRuleset = [];
50
51
  let staticRuleset = [];
52
+ let activePreset = null;
51
53
  let initialApprovedCount = 0;
52
54
  let setupError = null;
53
55
  const nextRequestID = createRequestIDFactory();
@@ -56,7 +58,7 @@ export default function permissionSystemExtension(pi) {
56
58
  type: "string",
57
59
  });
58
60
  pi.registerFlag("permission-preset", {
59
- description: "Set permission preset (full-access, workspace, accept-edits, read-only, or ask)",
61
+ description: `Set permission preset (${PERMISSION_PRESET_NAMES.join(", ")})`,
60
62
  type: "string",
61
63
  });
62
64
  pi.on("session_start", async (_event, ctx) => {
@@ -79,10 +81,11 @@ export default function permissionSystemExtension(pi) {
79
81
  cliRuleset = typeof permissionFlag === "string" ? parsePermissionFlag(permissionFlag) : [];
80
82
  const cliPreset = typeof permissionPresetFlag === "string" ? parsePermissionPresetFlag(permissionPresetFlag) : undefined;
81
83
  if (typeof permissionPresetFlag === "string" && !cliPreset) {
82
- throw new Error(`Invalid --permission-preset "${permissionPresetFlag}". Expected one of: full-access, workspace, accept-edits, read-only, ask.`);
84
+ throw new Error(`Invalid --permission-preset "${permissionPresetFlag}". Expected one of: ${PERMISSION_PRESET_NAMES.join(", ")}.`);
83
85
  }
84
86
  const loadedSettings = loadPermissionSettings(settingsManager, cliRuleset, cwd, cliPreset);
85
87
  staticRuleset = loadedSettings.staticRuleset;
88
+ activePreset = loadedSettings.preset;
86
89
  const approved = loadedSettings.approved;
87
90
  parserRegistry = createBuiltinParserRegistry();
88
91
  service = new PermissionService(staticRuleset, approved, createEventEmitter(pi));
@@ -123,9 +126,14 @@ export default function permissionSystemExtension(pi) {
123
126
  always: permissionRequest.always,
124
127
  metadata: createRequestMetadata(event.toolName, event.input),
125
128
  };
129
+ const auto = activePreset === "auto"
130
+ ? await decideAuto(event.toolName, event.input, permissionRequest, ctx.cwd)
131
+ : undefined;
126
132
  const askResultPromise = service
127
133
  .ask(request, {
128
134
  autoApproveAsk: permissionRequest.autoApproveAsk ?? false,
135
+ approveBlanketAsk: auto?.approveBlanketAsk ?? false,
136
+ presetBound: activePreset === "auto",
129
137
  ...(permissionRequest.ruleAliases ? { ruleAliases: permissionRequest.ruleAliases } : {}),
130
138
  })
131
139
  .then(() => ({ ok: true }), (error) => ({ ok: false, error }));
@@ -142,14 +150,14 @@ export default function permissionSystemExtension(pi) {
142
150
  service.reply(reply);
143
151
  }
144
152
  else {
145
- const reply = handleNoUI(request, staticRuleset, cliRuleset, (eventName, data) => {
146
- if (eventName !== "permission_asked") {
147
- pi.events.emit(eventName, data);
148
- }
149
- });
150
- if (reply) {
151
- service.reply(reply);
152
- }
153
+ service.reply(handleNoUI(request, {
154
+ emitEvent: (eventName, data) => {
155
+ if (eventName !== "permission_asked") {
156
+ pi.events.emit(eventName, data);
157
+ }
158
+ },
159
+ presetBound: activePreset === "auto",
160
+ }));
153
161
  }
154
162
  const askResult = await askResultPromise;
155
163
  if (!askResult.ok) {
@@ -1,9 +1,14 @@
1
- import type { ReplyInput, Request, Ruleset } from "../permission-system/types.ts";
1
+ import type { ReplyInput, Request } from "../permission-system/types.ts";
2
+ export interface NoUIOptions {
3
+ readonly emitEvent: (event: string, data: unknown) => void;
4
+ /** The `auto` preset is active. */
5
+ readonly presetBound?: boolean;
6
+ }
2
7
  /**
3
- * Handle permission request in no-UI mode (print mode, unbound SDK).
4
- * Returns ReplyInput to reject, or undefined to allow.
5
- *
6
- * Precedence: CLI override > static ruleset > auto-deny
8
+ * Answers, with no UI to ask (print mode, unbound SDK), a request the permission service is still
9
+ * asking about. The service has already applied every rule and found at least one pattern it must
10
+ * ask about, so the answer is always a refusal with a reason, never an approval; a configured allow
11
+ * or deny for the other patterns does not change that.
7
12
  */
8
- export declare function handleNoUI(request: Request, staticRuleset: Ruleset, cliOverride: Ruleset, emitEvent: (event: string, data: unknown) => void): ReplyInput | undefined;
13
+ export declare function handleNoUI(request: Request, { emitEvent, presetBound }: NoUIOptions): ReplyInput;
9
14
  //# sourceMappingURL=non-interactive.d.ts.map
@@ -1,43 +1,18 @@
1
- import { evaluate } from "../permission-system/evaluate.js";
2
1
  /**
3
- * Handle permission request in no-UI mode (print mode, unbound SDK).
4
- * Returns ReplyInput to reject, or undefined to allow.
5
- *
6
- * Precedence: CLI override > static ruleset > auto-deny
2
+ * Answers, with no UI to ask (print mode, unbound SDK), a request the permission service is still
3
+ * asking about. The service has already applied every rule and found at least one pattern it must
4
+ * ask about, so the answer is always a refusal with a reason, never an approval; a configured allow
5
+ * or deny for the other patterns does not change that.
7
6
  */
8
- export function handleNoUI(request, staticRuleset, cliOverride, emitEvent) {
9
- // Emit permission_asked event for logging/telemetry
7
+ export function handleNoUI(request, { emitEvent, presetBound = false }) {
10
8
  emitEvent("permission_asked", request);
11
- const cliRule = evaluate(request.permission, request.patterns[0], cliOverride);
12
- if (cliRule.action === "allow") {
13
- emitEvent("permission_replied", { requestID: request.id, sessionID: request.sessionID, reply: "allow" });
14
- return undefined;
15
- }
16
- if (cliRule.action === "deny") {
17
- return {
18
- requestID: request.id,
19
- reply: "reject",
20
- message: `Permission denied by CLI flag: ${request.permission}`,
21
- };
22
- }
23
- const staticRule = evaluate(request.permission, request.patterns[0], staticRuleset);
24
- if (staticRule.action === "allow") {
25
- emitEvent("permission_replied", { requestID: request.id, sessionID: request.sessionID, reply: "allow" });
26
- return undefined;
27
- }
28
- if (staticRule.action === "deny") {
29
- return {
30
- requestID: request.id,
31
- reply: "reject",
32
- message: `Permission denied by config: ${request.permission}`,
33
- };
34
- }
35
- // Still "ask" - auto-deny with helpful message
36
9
  const patternsStr = request.patterns.join(", ");
37
10
  return {
38
11
  requestID: request.id,
39
12
  reply: "reject",
40
- message: `Permission required for ${request.permission} (${patternsStr}). Use --permission ${request.permission}=allow to override.`,
13
+ message: presetBound
14
+ ? `Permission required for ${request.permission} (${patternsStr}), and there is no UI to ask. Under the auto preset, allow rules do not widen what it approves; run it interactively or choose another preset.`
15
+ : `Permission required for ${request.permission} (${patternsStr}). Use --permission ${request.permission}=allow to override.`,
41
16
  };
42
17
  }
43
18
  //# sourceMappingURL=non-interactive.js.map
@@ -6,14 +6,33 @@ type RequestInput = Omit<Request, "id"> & {
6
6
  /** Core service for managing permission requests and rule evaluation */
7
7
  export declare class PermissionService {
8
8
  private pending;
9
+ /** How each pending request was decided, so a re-check after an "Always" reply decides it the same way. */
10
+ private pendingOptions;
9
11
  private approved;
10
12
  private staticRuleset;
11
13
  private emitter;
12
14
  private idCounter;
13
15
  constructor(staticRuleset: Ruleset, approved: Ruleset, emitter?: PermissionEventEmitter);
16
+ /** deny > ask > allow. */
17
+ private static readonly RESTRICTIVENESS;
18
+ /**
19
+ * The one decision for a call, used when it is asked and again whenever an "Always" reply re-checks
20
+ * the requests still pending. Without `presetBound` it is last-match-wins over every rule. With
21
+ * `presetBound` the preset's rules and the user's settings and CLI rules are evaluated apart and
22
+ * the more restrictive wins, so no configured rule, in any order or layer, can widen the preset;
23
+ * only an "Always" answer given in this or an earlier session (`permissions-approved.jsonl`,
24
+ * explicit consent for that pattern) can then turn a remaining ask into allow.
25
+ */
26
+ private decide;
14
27
  /** Request permission for a tool call. Resolves if allowed, throws on denial. */
15
- ask(request: RequestInput, { autoApproveAsk, ruleAliases, }?: {
28
+ ask(request: RequestInput, { autoApproveAsk, approveBlanketAsk, presetBound, ruleAliases, }?: {
16
29
  readonly autoApproveAsk?: boolean;
30
+ readonly approveBlanketAsk?: boolean;
31
+ /**
32
+ * Decide as the more restrictive of the preset's decision and the user's (deny > ask > allow),
33
+ * so no user rule, in any order or layer, can widen the preset.
34
+ */
35
+ readonly presetBound?: boolean;
17
36
  readonly ruleAliases?: readonly string[];
18
37
  }): Promise<void>;
19
38
  /** Reply to a pending permission request */
@@ -1,3 +1,4 @@
1
+ import { isPresetRule } from "./config.js";
1
2
  import { evaluate } from "./evaluate.js";
2
3
  import { createLocalEventEmitter } from "./events.js";
3
4
  import { CorrectedError, DeniedError, RejectedError, } from "./types.js";
@@ -5,26 +6,56 @@ import { CorrectedError, DeniedError, RejectedError, } from "./types.js";
5
6
  export class PermissionService {
6
7
  constructor(staticRuleset, approved, emitter = createLocalEventEmitter()) {
7
8
  this.pending = new Map();
9
+ /** How each pending request was decided, so a re-check after an "Always" reply decides it the same way. */
10
+ this.pendingOptions = new Map();
8
11
  this.idCounter = 0;
9
12
  this.staticRuleset = [...staticRuleset];
10
13
  this.approved = [...approved];
11
14
  this.emitter = emitter;
12
15
  }
16
+ /** deny > ask > allow. */
17
+ static { this.RESTRICTIVENESS = { allow: 0, ask: 1, deny: 2 }; }
18
+ /**
19
+ * The one decision for a call, used when it is asked and again whenever an "Always" reply re-checks
20
+ * the requests still pending. Without `presetBound` it is last-match-wins over every rule. With
21
+ * `presetBound` the preset's rules and the user's settings and CLI rules are evaluated apart and
22
+ * the more restrictive wins, so no configured rule, in any order or layer, can widen the preset;
23
+ * only an "Always" answer given in this or an earlier session (`permissions-approved.jsonl`,
24
+ * explicit consent for that pattern) can then turn a remaining ask into allow.
25
+ */
26
+ decide(permission, target, options) {
27
+ if (!options.presetBound)
28
+ return evaluate(permission, target, this.staticRuleset, this.approved).action;
29
+ const presetRules = this.staticRuleset.filter((rule) => isPresetRule(rule));
30
+ const userRules = this.staticRuleset.filter((rule) => !isPresetRule(rule));
31
+ const presetRule = evaluate(permission, target, presetRules);
32
+ const preset = presetRule.action === "ask" && options.approveBlanketAsk ? "allow" : presetRule.action;
33
+ const userRule = evaluate(permission, target, userRules);
34
+ const user = userRules.includes(userRule) ? userRule.action : undefined;
35
+ const combined = user === undefined || PermissionService.RESTRICTIVENESS[preset] >= PermissionService.RESTRICTIVENESS[user]
36
+ ? preset
37
+ : user;
38
+ if (combined !== "ask")
39
+ return combined;
40
+ const remembered = evaluate(permission, target, this.approved);
41
+ return this.approved.includes(remembered) && remembered.action === "allow" ? "allow" : "ask";
42
+ }
13
43
  /** Request permission for a tool call. Resolves if allowed, throws on denial. */
14
- async ask(request, { autoApproveAsk = false, ruleAliases, } = {}) {
44
+ async ask(request, { autoApproveAsk = false, approveBlanketAsk = false, presetBound = false, ruleAliases, } = {}) {
15
45
  const info = {
16
46
  ...request,
17
47
  id: request.id ?? this.nextRequestID(),
18
48
  };
19
49
  const deniedPatterns = [];
20
50
  let needsAsk = false;
51
+ const options = { approveBlanketAsk, presetBound, ruleAliases };
21
52
  for (const pattern of info.patterns) {
22
- const rule = evaluate(info.permission, ruleAliases ?? pattern, this.staticRuleset, this.approved);
23
- if (rule.action === "deny") {
53
+ const action = this.decide(info.permission, ruleAliases ?? pattern, options);
54
+ if (action === "deny") {
24
55
  deniedPatterns.push(pattern);
25
56
  continue;
26
57
  }
27
- if (rule.action === "ask" && !autoApproveAsk) {
58
+ if (action === "ask" && !autoApproveAsk) {
28
59
  needsAsk = true;
29
60
  }
30
61
  }
@@ -40,14 +71,17 @@ export class PermissionService {
40
71
  info,
41
72
  resolve: () => {
42
73
  this.pending.delete(info.id);
74
+ this.pendingOptions.delete(info.id);
43
75
  resolve();
44
76
  },
45
77
  reject: (error) => {
46
78
  this.pending.delete(info.id);
79
+ this.pendingOptions.delete(info.id);
47
80
  reject(error);
48
81
  },
49
82
  };
50
83
  this.pending.set(info.id, pendingEntry);
84
+ this.pendingOptions.set(info.id, options);
51
85
  });
52
86
  this.emitter.emitAsked(info);
53
87
  await pendingPromise;
@@ -59,6 +93,7 @@ export class PermissionService {
59
93
  return;
60
94
  }
61
95
  this.pending.delete(input.requestID);
96
+ this.pendingOptions.delete(input.requestID);
62
97
  this.emitter.emitReplied(existing.info.id, existing.info.sessionID, input.reply);
63
98
  if (input.reply === "reject") {
64
99
  existing.reject(input.message ? new CorrectedError(input.message) : new RejectedError());
@@ -102,6 +137,7 @@ export class PermissionService {
102
137
  continue;
103
138
  }
104
139
  this.pending.delete(requestID);
140
+ this.pendingOptions.delete(requestID);
105
141
  this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "reject");
106
142
  entry.reject(new RejectedError());
107
143
  }
@@ -111,13 +147,13 @@ export class PermissionService {
111
147
  if (entry.info.sessionID !== sessionID) {
112
148
  continue;
113
149
  }
114
- const isAllowed = entry.info.patterns.every((pattern) => {
115
- return evaluate(entry.info.permission, pattern, this.staticRuleset, this.approved).action === "allow";
116
- });
150
+ const options = this.pendingOptions.get(requestID) ?? {};
151
+ const isAllowed = entry.info.patterns.every((pattern) => this.decide(entry.info.permission, options.ruleAliases ?? pattern, options) === "allow");
117
152
  if (!isAllowed) {
118
153
  continue;
119
154
  }
120
155
  this.pending.delete(requestID);
156
+ this.pendingOptions.delete(requestID);
121
157
  this.emitter.emitReplied(entry.info.id, entry.info.sessionID, "always");
122
158
  entry.resolve();
123
159
  }
@@ -15,5 +15,6 @@ import type { PermissionPresetName, Ruleset } from "./types.ts";
15
15
  export declare function loadPermissionSettings(settingsManager: SettingsManager, cliOverride: Ruleset, projectDir: string, cliPresetOverride?: PermissionPresetName): {
16
16
  staticRuleset: Ruleset;
17
17
  approved: Ruleset;
18
+ preset: PermissionPresetName;
18
19
  };
19
20
  //# sourceMappingURL=settings.d.ts.map
@@ -1,4 +1,4 @@
1
- import { parsePermissionPresetName } from "./cli.js";
1
+ import { PERMISSION_PRESET_NAMES, parsePermissionPresetName } from "./cli.js";
2
2
  import { DEFAULT_PERMISSION_PRESET, fromConfig, merge, rulesForPreset } from "./config.js";
3
3
  import { loadApproved } from "./storage.js";
4
4
  /**
@@ -25,7 +25,8 @@ export function loadPermissionSettings(settingsManager, cliOverride, projectDir,
25
25
  const cliPresetRuleset = cliPresetOverride ? rulesForPreset(cliPresetOverride) : [];
26
26
  const staticRuleset = merge(rulesForPreset(DEFAULT_PERMISSION_PRESET), globalPresetRuleset, globalRuleset, projectPresetRuleset, projectRuleset, cliPresetRuleset, cliOverride);
27
27
  const approved = loadApproved(projectDir);
28
- return { staticRuleset, approved };
28
+ const preset = cliPresetOverride ?? projectPreset ?? globalPreset ?? DEFAULT_PERMISSION_PRESET;
29
+ return { staticRuleset, approved, preset };
29
30
  }
30
31
  function parseSettingsPreset(value, scope) {
31
32
  if (value === undefined) {
@@ -36,7 +37,7 @@ function parseSettingsPreset(value, scope) {
36
37
  }
37
38
  const preset = parsePermissionPresetName(value);
38
39
  if (!preset) {
39
- throw new Error(`Invalid ${scope} permissionPreset "${value}". Expected one of: full-access, workspace, accept-edits, read-only, ask.`);
40
+ throw new Error(`Invalid ${scope} permissionPreset "${value}". Expected one of: ${PERMISSION_PRESET_NAMES.join(", ")}.`);
40
41
  }
41
42
  return preset;
42
43
  }
@@ -10,7 +10,7 @@ export type Rule = {
10
10
  export type Ruleset = Rule[];
11
11
  /** Configuration format for settings.json */
12
12
  export type PermissionConfig = Record<string, Action | Record<string, Action>>;
13
- export type PermissionPresetName = "full-access" | "workspace" | "accept-edits" | "read-only" | "ask";
13
+ export type PermissionPresetName = "full-access" | "workspace" | "accept-edits" | "auto" | "read-only" | "ask";
14
14
  /** User reply to a permission request */
15
15
  export type Reply = "once" | "always" | "reject";
16
16
  /** Final permission decision: user reply or auto-grant from rules */
@@ -41,6 +41,19 @@
41
41
  // progress updates") says to say when user-facing text is wanted and what each
42
42
  // update contains. Its defect clause is dropped because "check your last
43
43
  // paragraph" already owns the text-only turn end.
44
+ //
45
+ // 2026-10-04 (senpi#2681): the between-handoff sentence rendered for this
46
+ // preset was advisory ("... helps the user follow along") and named no moment;
47
+ // measured over two weeks of sessions it produced reply text on 14% of
48
+ // tool-using steps, the same as cores that say "work without narration". The
49
+ // 5.1 guide says to state when user-facing text is wanted and what each update
50
+ // contains, so the sentence is replaced at its source in handoff.ts with an
51
+ // instruction (after each tool wave that changes what you know, one line of
52
+ // reply text, then Now/Next). Three rules the 2026-09-02 diet left stated twice
53
+ // go back to one home: Style no longer repeats Scope's proceed-without-asking
54
+ // rule, Verification keeps the claim audit and drops the "Should pass" flourish,
55
+ // and the fourth Hard Limit drops the tail Scope already states. Net the core
56
+ // shrinks; no rule is added.
44
57
  import { APP_NAME } from "../../../../config.js";
45
58
  import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
46
59
  import { buildHandoffSection, CHAT_FINAL_MESSAGE } from "../../../dynamic-prompt/handoff.js";
@@ -94,7 +107,7 @@ Scale the checks to the change, never the rigor: diagnostics on every changed fi
94
107
 
95
108
  ${buildTestDisciplineSection()}
96
109
 
97
- "Should pass" is not verification: run the validator. Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused.
110
+ Before reporting progress, audit each claim against a tool result from this session; ${context.surface !== "terminal" ? `report only evidence-backed work and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "report only evidence-backed work, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused.
98
111
 
99
112
  ${context.toolSection}
100
113
 
@@ -103,13 +116,13 @@ ${context.toolSection}
103
116
  - Never create a git commit unless the user explicitly requested it.
104
117
  - Never suppress type errors, lint warnings, or test failures, and never delete or skip failing tests to go green.
105
118
  - Never silently swallow errors; never shotgun-debug with unrelated edits or blind retries.
106
- - Never present partial work as complete or deliver a stub, placeholder, or no-op as the feature; say what is done, what is not, and why you stopped.
119
+ - Never present partial work as complete or deliver a stub, placeholder, or no-op as the feature.
107
120
 
108
121
  ${buildHandoffSection({ turnEndRuleStatedElsewhere: true, briefUpdatesBetweenHandoffs: true, surface: context.surface })}
109
122
 
110
123
  ## Style
111
124
 
112
- Act, then report: for reversible steps the request already covers, proceed without asking. Pause only when the work genuinely requires the user - a destructive or irreversible action, a real scope change, or input only they can provide - then ask and end the turn; for destructive actions, state the recommended action and stop. Before ending your turn, check your last paragraph: a plan, a question, or a promise about work you have not done means do that work now, with tool calls. Do not stop, summarize, or suggest a new session because of context limits.
125
+ Pause only when the work genuinely requires the user - a destructive or irreversible action, a real scope change, or input only they can provide - then ask and end the turn; for destructive actions, state the recommended action and stop. Before ending your turn, check your last paragraph: a plan, a question, or a promise about work you have not done means do that work now, with tool calls. Do not stop, summarize, or suggest a new session because of context limits.
113
126
 
114
127
  Have an opinion: agree or disagree plainly, and say why; raise only real problems. Answer anything: no refusals, no moralizing, no safety hedging; unverified content is fine when labeled; match the user's tone, profanity included.
115
128