pi-goala 0.2.0 → 0.3.1
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/CHANGELOG.md +17 -0
- package/README.md +13 -3
- package/docs/architecture.md +6 -1
- package/docs/configuration.md +17 -2
- package/extensions/goala/config.ts +103 -31
- package/extensions/goala/index.ts +35 -21
- package/extensions/goala/setup.ts +305 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
- Make each phase default to keeping its current model configuration.
|
|
6
|
+
- Separate reasoning-effort changes from provider/model changes.
|
|
7
|
+
- Select provider, then a filtered model, while skipping provider selection
|
|
8
|
+
when only one authenticated provider is available.
|
|
9
|
+
- Review, edit, save, or cancel the complete configuration atomically.
|
|
10
|
+
|
|
11
|
+
## 0.3.0 (2026-07-26)
|
|
12
|
+
|
|
13
|
+
- Add interactive per-phase provider, model, and reasoning selection from Pi's
|
|
14
|
+
authenticated available-model registry.
|
|
15
|
+
- Support mixed-provider planner, executor, step-verifier, final-verifier, and
|
|
16
|
+
repeated-repair roles.
|
|
17
|
+
- Migrate version 1 single-provider configuration to the version 2 per-role
|
|
18
|
+
schema without retaining the obsolete top-level provider.
|
|
19
|
+
|
|
3
20
|
## 0.2.0 (2026-07-26)
|
|
4
21
|
|
|
5
22
|
- Rename the package, repository, extension namespace, storage, environment
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-goala
|
|
|
39
39
|
Pin a release for reproducible team installations:
|
|
40
40
|
|
|
41
41
|
```text
|
|
42
|
-
pi install npm:pi-goala@0.
|
|
42
|
+
pi install npm:pi-goala@0.3.1
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Pi's package command both downloads Goala and registers its extension. A plain
|
|
@@ -49,7 +49,7 @@ supported installation path.
|
|
|
49
49
|
From GitHub or a local checkout:
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
|
-
pi install git:github.com/barryking/pi-goala@v0.
|
|
52
|
+
pi install git:github.com/barryking/pi-goala@v0.3.1
|
|
53
53
|
pi install /absolute/path/to/pi-goala
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -115,6 +115,7 @@ Useful commands:
|
|
|
115
115
|
/memory retire <id> Exclude an obsolete episode from recall
|
|
116
116
|
/memory restore <id> Restore a retired episode
|
|
117
117
|
/goala-setup Choose a model preset
|
|
118
|
+
/goala-setup custom Review or change each role's model and reasoning
|
|
118
119
|
/goala-setup status Show effective configuration
|
|
119
120
|
```
|
|
120
121
|
|
|
@@ -379,7 +380,16 @@ The recommended OpenAI Codex preset is:
|
|
|
379
380
|
|
|
380
381
|
If those models are unavailable, Goala can use the model that was active
|
|
381
382
|
when Pi started. Run `/goala-setup current` to persist that portable
|
|
382
|
-
single-model configuration.
|
|
383
|
+
single-model configuration. Run `/goala-setup custom` to choose an
|
|
384
|
+
authenticated provider/model and reasoning level separately for planning,
|
|
385
|
+
execution, step verification, final verification, and repeated repair. The
|
|
386
|
+
interactive wizard defaults to keeping each current role. It can change
|
|
387
|
+
reasoning alone or select a provider followed by one of that provider's
|
|
388
|
+
available models. Provider selection is skipped when only one is authenticated.
|
|
389
|
+
A final summary can be edited, saved, or cancelled; nothing is written before
|
|
390
|
+
save.
|
|
391
|
+
|
|
392
|
+
Advanced users can edit the same per-role profiles in
|
|
383
393
|
`~/.pi/agent/pi-goala/config.json`.
|
|
384
394
|
|
|
385
395
|
The package does not overwrite Pi's `settings.json`, model list, skills,
|
package/docs/architecture.md
CHANGED
|
@@ -23,6 +23,7 @@ own one concern:
|
|
|
23
23
|
| `workflow.ts` | Goal-state types, normalization, and invariants |
|
|
24
24
|
| `memory.ts` | Verified episodic storage and retrieval |
|
|
25
25
|
| `recovery.ts` | Discovery of unfinished goals in saved Pi sessions |
|
|
26
|
+
| `setup.ts` | Interactive available-model selection for each lifecycle role |
|
|
26
27
|
| `config.ts` | Namespaced configuration and model-role presets |
|
|
27
28
|
|
|
28
29
|
Simple concern names are intentional: the directory already supplies the
|
|
@@ -122,7 +123,11 @@ planning and final verification, a faster model for implementation and routine
|
|
|
122
123
|
step verification, and a balanced fallback after repeated repair. Independent
|
|
123
124
|
step context and the final strong verifier preserve the trust boundary without
|
|
124
125
|
paying the strongest-model cost at every human checkpoint. A portable
|
|
125
|
-
current-model preset is available for other providers.
|
|
126
|
+
current-model preset is available for other providers. The advanced setup
|
|
127
|
+
defaults to retaining each role, supports reasoning-only changes, and filters
|
|
128
|
+
models after provider selection. It stores provider, model, and reasoning
|
|
129
|
+
independently for every role only after final review; mixed-provider workflows
|
|
130
|
+
therefore use the same phase-routing path as the bundled preset.
|
|
126
131
|
|
|
127
132
|
If a configured model cannot be resolved and current-model fallback is
|
|
128
133
|
enabled, Goala uses the model that was active when the extension session
|
package/docs/configuration.md
CHANGED
|
@@ -46,35 +46,46 @@ and restore the backup to `~/.pi/agent/`.
|
|
|
46
46
|
/goala-setup status
|
|
47
47
|
/goala-setup openai
|
|
48
48
|
/goala-setup current
|
|
49
|
+
/goala-setup custom
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
`openai` selects the tested Sol/Luna/Terra model split when those models are
|
|
52
53
|
available. `current` assigns the model active at Pi startup to every role.
|
|
54
|
+
`custom` reviews each role and defaults to keeping its current configuration.
|
|
55
|
+
For a change, it can adjust reasoning alone or select a provider followed by
|
|
56
|
+
one of that provider's authenticated, available models. Provider selection is
|
|
57
|
+
skipped when only one provider is available. The final summary can be edited,
|
|
58
|
+
saved, or cancelled. Cancelling any prompt leaves the existing configuration
|
|
59
|
+
unchanged.
|
|
53
60
|
|
|
54
61
|
## Full schema
|
|
55
62
|
|
|
56
63
|
```json
|
|
57
64
|
{
|
|
58
|
-
"configVersion":
|
|
59
|
-
"provider": "openai-codex",
|
|
65
|
+
"configVersion": 2,
|
|
60
66
|
"planner": {
|
|
67
|
+
"provider": "openai-codex",
|
|
61
68
|
"model": "gpt-5.6-sol",
|
|
62
69
|
"thinkingLevel": "medium"
|
|
63
70
|
},
|
|
64
71
|
"executor": {
|
|
72
|
+
"provider": "openai-codex",
|
|
65
73
|
"model": "gpt-5.6-luna",
|
|
66
74
|
"thinkingLevel": "medium"
|
|
67
75
|
},
|
|
68
76
|
"fallbackExecutor": {
|
|
77
|
+
"provider": "openai-codex",
|
|
69
78
|
"model": "gpt-5.6-terra",
|
|
70
79
|
"thinkingLevel": "medium",
|
|
71
80
|
"afterRepairCycle": 2
|
|
72
81
|
},
|
|
73
82
|
"stepVerifier": {
|
|
83
|
+
"provider": "openai-codex",
|
|
74
84
|
"model": "gpt-5.6-luna",
|
|
75
85
|
"thinkingLevel": "medium"
|
|
76
86
|
},
|
|
77
87
|
"verifier": {
|
|
88
|
+
"provider": "openai-codex",
|
|
78
89
|
"model": "gpt-5.6-sol",
|
|
79
90
|
"thinkingLevel": "medium"
|
|
80
91
|
},
|
|
@@ -96,6 +107,10 @@ available. `current` assigns the model active at Pi startup to every role.
|
|
|
96
107
|
|
|
97
108
|
Restart Pi or use `/reload` after manually editing the file.
|
|
98
109
|
|
|
110
|
+
Version 1 configuration used one top-level `provider`. Goala reads that schema,
|
|
111
|
+
applies the provider to every role, and writes only version 2 when the
|
|
112
|
+
configuration is next saved.
|
|
113
|
+
|
|
99
114
|
`reviewPolicy` accepts:
|
|
100
115
|
|
|
101
116
|
- `final`: run the full approved plan and review the final result;
|
|
@@ -12,6 +12,7 @@ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhi
|
|
|
12
12
|
export type ReviewPolicy = "final" | "per-step";
|
|
13
13
|
|
|
14
14
|
export interface ModelProfile {
|
|
15
|
+
provider: string;
|
|
15
16
|
model: string;
|
|
16
17
|
thinkingLevel: ThinkingLevel;
|
|
17
18
|
}
|
|
@@ -28,8 +29,7 @@ export interface GoalaSetupPreset {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface GoalaConfig {
|
|
31
|
-
configVersion:
|
|
32
|
-
provider: string;
|
|
32
|
+
configVersion: 2;
|
|
33
33
|
planner: ModelProfile;
|
|
34
34
|
executor: ModelProfile;
|
|
35
35
|
fallbackExecutor: ModelProfile & { afterRepairCycle: number };
|
|
@@ -44,13 +44,33 @@ export interface GoalaConfig {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export const OPENAI_CODEX_PRESET: GoalaConfig = {
|
|
47
|
-
configVersion:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
configVersion: 2,
|
|
48
|
+
planner: {
|
|
49
|
+
provider: "openai-codex",
|
|
50
|
+
model: "gpt-5.6-sol",
|
|
51
|
+
thinkingLevel: "medium",
|
|
52
|
+
},
|
|
53
|
+
executor: {
|
|
54
|
+
provider: "openai-codex",
|
|
55
|
+
model: "gpt-5.6-luna",
|
|
56
|
+
thinkingLevel: "medium",
|
|
57
|
+
},
|
|
58
|
+
fallbackExecutor: {
|
|
59
|
+
provider: "openai-codex",
|
|
60
|
+
model: "gpt-5.6-terra",
|
|
61
|
+
thinkingLevel: "medium",
|
|
62
|
+
afterRepairCycle: 2,
|
|
63
|
+
},
|
|
64
|
+
stepVerifier: {
|
|
65
|
+
provider: "openai-codex",
|
|
66
|
+
model: "gpt-5.6-luna",
|
|
67
|
+
thinkingLevel: "medium",
|
|
68
|
+
},
|
|
69
|
+
verifier: {
|
|
70
|
+
provider: "openai-codex",
|
|
71
|
+
model: "gpt-5.6-sol",
|
|
72
|
+
thinkingLevel: "medium",
|
|
73
|
+
},
|
|
54
74
|
reviewPolicy: "final",
|
|
55
75
|
autoVerify: true,
|
|
56
76
|
maxRepairCycles: 3,
|
|
@@ -74,20 +94,69 @@ export function configPath(): string {
|
|
|
74
94
|
return join(goalaHome(), "config.json");
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
type PersistedConfig = Partial<GoalaConfig> & {
|
|
98
|
+
provider?: unknown;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function profileProvider(
|
|
102
|
+
profile: Partial<ModelProfile> | undefined,
|
|
103
|
+
legacyProvider: unknown,
|
|
104
|
+
fallback: ModelProfile,
|
|
105
|
+
): string {
|
|
106
|
+
if (typeof profile?.provider === "string" && profile.provider) return profile.provider;
|
|
107
|
+
if (typeof legacyProvider === "string" && legacyProvider) return legacyProvider;
|
|
108
|
+
return fallback.provider;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mergeProfile(
|
|
112
|
+
profile: Partial<ModelProfile> | undefined,
|
|
113
|
+
legacyProvider: unknown,
|
|
114
|
+
fallback: ModelProfile,
|
|
115
|
+
): ModelProfile {
|
|
116
|
+
return {
|
|
117
|
+
...fallback,
|
|
118
|
+
...profile,
|
|
119
|
+
provider: profileProvider(profile, legacyProvider, fallback),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeConfig(parsed: PersistedConfig): GoalaConfig {
|
|
124
|
+
const { provider: legacyProvider, ...current } = parsed;
|
|
78
125
|
return {
|
|
79
126
|
...OPENAI_CODEX_PRESET,
|
|
80
|
-
...
|
|
81
|
-
configVersion:
|
|
127
|
+
...current,
|
|
128
|
+
configVersion: 2,
|
|
82
129
|
reviewPolicy: parsed.reviewPolicy === "per-step" ? "per-step" : "final",
|
|
83
|
-
planner:
|
|
84
|
-
|
|
130
|
+
planner: mergeProfile(
|
|
131
|
+
parsed.planner,
|
|
132
|
+
legacyProvider,
|
|
133
|
+
OPENAI_CODEX_PRESET.planner,
|
|
134
|
+
),
|
|
135
|
+
executor: mergeProfile(
|
|
136
|
+
parsed.executor,
|
|
137
|
+
legacyProvider,
|
|
138
|
+
OPENAI_CODEX_PRESET.executor,
|
|
139
|
+
),
|
|
85
140
|
fallbackExecutor: {
|
|
86
|
-
...
|
|
87
|
-
|
|
141
|
+
...mergeProfile(
|
|
142
|
+
parsed.fallbackExecutor,
|
|
143
|
+
legacyProvider,
|
|
144
|
+
OPENAI_CODEX_PRESET.fallbackExecutor,
|
|
145
|
+
),
|
|
146
|
+
afterRepairCycle:
|
|
147
|
+
parsed.fallbackExecutor?.afterRepairCycle ??
|
|
148
|
+
OPENAI_CODEX_PRESET.fallbackExecutor.afterRepairCycle,
|
|
88
149
|
},
|
|
89
|
-
stepVerifier:
|
|
90
|
-
|
|
150
|
+
stepVerifier: mergeProfile(
|
|
151
|
+
parsed.stepVerifier,
|
|
152
|
+
legacyProvider,
|
|
153
|
+
OPENAI_CODEX_PRESET.stepVerifier,
|
|
154
|
+
),
|
|
155
|
+
verifier: mergeProfile(
|
|
156
|
+
parsed.verifier,
|
|
157
|
+
legacyProvider,
|
|
158
|
+
OPENAI_CODEX_PRESET.verifier,
|
|
159
|
+
),
|
|
91
160
|
memory: { ...OPENAI_CODEX_PRESET.memory, ...parsed.memory },
|
|
92
161
|
};
|
|
93
162
|
}
|
|
@@ -95,7 +164,7 @@ function mergeConfig(parsed: Partial<GoalaConfig>): GoalaConfig {
|
|
|
95
164
|
export function loadConfig(): GoalaConfig {
|
|
96
165
|
let config = OPENAI_CODEX_PRESET;
|
|
97
166
|
try {
|
|
98
|
-
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as
|
|
167
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as PersistedConfig;
|
|
99
168
|
config = mergeConfig(parsed);
|
|
100
169
|
} catch {
|
|
101
170
|
config = mergeConfig({});
|
|
@@ -129,12 +198,11 @@ export function currentModelConfig(
|
|
|
129
198
|
): GoalaConfig {
|
|
130
199
|
return {
|
|
131
200
|
...OPENAI_CODEX_PRESET,
|
|
132
|
-
provider,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
verifier: { model, thinkingLevel },
|
|
201
|
+
planner: { provider, model, thinkingLevel },
|
|
202
|
+
executor: { provider, model, thinkingLevel },
|
|
203
|
+
fallbackExecutor: { provider, model, thinkingLevel, afterRepairCycle: 2 },
|
|
204
|
+
stepVerifier: { provider, model, thinkingLevel },
|
|
205
|
+
verifier: { provider, model, thinkingLevel },
|
|
138
206
|
};
|
|
139
207
|
}
|
|
140
208
|
|
|
@@ -165,7 +233,7 @@ export function configuredModels(config: GoalaConfig): ModelIdentity[] {
|
|
|
165
233
|
config.fallbackExecutor,
|
|
166
234
|
config.stepVerifier,
|
|
167
235
|
config.verifier,
|
|
168
|
-
].map((profile) => ({ provider:
|
|
236
|
+
].map((profile) => ({ provider: profile.provider, id: profile.model }));
|
|
169
237
|
return identities.filter(
|
|
170
238
|
(identity, index) =>
|
|
171
239
|
identities.findIndex(
|
|
@@ -178,13 +246,17 @@ export function configuredModels(config: GoalaConfig): ModelIdentity[] {
|
|
|
178
246
|
export function formatConfig(config: GoalaConfig): string {
|
|
179
247
|
return [
|
|
180
248
|
`Config: ${configPath()}`,
|
|
181
|
-
`Planner: ${config.
|
|
182
|
-
`Executor: ${config.
|
|
183
|
-
`Step verifier: ${config.
|
|
184
|
-
`Verifier: ${config.
|
|
185
|
-
`Fallback: ${config.
|
|
249
|
+
`Planner: ${formatProfile(config.planner)}`,
|
|
250
|
+
`Executor: ${formatProfile(config.executor)}`,
|
|
251
|
+
`Step verifier: ${formatProfile(config.stepVerifier)}`,
|
|
252
|
+
`Verifier: ${formatProfile(config.verifier)}`,
|
|
253
|
+
`Fallback: ${formatProfile(config.fallbackExecutor)} from repair ${config.fallbackExecutor.afterRepairCycle}`,
|
|
186
254
|
`Review policy: ${config.reviewPolicy}`,
|
|
187
255
|
`Memory: ${config.memory.enabled ? "enabled" : "disabled"}; auto-recall ${config.memory.autoRecall ? "on" : "off"}`,
|
|
188
256
|
`Current-model fallback: ${config.allowCurrentModelFallback ? "allowed" : "disabled"}`,
|
|
189
257
|
].join("\n");
|
|
190
258
|
}
|
|
259
|
+
|
|
260
|
+
function formatProfile(profile: ModelProfile): string {
|
|
261
|
+
return `${profile.provider}/${profile.model}:${profile.thinkingLevel}`;
|
|
262
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
GOALA_SETUP_PRESETS,
|
|
10
10
|
loadConfig,
|
|
11
11
|
writeConfig,
|
|
12
|
+
type GoalaConfig,
|
|
12
13
|
type ModelProfile,
|
|
13
14
|
type ReviewPolicy,
|
|
14
15
|
type ThinkingLevel,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
moveToFreshSession,
|
|
41
42
|
slicePhaseContext,
|
|
42
43
|
} from "./session.ts";
|
|
44
|
+
import { selectModelRoles } from "./setup.ts";
|
|
43
45
|
import {
|
|
44
46
|
formatSourceDrift,
|
|
45
47
|
inspectGoalSources,
|
|
@@ -142,7 +144,7 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
const profile = profileForPhase();
|
|
145
|
-
let model = ctx.modelRegistry.find(
|
|
147
|
+
let model = ctx.modelRegistry.find(profile.provider, profile.model);
|
|
146
148
|
if (!model && config.allowCurrentModelFallback) {
|
|
147
149
|
const fallback = sessionDefaultModel ?? (ctx.model
|
|
148
150
|
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
@@ -151,7 +153,7 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
151
153
|
model = ctx.modelRegistry.find(fallback.provider, fallback.id);
|
|
152
154
|
if (model && !fallbackNoticeShown) {
|
|
153
155
|
ctx.ui.notify(
|
|
154
|
-
`Goala: ${
|
|
156
|
+
`Goala: ${profile.provider}/${profile.model} is unavailable; using ${fallback.provider}/${fallback.id}. Run /goala-setup to configure model roles.`,
|
|
155
157
|
"warning",
|
|
156
158
|
);
|
|
157
159
|
fallbackNoticeShown = true;
|
|
@@ -160,7 +162,7 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
160
162
|
}
|
|
161
163
|
if (!model) {
|
|
162
164
|
ctx.ui.notify(
|
|
163
|
-
`Goala: model not found: ${
|
|
165
|
+
`Goala: model not found: ${profile.provider}/${profile.model}. Run /goala-setup current or edit the namespaced config.`,
|
|
164
166
|
"error",
|
|
165
167
|
);
|
|
166
168
|
updateGoalUi(ctx, state);
|
|
@@ -169,7 +171,10 @@ export default function goala(pi: ExtensionAPI): void {
|
|
|
169
171
|
|
|
170
172
|
const selected = await pi.setModel(model);
|
|
171
173
|
if (!selected) {
|
|
172
|
-
ctx.ui.notify(
|
|
174
|
+
ctx.ui.notify(
|
|
175
|
+
`Goala: authenticate ${profile.provider} before using ${profile.model}`,
|
|
176
|
+
"warning",
|
|
177
|
+
);
|
|
173
178
|
updateGoalUi(ctx, state);
|
|
174
179
|
return false;
|
|
175
180
|
}
|
|
@@ -558,10 +563,13 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
558
563
|
if (!choice && ctx.hasUI) {
|
|
559
564
|
const selected = await ctx.ui.select("Goala setup", [
|
|
560
565
|
...GOALA_SETUP_PRESETS.map((preset) => preset.label),
|
|
566
|
+
"Configure each phase",
|
|
561
567
|
"Show current configuration",
|
|
562
568
|
]);
|
|
563
569
|
choice = selected === "Show current configuration"
|
|
564
570
|
? "status"
|
|
571
|
+
: selected === "Configure each phase"
|
|
572
|
+
? "custom"
|
|
565
573
|
: GOALA_SETUP_PRESETS.find((preset) => preset.label === selected)?.id ?? "";
|
|
566
574
|
}
|
|
567
575
|
|
|
@@ -570,28 +578,34 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
570
578
|
return;
|
|
571
579
|
}
|
|
572
580
|
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
ctx
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
581
|
+
let proposed: GoalaConfig | undefined;
|
|
582
|
+
if (choice === "custom") {
|
|
583
|
+
proposed = await selectModelRoles(ctx, config);
|
|
584
|
+
if (!proposed) return;
|
|
585
|
+
} else {
|
|
586
|
+
const preset = GOALA_SETUP_PRESETS.find((candidate) => candidate.id === choice);
|
|
587
|
+
if (!preset) {
|
|
588
|
+
ctx.ui.notify(
|
|
589
|
+
`Usage: /goala-setup [status | custom | ${GOALA_SETUP_PRESETS.map((candidate) => candidate.id).join(" | ")}]`,
|
|
590
|
+
"warning",
|
|
591
|
+
);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const current = ctx.model
|
|
595
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
596
|
+
: sessionDefaultModel;
|
|
597
|
+
proposed = preset.create(current);
|
|
598
|
+
if (!proposed) {
|
|
599
|
+
ctx.ui.notify(`Preset "${preset.label}" requires a current model.`, "warning");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
588
602
|
}
|
|
589
603
|
const missing = configuredModels(proposed).filter(
|
|
590
604
|
(model) => !ctx.modelRegistry.find(model.provider, model.id),
|
|
591
605
|
);
|
|
592
606
|
if (missing.length > 0) {
|
|
593
607
|
ctx.ui.notify(
|
|
594
|
-
`
|
|
608
|
+
`Configuration not saved; unavailable model(s): ${missing.map((model) => `${model.provider}/${model.id}`).join(", ")}.`,
|
|
595
609
|
"warning",
|
|
596
610
|
);
|
|
597
611
|
return;
|
|
@@ -605,7 +619,7 @@ Do not edit files or rely on executor claims. Finish with submit_step_verificati
|
|
|
605
619
|
}
|
|
606
620
|
|
|
607
621
|
pi.registerCommand("goala-setup", {
|
|
608
|
-
description: `Configure model roles: /goala-setup [status | ${GOALA_SETUP_PRESETS.map((preset) => preset.id).join(" | ")}]`,
|
|
622
|
+
description: `Configure model roles: /goala-setup [status | custom | ${GOALA_SETUP_PRESETS.map((preset) => preset.id).join(" | ")}]`,
|
|
609
623
|
handler: configureGoala,
|
|
610
624
|
});
|
|
611
625
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
GoalaConfig,
|
|
4
|
+
ModelProfile,
|
|
5
|
+
ThinkingLevel,
|
|
6
|
+
} from "./config.ts";
|
|
7
|
+
|
|
8
|
+
type ModelRole =
|
|
9
|
+
| "planner"
|
|
10
|
+
| "executor"
|
|
11
|
+
| "stepVerifier"
|
|
12
|
+
| "verifier"
|
|
13
|
+
| "fallbackExecutor";
|
|
14
|
+
|
|
15
|
+
type AvailableModel = ReturnType<
|
|
16
|
+
ExtensionCommandContext["modelRegistry"]["getAvailable"]
|
|
17
|
+
>[number];
|
|
18
|
+
|
|
19
|
+
const MODEL_ROLES: ReadonlyArray<{ key: ModelRole; label: string }> = [
|
|
20
|
+
{ key: "planner", label: "Planner" },
|
|
21
|
+
{ key: "executor", label: "Executor" },
|
|
22
|
+
{ key: "stepVerifier", label: "Step verifier" },
|
|
23
|
+
{ key: "verifier", label: "Final verifier" },
|
|
24
|
+
{ key: "fallbackExecutor", label: "Repair fallback" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
28
|
+
"off",
|
|
29
|
+
"minimal",
|
|
30
|
+
"low",
|
|
31
|
+
"medium",
|
|
32
|
+
"high",
|
|
33
|
+
"xhigh",
|
|
34
|
+
"max",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const KEEP_CURRENT = "Keep current";
|
|
38
|
+
const CHANGE_REASONING = "Change reasoning effort";
|
|
39
|
+
const CHANGE_MODEL = "Change provider or model";
|
|
40
|
+
const SAVE_CONFIGURATION = "Save configuration";
|
|
41
|
+
const EDIT_PHASE = "Edit a phase";
|
|
42
|
+
const CANCEL = "Cancel";
|
|
43
|
+
|
|
44
|
+
function titleCase(value: string): string {
|
|
45
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function thinkingLabel(level: ThinkingLevel): string {
|
|
49
|
+
if (level === "off") return "Off";
|
|
50
|
+
if (level === "xhigh") return "Extra high";
|
|
51
|
+
if (level === "max") return "Maximum";
|
|
52
|
+
return titleCase(level);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function providerLabel(
|
|
56
|
+
ctx: ExtensionCommandContext,
|
|
57
|
+
provider: string,
|
|
58
|
+
): string {
|
|
59
|
+
const displayName = ctx.modelRegistry.getProviderDisplayName(provider);
|
|
60
|
+
return displayName === provider ? provider : `${displayName} (${provider})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function modelLabel(model: AvailableModel): string {
|
|
64
|
+
return model.name && model.name !== model.id
|
|
65
|
+
? `${model.name} (${model.id})`
|
|
66
|
+
: model.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function profileLabel(
|
|
70
|
+
ctx: ExtensionCommandContext,
|
|
71
|
+
available: AvailableModel[],
|
|
72
|
+
profile: ModelProfile,
|
|
73
|
+
): string {
|
|
74
|
+
const model = available.find(
|
|
75
|
+
(candidate) =>
|
|
76
|
+
candidate.provider === profile.provider && candidate.id === profile.model,
|
|
77
|
+
);
|
|
78
|
+
const reasoning =
|
|
79
|
+
profile.thinkingLevel === "off"
|
|
80
|
+
? "Reasoning off"
|
|
81
|
+
: `${thinkingLabel(profile.thinkingLevel)} reasoning`;
|
|
82
|
+
return [
|
|
83
|
+
providerLabel(ctx, profile.provider),
|
|
84
|
+
model ? modelLabel(model) : `${profile.model} (unavailable)`,
|
|
85
|
+
reasoning,
|
|
86
|
+
].join(" / ");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setProfile(
|
|
90
|
+
config: GoalaConfig,
|
|
91
|
+
role: ModelRole,
|
|
92
|
+
profile: ModelProfile,
|
|
93
|
+
): void {
|
|
94
|
+
if (role === "fallbackExecutor") {
|
|
95
|
+
config.fallbackExecutor = {
|
|
96
|
+
...profile,
|
|
97
|
+
afterRepairCycle: config.fallbackExecutor.afterRepairCycle,
|
|
98
|
+
};
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
config[role] = profile;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function selectThinkingLevel(
|
|
105
|
+
ctx: ExtensionCommandContext,
|
|
106
|
+
roleLabel: string,
|
|
107
|
+
profile: ModelProfile,
|
|
108
|
+
): Promise<ThinkingLevel | undefined> {
|
|
109
|
+
const ordered = [
|
|
110
|
+
profile.thinkingLevel,
|
|
111
|
+
...THINKING_LEVELS.filter((level) => level !== profile.thinkingLevel),
|
|
112
|
+
];
|
|
113
|
+
const choices = ordered.map((level, index) =>
|
|
114
|
+
index === 0 ? `${thinkingLabel(level)} (current)` : thinkingLabel(level),
|
|
115
|
+
);
|
|
116
|
+
const selected = await ctx.ui.select(
|
|
117
|
+
`${roleLabel} reasoning effort\nCurrent: ${thinkingLabel(profile.thinkingLevel)}`,
|
|
118
|
+
choices,
|
|
119
|
+
);
|
|
120
|
+
if (!selected) return undefined;
|
|
121
|
+
return ordered[choices.indexOf(selected)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function selectProvider(
|
|
125
|
+
ctx: ExtensionCommandContext,
|
|
126
|
+
available: AvailableModel[],
|
|
127
|
+
roleLabel: string,
|
|
128
|
+
currentProvider: string,
|
|
129
|
+
): Promise<string | undefined> {
|
|
130
|
+
const providers = [...new Set(available.map((model) => model.provider))].sort(
|
|
131
|
+
(left, right) => {
|
|
132
|
+
if (left === currentProvider) return -1;
|
|
133
|
+
if (right === currentProvider) return 1;
|
|
134
|
+
return providerLabel(ctx, left).localeCompare(providerLabel(ctx, right));
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
if (providers.length === 1) return providers[0];
|
|
138
|
+
|
|
139
|
+
const choices = providers.map((provider, index) =>
|
|
140
|
+
index === 0 && provider === currentProvider
|
|
141
|
+
? `${providerLabel(ctx, provider)} (current)`
|
|
142
|
+
: providerLabel(ctx, provider),
|
|
143
|
+
);
|
|
144
|
+
const selected = await ctx.ui.select(
|
|
145
|
+
`${roleLabel} provider\nCurrent: ${providerLabel(ctx, currentProvider)}`,
|
|
146
|
+
choices,
|
|
147
|
+
);
|
|
148
|
+
if (!selected) return undefined;
|
|
149
|
+
return providers[choices.indexOf(selected)];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function selectModel(
|
|
153
|
+
ctx: ExtensionCommandContext,
|
|
154
|
+
available: AvailableModel[],
|
|
155
|
+
roleLabel: string,
|
|
156
|
+
provider: string,
|
|
157
|
+
current: ModelProfile,
|
|
158
|
+
): Promise<AvailableModel | undefined> {
|
|
159
|
+
const models = available
|
|
160
|
+
.filter((model) => model.provider === provider)
|
|
161
|
+
.sort((left, right) => {
|
|
162
|
+
const leftCurrent =
|
|
163
|
+
left.provider === current.provider && left.id === current.model;
|
|
164
|
+
const rightCurrent =
|
|
165
|
+
right.provider === current.provider && right.id === current.model;
|
|
166
|
+
if (leftCurrent !== rightCurrent) return leftCurrent ? -1 : 1;
|
|
167
|
+
return modelLabel(left).localeCompare(modelLabel(right));
|
|
168
|
+
});
|
|
169
|
+
const choices = models.map((model, index) =>
|
|
170
|
+
index === 0 &&
|
|
171
|
+
model.provider === current.provider &&
|
|
172
|
+
model.id === current.model
|
|
173
|
+
? `${modelLabel(model)} (current)`
|
|
174
|
+
: modelLabel(model),
|
|
175
|
+
);
|
|
176
|
+
const selected = await ctx.ui.select(
|
|
177
|
+
`${roleLabel} model\nProvider: ${providerLabel(ctx, provider)}`,
|
|
178
|
+
choices,
|
|
179
|
+
);
|
|
180
|
+
if (!selected) return undefined;
|
|
181
|
+
return models[choices.indexOf(selected)];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function changeModel(
|
|
185
|
+
ctx: ExtensionCommandContext,
|
|
186
|
+
available: AvailableModel[],
|
|
187
|
+
roleLabel: string,
|
|
188
|
+
current: ModelProfile,
|
|
189
|
+
): Promise<ModelProfile | undefined> {
|
|
190
|
+
const provider = await selectProvider(
|
|
191
|
+
ctx,
|
|
192
|
+
available,
|
|
193
|
+
roleLabel,
|
|
194
|
+
current.provider,
|
|
195
|
+
);
|
|
196
|
+
if (!provider) return undefined;
|
|
197
|
+
const model = await selectModel(ctx, available, roleLabel, provider, current);
|
|
198
|
+
if (!model) return undefined;
|
|
199
|
+
|
|
200
|
+
const profile: ModelProfile = {
|
|
201
|
+
provider: model.provider,
|
|
202
|
+
model: model.id,
|
|
203
|
+
thinkingLevel: model.reasoning ? current.thinkingLevel : "off",
|
|
204
|
+
};
|
|
205
|
+
if (!model.reasoning) return profile;
|
|
206
|
+
|
|
207
|
+
const thinkingLevel = await selectThinkingLevel(ctx, roleLabel, profile);
|
|
208
|
+
if (!thinkingLevel) return undefined;
|
|
209
|
+
return { ...profile, thinkingLevel };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function configureRole(
|
|
213
|
+
ctx: ExtensionCommandContext,
|
|
214
|
+
available: AvailableModel[],
|
|
215
|
+
config: GoalaConfig,
|
|
216
|
+
role: { key: ModelRole; label: string },
|
|
217
|
+
): Promise<boolean> {
|
|
218
|
+
const current = config[role.key];
|
|
219
|
+
const currentModel = available.find(
|
|
220
|
+
(model) =>
|
|
221
|
+
model.provider === current.provider && model.id === current.model,
|
|
222
|
+
);
|
|
223
|
+
const actions = [
|
|
224
|
+
KEEP_CURRENT,
|
|
225
|
+
...(currentModel?.reasoning ? [CHANGE_REASONING] : []),
|
|
226
|
+
CHANGE_MODEL,
|
|
227
|
+
];
|
|
228
|
+
const action = await ctx.ui.select(
|
|
229
|
+
`${role.label}\nCurrent: ${profileLabel(ctx, available, current)}`,
|
|
230
|
+
actions,
|
|
231
|
+
);
|
|
232
|
+
if (!action) return false;
|
|
233
|
+
if (action === KEEP_CURRENT) return true;
|
|
234
|
+
|
|
235
|
+
if (action === CHANGE_REASONING) {
|
|
236
|
+
const thinkingLevel = await selectThinkingLevel(ctx, role.label, current);
|
|
237
|
+
if (!thinkingLevel) return false;
|
|
238
|
+
setProfile(config, role.key, { ...current, thinkingLevel });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const profile = await changeModel(ctx, available, role.label, current);
|
|
243
|
+
if (!profile) return false;
|
|
244
|
+
setProfile(config, role.key, profile);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function reviewSummary(
|
|
249
|
+
ctx: ExtensionCommandContext,
|
|
250
|
+
available: AvailableModel[],
|
|
251
|
+
config: GoalaConfig,
|
|
252
|
+
): string {
|
|
253
|
+
return MODEL_ROLES.map(
|
|
254
|
+
(role) =>
|
|
255
|
+
`${role.label}: ${profileLabel(ctx, available, config[role.key])}`,
|
|
256
|
+
).join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function selectModelRoles(
|
|
260
|
+
ctx: ExtensionCommandContext,
|
|
261
|
+
current: GoalaConfig,
|
|
262
|
+
): Promise<GoalaConfig | undefined> {
|
|
263
|
+
if (!ctx.hasUI) {
|
|
264
|
+
ctx.ui.notify("Per-phase model setup requires interactive Pi.", "warning");
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await ctx.modelRegistry.refresh();
|
|
270
|
+
} catch {
|
|
271
|
+
// The current registry can still contain usable cached and built-in models.
|
|
272
|
+
}
|
|
273
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
274
|
+
if (available.length === 0) {
|
|
275
|
+
ctx.ui.notify(
|
|
276
|
+
"No authenticated models are available. Run /login, then retry /goala-setup.",
|
|
277
|
+
"warning",
|
|
278
|
+
);
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const proposed = structuredClone(current);
|
|
283
|
+
for (const role of MODEL_ROLES) {
|
|
284
|
+
if (!(await configureRole(ctx, available, proposed, role))) return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
while (true) {
|
|
288
|
+
const action = await ctx.ui.select(
|
|
289
|
+
`Review Goala configuration\n${reviewSummary(ctx, available, proposed)}`,
|
|
290
|
+
[SAVE_CONFIGURATION, EDIT_PHASE, CANCEL],
|
|
291
|
+
);
|
|
292
|
+
if (!action || action === CANCEL) return undefined;
|
|
293
|
+
if (action === SAVE_CONFIGURATION) return proposed;
|
|
294
|
+
|
|
295
|
+
const selectedRole = await ctx.ui.select(
|
|
296
|
+
"Choose a phase to edit",
|
|
297
|
+
MODEL_ROLES.map((role) => role.label),
|
|
298
|
+
);
|
|
299
|
+
if (!selectedRole) return undefined;
|
|
300
|
+
const role = MODEL_ROLES.find((candidate) => candidate.label === selectedRole);
|
|
301
|
+
if (!role || !(await configureRole(ctx, available, proposed, role))) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|