@czottmann/pi-automode 1.11.0 → 1.13.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.
@@ -1,8 +1,17 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ linkSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
2
10
  import { dirname, resolve } from "node:path";
3
11
  import {
4
12
  DEFAULT_ALLOW,
5
13
  DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
14
+ DEFAULT_CLASSIFIER_TIMEOUT_MS,
6
15
  DEFAULT_CLASSIFY_READ_ONLY_TOOLS,
7
16
  DEFAULT_DENIED_PATHS,
8
17
  DEFAULT_ENVIRONMENT,
@@ -14,10 +23,14 @@ import {
14
23
  DEFAULT_PROTECTED_PATHS,
15
24
  DEFAULT_SOFT_DENY,
16
25
  PI_GLOBAL_SETTINGS,
26
+ PI_LEGACY_GLOBAL_SETTINGS,
17
27
  PI_PROJECT_LOCAL_SETTINGS,
18
28
  PI_PROJECT_SHARED_SETTINGS,
19
29
  } from "./constants.ts";
20
- import { parseToolPattern } from "./permissions.ts";
30
+ import {
31
+ MAX_WILDCARD_PATTERN_LENGTH,
32
+ parseToolPattern,
33
+ } from "./permissions.ts";
21
34
  import type {
22
35
  AutoModeSettings,
23
36
  ClassifierReasoningLevel,
@@ -31,6 +44,169 @@ import type {
31
44
  } from "./types.ts";
32
45
  import { hasOwn, stringArray } from "./utils.ts";
33
46
 
47
+ export type GlobalConfigPreparation = {
48
+ status: "current" | "migrated" | "conflict" | "failed";
49
+ activePath: string;
50
+ diagnostic?: string;
51
+ notification?: string;
52
+ writeBlockedReason?: string;
53
+ };
54
+
55
+ export type PrepareGlobalConfigOptions = {
56
+ currentPath?: string;
57
+ legacyPath?: string;
58
+ moveFile?: (source: string, destination: string) => void;
59
+ unlinkFile?: (path: string) => void;
60
+ };
61
+
62
+ function sameFileIdentity(firstPath: string, secondPath: string): boolean {
63
+ try {
64
+ const first = lstatSync(firstPath);
65
+ const second = lstatSync(secondPath);
66
+ return first.dev === second.dev && first.ino === second.ino;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function cleanupPublishedDestination(
73
+ destination: string,
74
+ unlinkFile: (path: string) => void,
75
+ originalError: unknown,
76
+ ): AggregateError | undefined {
77
+ try {
78
+ unlinkFile(destination);
79
+ return undefined;
80
+ } catch (cleanupError) {
81
+ return new AggregateError(
82
+ [originalError, cleanupError],
83
+ `Could not clean up interrupted Auto Mode config migration at ${destination}`,
84
+ );
85
+ }
86
+ }
87
+
88
+ function moveFileWithoutOverwrite(
89
+ source: string,
90
+ destination: string,
91
+ unlinkFile: (path: string) => void,
92
+ ): void {
93
+ linkSync(source, destination);
94
+ try {
95
+ unlinkFile(source);
96
+ } catch (error) {
97
+ throw cleanupPublishedDestination(destination, unlinkFile, error) ?? error;
98
+ }
99
+ }
100
+
101
+ function globalConfigFailure(
102
+ currentPath: string,
103
+ legacyPath: string,
104
+ error: unknown,
105
+ writeBlockedReason?: string,
106
+ ): GlobalConfigPreparation {
107
+ const message =
108
+ `Could not move Auto Mode config from ${legacyPath} to ${currentPath}: ${
109
+ error instanceof Error ? error.message : String(error)
110
+ }. Using the legacy config for this session${
111
+ writeBlockedReason ? "; global config writes are disabled" : ""
112
+ }.`;
113
+ return {
114
+ status: "failed",
115
+ activePath: legacyPath,
116
+ diagnostic: message,
117
+ notification: message,
118
+ writeBlockedReason,
119
+ };
120
+ }
121
+
122
+ function globalConfigConflict(
123
+ currentPath: string,
124
+ legacyPath: string,
125
+ ): GlobalConfigPreparation {
126
+ const message =
127
+ `Auto Mode config conflict: using ${currentPath}; legacy config ${legacyPath} is ignored and was not changed.`;
128
+ return {
129
+ status: "conflict",
130
+ activePath: currentPath,
131
+ diagnostic: message,
132
+ notification: message,
133
+ };
134
+ }
135
+
136
+ /** Select one global config path for this runtime and migrate a legacy file when possible. */
137
+ export function prepareGlobalConfig(
138
+ options: PrepareGlobalConfigOptions = {},
139
+ ): GlobalConfigPreparation {
140
+ const currentPath = options.currentPath ?? PI_GLOBAL_SETTINGS[0];
141
+ const legacyPath = options.legacyPath ?? PI_LEGACY_GLOBAL_SETTINGS;
142
+ const currentExists = existsSync(currentPath);
143
+ const legacyExists = existsSync(legacyPath);
144
+ const unlinkFile = options.unlinkFile ?? unlinkSync;
145
+
146
+ if (currentExists && legacyExists) {
147
+ if (!sameFileIdentity(currentPath, legacyPath)) {
148
+ return globalConfigConflict(currentPath, legacyPath);
149
+ }
150
+ try {
151
+ unlinkFile(legacyPath);
152
+ return {
153
+ status: "migrated",
154
+ activePath: currentPath,
155
+ notification:
156
+ `Completed interrupted Auto Mode config migration from ${legacyPath} to ${currentPath}.`,
157
+ };
158
+ } catch (error) {
159
+ const cleanupError = cleanupPublishedDestination(
160
+ currentPath,
161
+ unlinkFile,
162
+ error,
163
+ );
164
+ return globalConfigFailure(
165
+ currentPath,
166
+ legacyPath,
167
+ cleanupError ?? error,
168
+ cleanupError?.message,
169
+ );
170
+ }
171
+ }
172
+ if (currentExists || !legacyExists) {
173
+ return { status: "current", activePath: currentPath };
174
+ }
175
+
176
+ try {
177
+ mkdirSync(dirname(currentPath), { recursive: true });
178
+ const moveFile = options.moveFile ??
179
+ ((source: string, destination: string) =>
180
+ moveFileWithoutOverwrite(source, destination, unlinkFile));
181
+ moveFile(legacyPath, currentPath);
182
+ return {
183
+ status: "migrated",
184
+ activePath: currentPath,
185
+ notification: `Moved Auto Mode config from ${legacyPath} to ${currentPath}.`,
186
+ };
187
+ } catch (error) {
188
+ if (existsSync(currentPath)) {
189
+ if (!sameFileIdentity(currentPath, legacyPath)) {
190
+ return globalConfigConflict(currentPath, legacyPath);
191
+ }
192
+ const cleanupError = cleanupPublishedDestination(
193
+ currentPath,
194
+ unlinkFile,
195
+ error,
196
+ );
197
+ if (cleanupError) {
198
+ return globalConfigFailure(
199
+ currentPath,
200
+ legacyPath,
201
+ cleanupError,
202
+ cleanupError.message,
203
+ );
204
+ }
205
+ }
206
+ return globalConfigFailure(currentPath, legacyPath, error);
207
+ }
208
+ }
209
+
34
210
  function readSettingsFile(path: string): LoadedSettingsFile | undefined {
35
211
  if (!existsSync(path)) return undefined;
36
212
  try {
@@ -103,6 +279,7 @@ export function validateSettingsFile(
103
279
  "enabled",
104
280
  "classifierModel",
105
281
  "classifierReasoningLevel",
282
+ "classifierTimeoutMs",
106
283
  "classifyReadOnlyTools",
107
284
  "fastClassifierMaxTokens",
108
285
  "allowInsideWorkingDirectory",
@@ -144,6 +321,15 @@ export function validateSettingsFile(
144
321
  `${source}: autoMode.classifierReasoningLevel must be one of low, medium, high, xhigh, max`,
145
322
  );
146
323
  }
324
+ if (
325
+ hasOwn(autoMode, "classifierTimeoutMs") &&
326
+ (!Number.isInteger(autoMode.classifierTimeoutMs) ||
327
+ (autoMode.classifierTimeoutMs as number) < 1000)
328
+ ) {
329
+ diagnostics.push(
330
+ `${source}: autoMode.classifierTimeoutMs must be an integer of at least 1000`,
331
+ );
332
+ }
147
333
  if (
148
334
  hasOwn(autoMode, "classifyReadOnlyTools") &&
149
335
  typeof autoMode.classifyReadOnlyTools !== "boolean"
@@ -235,11 +421,11 @@ export function validateSettingsFile(
235
421
  } else {
236
422
  const permissions = settings.permissions as Record<string, unknown>;
237
423
  for (const key of Object.keys(permissions)) {
238
- if (key !== "deny" && key !== "ask") {
424
+ if (key !== "deny" && key !== "ask" && key !== "allow") {
239
425
  diagnostics.push(`${source}: unknown permissions key ${key}`);
240
426
  }
241
427
  }
242
- for (const key of ["deny", "ask"] as const) {
428
+ for (const key of ["deny", "ask", "allow"] as const) {
243
429
  const value = permissions[key];
244
430
  if (value === undefined) continue;
245
431
  if (!Array.isArray(value)) {
@@ -253,6 +439,10 @@ export function validateSettingsFile(
253
439
  diagnostics.push(
254
440
  `${source}: permissions.${key}[${index}] must be a tool pattern string`,
255
441
  );
442
+ } else if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
443
+ diagnostics.push(
444
+ `${source}: permissions.${key}[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
445
+ );
256
446
  }
257
447
  }
258
448
  }
@@ -265,28 +455,36 @@ export function validateSettingsFile(
265
455
  type RuleAccumulator = {
266
456
  defaults: string[];
267
457
  includeDefaults: boolean;
268
- seen: boolean;
269
458
  entries: string[];
270
459
  };
271
460
 
272
461
  function createRuleAccumulator(defaults: string[]): RuleAccumulator {
273
- return { defaults, includeDefaults: true, seen: false, entries: [] };
462
+ return { defaults, includeDefaults: true, entries: [] };
274
463
  }
275
464
 
276
- function applyRuleSetting(accumulator: RuleAccumulator, value: unknown): void {
465
+ function applyRuleSetting(
466
+ accumulator: RuleAccumulator,
467
+ value: unknown,
468
+ acceptEntry: (entry: string) => boolean = () => true,
469
+ ): void {
277
470
  const entries = stringArray(value);
278
471
  if (!entries) return;
279
- accumulator.seen = true;
280
- accumulator.includeDefaults = entries.includes("$defaults");
472
+ // Any entry that stringArray or acceptEntry drops marks the list malformed.
473
+ // Fail conservative: keep defaults rather than replace them with a partial list.
474
+ let malformed = Array.isArray(value) && value.length !== entries.length;
281
475
  for (const entry of entries) {
282
- if (entry !== "$defaults") accumulator.entries.push(entry);
476
+ if (entry === "$defaults") continue;
477
+ if (acceptEntry(entry)) {
478
+ accumulator.entries.push(entry);
479
+ } else {
480
+ malformed = true;
481
+ }
283
482
  }
483
+ accumulator.includeDefaults = entries.includes("$defaults") || malformed;
284
484
  }
285
485
 
286
486
  function finalizeRuleSetting(accumulator: RuleAccumulator): string[] {
287
- const base = accumulator.includeDefaults || !accumulator.seen
288
- ? accumulator.defaults
289
- : [];
487
+ const base = accumulator.includeDefaults ? accumulator.defaults : [];
290
488
  return [...new Set([...base, ...accumulator.entries])];
291
489
  }
292
490
 
@@ -316,8 +514,8 @@ function mergeLog(
316
514
  ): LogConfig {
317
515
  if (!patch) return base;
318
516
  return {
319
- enabled: patch.enabled ?? base.enabled,
320
- classifierIo: patch.classifierIo ?? base.classifierIo,
517
+ enabled: typeof patch.enabled === "boolean" ? patch.enabled : base.enabled,
518
+ classifierIo: typeof patch.classifierIo === "boolean" ? patch.classifierIo : base.classifierIo,
321
519
  };
322
520
  }
323
521
 
@@ -345,6 +543,12 @@ function validateDeniedPathsSetting(
345
543
  );
346
544
  continue;
347
545
  }
546
+ if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
547
+ diagnostics.push(
548
+ `${source}: deniedPaths[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
549
+ );
550
+ continue;
551
+ }
348
552
  if (!DENIED_PATH_PATTERN_PREFIX.test(entry)) {
349
553
  diagnostics.push(
350
554
  `${source}: deniedPaths[${index}] "${entry}" can never match a resolved absolute path; start it with *, ~, $HOME, \${HOME}, or / (e.g. "**/${entry}")`,
@@ -386,6 +590,10 @@ function validFastClassifierBudget(value: unknown): value is number {
386
590
  return Number.isInteger(value) && Number(value) >= 16;
387
591
  }
388
592
 
593
+ function validClassifierTimeout(value: unknown): value is number {
594
+ return Number.isInteger(value) && Number(value) >= 1000;
595
+ }
596
+
389
597
  function applyAutoModeScalars(
390
598
  base: EffectiveConfig,
391
599
  settings: AutoModeSettings | undefined,
@@ -393,22 +601,28 @@ function applyAutoModeScalars(
393
601
  if (!settings) return base;
394
602
  return {
395
603
  ...base,
396
- enabled: settings.enabled ?? base.enabled,
604
+ enabled: typeof settings.enabled === "boolean" ? settings.enabled : base.enabled,
397
605
  classifierModel: settings.classifierModel ?? base.classifierModel,
398
606
  classifierReasoningLevel: isClassifierReasoningLevel(
399
607
  settings.classifierReasoningLevel,
400
608
  )
401
609
  ? settings.classifierReasoningLevel
402
610
  : base.classifierReasoningLevel,
403
- classifyReadOnlyTools: settings.classifyReadOnlyTools ??
404
- base.classifyReadOnlyTools,
611
+ classifyReadOnlyTools: typeof settings.classifyReadOnlyTools === "boolean"
612
+ ? settings.classifyReadOnlyTools
613
+ : base.classifyReadOnlyTools,
405
614
  allowInsideWorkingDirectory:
406
- settings.allowInsideWorkingDirectory ?? base.allowInsideWorkingDirectory,
615
+ typeof settings.allowInsideWorkingDirectory === "boolean"
616
+ ? settings.allowInsideWorkingDirectory
617
+ : base.allowInsideWorkingDirectory,
407
618
  fastClassifierMaxTokens: validFastClassifierBudget(
408
619
  settings.fastClassifierMaxTokens,
409
620
  )
410
621
  ? settings.fastClassifierMaxTokens
411
622
  : base.fastClassifierMaxTokens,
623
+ classifierTimeoutMs: validClassifierTimeout(settings.classifierTimeoutMs)
624
+ ? settings.classifierTimeoutMs
625
+ : base.classifierTimeoutMs,
412
626
  maxUserTranscriptTokens: validTranscriptBudget(
413
627
  settings.maxUserTranscriptTokens,
414
628
  )
@@ -426,11 +640,12 @@ function applyAutoModeScalars(
426
640
  function appendPermissionPatterns(
427
641
  target: ToolPattern[],
428
642
  settings: SettingsFile | undefined,
429
- key: "deny" | "ask",
643
+ key: "deny" | "ask" | "allow",
430
644
  ): void {
431
645
  const values = stringArray(settings?.permissions?.[key]);
432
646
  if (!values) return;
433
647
  for (const value of values) {
648
+ if (value.length > MAX_WILDCARD_PATTERN_LENGTH) continue;
434
649
  const pattern = parseToolPattern(value);
435
650
  if (pattern) target.push(pattern);
436
651
  }
@@ -440,8 +655,9 @@ function appendPermissionPatterns(
440
655
  * Merge settings with Claude Code-style precedence using Pi-owned config files.
441
656
  *
442
657
  * Important details:
443
- * - shared project `.pi/automode.json` contributes `permissions.*` but not `autoMode`,
444
- * so a checked-in repo cannot weaken classifier rules;
658
+ * - shared project `.pi/automode.json` contributes `permissions.deny` and
659
+ * `permissions.ask` but not `permissions.allow` or `autoMode`, so checked-in
660
+ * config can only add permission barriers;
445
661
  * - global, project-local, and inline `autoMode` settings combine additively across scopes;
446
662
  * - omitting `$defaults` in any scope for a rule list means "replace built-ins" for that list.
447
663
  */
@@ -454,6 +670,7 @@ export function buildEffectiveConfigFromSources(
454
670
  allowInsideWorkingDirectory: DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
455
671
  deniedPaths: [...DEFAULT_DENIED_PATHS],
456
672
  fastClassifierMaxTokens: DEFAULT_FAST_CLASSIFIER_MAX_TOKENS,
673
+ classifierTimeoutMs: DEFAULT_CLASSIFIER_TIMEOUT_MS,
457
674
  maxUserTranscriptTokens: DEFAULT_MAX_USER_TRANSCRIPT_TOKENS,
458
675
  maxToolTranscriptTokens: DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS,
459
676
  environment: [...DEFAULT_ENVIRONMENT],
@@ -463,6 +680,7 @@ export function buildEffectiveConfigFromSources(
463
680
  hardDeny: [...DEFAULT_HARD_DENY],
464
681
  permissionDeny: [],
465
682
  permissionAsk: [],
683
+ permissionAllow: [],
466
684
  log: { ...DEFAULT_LOG_CONFIG },
467
685
  };
468
686
 
@@ -488,7 +706,11 @@ export function buildEffectiveConfigFromSources(
488
706
  applyRuleSetting(environment, settings.autoMode?.environment);
489
707
  applyRuleSetting(allow, settings.autoMode?.allow);
490
708
  applyRuleSetting(protectedPaths, settings.autoMode?.protectedPaths);
491
- applyRuleSetting(deniedPaths, settings.autoMode?.deniedPaths);
709
+ applyRuleSetting(
710
+ deniedPaths,
711
+ settings.autoMode?.deniedPaths,
712
+ (entry) => entry.length <= MAX_WILDCARD_PATTERN_LENGTH,
713
+ );
492
714
  applyRuleSetting(
493
715
  softDeny,
494
716
  settings.autoMode?.soft_deny ?? settings.autoMode?.softDeny,
@@ -520,6 +742,15 @@ export function buildEffectiveConfigFromSources(
520
742
  appendPermissionPatterns(config.permissionDeny, settings, "deny");
521
743
  appendPermissionPatterns(config.permissionAsk, settings, "ask");
522
744
  }
745
+ for (
746
+ const settings of [
747
+ ...globalSettings,
748
+ ...projectLocalSettings,
749
+ ...inlineSettings,
750
+ ]
751
+ ) {
752
+ appendPermissionPatterns(config.permissionAllow, settings, "allow");
753
+ }
523
754
 
524
755
  return config;
525
756
  }
@@ -536,9 +767,23 @@ function loadedSettingsDiagnostics(
536
767
  return files.flatMap((file) => file?.diagnostics ?? []);
537
768
  }
538
769
 
539
- /** Load config from disk and environment variables, including diagnostics for `/automode config`. */
770
+ function ignoredSharedAllowDiagnostics(
771
+ files: Array<LoadedSettingsFile | undefined>,
772
+ ): string[] {
773
+ return files.flatMap((file) => {
774
+ const permissions = file?.settings?.permissions;
775
+ if (!file || !permissions || !hasOwn(permissions, "allow")) return [];
776
+ return [
777
+ `${file.path}: permissions.allow is ignored in shared project config. Use a user-owned config source instead`,
778
+ ];
779
+ });
780
+ }
781
+
782
+ /** Load config from disk and environment variables, including diagnostics for `/automode config`. Project files require explicit trust. */
540
783
  export function loadEffectiveConfigWithDiagnostics(
541
784
  cwd: string,
785
+ projectTrusted = false,
786
+ globalSettingsPath = PI_GLOBAL_SETTINGS[0],
542
787
  ): ConfigLoadResult {
543
788
  const inlineSettings: SettingsFile[] = [];
544
789
  const diagnostics: string[] = [];
@@ -560,18 +805,36 @@ export function loadEffectiveConfigWithDiagnostics(
560
805
  }
561
806
  }
562
807
 
563
- const globalFiles = PI_GLOBAL_SETTINGS.map(readSettingsFile);
564
- const projectLocalFiles = PI_PROJECT_LOCAL_SETTINGS.map((file) =>
565
- readSettingsFile(resolve(cwd, file))
808
+ const globalFiles = [readSettingsFile(globalSettingsPath)];
809
+ const projectLocalPaths = PI_PROJECT_LOCAL_SETTINGS.map((file) =>
810
+ resolve(cwd, file)
566
811
  );
567
- const projectSharedFiles = PI_PROJECT_SHARED_SETTINGS.map((file) =>
568
- readSettingsFile(resolve(cwd, file))
812
+ const projectSharedPaths = PI_PROJECT_SHARED_SETTINGS.map((file) =>
813
+ resolve(cwd, file)
569
814
  );
815
+ const projectLocalFiles = projectTrusted
816
+ ? projectLocalPaths.map(readSettingsFile)
817
+ : [];
818
+ const projectSharedFiles = projectTrusted
819
+ ? projectSharedPaths.map(readSettingsFile)
820
+ : [];
821
+ if (!projectTrusted) {
822
+ for (
823
+ const path of [...projectLocalPaths, ...projectSharedPaths].filter(
824
+ existsSync,
825
+ )
826
+ ) {
827
+ diagnostics.push(`${path}: ignored because project is not trusted`);
828
+ }
829
+ }
570
830
  const fileDiagnostics = loadedSettingsDiagnostics([
571
831
  ...globalFiles,
572
832
  ...projectLocalFiles,
573
833
  ...projectSharedFiles,
574
834
  ]);
835
+ const sharedAllowDiagnostics = ignoredSharedAllowDiagnostics(
836
+ projectSharedFiles,
837
+ );
575
838
 
576
839
  return {
577
840
  config: buildEffectiveConfigFromSources({
@@ -580,13 +843,25 @@ export function loadEffectiveConfigWithDiagnostics(
580
843
  projectSharedSettings: loadedSettingsToSettings(projectSharedFiles),
581
844
  inlineSettings,
582
845
  }),
583
- diagnostics: [...fileDiagnostics, ...diagnostics],
846
+ diagnostics: [
847
+ ...fileDiagnostics,
848
+ ...sharedAllowDiagnostics,
849
+ ...diagnostics,
850
+ ],
584
851
  };
585
852
  }
586
853
 
587
854
  /** Load config from disk and environment variables. Exported for tests and diagnostics. */
588
- export function loadEffectiveConfig(cwd: string): EffectiveConfig {
589
- return loadEffectiveConfigWithDiagnostics(cwd).config;
855
+ export function loadEffectiveConfig(
856
+ cwd: string,
857
+ projectTrusted = false,
858
+ globalSettingsPath = PI_GLOBAL_SETTINGS[0],
859
+ ): EffectiveConfig {
860
+ return loadEffectiveConfigWithDiagnostics(
861
+ cwd,
862
+ projectTrusted,
863
+ globalSettingsPath,
864
+ ).config;
590
865
  }
591
866
 
592
867
  function readWritableSettingsFile(path: string): SettingsFile {
@@ -59,6 +59,9 @@ export const DEFAULT_MAX_USER_TRANSCRIPT_TOKENS = 4000;
59
59
  export const DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS = 4000;
60
60
  export const DENIAL_HISTORY_LIMIT = 12;
61
61
 
62
+ /** Per-request timeout for classifier completions (fast and detailed stages). */
63
+ export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 20_000;
64
+
62
65
  /** Built-in trusted environment. Users extend this with `$defaults`. */
63
66
  export const DEFAULT_ENVIRONMENT = [
64
67
  "Trusted repo: the repository pi started in and its configured git remotes.",
@@ -161,7 +164,10 @@ Valid decision/tier combinations:
161
164
  - block: hard_deny, soft_deny, or none
162
165
  If an allow exception or explicit user intent overrides a soft-deny rule, return allow with tier allow or explicit_intent, never soft_deny.`;
163
166
 
164
- export const PI_GLOBAL_SETTINGS = [resolve(HOME, ".pi/agent/automode.json")];
167
+ export const PI_GLOBAL_SETTINGS = [
168
+ resolve(HOME, ".pi/agent/extensions/pi-automode/config.json"),
169
+ ];
170
+ export const PI_LEGACY_GLOBAL_SETTINGS = resolve(HOME, ".pi/agent/automode.json");
165
171
  export const PI_PROJECT_LOCAL_SETTINGS = [".pi/automode.local.json"];
166
172
  export const PI_PROJECT_SHARED_SETTINGS = [".pi/automode.json"];
167
173