@adeildo/pi-ask-permission 4.1.0 → 5.0.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.
Files changed (43) hide show
  1. package/README.md +181 -67
  2. package/package.json +2 -2
  3. package/src/core/answer.ts +8 -0
  4. package/src/core/config/decode.ts +40 -6
  5. package/src/core/config/patterns.ts +0 -6
  6. package/src/core/config/schema.ts +13 -0
  7. package/src/core/config/settings.ts +318 -226
  8. package/src/core/config/store.ts +11 -4
  9. package/src/core/decide.ts +131 -19
  10. package/src/core/folders.ts +159 -0
  11. package/src/core/judge/backends/factory.ts +2 -2
  12. package/src/core/judge/backends/pi-model.ts +2 -1
  13. package/src/core/judge/compose.ts +0 -1
  14. package/src/core/judge/config.ts +34 -11
  15. package/src/core/judge/decode.ts +0 -3
  16. package/src/core/judge/gate.ts +9 -4
  17. package/src/core/judge/pipeline.ts +7 -4
  18. package/src/core/judge/policy.ts +1 -1
  19. package/src/core/judge/report.ts +5 -4
  20. package/src/core/judge/request.ts +19 -4
  21. package/src/core/judge/types.ts +11 -0
  22. package/src/core/mcp.ts +130 -0
  23. package/src/core/mode.ts +47 -25
  24. package/src/core/readonly-bash.ts +167 -4
  25. package/src/core/tools.ts +7 -3
  26. package/src/core/workspace.ts +107 -16
  27. package/src/index.ts +15 -2
  28. package/src/pi/commands.ts +13 -185
  29. package/src/pi/events.ts +87 -28
  30. package/src/pi/intent.ts +34 -0
  31. package/src/pi/mode.ts +63 -16
  32. package/src/pi/screen.ts +293 -0
  33. package/src/pi/session-entries.ts +46 -8
  34. package/src/pi/session.ts +52 -11
  35. package/src/ui/decision-options.ts +58 -12
  36. package/src/ui/dialog.ts +135 -23
  37. package/src/ui/judge-entry.ts +6 -16
  38. package/src/ui/questions.ts +170 -0
  39. package/src/ui/selector.ts +13 -5
  40. package/src/ui/picker.ts +0 -90
  41. package/src/ui/settings/judge.ts +0 -299
  42. package/src/ui/settings/screen.ts +0 -268
  43. package/src/ui/settings/status.ts +0 -53
@@ -1,299 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import type { SettingItem } from "@earendil-works/pi-tui";
3
-
4
- import type { JudgeBackendId, JudgeConfig, JudgeFallback } from "#core/judge/config.ts";
5
- import {
6
- detectPolicyPreset,
7
- getPolicyPreset,
8
- POLICY_PRESETS,
9
- policyWarning,
10
- } from "#core/judge/policy.ts";
11
- import { PickerList } from "#ui/picker.ts";
12
-
13
- const PROVIDER_LABEL: Record<JudgeBackendId, string> = {
14
- jev: "Jev",
15
- pi: "A pi model",
16
- };
17
-
18
- const FALLBACK_LABEL: Record<JudgeFallback, string> = {
19
- ask: "Ask me",
20
- allow: "Allow",
21
- deny: "Deny",
22
- };
23
-
24
- export const JEV_MODELS = ["jev-latest", "jev-preview", "jev-1.13.0"];
25
-
26
- interface ToolPreset {
27
- label: string;
28
- tools: string[];
29
- }
30
-
31
- const TOOL_PRESETS: ToolPreset[] = [
32
- { label: "Bash only", tools: ["bash"] },
33
- { label: "Bash and file writes", tools: ["bash", "write", "edit"] },
34
- { label: "Every tool", tools: ["*"] },
35
- ];
36
-
37
- export interface JudgeSettingsHooks {
38
- config: JudgeConfig;
39
- theme: Theme;
40
-
41
- piModels: string[];
42
- save: () => void;
43
- editPolicy: () => void;
44
- editModel: () => void;
45
- }
46
-
47
- export interface JudgeSettings {
48
- items: SettingItem[];
49
- onChange: (id: string, value: string) => void;
50
- }
51
-
52
- type SubmenuDone = (selectedValue?: string, options?: { navigateTo?: string }) => void;
53
-
54
- export function buildJudgeSettings(hooks: JudgeSettingsHooks): JudgeSettings {
55
- const values = judgeValues(hooks.config);
56
- const item = (id: JudgeSettingId, label: string, description: string): SettingItem => ({
57
- id,
58
- label,
59
- currentValue: values[id],
60
- description,
61
- });
62
- const choice = (id: JudgeSettingId, label: string, choices: string[], description: string) => ({
63
- ...item(id, label, description),
64
- values: choices,
65
- });
66
- const onOff = (id: JudgeSettingId, label: string, description: string) =>
67
- choice(id, label, ["off", "on"], description);
68
- const fallbacks = Object.values(FALLBACK_LABEL);
69
-
70
- return {
71
- items: [
72
- choice(
73
- "judge.provider",
74
- "Provider",
75
- Object.values(PROVIDER_LABEL),
76
- "Jev answers fast, with a confidence. A pi model uses one you already set up.",
77
- ),
78
- {
79
- ...item(
80
- "judge.model",
81
- "Model",
82
- hooks.config.provider === "jev"
83
- ? "A Jev alias, or a pinned version."
84
- : "The judge asks it for strict JSON.",
85
- ),
86
- submenu: (_current, done) => modelPicker(hooks, done),
87
- },
88
- onOff("judge.canDeny", "Can deny", "A confident no blocks the call. Off, it comes to you."),
89
- choice("judge.whenUnsure", "When unsure", fallbacks, "When the judge is not confident."),
90
- choice(
91
- "judge.whenItFails",
92
- "When it fails",
93
- fallbacks,
94
- "On a timeout, an error, or a missing key.",
95
- ),
96
- choice(
97
- "judge.tools",
98
- "Tools",
99
- TOOL_PRESETS.map((preset) => preset.label),
100
- "The judge decides these. The rest come to you.",
101
- ),
102
- {
103
- ...item("judge.policy", "Policy", policyDescription(hooks.config.policy)),
104
- submenu: (_current, done) => policyPicker(hooks, done),
105
- },
106
- onOff("judge.dryRun", "Dry run", "Show the verdict, and still ask you."),
107
- onOff("judge.noUI", "Judge with no UI", "Also judge print, JSON, and subagent runs."),
108
- onOff(
109
- "judge.rememberApprovals",
110
- "Remember approvals",
111
- "A judge approval becomes always yes for this session.",
112
- ),
113
- ],
114
- onChange: (id, value) => {
115
- apply(hooks, id, value);
116
- hooks.save();
117
- },
118
- };
119
- }
120
-
121
- export type JudgeSettingId =
122
- | "judge.provider"
123
- | "judge.model"
124
- | "judge.canDeny"
125
- | "judge.whenUnsure"
126
- | "judge.whenItFails"
127
- | "judge.tools"
128
- | "judge.policy"
129
- | "judge.dryRun"
130
- | "judge.noUI"
131
- | "judge.rememberApprovals";
132
-
133
- export function judgeValues(config: JudgeConfig): Record<JudgeSettingId, string> {
134
- return {
135
- "judge.provider": PROVIDER_LABEL[config.provider],
136
- "judge.model": config.model || "(none)",
137
- "judge.canDeny": toggle(config.canDeny),
138
- "judge.whenUnsure": FALLBACK_LABEL[config.whenUnsure],
139
- "judge.whenItFails": FALLBACK_LABEL[config.whenItFails],
140
- "judge.tools": toolsLabel(config.tools),
141
- "judge.policy": policyLabel(config.policy),
142
- "judge.dryRun": toggle(config.dryRun),
143
- "judge.noUI": toggle(config.noUI),
144
- "judge.rememberApprovals": toggle(config.rememberApprovals),
145
- };
146
- }
147
-
148
- function apply(hooks: JudgeSettingsHooks, id: string, value: string): void {
149
- const judge = hooks.config;
150
-
151
- switch (id) {
152
- case "judge.provider":
153
- judge.provider = value === PROVIDER_LABEL.pi ? "pi" : "jev";
154
- judge.model = defaultModelFor(judge.provider, hooks.piModels, judge.model);
155
- return;
156
- case "judge.model":
157
- judge.model = value;
158
- return;
159
- case "judge.canDeny":
160
- judge.canDeny = value === "on";
161
- return;
162
- case "judge.whenUnsure":
163
- judge.whenUnsure = fallbackFromLabel(value);
164
- return;
165
- case "judge.whenItFails":
166
- judge.whenItFails = fallbackFromLabel(value);
167
- return;
168
- case "judge.tools":
169
- judge.tools = toolsFromLabel(value) ?? judge.tools;
170
- return;
171
- case "judge.policy": {
172
- const preset = POLICY_PRESETS.find((entry) => entry.label === value);
173
- if (preset) judge.policy = preset.policy;
174
- return;
175
- }
176
- case "judge.dryRun":
177
- judge.dryRun = value === "on";
178
- return;
179
- case "judge.noUI":
180
- judge.noUI = value === "on";
181
- return;
182
- case "judge.rememberApprovals":
183
- judge.rememberApprovals = value === "on";
184
- return;
185
- }
186
- }
187
-
188
- function defaultModelFor(provider: JudgeBackendId, piModels: string[], current: string): string {
189
- if (provider === "jev") return JEV_MODELS.includes(current) ? current : "jev-latest";
190
- if (piModels.includes(current)) return current;
191
- return piModels[0] ?? "";
192
- }
193
-
194
- function modelPicker(hooks: JudgeSettingsHooks, done: SubmenuDone): PickerList {
195
- const items =
196
- hooks.config.provider === "jev"
197
- ? [
198
- ...JEV_MODELS.map((model) => ({
199
- id: model,
200
- label: model,
201
- description:
202
- model === "jev-latest" ? "The current stable release (recommended)" : undefined,
203
- })),
204
- { id: "__other", label: "Other\u2026", description: "Type a model id or pinned version" },
205
- ]
206
- : hooks.piModels.length > 0
207
- ? hooks.piModels.map((model) => ({ id: model, label: model }))
208
- : [
209
- {
210
- id: "__none",
211
- label: "No models available",
212
- description: "Configure a model in pi first",
213
- },
214
- ];
215
-
216
- return new PickerList(
217
- hooks.config.provider === "jev" ? "Jev model" : "pi model",
218
- items,
219
- hooks.theme,
220
- (id) => {
221
- if (id === "__other") {
222
- hooks.editModel();
223
- return;
224
- }
225
- if (id !== "__none") done(id);
226
- },
227
- () => done(),
228
- undefined,
229
- hooks.config.model,
230
- );
231
- }
232
-
233
- function policyPicker(hooks: JudgeSettingsHooks, done: SubmenuDone): PickerList {
234
- const items = [
235
- ...POLICY_PRESETS.filter((preset) => preset.id !== "custom").map((preset) => ({
236
- id: preset.id,
237
- label: preset.label,
238
- description: preset.description,
239
- })),
240
- {
241
- id: "__edit",
242
- label: "Edit policy\u2026",
243
- description: "Write your own rules in a full editor",
244
- },
245
- ];
246
-
247
- return new PickerList(
248
- "Policy",
249
- items,
250
- hooks.theme,
251
- (id) => {
252
- if (id === "__edit") {
253
- hooks.editPolicy();
254
- return;
255
- }
256
- const preset = getPolicyPreset(id);
257
- if (preset) done(preset.label);
258
- },
259
- () => done(),
260
- undefined,
261
- detectPolicyPreset(hooks.config.policy),
262
- );
263
- }
264
-
265
- function toggle(value: boolean): string {
266
- return value ? "on" : "off";
267
- }
268
-
269
- function fallbackFromLabel(label: string): JudgeFallback {
270
- if (label === "Allow") return "allow";
271
- if (label === "Deny") return "deny";
272
- return "ask";
273
- }
274
-
275
- function toolsLabel(tools: string[]): string {
276
- const match = TOOL_PRESETS.find((preset) => sameList(preset.tools, tools));
277
- return match?.label ?? "Custom";
278
- }
279
-
280
- function toolsFromLabel(label: string): string[] | undefined {
281
- return TOOL_PRESETS.find((preset) => preset.label === label)?.tools;
282
- }
283
-
284
- function sameList(left: string[], right: string[]): boolean {
285
- return left.length === right.length && left.every((value, index) => value === right[index]);
286
- }
287
-
288
- function policyLabel(policy: string): string {
289
- if (policy.trim() === "") return "(none yet)";
290
- return getPolicyPreset(detectPolicyPreset(policy))?.label ?? "Custom";
291
- }
292
-
293
- function policyDescription(policy: string): string {
294
- const warning = policyWarning(policy);
295
- if (warning) return warning;
296
-
297
- const preset = getPolicyPreset(detectPolicyPreset(policy));
298
- return preset?.description ?? "The judge reads this as the rulebook for what may run.";
299
- }
@@ -1,268 +0,0 @@
1
- import { type ExtensionContext, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
2
- import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
3
-
4
- import type { AlwaysYes } from "#core/always-yes.ts";
5
- import {
6
- isNoteDelivery,
7
- isNoUIMode,
8
- isOutsideScope,
9
- type OutsideScope,
10
- type PermissionConfig,
11
- } from "#core/config/schema.ts";
12
- import { POLICY_TEMPLATE, policyWarning } from "#core/judge/policy.ts";
13
- import { MODE_LABEL, modeFromLabel, PERMISSION_MODES, type PermissionMode } from "#core/mode.ts";
14
- import { NAME } from "#identity";
15
- import { buildJudgeSettings, type JudgeSettings, judgeValues } from "#ui/settings/judge.ts";
16
- import { notifyJudgePolicyWarning } from "#ui/settings/status.ts";
17
-
18
- export interface SettingsState {
19
- config: PermissionConfig;
20
- alwaysYes: AlwaysYes;
21
- mode: () => PermissionMode;
22
- setMode: (mode: PermissionMode) => void;
23
- save: () => void;
24
- onJudgeChange: () => void;
25
- }
26
-
27
- type SettingsRequest = { kind: "policy" } | { kind: "model" } | undefined;
28
-
29
- export async function openSettings(ctx: ExtensionContext, state: SettingsState): Promise<void> {
30
- for (;;) {
31
- // oxlint-disable-next-line no-await-in-loop -- the menu loop is sequential by design.
32
- const request = await showSettings(ctx, state);
33
- if (!request) return;
34
-
35
- if (request.kind === "policy") {
36
- // oxlint-disable-next-line no-await-in-loop -- one editor at a time.
37
- await editPolicy(ctx, state);
38
- continue;
39
- }
40
-
41
- // oxlint-disable-next-line no-await-in-loop -- one prompt at a time.
42
- await editModel(ctx, state);
43
- }
44
- }
45
-
46
- async function editPolicy(ctx: ExtensionContext, state: SettingsState): Promise<void> {
47
- const judge = state.config.judge;
48
- const text = await ctx.ui.editor("Judge policy", judge.policy || POLICY_TEMPLATE);
49
- if (text === undefined) return;
50
-
51
- judge.policy = text;
52
- state.save();
53
- state.onJudgeChange();
54
-
55
- const warning = policyWarning(text);
56
- if (warning) ctx.ui.notify(`${NAME}: ${warning}`, "warning");
57
- }
58
-
59
- async function editModel(ctx: ExtensionContext, state: SettingsState): Promise<void> {
60
- const text = await ctx.ui.input("Jev model", "jev-latest");
61
- const model = text?.trim();
62
- if (!model) return;
63
-
64
- state.config.judge.model = model;
65
- state.save();
66
- state.onJudgeChange();
67
- }
68
-
69
- const SETTINGS_VISIBLE = 18;
70
-
71
- async function showSettings(ctx: ExtensionContext, state: SettingsState): Promise<SettingsRequest> {
72
- let request: SettingsRequest;
73
-
74
- await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => {
75
- const close = () => done(undefined);
76
- const container = new Container();
77
- let settings: SettingsList | undefined;
78
- let judgeSettings: JudgeSettings | undefined;
79
-
80
- const hooks = {
81
- config: state.config.judge,
82
- theme,
83
- piModels: piModelIds(ctx),
84
- save: () => {
85
- state.save();
86
- state.onJudgeChange();
87
- },
88
- editPolicy: () => {
89
- request = { kind: "policy" };
90
- close();
91
- },
92
- editModel: () => {
93
- request = { kind: "model" };
94
- close();
95
- },
96
- };
97
-
98
- const buildItems = (): SettingItem[] => {
99
- judgeSettings = buildJudgeSettings(hooks);
100
- const children = state.config.judge.enabled
101
- ? judgeSettings.items.map((item) => ({ ...item, label: ` ${item.label}` }))
102
- : [];
103
-
104
- return topLevelItems(state.config, state.mode(), children);
105
- };
106
-
107
- const install = (focusId: string): void => {
108
- if (settings) container.removeChild(settings);
109
- settings = new SettingsList(
110
- buildItems(),
111
- SETTINGS_VISIBLE,
112
- getSettingsListTheme(),
113
- onChange,
114
- close,
115
- );
116
- container.addChild(settings);
117
- settings.selectItem(focusId);
118
- };
119
-
120
- const refreshModelRow = (): void => {
121
- settings?.updateValue("judge.model", judgeValues(state.config.judge)["judge.model"]);
122
- };
123
-
124
- const onChange = (id: string, value: string): void => {
125
- if (id === "judge.enabled") {
126
- state.config.judge.enabled = value === "on";
127
- state.save();
128
- state.onJudgeChange();
129
- install("judge.enabled");
130
- notifyJudgePolicyWarning(state.config, ctx);
131
- return;
132
- }
133
-
134
- if (id.startsWith("judge.")) {
135
- judgeSettings?.onChange(id, value);
136
- refreshModelRow();
137
- return;
138
- }
139
-
140
- if (id === "mode") {
141
- const mode = modeFromLabel(value);
142
- if (mode) state.setMode(mode);
143
- install("mode");
144
- return;
145
- }
146
-
147
- if (id === "workspace.outside" && isOutsideScope(value)) {
148
- state.config.workspace.outside = value;
149
- state.save();
150
- state.setMode(state.mode());
151
- return;
152
- }
153
-
154
- if (id === "notes" && isNoteDelivery(value)) state.config.notes = value;
155
- else if (id === "noUI" && isNoUIMode(value)) state.config.noUI = value;
156
- else if (id === "readOnlyBash") state.config.readOnlyBash = value === "on";
157
- state.save();
158
- };
159
-
160
- container.addChild(
161
- new Text(
162
- theme.fg("accent", theme.bold(NAME)) +
163
- theme.fg("dim", ` \u00b7 ${alwaysYesCount(state.alwaysYes.total())}`),
164
- 1,
165
- 1,
166
- ),
167
- );
168
- install("mode");
169
-
170
- return {
171
- render: (width: number) => container.render(width),
172
- invalidate: () => container.invalidate(),
173
- handleInput: (data: string) => settings?.handleInput?.(data),
174
- };
175
- });
176
-
177
- return request;
178
- }
179
-
180
- export function topLevelItems(
181
- config: PermissionConfig,
182
- mode: PermissionMode,
183
- judgeChildren: SettingItem[],
184
- ): SettingItem[] {
185
- const items = [modeItem(mode), outsideItem(config), notesItem(config)];
186
- if (mode !== "auto") items.push(readOnlyBashItem(config));
187
-
188
- return [...items, judgeToggleItem(config), ...judgeChildren, noUIItem(config)];
189
- }
190
-
191
- function notesItem(config: PermissionConfig): SettingItem {
192
- return {
193
- id: "notes",
194
- label: "Notes",
195
- currentValue: config.notes,
196
- values: ["result", "message"],
197
- description: "result adds your note to the tool result. message sends it on its own.",
198
- };
199
- }
200
-
201
- export function outsideItem(config: PermissionConfig): SettingItem {
202
- const outside = config.workspace.outside;
203
- return {
204
- id: "workspace.outside",
205
- label: "Outside the workspace",
206
- currentValue: outside,
207
- values: ["ask", "deny", "allow"],
208
- description: OUTSIDE_DESCRIPTION[outside],
209
- };
210
- }
211
-
212
- const OUTSIDE_DESCRIPTION: Record<OutsideScope, string> = {
213
- ask: "A call outside workspace.roots asks you. The judge never sees it.",
214
- deny: "A call outside workspace.roots is blocked.",
215
- allow: "No boundary. auto runs everything, anywhere.",
216
- };
217
-
218
- export function readOnlyBashItem(config: PermissionConfig): SettingItem {
219
- return {
220
- id: "readOnlyBash",
221
- label: "Read-only bash",
222
- currentValue: config.readOnlyBash ? "on" : "off",
223
- values: ["off", "on"],
224
- description: "Commands that only read run without asking.",
225
- };
226
- }
227
-
228
- export function judgeToggleItem(config: PermissionConfig): SettingItem {
229
- return {
230
- id: "judge.enabled",
231
- label: "Judge",
232
- currentValue: config.judge.enabled ? "on" : "off",
233
- values: ["off", "on"],
234
- description: "A model answers first. Only the calls it is unsure about reach you.",
235
- };
236
- }
237
-
238
- function noUIItem(config: PermissionConfig): SettingItem {
239
- return {
240
- id: "noUI",
241
- label: "With no UI",
242
- currentValue: typeof config.noUI === "string" ? config.noUI : "per tool",
243
- values: ["deny", "allow"],
244
- description: "When nobody can answer, as in print mode or a subagent.",
245
- };
246
- }
247
-
248
- export function modeItem(mode: PermissionMode): SettingItem {
249
- return {
250
- id: "mode",
251
- label: "Mode (this session)",
252
- currentValue: MODE_LABEL[mode],
253
- values: PERMISSION_MODES.map((entry) => MODE_LABEL[entry]),
254
- description:
255
- "manual asks. accept edits runs file edits. auto runs everything in the workspace.",
256
- };
257
- }
258
-
259
- function piModelIds(ctx: ExtensionContext): string[] {
260
- return ctx.modelRegistry
261
- .getAvailable()
262
- .map((model) => `${model.provider}/${model.id}`)
263
- .toSorted((left, right) => left.localeCompare(right));
264
- }
265
-
266
- export function alwaysYesCount(count: number): string {
267
- return `${count} always yes`;
268
- }
@@ -1,53 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
-
3
- import { type AlwaysYes, SCOPE_LABEL, SCOPES } from "#core/always-yes.ts";
4
- import type { PermissionConfig } from "#core/config/schema.ts";
5
- import { configPath, globalAlwaysYesPath, projectAlwaysYesPath } from "#core/config/store.ts";
6
- import { policyWarning } from "#core/judge/policy.ts";
7
- import { MODE_LABEL, type PermissionMode } from "#core/mode.ts";
8
- import { NAME } from "#identity";
9
-
10
- export function notifyJudgePolicyWarning(config: PermissionConfig, ctx: ExtensionContext): void {
11
- if (!config.judge.enabled) return;
12
-
13
- const warning = policyWarning(config.judge.policy);
14
- if (warning) ctx.ui.notify(`${NAME}: ${warning}`, "warning");
15
- }
16
-
17
- export function statusText(
18
- config: PermissionConfig,
19
- alwaysYes: AlwaysYes,
20
- cwd: string,
21
- mode: PermissionMode,
22
- ): string {
23
- const noUI = typeof config.noUI === "string" ? config.noUI : JSON.stringify(config.noUI);
24
-
25
- return [
26
- `${NAME} \u00b7 ${configPath()}`,
27
- `allow: ${config.allow.join(", ") || "(none)"}`,
28
- `mode: ${MODE_LABEL[mode]} (this session)`,
29
- `notes: ${config.notes} \u00b7 noUI: ${noUI}`,
30
- `typing: pause ${config.typing.pause}ms \u00b7 maxWait ${config.typing.maxWait ?? "none"}`,
31
- `readOnlyBash: ${config.readOnlyBash ? "on" : "off"}`,
32
- `workspace: ${config.workspace.roots.join(", ")} \u00b7 outside: ${config.workspace.outside}`,
33
- judgeLine(config),
34
- `always yes: ${SCOPES.map((scope) => `${alwaysYes.size(scope)} ${SCOPE_LABEL[scope]}`).join(" \u00b7 ")}`,
35
- `always yes for this project: ${projectAlwaysYesPath(cwd)}`,
36
- `always yes everywhere: ${globalAlwaysYesPath()}`,
37
- ].join("\n");
38
- }
39
-
40
- function judgeLine(config: PermissionConfig): string {
41
- const judge = config.judge;
42
- if (!judge.enabled) return `judge: off \u00b7 ${judge.provider}/${judge.model}`;
43
-
44
- const policy = judge.policy.trim() === "" ? "no policy" : "policy set";
45
- const tags = [
46
- judge.dryRun ? "dry run" : undefined,
47
- judge.canDeny ? undefined : "cannot deny",
48
- judge.noUI ? "no UI" : undefined,
49
- policy,
50
- ].filter((tag): tag is string => tag !== undefined);
51
-
52
- return `judge: on \u00b7 ${judge.provider}/${judge.model} \u00b7 tools ${judge.tools.join(",") || "(none)"} \u00b7 ${tags.join(" \u00b7 ")}`;
53
- }