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 +17 -27
- package/docs/architecture.md +1 -1
- package/extensions/goal-auditor.ts +3 -66
- package/extensions/goal-settings.ts +91 -13
- package/extensions/goal.ts +78 -66
- package/package.json +1 -1
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-
|
|
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-
|
|
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
|
-
##
|
|
274
|
+
## Configuration
|
|
275
275
|
|
|
276
|
-
|
|
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
|
-
| `
|
|
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: `
|
|
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:**
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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-
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/extensions/goal.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
499
|
-
model:
|
|
500
|
-
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
|
|
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
|
|
1319
|
+
function settingsLines(config: GoalSettings): string[] {
|
|
1315
1320
|
return [
|
|
1316
|
-
`disabled: ${
|
|
1317
|
-
`provider: ${
|
|
1318
|
-
`model: ${
|
|
1319
|
-
`thinking_level: ${
|
|
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
|
|
1331
|
+
async function handleSettingsMenu(ctx: ExtensionContext): Promise<void> {
|
|
1324
1332
|
if (!ctx.hasUI) {
|
|
1325
|
-
ctx.ui.notify(`
|
|
1333
|
+
ctx.ui.notify(`Settings file: ${goalSettingsPath(ctx.cwd)}`, "info");
|
|
1326
1334
|
return;
|
|
1327
1335
|
}
|
|
1328
|
-
const
|
|
1336
|
+
const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth"] as const;
|
|
1329
1337
|
while (true) {
|
|
1330
|
-
const config =
|
|
1331
|
-
const options =
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const field =
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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 =
|
|
1351
|
-
const input = await ctx.ui.input(`Set
|
|
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:
|
|
1354
|
-
const
|
|
1355
|
-
if (!
|
|
1356
|
-
delete next[key
|
|
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(
|
|
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 =
|
|
1383
|
+
next.thinkingLevel = inputTrimmed as GoalSettings["thinkingLevel"];
|
|
1363
1384
|
} else if (key === "provider" || key === "model") {
|
|
1364
|
-
next[key] =
|
|
1385
|
+
next[key] = inputTrimmed;
|
|
1365
1386
|
}
|
|
1366
|
-
|
|
1367
|
-
ctx.ui.notify(`
|
|
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
|
|
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
|
|
2043
|
-
const auditorLabel =
|
|
2044
|
-
? `${
|
|
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 (
|
|
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:
|
|
2076
|
-
model:
|
|
2077
|
-
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:
|
|
2132
|
-
model:
|
|
2133
|
-
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.
|
|
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",
|