pi-goal-x 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,7 +54,7 @@ All core features of [@capyup/pi-goal](https://github.com/capyup/pi-goal) are pr
54
54
 
55
55
  - **Live progress widget** — when the auditor runs, the TUI shows a spinner, a progress bar (`[████░░░░] 40%`), step labels (`Inspecting files...`, `Verifying success criteria...`), the current tool being executed, and recent output lines. No more wondering if anything is happening.
56
56
  - **Escape to skip** — press Escape during an audit to abort it and complete the goal immediately. The skip is recorded in the ledger as `audit_skipped` with reason `user_aborted` and auditor model metadata.
57
- - **Disable the auditor entirely** — set `disabled: true` in `.pi/goal-auditor.json` (or toggle it via `/goal-settings` → `disabled`). The agent can still bypass with user confirmation by passing `confirmBypassAuditor: true` to `update_goal`.
57
+ - **Disable the auditor entirely** — set `disabled: true` in `.pi/pi-goal-x-settings.json` (or toggle it via `/goal-settings`). The agent can still bypass with user confirmation by passing `confirmBypassAuditor: true` to `update_goal`.
58
58
  - **Skipped audits are recorded** — every skip (whether disabled or Escape-aborted) is logged to the ledger with the reason, provider, model, and thinking level for full traceability.
59
59
  - **Robust abort detection** — the auditor detects aborts both from exceptions *and* from `session.prompt()` returning after an abort signal, preventing stuck goals or ghost states.
60
60
  - **Cleaner lifecycle** — `AbortSignal` is properly wired to `session.abort()`, animation timers are cleaned up, and the unsubscribe path is always executed. No more having to kill the session.
@@ -223,7 +223,7 @@ Before archiving the goal, `update_goal` starts a separate pi agent in an isolat
223
223
 
224
224
  The auditor is semantic, not a paperwork checklist: it should reject scaffold-only, alpha, generated-template, proxy-metric, build-only, or weakly verified completions when the real user outcome is not satisfied.
225
225
 
226
- By default the auditor uses the current/default pi model. Configure it via `.pi/goal-auditor.json`, interactively with `/goal-settings` → `auditor`, or environment variables (see [Settings files](#settings-files)).
226
+ By default the auditor uses the current/default pi model. Configure it via `.pi/pi-goal-x-settings.json`, or interactively with `/goal-settings` (see [Configuration](#configuration)).
227
227
 
228
228
  The completion result prints a full report into the conversation:
229
229
 
@@ -271,11 +271,9 @@ Before commands, tools, and lifecycle hooks act on a focused goal, the runtime r
271
271
 
272
272
  Goal paths are constrained to `.pi/goals/` and `.pi/goals/archived/`; absolute paths, traversal, NUL bytes, symlinks, and unsafe metadata paths are rejected.
273
273
 
274
- ## Settings files
274
+ ## Configuration
275
275
 
276
- Configuration is split across two files under `.pi/`.
277
-
278
- ### `.pi/goal-settings.json`
276
+ All settings live in a single file: **`.pi/pi-goal-x-settings.json`**
279
277
 
280
278
  Configured interactively via `/goal-settings`, or edited directly:
281
279
 
@@ -283,24 +281,7 @@ Configured interactively via `/goal-settings`, or edited directly:
283
281
  {
284
282
  "disableTasks": false,
285
283
  "disableContracts": false,
286
- "subtaskDepth": 1
287
- }
288
- ```
289
-
290
- | Field | Default | Purpose |
291
- |---|---:|---|
292
- | `disableTasks` | `false` | Suppress task list features entirely when `true` |
293
- | `disableContracts` | `false` | Suppress verification contract enforcement when `true` |
294
- | `subtaskDepth` | `1` | Maximum nesting depth for subtasks (`1` = tasks → subtasks, `2` = tasks → subtasks → sub-subtasks) |
295
-
296
- **Env var overrides:** `PI_GOAL_DISABLE_TASKS=1` and `PI_GOAL_DISABLE_CONTRACTS=1` take precedence over the file. Set to any truthy string to disable.
297
-
298
- ### `.pi/goal-auditor.json`
299
-
300
- Configured interactively via `/goal-settings` → `auditor`, or edited directly:
301
-
302
- ```json
303
- {
284
+ "subtaskDepth": 1,
304
285
  "provider": "fireworks",
305
286
  "model": "accounts/fireworks/models/deepseek-v4-flash",
306
287
  "thinkingLevel": "high",
@@ -310,18 +291,27 @@ Configured interactively via `/goal-settings` → `auditor`, or edited directly:
310
291
 
311
292
  | Field | Default | Purpose |
312
293
  |---|---:|---|
313
- | `provider` | system default | Provider name for the auditor agent (`anthropic`, `fireworks`, `google`, `groq`, etc.) |
294
+ | `disableTasks` | `false` | Suppress task list features entirely when `true` |
295
+ | `disableContracts` | `false` | Suppress verification contract enforcement when `true` |
296
+ | `subtaskDepth` | `1` | Maximum nesting depth for subtasks |
297
+ | `provider` | system default | Provider name for the auditor agent |
314
298
  | `model` | system default | Model name for the auditor agent |
315
- | `thinkingLevel` | system default | Thinking level: `none`, `low`, `medium`, `high` |
299
+ | `thinkingLevel` | system default | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
316
300
  | `disabled` | `false` | When `true`, skip the completion audit entirely |
317
301
 
318
- **Env var overrides:** `PI_GOAL_AUDITOR_PROVIDER`, `PI_GOAL_AUDITOR_MODEL`, and `PI_GOAL_AUDITOR_THINKING_LEVEL` take precedence over file config. `PI_GOAL_AUDITOR_THINKING` is also accepted as an alias for the thinking level.
302
+ **Env var overrides:**
303
+ - `PI_GOAL_DISABLE_TASKS=1` — disable task features (takes precedence over file)
304
+ - `PI_GOAL_DISABLE_CONTRACTS=1` — disable contract enforcement (takes precedence over file)
305
+ - `PI_GOAL_SETTINGS_FILE=custom-path.json` — alternative settings file path (relative to cwd or absolute)
319
306
 
320
307
  ## Environment variables
321
308
 
322
309
  | Variable | Default | Purpose |
323
310
  |---|---:|---|
324
311
  | `PI_GOAL_AUTO_CONFIRM` | unset | When `1`, auto-confirms drafts in headless/test contexts |
312
+ | `PI_GOAL_DISABLE_TASKS` | — | When `1`, disable task features (overrides settings file) |
313
+ | `PI_GOAL_DISABLE_CONTRACTS` | — | When `1`, disable contract enforcement (overrides settings file) |
314
+ | `PI_GOAL_SETTINGS_FILE` | `.pi/pi-goal-x-settings.json` | Alternative settings file path (relative to cwd or absolute) |
325
315
 
326
316
  ## Development
327
317
 
@@ -200,7 +200,7 @@ Before archiving, the tool starts a separate in-memory pi session with a focused
200
200
  - `<approved/>` allows archiving;
201
201
  - `<disapproved/>`, no marker, an error, or abort rejects completion and leaves the goal open.
202
202
 
203
- The auditor uses the current/default model unless `.pi/goal-auditor.json` or `PI_GOAL_AUDITOR_PROVIDER`, `PI_GOAL_AUDITOR_MODEL`, and `PI_GOAL_AUDITOR_THINKING_LEVEL` override provider/model/thinking. `/goal-settings` opens a small UI menu with an `auditor` item; inside it, `provider`, `model`, and `thinking_level` each open a free-text input and save back to `.pi/goal-auditor.json`.
203
+ The auditor uses the current/default model unless `.pi/pi-goal-x-settings.json` overrides `provider`, `model`, or `thinkingLevel`. `/goal-settings` opens a TUI showing all settings (disabled, provider, model, thinking_level, subtaskDepth, disableTasks, disableContracts); each editable field opens a free-text input and saves back to `.pi/pi-goal-x-settings.json`.
204
204
 
205
205
  The user sees:
206
206
 
@@ -14,14 +14,7 @@ import {
14
14
  type ResourceLoader,
15
15
  } from "@earendil-works/pi-coding-agent";
16
16
  import type { GoalRecord, GoalTask, GoalTaskList } from "./goal-record.ts";
17
- import type { GoalSettings } from "./goal-settings.ts";
18
-
19
- export interface GoalAuditorConfig {
20
- provider?: string;
21
- model?: string;
22
- thinkingLevel?: ThinkingLevel;
23
- disabled?: boolean;
24
- }
17
+ import { loadGoalSettings, type GoalSettings } from "./goal-settings.ts";
25
18
 
26
19
  export interface AuditorProgress {
27
20
  /** Current tool being executed by the auditor, if any */
@@ -55,10 +48,6 @@ export interface GoalAuditorResult {
55
48
 
56
49
  const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
57
50
 
58
- export function goalAuditorConfigPath(cwd: string): string {
59
- return path.join(cwd, ".pi", "goal-auditor.json");
60
- }
61
-
62
51
  function asNonEmptyString(value: unknown): string | undefined {
63
52
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
64
53
  }
@@ -68,59 +57,7 @@ function asThinkingLevel(value: unknown): ThinkingLevel | undefined {
68
57
  return text && THINKING_LEVELS.has(text) ? text as ThinkingLevel : undefined;
69
58
  }
70
59
 
71
- export function parseGoalAuditorConfig(raw: unknown): GoalAuditorConfig {
72
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
73
- const record = raw as Record<string, unknown>;
74
- const config: GoalAuditorConfig = {};
75
- const provider = asNonEmptyString(record.provider);
76
- const model = asNonEmptyString(record.model);
77
- const thinkingLevel = asThinkingLevel(record.thinkingLevel ?? record.thinking_level);
78
- if (provider) config.provider = provider;
79
- if (model) config.model = model;
80
- if (thinkingLevel) config.thinkingLevel = thinkingLevel;
81
- if (record.disabled === true || record.disabled === "true") config.disabled = true;
82
- return config;
83
- }
84
-
85
- export function loadGoalAuditorFileConfig(cwd: string): GoalAuditorConfig {
86
- try {
87
- const configPath = goalAuditorConfigPath(cwd);
88
- if (fs.existsSync(configPath)) return parseGoalAuditorConfig(JSON.parse(fs.readFileSync(configPath, "utf8")));
89
- } catch {
90
- return {};
91
- }
92
- return {};
93
- }
94
-
95
- export function loadGoalAuditorConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): GoalAuditorConfig {
96
- const fileConfig = loadGoalAuditorFileConfig(cwd);
97
- return {
98
- ...fileConfig,
99
- provider: asNonEmptyString(env.PI_GOAL_AUDITOR_PROVIDER) ?? fileConfig.provider,
100
- model: asNonEmptyString(env.PI_GOAL_AUDITOR_MODEL) ?? fileConfig.model,
101
- thinkingLevel: asThinkingLevel(env.PI_GOAL_AUDITOR_THINKING_LEVEL ?? env.PI_GOAL_AUDITOR_THINKING) ?? fileConfig.thinkingLevel,
102
- };
103
- }
104
60
 
105
- export function saveGoalAuditorFileConfig(cwd: string, config: GoalAuditorConfig): GoalAuditorConfig {
106
- const clean: GoalAuditorConfig = {};
107
- const provider = asNonEmptyString(config.provider);
108
- const model = asNonEmptyString(config.model);
109
- const thinkingLevel = asThinkingLevel(config.thinkingLevel);
110
- if (provider) clean.provider = provider;
111
- if (model) clean.model = model;
112
- if (thinkingLevel) clean.thinkingLevel = thinkingLevel;
113
- if (config.disabled === true) clean.disabled = true;
114
- const configPath = goalAuditorConfigPath(cwd);
115
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
116
- const persisted: Record<string, unknown> = {};
117
- if (clean.provider) persisted.provider = clean.provider;
118
- if (clean.model) persisted.model = clean.model;
119
- if (clean.thinkingLevel) persisted.thinking_level = clean.thinkingLevel;
120
- if (clean.disabled) persisted.disabled = true;
121
- fs.writeFileSync(configPath, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
122
- return clean;
123
- }
124
61
 
125
62
  export function parseAuditorDecision(output: string): { approved: boolean; disapproved: boolean } {
126
63
  const approved = /<approved\s*\/>/.test(output);
@@ -282,7 +219,7 @@ function makeAuditorResourceLoader(): ResourceLoader {
282
219
  };
283
220
  }
284
221
 
285
- function resolveAuditorModel(ctx: ExtensionContext, config: GoalAuditorConfig): { model: Model<any> | undefined; error?: string } {
222
+ function resolveAuditorModel(ctx: ExtensionContext, config: GoalSettings): { model: Model<any> | undefined; error?: string } {
286
223
  if (!config.model && !config.provider) return { model: ctx.model };
287
224
  if (config.provider && config.model) {
288
225
  const model = ctx.modelRegistry.find(config.provider, config.model);
@@ -325,7 +262,7 @@ export async function runGoalCompletionAuditor(args: {
325
262
  */
326
263
  createSession?: typeof createAgentSession;
327
264
  }): Promise<GoalAuditorResult> {
328
- const config = loadGoalAuditorConfig(args.ctx.cwd);
265
+ const config = loadGoalSettings(args.ctx.cwd);
329
266
  const resolved = resolveAuditorModel(args.ctx, config);
330
267
  const model = resolved.model;
331
268
  const thinkingLevel = config.thinkingLevel;
@@ -1,28 +1,59 @@
1
1
  /**
2
- * Global goal settings: config file + env var overrides for disabling
3
- * task lists and/or verification contracts.
2
+ * Unified global goal settings.
4
3
  *
5
- * Reads `.pi/goal-settings.json` with env var overrides:
4
+ * Reads `.pi/pi-goal-x-settings.json` with env var overrides:
6
5
  * PI_GOAL_DISABLE_TASKS — "true" to disable, any other value = use file config
7
6
  * PI_GOAL_DISABLE_CONTRACTS — "true" to disable, any other value = use file config
7
+ * PI_GOAL_SETTINGS_FILE — alternative settings file path (relative to cwd or absolute)
8
8
  *
9
- * Pattern mirrors `goalAuditorConfig` in goal-auditor.ts.
9
+ * The file may contain:
10
+ * disableTasks, disableContracts, subtaskDepth,
11
+ * provider, model, thinkingLevel, disabled
12
+ *
13
+ * additionalProperties: false — unknown keys are rejected.
10
14
  */
11
15
 
12
16
  import * as fs from "node:fs";
13
17
  import * as path from "node:path";
14
18
 
19
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
20
+
15
21
  export interface GoalSettings {
16
22
  disableTasks?: boolean;
17
23
  disableContracts?: boolean;
18
24
  subtaskDepth?: number;
25
+ provider?: string;
26
+ model?: string;
27
+ thinkingLevel?: ThinkingLevel;
28
+ disabled?: boolean;
19
29
  }
20
30
 
31
+ export const PI_GOAL_SETTINGS_FILE_ENV = "PI_GOAL_SETTINGS_FILE";
32
+
33
+ const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
34
+
35
+ const ALLOWED_SETTINGS_KEYS = new Set([
36
+ "disableTasks",
37
+ "disableContracts",
38
+ "subtaskDepth",
39
+ "provider",
40
+ "model",
41
+ "thinkingLevel",
42
+ "thinking_level",
43
+ "disabled",
44
+ ]);
45
+
21
46
  /**
22
- * Resolve the path to the global goal-settings.json file.
47
+ * Resolve the path to the unified settings file.
48
+ * Uses `PI_GOAL_SETTINGS_FILE` env var if set (relative to cwd or absolute).
49
+ * Otherwise defaults to `.pi/pi-goal-x-settings.json`.
23
50
  */
24
- export function goalSettingsPath(cwd: string): string {
25
- return path.join(cwd, ".pi", "goal-settings.json");
51
+ export function goalSettingsPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
52
+ const override = asNonEmptyString(env[PI_GOAL_SETTINGS_FILE_ENV]);
53
+ if (override) {
54
+ return path.isAbsolute(override) ? override : path.join(cwd, override);
55
+ }
56
+ return path.join(cwd, ".pi", "pi-goal-x-settings.json");
26
57
  }
27
58
 
28
59
  function asNonEmptyString(value: unknown): string | undefined {
@@ -44,7 +75,10 @@ function asPositiveInt(value: unknown): number | undefined {
44
75
  return undefined;
45
76
  }
46
77
 
47
- const ALLOWED_SETTINGS_KEYS = new Set(["disableTasks", "disableContracts", "subtaskDepth"]);
78
+ function asThinkingLevel(value: unknown): ThinkingLevel | undefined {
79
+ const text = asNonEmptyString(value);
80
+ return text && THINKING_LEVELS.has(text) ? text as ThinkingLevel : undefined;
81
+ }
48
82
 
49
83
  /**
50
84
  * Parse raw (deserialized JSON) into a GoalSettings object.
@@ -55,24 +89,31 @@ export function parseGoalSettings(raw: unknown): GoalSettings {
55
89
  const record = raw as Record<string, unknown>;
56
90
  const unknownKeys = Object.keys(record).filter((k) => !ALLOWED_SETTINGS_KEYS.has(k));
57
91
  if (unknownKeys.length > 0) {
58
- throw new Error(`Unknown goal-settings.json key(s): ${unknownKeys.join(", ")}`);
92
+ throw new Error(`Unknown pi-goal-x-settings.json key(s): ${unknownKeys.join(", ")}`);
59
93
  }
60
94
  const settings: GoalSettings = {};
61
95
  const disableTasks = asBool(record.disableTasks);
62
96
  const disableContracts = asBool(record.disableContracts);
63
97
  const subtaskDepth = asPositiveInt(record.subtaskDepth);
98
+ const provider = asNonEmptyString(record.provider);
99
+ const model = asNonEmptyString(record.model);
100
+ const thinkingLevel = asThinkingLevel(record.thinkingLevel ?? record.thinking_level);
64
101
  if (disableTasks !== undefined) settings.disableTasks = disableTasks;
65
102
  if (disableContracts !== undefined) settings.disableContracts = disableContracts;
66
103
  if (subtaskDepth !== undefined) settings.subtaskDepth = subtaskDepth;
104
+ if (provider !== undefined) settings.provider = provider;
105
+ if (model !== undefined) settings.model = model;
106
+ if (thinkingLevel !== undefined) settings.thinkingLevel = thinkingLevel;
107
+ if (record.disabled === true || record.disabled === "true") settings.disabled = true;
67
108
  return settings;
68
109
  }
69
110
 
70
111
  /**
71
112
  * Load settings from the file on disk. Returns {} if file missing or invalid.
72
113
  */
73
- export function loadGoalSettingsFileConfig(cwd: string): GoalSettings {
114
+ export function loadGoalSettingsFileConfig(cwd: string, env?: NodeJS.ProcessEnv): GoalSettings {
74
115
  try {
75
- const configPath = goalSettingsPath(cwd);
116
+ const configPath = goalSettingsPath(cwd, env);
76
117
  if (fs.existsSync(configPath)) return parseGoalSettings(JSON.parse(fs.readFileSync(configPath, "utf8")));
77
118
  } catch {
78
119
  // file missing, malformed JSON, etc. — use defaults
@@ -83,13 +124,50 @@ export function loadGoalSettingsFileConfig(cwd: string): GoalSettings {
83
124
  /**
84
125
  * Load settings with env var overrides.
85
126
  * Env vars take precedence over file config.
86
- * Default: both flags false (features enabled).
127
+ * Default: all flags false/undefined (features enabled, default model).
87
128
  */
88
129
  export function loadGoalSettings(cwd: string, env: NodeJS.ProcessEnv = process.env): GoalSettings {
89
- const fileConfig = loadGoalSettingsFileConfig(cwd);
130
+ const fileConfig = loadGoalSettingsFileConfig(cwd, env);
90
131
  return {
91
132
  disableTasks: asBool(env.PI_GOAL_DISABLE_TASKS) ?? fileConfig.disableTasks ?? false,
92
133
  disableContracts: asBool(env.PI_GOAL_DISABLE_CONTRACTS) ?? fileConfig.disableContracts ?? false,
93
134
  subtaskDepth: fileConfig.subtaskDepth ?? 1,
135
+ provider: fileConfig.provider,
136
+ model: fileConfig.model,
137
+ thinkingLevel: fileConfig.thinkingLevel,
138
+ disabled: fileConfig.disabled,
94
139
  };
95
140
  }
141
+
142
+ /**
143
+ * Save settings to the unified settings file on disk.
144
+ * Persists only non-default values using the canonical key names.
145
+ */
146
+ export function saveGoalSettingsFileConfig(cwd: string, settings: GoalSettings): GoalSettings {
147
+ const clean: GoalSettings = {};
148
+ const provider = asNonEmptyString(settings.provider);
149
+ const model = asNonEmptyString(settings.model);
150
+ const thinkingLevel = asThinkingLevel(settings.thinkingLevel);
151
+ const disableTasks = asBool(settings.disableTasks);
152
+ const disableContracts = asBool(settings.disableContracts);
153
+ const subtaskDepth = asPositiveInt(settings.subtaskDepth);
154
+ if (provider) clean.provider = provider;
155
+ if (model) clean.model = model;
156
+ if (thinkingLevel) clean.thinkingLevel = thinkingLevel;
157
+ if (settings.disabled === true) clean.disabled = true;
158
+ if (disableTasks === true) clean.disableTasks = true;
159
+ if (disableContracts === true) clean.disableContracts = true;
160
+ if (subtaskDepth !== undefined) clean.subtaskDepth = subtaskDepth;
161
+ const configPath = goalSettingsPath(cwd);
162
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
163
+ const persisted: Record<string, unknown> = {};
164
+ if (clean.provider) persisted.provider = clean.provider;
165
+ if (clean.model) persisted.model = clean.model;
166
+ if (clean.thinkingLevel) persisted.thinking_level = clean.thinkingLevel;
167
+ if (clean.disabled) persisted.disabled = true;
168
+ if (clean.disableTasks) persisted.disableTasks = true;
169
+ if (clean.disableContracts) persisted.disableContracts = true;
170
+ if (clean.subtaskDepth !== undefined) persisted.subtaskDepth = clean.subtaskDepth;
171
+ fs.writeFileSync(configPath, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
172
+ return clean;
173
+ }
@@ -17,13 +17,15 @@ import {
17
17
  type GoalDraftingFocus,
18
18
  } from "./goal-draft.ts";
19
19
  import {
20
- goalAuditorConfigPath,
21
- loadGoalAuditorFileConfig,
22
20
  runGoalCompletionAuditor,
23
- saveGoalAuditorFileConfig,
24
- type GoalAuditorConfig,
25
21
  } from "./goal-auditor.ts";
26
- import { loadGoalSettings, type GoalSettings } from "./goal-settings.ts";
22
+ import {
23
+ goalSettingsPath,
24
+ loadGoalSettings,
25
+ loadGoalSettingsFileConfig,
26
+ saveGoalSettingsFileConfig,
27
+ type GoalSettings,
28
+ } from "./goal-settings.ts";
27
29
  import {
28
30
  proposalDialogFailureMessage,
29
31
  registerQuestionnaireTools,
@@ -482,7 +484,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
482
484
 
483
485
  function abortAudit(ctx: ExtensionContext): void {
484
486
  if (!auditAbortController || !auditProgress) return;
485
- const auditorConfig = loadGoalAuditorFileConfig(ctx.cwd);
487
+ const settings = loadGoalSettingsFileConfig(ctx.cwd);
486
488
  auditAbortController.abort();
487
489
  auditAbortController = null;
488
490
  stopAuditAnimation();
@@ -495,9 +497,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
495
497
  type: "audit_skipped",
496
498
  goalId: state.goal.id,
497
499
  reason: "user_aborted",
498
- provider: auditorConfig.provider,
499
- model: auditorConfig.model,
500
- thinkingLevel: auditorConfig.thinkingLevel,
500
+ provider: settings.provider,
501
+ model: settings.model,
502
+ thinkingLevel: settings.thinkingLevel,
501
503
  at: nowIso(),
502
504
  });
503
505
  } catch {
@@ -1306,75 +1308,85 @@ export default function goalExtension(pi: ExtensionAPI): void {
1306
1308
  }
1307
1309
  }
1308
1310
 
1309
- function auditorConfigValue(config: GoalAuditorConfig, key: keyof GoalAuditorConfig): string {
1311
+ function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
1310
1312
  if (key === "disabled") return config.disabled === true ? "true" : "false";
1313
+ if (key === "disableTasks") return config.disableTasks === true ? "true" : "false";
1314
+ if (key === "disableContracts") return config.disableContracts === true ? "true" : "false";
1315
+ if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
1311
1316
  return config[key] ?? "(default)";
1312
1317
  }
1313
1318
 
1314
- function auditorSettingsLines(config: GoalAuditorConfig): string[] {
1319
+ function settingsLines(config: GoalSettings): string[] {
1315
1320
  return [
1316
- `disabled: ${auditorConfigValue(config, "disabled")}`,
1317
- `provider: ${auditorConfigValue(config, "provider")}`,
1318
- `model: ${auditorConfigValue(config, "model")}`,
1319
- `thinking_level: ${auditorConfigValue(config, "thinkingLevel")}`,
1321
+ `disabled: ${settingsValue(config, "disabled")}`,
1322
+ `provider: ${settingsValue(config, "provider")}`,
1323
+ `model: ${settingsValue(config, "model")}`,
1324
+ `thinking_level: ${settingsValue(config, "thinkingLevel")}`,
1325
+ `disableTasks: ${settingsValue(config, "disableTasks")}`,
1326
+ `disableContracts: ${settingsValue(config, "disableContracts")}`,
1327
+ `subtaskDepth: ${settingsValue(config, "subtaskDepth")}`,
1320
1328
  ];
1321
1329
  }
1322
1330
 
1323
- async function handleGoalAuditorSettings(ctx: ExtensionContext): Promise<void> {
1331
+ async function handleSettingsMenu(ctx: ExtensionContext): Promise<void> {
1324
1332
  if (!ctx.hasUI) {
1325
- ctx.ui.notify(`Goal auditor settings file: ${goalAuditorConfigPath(ctx.cwd)}`, "info");
1333
+ ctx.ui.notify(`Settings file: ${goalSettingsPath(ctx.cwd)}`, "info");
1326
1334
  return;
1327
1335
  }
1328
- const fieldLabels = ["disabled", "provider", "model", "thinking_level"] as const;
1336
+ const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth"] as const;
1329
1337
  while (true) {
1330
- const config = loadGoalAuditorFileConfig(ctx.cwd);
1331
- const options = [
1332
- `disabled: ${auditorConfigValue(config, "disabled")}`,
1333
- `provider: ${auditorConfigValue(config, "provider")}`,
1334
- `model: ${auditorConfigValue(config, "model")}`,
1335
- `thinking_level: ${auditorConfigValue(config, "thinkingLevel")}`,
1336
- ];
1337
- const selected = await ctx.ui.select("Goal auditor settings", options);
1338
- if (!selected) return;
1339
- const index = options.indexOf(selected);
1340
- const field = fieldLabels[index];
1341
- if (!field) return;
1342
- const key = field === "thinking_level" ? "thinkingLevel" : field;
1338
+ const config = loadGoalSettingsFileConfig(ctx.cwd);
1339
+ const options = settingsLines(config).map((line) => ` ${line}`);
1340
+ options.unshift("─── Settings ───");
1341
+ options.push("Done");
1342
+ const selected = await ctx.ui.select("Goal settings", options);
1343
+ if (!selected || selected === "Done" || selected === "─── Settings ───") return;
1344
+ // Strip leading spaces from selection
1345
+ const selectedTrimmed = selected.trim();
1346
+ const colon = selectedTrimmed.indexOf(":");
1347
+ if (colon === -1) continue;
1348
+ const field = selectedTrimmed.slice(0, colon).trim();
1349
+ const editorKey = field === "thinking_level" ? "thinkingLevel" : field;
1350
+ if (!(editorKeys as readonly string[]).includes(editorKey)) continue;
1351
+ const key = editorKey as keyof GoalSettings;
1343
1352
  if (key === "disabled") {
1344
- // Toggle the disabled flag
1345
- const next: GoalAuditorConfig = { ...config, disabled: !config.disabled };
1346
- saveGoalAuditorFileConfig(ctx.cwd, next);
1347
- ctx.ui.notify(`Goal auditor settings saved:\n${auditorSettingsLines(loadGoalAuditorFileConfig(ctx.cwd)).join("\n")}`, "info");
1353
+ const next = { ...config, disabled: !config.disabled };
1354
+ saveGoalSettingsFileConfig(ctx.cwd, next);
1355
+ ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
1356
+ continue;
1357
+ }
1358
+ if (key === "subtaskDepth") {
1359
+ const input = await ctx.ui.input("Set subtaskDepth", String(config.subtaskDepth ?? 1));
1360
+ if (input === undefined) continue;
1361
+ const n = parseInt(input.trim(), 10);
1362
+ if (isNaN(n) || n < 1) {
1363
+ ctx.ui.notify("subtaskDepth must be a positive integer", "warning");
1364
+ continue;
1365
+ }
1366
+ const next = { ...config, subtaskDepth: n };
1367
+ saveGoalSettingsFileConfig(ctx.cwd, next);
1368
+ ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
1348
1369
  continue;
1349
1370
  }
1350
- const currentValue = auditorConfigValue(config, key as keyof GoalAuditorConfig);
1351
- const input = await ctx.ui.input(`Set auditor ${field}`, currentValue === "(default)" ? "Leave empty for default" : currentValue);
1371
+ const currentValue = settingsValue(config, key);
1372
+ const input = await ctx.ui.input(`Set ${field}`, currentValue === "(default)" ? "Leave empty for default" : currentValue);
1352
1373
  if (input === undefined) continue;
1353
- const next: GoalAuditorConfig = { ...config };
1354
- const trimmed = input.trim();
1355
- if (!trimmed) {
1356
- delete next[key as keyof GoalAuditorConfig];
1374
+ const next: GoalSettings = { ...config };
1375
+ const inputTrimmed = input.trim();
1376
+ if (!inputTrimmed) {
1377
+ delete next[key];
1357
1378
  } else if (key === "thinkingLevel") {
1358
- if (!["off", "minimal", "low", "medium", "high", "xhigh"].includes(trimmed)) {
1379
+ if (!["off", "minimal", "low", "medium", "high", "xhigh"].includes(inputTrimmed)) {
1359
1380
  ctx.ui.notify("thinking_level must be one of: off, minimal, low, medium, high, xhigh", "warning");
1360
1381
  continue;
1361
1382
  }
1362
- next.thinkingLevel = trimmed as GoalAuditorConfig["thinkingLevel"];
1383
+ next.thinkingLevel = inputTrimmed as GoalSettings["thinkingLevel"];
1363
1384
  } else if (key === "provider" || key === "model") {
1364
- next[key] = trimmed;
1385
+ next[key] = inputTrimmed;
1365
1386
  }
1366
- saveGoalAuditorFileConfig(ctx.cwd, next);
1367
- ctx.ui.notify(`Goal auditor settings saved:\n${auditorSettingsLines(loadGoalAuditorFileConfig(ctx.cwd)).join("\n")}`, "info");
1368
- }
1369
- }
1370
-
1371
- async function handleGoalSettings(ctx: ExtensionContext): Promise<void> {
1372
- if (!ctx.hasUI) {
1373
- ctx.ui.notify(`Goal settings require UI. Auditor config file: ${goalAuditorConfigPath(ctx.cwd)}`, "warning");
1374
- return;
1387
+ saveGoalSettingsFileConfig(ctx.cwd, next);
1388
+ ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
1375
1389
  }
1376
- const selected = await ctx.ui.select("Goal settings", ["auditor"]);
1377
- if (selected === "auditor") await handleGoalAuditorSettings(ctx);
1378
1390
  }
1379
1391
 
1380
1392
  async function handleGoalClear(ctx: ExtensionContext): Promise<void> {
@@ -1461,7 +1473,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
1461
1473
  pi.registerCommand("goal-settings", {
1462
1474
  description: "Open pi-goal settings, including auditor provider/model/thinking_level.",
1463
1475
  handler: async (_rawArgs, ctx) => {
1464
- await handleGoalSettings(ctx);
1476
+ await handleSettingsMenu(ctx);
1465
1477
  },
1466
1478
  });
1467
1479
 
@@ -2039,13 +2051,13 @@ export default function goalExtension(pi: ExtensionAPI): void {
2039
2051
  } catch {
2040
2052
  // Ledger append failure should not block completion
2041
2053
  }
2042
- const auditorConfig = loadGoalAuditorFileConfig(ctx.cwd);
2043
- const auditorLabel = auditorConfig.provider || auditorConfig.model || auditorConfig.thinkingLevel
2044
- ? `${auditorConfig.provider ?? "default"}/${auditorConfig.model ?? "default"}${auditorConfig.thinkingLevel ? `:${auditorConfig.thinkingLevel}` : ""}`
2054
+ const settings = loadGoalSettingsFileConfig(ctx.cwd);
2055
+ const auditorLabel = settings.provider || settings.model || settings.thinkingLevel
2056
+ ? `${settings.provider ?? "default"}/${settings.model ?? "default"}${settings.thinkingLevel ? `:${settings.thinkingLevel}` : ""}`
2045
2057
  : "default";
2046
2058
 
2047
2059
  // Check if auditor is disabled
2048
- if (auditorConfig.disabled === true) {
2060
+ if (settings.disabled === true) {
2049
2061
  if (params.confirmBypassAuditor !== true) {
2050
2062
  return {
2051
2063
  content: [{ type: "text", text: [
@@ -2072,9 +2084,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
2072
2084
  type: "audit_skipped",
2073
2085
  goalId: auditTarget.id,
2074
2086
  reason: "disabled",
2075
- provider: auditorConfig.provider,
2076
- model: auditorConfig.model,
2077
- thinkingLevel: auditorConfig.thinkingLevel,
2087
+ provider: settings.provider,
2088
+ model: settings.model,
2089
+ thinkingLevel: settings.thinkingLevel,
2078
2090
  at: nowIso(),
2079
2091
  });
2080
2092
  } catch {
@@ -2128,9 +2140,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
2128
2140
  appendGoalEvent(ctx, {
2129
2141
  type: "audit_started",
2130
2142
  goalId: auditTarget.id,
2131
- provider: auditorConfig.provider,
2132
- model: auditorConfig.model,
2133
- thinkingLevel: auditorConfig.thinkingLevel,
2143
+ provider: settings.provider,
2144
+ model: settings.model,
2145
+ thinkingLevel: settings.thinkingLevel,
2134
2146
  at: nowIso(),
2135
2147
  });
2136
2148
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",