@sangyaahub/pi-marg 1.0.5 → 1.0.7
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/.omp-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +11 -0
- package/MODEL-SETUP.md +4 -4
- package/README.md +9 -3
- package/adapters/omp/extensions/model-router.ts +243 -59
- package/adapters/omp/extensions/provider-setup.ts +177 -0
- package/adapters/pi/extensions/model-router.ts +13 -4
- package/adapters/pi/prompts/auto-models.md +2 -2
- package/agents/auto-intake.md +2 -2
- package/commands/auto-models.md +2 -2
- package/core/model-routing.ts +133 -1
- package/core/provider-config.ts +142 -0
- package/core/user-route.ts +100 -0
- package/package.json +1 -1
- package/rules/auto-mode.md +1 -0
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +1 -1
- package/skills/auto-mode-router/SKILL.md +1 -1
- package/skills/auto-mode-router/references/intake-and-state.md +30 -17
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "PiMarg — a governed development path for Pi Agent and Oh My Pi.",
|
|
10
|
-
"version": "1.0.
|
|
10
|
+
"version": "1.0.7"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
14
14
|
"name": "pi-marg",
|
|
15
15
|
"description": "Intent-led development workflow with live A/B/C/D routing, capability detection, verification, and approval gates.",
|
|
16
16
|
"source": "./",
|
|
17
|
-
"version": "1.0.
|
|
17
|
+
"version": "1.0.7",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Sangyaa",
|
|
20
20
|
"email": "contact@sangyaa.co"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.7 — 2026-10-03
|
|
4
|
+
|
|
5
|
+
- Stages 1–3 of Auto Mode intake now use the host selection tool (`ask` on OMP) instead of asking the user to type numbers in chat.
|
|
6
|
+
- The `auto-intake` agent can call `ask`, so the selection window is available when intake runs as a subagent.
|
|
7
|
+
|
|
8
|
+
## 1.0.6 — 2026-10-03
|
|
9
|
+
|
|
10
|
+
- Reopening `/pi-marg` highlights the saved boundary, work type, skill, and A/B/C/D selectors. Enter keeps that row, and intake activates the first required stage model before the work message is sent.
|
|
11
|
+
- A saved route now survives a new OMP or Pi process. A selector that disappeared from the live catalog is reported missing and is not remapped or activated.
|
|
12
|
+
- An empty OMP catalog can sign in, add a custom OpenAI-compatible provider, or cancel. Provider files store the env var name, and the secret is written only to the runtime `.env`.
|
|
13
|
+
|
|
3
14
|
## 1.0.5 — 2026-09-26
|
|
4
15
|
|
|
5
16
|
- Documented how to register CheaperInference in Oh My Pi so Kimi and GLM models can be chosen at every A/B/C/D stage.
|
package/MODEL-SETUP.md
CHANGED
|
@@ -16,7 +16,7 @@ LIVE AUTHENTICATED CATALOG
|
|
|
16
16
|
high backup ──► low backup
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If the live catalog is empty,
|
|
19
|
+
If the live catalog is empty, `/pi-marg` offers three actions: sign in with `/login`, add a custom OpenAI-compatible provider, or cancel. The custom path writes `apiKey: ENV_VAR_NAME` into `~/.omp/agent/models.yml` and the secret only into `~/.omp/agent/.env`. Cancelling writes neither file.
|
|
20
20
|
|
|
21
21
|
## Roles
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ Use Pi's normal provider configuration or environment variables, then verify ava
|
|
|
37
37
|
pi --list-models
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
The Pi adapter reads `ctx.scopedModels` when the session has a model allowlist; otherwise it reads `ctx.modelRegistry.getAvailable()`.
|
|
40
|
+
The Pi adapter reads `ctx.scopedModels` when the session has a model allowlist; otherwise it reads `ctx.modelRegistry.getAvailable()`. A model can be authenticated and still hidden because that session allowlist is active. This is a scope limit, not a missing login.
|
|
41
41
|
|
|
42
42
|
## Oh My Pi
|
|
43
43
|
|
|
@@ -72,8 +72,8 @@ Any of those four models can be chosen for any stage. Calls spend CheaperInferen
|
|
|
72
72
|
|
|
73
73
|
## Selection workflow
|
|
74
74
|
|
|
75
|
-
1. Run `/pi-marg` for the full native intake, or `/auto-models choose` to change saved models.
|
|
76
|
-
2. Confirm the work type so PiMarg knows which stages are required.
|
|
75
|
+
1. Run `/pi-marg` for the full native intake, or `/auto-models choose` to change saved models. Reopening either one highlights the saved boundary, work type, skill, and each saved A/B/C/D and backup selector. Pressing Enter keeps that highlighted row.
|
|
76
|
+
2. Confirm the work type so PiMarg knows which stages are required. A saved choice that is no longer in the live catalog is reported as missing and is not activated or remapped.
|
|
77
77
|
3. Required stage prompts and catalog sections are always shown in ascending order: **A, then B, then C, then D** (skipping stages that do not apply).
|
|
78
78
|
4. For each required stage, choose a **provider** first (numbered `1. …`), then a **model** from that provider (numbered `1. …`). Exact `provider/id` selectors are the selection keys—not display numbers alone.
|
|
79
79
|
5. Select A and C when discovery/review is required; recommended frontier choices appear first within their lists when recognizable, but every live model remains selectable.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.
|
|
6
|
+
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.7" src="https://img.shields.io/badge/version-1.0.7-0451F7"></a>
|
|
7
7
|
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-C27B1A"></a>
|
|
8
8
|
<a href="https://pi.dev/packages?name=pi-marg"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-0338A8"></a>
|
|
9
9
|
<a href="https://omp.sh"><img alt="Oh My Pi plugin" src="https://img.shields.io/badge/OMP-plugin-021E5C"></a>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
# PiMarg
|
|
13
13
|
|
|
14
|
-
**Version 1.0.
|
|
14
|
+
**Version 1.0.7 · MIT licensed · Pi Agent + Oh My Pi · by [Sangyaa](https://sangyaa.co)**
|
|
15
15
|
|
|
16
16
|
PiMarg turns an ordinary prompt into a governed development path. It identifies the work boundary, selects one engineering method, discovers the models and tools the active runtime really provides, separates thinking from execution, records evidence, and stops before protected actions.
|
|
17
17
|
|
|
@@ -122,7 +122,13 @@ Authenticate providers using the active runtime's normal login flow, then run:
|
|
|
122
122
|
/auto-models choose
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
The router shows every model in the live authenticated and enabled registry. Stage-appropriate choices appear first and all other enabled choices remain selectable. It does not promise that a subscription route is free or unlimited. See [MODEL-SETUP.md](MODEL-SETUP.md).
|
|
125
|
+
The router shows every model in the live authenticated and enabled registry. Stage-appropriate choices appear first and all other enabled choices remain selectable. Reopening `/pi-marg` highlights the saved selector; Enter keeps it and activates the first required stage model. It does not promise that a subscription route is free or unlimited. See [MODEL-SETUP.md](MODEL-SETUP.md).
|
|
126
|
+
|
|
127
|
+
### Custom OpenAI-compatible provider
|
|
128
|
+
|
|
129
|
+
When OMP has no models yet, `/pi-marg` can register a custom provider without leaving the intake. It asks for a provider id, display name, base URL, env var name, and model ids. `models.yml` stores `apiKey: YOUR_ENV_VAR`. The secret goes only in `~/.omp/agent/.env`. After the runtime reloads, every model from that provider is offered at A, B, C, and D. A and C must still be different models, and B and D must still be different models. With only one model, PiMarg says so before stage A and lets you add another model or stop before the paired stage.
|
|
130
|
+
|
|
131
|
+
CheaperInference below is one worked example of that same shape, not the only custom provider PiMarg accepts.
|
|
126
132
|
|
|
127
133
|
### CheaperInference (Kimi and GLM)
|
|
128
134
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
buildFallbackCatalog,
|
|
6
6
|
buildModelCatalog,
|
|
7
7
|
candidatesForProvider,
|
|
8
|
-
|
|
8
|
+
commitIntakeRoute,
|
|
9
9
|
emptyModelRouteState,
|
|
10
10
|
executeAutomaticFailover,
|
|
11
11
|
executeModelRoute,
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
findTerminalUsageLimitFailure,
|
|
14
14
|
formatFailoverRecoveryMessage,
|
|
15
15
|
hasConfiguredFallback,
|
|
16
|
-
matchNumberedOption,
|
|
17
16
|
modelFailureKey,
|
|
18
17
|
modelToolUsageLimitFailure,
|
|
19
18
|
numberedOptionLabel,
|
|
20
19
|
requiredStagesForWorkType,
|
|
21
|
-
|
|
20
|
+
resolveSelectAnswer,
|
|
21
|
+
revalidateSavedRoute,
|
|
22
22
|
uniqueProviders,
|
|
23
23
|
type AutoStage,
|
|
24
24
|
type FallbackLevel,
|
|
@@ -26,8 +26,12 @@ import {
|
|
|
26
26
|
type ModelLike,
|
|
27
27
|
type ModelRouteParams,
|
|
28
28
|
type ModelRouteState,
|
|
29
|
+
type SelectAnswerResolution,
|
|
30
|
+
type WorkBoundary,
|
|
29
31
|
} from "../../../core/model-routing";
|
|
30
32
|
import { normalizeCapabilityNames } from "../../../core/runtime-capabilities";
|
|
33
|
+
import { loadRuntimeRoute, rememberRuntimeRoute } from "../../../core/user-route";
|
|
34
|
+
import { configureEmptyCatalog, resolveSingleModel } from "./provider-setup";
|
|
31
35
|
|
|
32
36
|
const WORK_BOUNDARIES = ["1. Job/client", "2. Personal"] as const;
|
|
33
37
|
|
|
@@ -71,10 +75,68 @@ function commandPrompt(args: string): string {
|
|
|
71
75
|
return trimmed;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
const WORK_BOUNDARY_ITEMS = ["Job/client", "Personal"] as const;
|
|
79
|
+
const WORK_TYPE_ITEMS = [
|
|
80
|
+
"New idea or development from scratch",
|
|
81
|
+
"New feature for an existing repository",
|
|
82
|
+
"Improve an existing feature",
|
|
83
|
+
"Pro-active bug fix",
|
|
84
|
+
"Security review or fixes",
|
|
85
|
+
"Thoughts about an existing repository or features",
|
|
86
|
+
"Business, sales, or other non-technical analysis",
|
|
87
|
+
] as const;
|
|
88
|
+
const SKILL_MODE_ITEMS = [
|
|
89
|
+
"Compound Engineering",
|
|
90
|
+
"Superpowers",
|
|
91
|
+
"GSD Core",
|
|
92
|
+
"PiMarg chooses the best available skill",
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
74
95
|
function optionLabel(candidate: ModelCandidate): string {
|
|
75
96
|
return `${candidate.label} — ${candidate.selector}`;
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
function savedIndex<T>(items: readonly T[], matches: (item: T) => boolean): number | undefined {
|
|
100
|
+
const index = items.findIndex(matches);
|
|
101
|
+
return index >= 0 ? index : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function takeAnswer<T>(ctx: any, resolution: SelectAnswerResolution<T>): T | undefined {
|
|
105
|
+
switch (resolution.status) {
|
|
106
|
+
case "matched":
|
|
107
|
+
return resolution.item;
|
|
108
|
+
case "cancelled":
|
|
109
|
+
ctx.ui.notify("Selection cancelled; no model was saved.", "warning");
|
|
110
|
+
return undefined;
|
|
111
|
+
case "conflict":
|
|
112
|
+
ctx.ui.notify("That answer matches more than one option; no model was saved.", "error");
|
|
113
|
+
return undefined;
|
|
114
|
+
case "unrecognized":
|
|
115
|
+
ctx.ui.notify(`Unrecognized selection ${JSON.stringify(resolution.received)}; no model was saved.`, "error");
|
|
116
|
+
return undefined;
|
|
117
|
+
default: {
|
|
118
|
+
const unreachable: never = resolution;
|
|
119
|
+
return unreachable;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function askChoice<T>(
|
|
125
|
+
ctx: any,
|
|
126
|
+
title: string,
|
|
127
|
+
items: readonly T[],
|
|
128
|
+
labelOf: (item: T) => string,
|
|
129
|
+
options: ReadonlyArray<string | { label: string; description?: string }>,
|
|
130
|
+
settings: { initialIndex?: number; helpText?: string } = {},
|
|
131
|
+
): Promise<T | undefined> {
|
|
132
|
+
const answer = await ctx.ui.select(title, [...options], {
|
|
133
|
+
selectionMarker: "radio",
|
|
134
|
+
...(settings.helpText ? { helpText: settings.helpText } : {}),
|
|
135
|
+
...(typeof settings.initialIndex === "number" ? { initialIndex: settings.initialIndex } : {}),
|
|
136
|
+
});
|
|
137
|
+
return takeAnswer(ctx, resolveSelectAnswer(answer, items, labelOf));
|
|
138
|
+
}
|
|
139
|
+
|
|
78
140
|
function optionDescription(stage: AutoStage, candidate: ModelCandidate): string {
|
|
79
141
|
const preferredTier = stage === "A" || stage === "C" ? "frontier" : "execution";
|
|
80
142
|
const fit = candidate.tier === preferredTier ? "Recommended fit" : "Available";
|
|
@@ -86,6 +148,7 @@ async function chooseModel(
|
|
|
86
148
|
stage: AutoStage,
|
|
87
149
|
candidates: ModelCandidate[],
|
|
88
150
|
selected: Partial<Record<AutoStage, ModelCandidate>>,
|
|
151
|
+
saved?: ModelCandidate,
|
|
89
152
|
): Promise<ModelCandidate | undefined> {
|
|
90
153
|
const opposite: Record<AutoStage, AutoStage> = { A: "C", C: "A", B: "D", D: "B" };
|
|
91
154
|
const other = selected[opposite[stage]];
|
|
@@ -98,13 +161,14 @@ async function chooseModel(
|
|
|
98
161
|
return undefined;
|
|
99
162
|
}
|
|
100
163
|
|
|
164
|
+
const highlighted = saved && eligible.some((candidate) => candidate.selector === saved.selector) ? saved : undefined;
|
|
101
165
|
return chooseCandidateByProvider(ctx, eligible, {
|
|
102
166
|
providerTitle: `Stage ${stage} provider`,
|
|
103
167
|
modelTitle: `Stage ${stage} model`,
|
|
104
168
|
purpose: STAGE_PURPOSE[stage],
|
|
105
169
|
providerDescription: (name) => `All authenticated ${name} models for ${STAGE_PURPOSE[stage]}`,
|
|
106
170
|
modelDescription: (candidate) => optionDescription(stage, candidate),
|
|
107
|
-
});
|
|
171
|
+
}, highlighted);
|
|
108
172
|
}
|
|
109
173
|
|
|
110
174
|
async function chooseFallbackModel(
|
|
@@ -112,6 +176,7 @@ async function chooseFallbackModel(
|
|
|
112
176
|
level: FallbackLevel,
|
|
113
177
|
candidates: ModelCandidate[],
|
|
114
178
|
other?: ModelCandidate,
|
|
179
|
+
saved?: ModelCandidate,
|
|
115
180
|
): Promise<ModelCandidate | undefined> {
|
|
116
181
|
const eligible = candidates.filter((candidate) => (
|
|
117
182
|
candidate.access === "runtime-model" && (!other || candidate.identity !== other.identity)
|
|
@@ -125,6 +190,7 @@ async function chooseFallbackModel(
|
|
|
125
190
|
return undefined;
|
|
126
191
|
}
|
|
127
192
|
|
|
193
|
+
const highlighted = saved && eligible.some((candidate) => candidate.selector === saved.selector) ? saved : undefined;
|
|
128
194
|
return chooseCandidateByProvider(ctx, eligible, {
|
|
129
195
|
providerTitle: `${title} backup provider`,
|
|
130
196
|
modelTitle: `${title} backup model`,
|
|
@@ -133,7 +199,7 @@ async function chooseFallbackModel(
|
|
|
133
199
|
modelDescription: (candidate) => (
|
|
134
200
|
`${FALLBACK_PURPOSE[level]}${candidate.billingNote ? ` · ${candidate.billingNote}` : ""}`
|
|
135
201
|
),
|
|
136
|
-
});
|
|
202
|
+
}, highlighted);
|
|
137
203
|
}
|
|
138
204
|
|
|
139
205
|
async function chooseCandidateByProvider(
|
|
@@ -146,20 +212,23 @@ async function chooseCandidateByProvider(
|
|
|
146
212
|
providerDescription(provider: string): string;
|
|
147
213
|
modelDescription(candidate: ModelCandidate): string;
|
|
148
214
|
},
|
|
215
|
+
saved?: ModelCandidate,
|
|
149
216
|
): Promise<ModelCandidate | undefined> {
|
|
150
217
|
const providers = uniqueProviders(candidates);
|
|
151
218
|
const providerOptions = providers.map((name, index) => ({
|
|
152
219
|
label: numberedOptionLabel(index, `${name} (${candidatesForProvider(candidates, name).length})`),
|
|
153
220
|
description: copy.providerDescription(name),
|
|
154
221
|
}));
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
const provider = matchNumberedOption(
|
|
160
|
-
providerAnswer,
|
|
222
|
+
const provider = await askChoice(
|
|
223
|
+
ctx,
|
|
224
|
+
copy.providerTitle,
|
|
161
225
|
providers,
|
|
162
226
|
(name) => `${name} (${candidatesForProvider(candidates, name).length})`,
|
|
227
|
+
providerOptions,
|
|
228
|
+
{
|
|
229
|
+
initialIndex: saved ? savedIndex(providers, (name) => name === saved.provider) : undefined,
|
|
230
|
+
helpText: `${providers.length} providers · ${candidates.length} models · ${copy.purpose}`,
|
|
231
|
+
},
|
|
163
232
|
);
|
|
164
233
|
if (!provider) return undefined;
|
|
165
234
|
|
|
@@ -168,29 +237,11 @@ async function chooseCandidateByProvider(
|
|
|
168
237
|
label: numberedOptionLabel(index, optionLabel(candidate)),
|
|
169
238
|
description: copy.modelDescription(candidate),
|
|
170
239
|
}));
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
240
|
+
const savedHere = saved?.provider === provider ? saved.selector : undefined;
|
|
241
|
+
return askChoice(ctx, copy.modelTitle, providerModels, optionLabel, modelOptions, {
|
|
242
|
+
initialIndex: savedHere ? savedIndex(providerModels, (candidate) => candidate.selector === savedHere) : undefined,
|
|
243
|
+
helpText: `${providerModels.length} ${provider} choices · ${copy.purpose}${savedHere ? ` · saved ${savedHere}` : ""}`,
|
|
174
244
|
});
|
|
175
|
-
return matchNumberedOption(answer, providerModels, optionLabel);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function selectedState(
|
|
179
|
-
workType: number,
|
|
180
|
-
selected: Partial<Record<AutoStage, ModelCandidate>>,
|
|
181
|
-
fallbacks: Record<FallbackLevel, ModelCandidate>,
|
|
182
|
-
): ModelRouteState {
|
|
183
|
-
const selectedAt = new Date().toISOString();
|
|
184
|
-
return {
|
|
185
|
-
version: 2,
|
|
186
|
-
workType,
|
|
187
|
-
selections: Object.fromEntries(
|
|
188
|
-
Object.entries(selected).map(([stage, candidate]) => [stage, { ...candidate, selectedAt }]),
|
|
189
|
-
) as ModelRouteState["selections"],
|
|
190
|
-
fallbacks: Object.fromEntries(
|
|
191
|
-
Object.entries(fallbacks).map(([level, candidate]) => [level, { ...candidate, selectedAt }]),
|
|
192
|
-
) as ModelRouteState["fallbacks"],
|
|
193
|
-
};
|
|
194
245
|
}
|
|
195
246
|
|
|
196
247
|
function intakeMessage(
|
|
@@ -227,11 +278,20 @@ export default function modelRouter(pi: any) {
|
|
|
227
278
|
let recoveryToken: symbol | undefined;
|
|
228
279
|
let sessionGeneration = 0;
|
|
229
280
|
let pendingToolFailure: { selector: string; key: string } | undefined;
|
|
281
|
+
const routeHome = (ctx: any) => typeof ctx?.agentHome === "string" ? ctx.agentHome : undefined;
|
|
282
|
+
const persistRoute = (nextState: ModelRouteState, ctx?: any) => {
|
|
283
|
+
pi.appendEntry(MODEL_STATE_TYPE, nextState);
|
|
284
|
+
try {
|
|
285
|
+
rememberRuntimeRoute("OMP", nextState, routeHome(ctx));
|
|
286
|
+
} catch {
|
|
287
|
+
ctx?.ui?.notify?.("PiMarg saved the route in this session, but could not update the user route file.", "warning");
|
|
288
|
+
}
|
|
289
|
+
};
|
|
230
290
|
const restore = (_event: unknown, ctx: any) => {
|
|
231
291
|
sessionGeneration += 1;
|
|
232
292
|
recoveryToken = undefined;
|
|
233
293
|
pendingToolFailure = undefined;
|
|
234
|
-
state =
|
|
294
|
+
state = loadRuntimeRoute("OMP", ctx.sessionManager.getBranch(), routeHome(ctx));
|
|
235
295
|
};
|
|
236
296
|
|
|
237
297
|
pi.on("session_start", restore);
|
|
@@ -253,7 +313,7 @@ export default function modelRouter(pi: any) {
|
|
|
253
313
|
activate: (model) => pi.setModel(model),
|
|
254
314
|
persist: (nextState) => {
|
|
255
315
|
if (sessionGeneration !== generation) throw new Error("session changed during automatic recovery");
|
|
256
|
-
|
|
316
|
+
persistRoute(nextState, ctx);
|
|
257
317
|
},
|
|
258
318
|
}, failure.key);
|
|
259
319
|
if (sessionGeneration !== generation) {
|
|
@@ -348,43 +408,167 @@ export default function modelRouter(pi: any) {
|
|
|
348
408
|
}
|
|
349
409
|
if (!request) return;
|
|
350
410
|
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
const boundary = await askChoice(
|
|
412
|
+
ctx,
|
|
413
|
+
"Work boundary",
|
|
414
|
+
WORK_BOUNDARY_ITEMS,
|
|
415
|
+
(item) => item,
|
|
416
|
+
WORK_BOUNDARIES,
|
|
417
|
+
{ initialIndex: savedIndex(WORK_BOUNDARY_ITEMS, (item) => item === state.boundary) },
|
|
418
|
+
);
|
|
355
419
|
if (!boundary) return;
|
|
356
420
|
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
421
|
+
const workTypeItem = await askChoice(
|
|
422
|
+
ctx,
|
|
423
|
+
"Work type",
|
|
424
|
+
WORK_TYPE_ITEMS,
|
|
425
|
+
(item) => item,
|
|
426
|
+
WORK_TYPES,
|
|
427
|
+
{
|
|
428
|
+
initialIndex: typeof state.workType === "number" && state.workType >= 1 && state.workType <= WORK_TYPE_ITEMS.length
|
|
429
|
+
? state.workType - 1
|
|
430
|
+
: undefined,
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
if (!workTypeItem) return;
|
|
434
|
+
const workType = WORK_TYPE_ITEMS.indexOf(workTypeItem) + 1;
|
|
435
|
+
const workTypeLabel = WORK_TYPES[workType - 1]!;
|
|
436
|
+
|
|
437
|
+
const skillItem = await askChoice(
|
|
438
|
+
ctx,
|
|
439
|
+
"Workflow skill",
|
|
440
|
+
SKILL_MODE_ITEMS,
|
|
441
|
+
(item) => item,
|
|
442
|
+
SKILL_MODES,
|
|
443
|
+
{ initialIndex: savedIndex(SKILL_MODES, (item) => item === state.skillMode) },
|
|
444
|
+
);
|
|
445
|
+
if (!skillItem) return;
|
|
446
|
+
const skillMode = SKILL_MODES[SKILL_MODE_ITEMS.indexOf(skillItem)]!;
|
|
447
|
+
|
|
448
|
+
const home = routeHome(ctx);
|
|
449
|
+
const pauseForRestart = () => {
|
|
450
|
+
rememberRuntimeRoute("OMP", {
|
|
451
|
+
version: 2,
|
|
452
|
+
workType,
|
|
453
|
+
boundary,
|
|
454
|
+
skillMode,
|
|
455
|
+
selections: {},
|
|
456
|
+
fallbacks: {},
|
|
457
|
+
}, home);
|
|
458
|
+
ctx.ui.notify(
|
|
459
|
+
"Restart OMP so it reloads models.yml, then run /pi-marg again. Work type and skill answers are saved and model selection will resume.",
|
|
460
|
+
"warning",
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
let liveModels = ctx.models.list() as ModelLike[];
|
|
365
464
|
if (liveModels.length === 0) {
|
|
366
|
-
ctx
|
|
367
|
-
|
|
465
|
+
const setup = await configureEmptyCatalog(ctx, home);
|
|
466
|
+
if (setup.status === "restart") {
|
|
467
|
+
pauseForRestart();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (setup.status !== "ready") return;
|
|
471
|
+
liveModels = setup.models;
|
|
368
472
|
}
|
|
473
|
+
|
|
474
|
+
let stopBeforePair = false;
|
|
475
|
+
if (liveModels.length === 1) {
|
|
476
|
+
const decision = await resolveSingleModel(ctx, home);
|
|
477
|
+
if (decision === "defer") {
|
|
478
|
+
stopBeforePair = true;
|
|
479
|
+
} else if (decision.status === "restart") {
|
|
480
|
+
pauseForRestart();
|
|
481
|
+
return;
|
|
482
|
+
} else if (decision.status !== "ready") {
|
|
483
|
+
return;
|
|
484
|
+
} else {
|
|
485
|
+
liveModels = decision.models;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
369
489
|
const catalog = buildModelCatalog(liveModels, hasExternalDevin(pi));
|
|
370
490
|
const fallbackCatalog = buildFallbackCatalog(catalog);
|
|
491
|
+
const availability = revalidateSavedRoute(state, liveModels, hasExternalDevin(pi));
|
|
371
492
|
const selected: Partial<Record<AutoStage, ModelCandidate>> = {};
|
|
372
|
-
|
|
373
|
-
|
|
493
|
+
const stages = requiredStagesForWorkType(workType);
|
|
494
|
+
const oppositeStage: Record<AutoStage, AutoStage> = { A: "C", C: "A", B: "D", D: "B" };
|
|
495
|
+
for (const stage of stages) {
|
|
496
|
+
if (stopBeforePair && selected[oppositeStage[stage]]) {
|
|
497
|
+
ctx.ui.notify(
|
|
498
|
+
`Stage ${stage} needs a different model from Stage ${oppositeStage[stage]}. Stopping before that paired stage; nothing was saved as a complete route.`,
|
|
499
|
+
"warning",
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const savedChoice = availability.selections[stage];
|
|
504
|
+
if (savedChoice?.status === "missing") {
|
|
505
|
+
ctx.ui.notify(
|
|
506
|
+
`Saved Stage ${stage} selector ${savedChoice.selection.selector} is missing from the live catalog. Choose a replacement.`,
|
|
507
|
+
"warning",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const choice = await chooseModel(
|
|
511
|
+
ctx,
|
|
512
|
+
stage,
|
|
513
|
+
catalog[stage],
|
|
514
|
+
selected,
|
|
515
|
+
savedChoice?.status === "present" ? savedChoice.selection : undefined,
|
|
516
|
+
);
|
|
374
517
|
if (!choice) return;
|
|
375
518
|
selected[stage] = choice;
|
|
376
519
|
}
|
|
377
520
|
|
|
378
|
-
const
|
|
521
|
+
const savedHigh = availability.fallbacks.high;
|
|
522
|
+
if (savedHigh?.status === "missing") {
|
|
523
|
+
ctx.ui.notify(
|
|
524
|
+
`Saved high backup ${savedHigh.selection.selector} is missing from the live catalog. Choose a replacement.`,
|
|
525
|
+
"warning",
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const high = await chooseFallbackModel(
|
|
529
|
+
ctx,
|
|
530
|
+
"high",
|
|
531
|
+
fallbackCatalog.high,
|
|
532
|
+
undefined,
|
|
533
|
+
savedHigh?.status === "present" ? savedHigh.selection : undefined,
|
|
534
|
+
);
|
|
379
535
|
if (!high) return;
|
|
380
|
-
const
|
|
536
|
+
const savedLow = availability.fallbacks.low;
|
|
537
|
+
if (savedLow?.status === "missing") {
|
|
538
|
+
ctx.ui.notify(
|
|
539
|
+
`Saved low backup ${savedLow.selection.selector} is missing from the live catalog. Choose a replacement.`,
|
|
540
|
+
"warning",
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const low = await chooseFallbackModel(
|
|
544
|
+
ctx,
|
|
545
|
+
"low",
|
|
546
|
+
fallbackCatalog.low,
|
|
547
|
+
high,
|
|
548
|
+
savedLow?.status === "present" ? savedLow.selection : undefined,
|
|
549
|
+
);
|
|
381
550
|
if (!low) return;
|
|
382
|
-
const fallbacks = { high, low };
|
|
383
551
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
552
|
+
const committed = await commitIntakeRoute(state, {
|
|
553
|
+
workType,
|
|
554
|
+
boundary: boundary as WorkBoundary,
|
|
555
|
+
skillMode,
|
|
556
|
+
selections: selected,
|
|
557
|
+
fallbacks: { high, low },
|
|
558
|
+
activateStage: stages[0]!,
|
|
559
|
+
}, {
|
|
560
|
+
runtimeName: "OMP",
|
|
561
|
+
models: liveModels,
|
|
562
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
563
|
+
activate: (model) => pi.setModel(model),
|
|
564
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
565
|
+
});
|
|
566
|
+
if (committed.error) {
|
|
567
|
+
ctx.ui.notify(committed.error, "error");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
state = committed.state;
|
|
571
|
+
pi.sendUserMessage(intakeMessage(request, boundary, workTypeLabel, skillMode, selected, { high, low }));
|
|
388
572
|
},
|
|
389
573
|
});
|
|
390
574
|
|
|
@@ -407,7 +591,7 @@ export default function modelRouter(pi: any) {
|
|
|
407
591
|
models,
|
|
408
592
|
hasExternalDevin: hasExternalDevin(pi),
|
|
409
593
|
activate: (model) => pi.setModel(model),
|
|
410
|
-
persist: (nextState) =>
|
|
594
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
411
595
|
});
|
|
412
596
|
state = execution.state;
|
|
413
597
|
return execution.response;
|