@sangyaahub/pi-marg 1.0.1 → 1.0.3
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 +12 -0
- package/MODEL-SETUP.md +44 -9
- package/README.md +2 -2
- package/adapters/omp/extensions/model-router.ts +34 -9
- package/adapters/pi/extensions/model-router.ts +1 -0
- package/assets/pi-marg-banner.png +0 -0
- package/core/model-routing.ts +169 -7
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +1 -1
- package/skills/auto-mode-router/references/intake-and-state.md +14 -3
- package/skills/auto-mode-router/references/models-and-tools.md +6 -2
|
@@ -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.3"
|
|
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.3",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Sangyaa",
|
|
20
20
|
"email": "contact@sangyaa.co"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3 — 2026-09-14
|
|
4
|
+
|
|
5
|
+
- Made model routing explicitly model-agnostic: live OMP/Pi authenticated catalogs are the only source of truth, with soft preferred-class ranking that never requires a maintainer's subscriptions.
|
|
6
|
+
- Promotes provider/subscription setup when the live catalog is empty (`/login` + `omp models` on OMP; `pi --list-models` on Pi).
|
|
7
|
+
- Shows required A/B/C/D stage options and catalog sections in ascending letter order.
|
|
8
|
+
|
|
9
|
+
## 1.0.2 — 2026-09-12
|
|
10
|
+
|
|
11
|
+
- Changed live catalog presentation to a provider-first numbered two-step: list every authenticated provider, then every model for the chosen provider.
|
|
12
|
+
- Added optional `provider` filter on `auto_model_route` catalog so agents can request one provider's full model list without truncating giant catalogs.
|
|
13
|
+
- Documented an OMP catalog advisory when both `cursor` and `openai-codex` are live: mid-session switches can hit a harness `call_id` max-length 64 rejection (tracked upstream in `@oh-my-pi/pi-coding-agent`); recover with a fresh session that does not replay the affected tool history.
|
|
14
|
+
|
|
3
15
|
## 1.0.1 — 2026-09-10
|
|
4
16
|
|
|
5
17
|
- Added the global `pi-marg start` and `pi-marg "<work prompt>"` OMP launch commands.
|
package/MODEL-SETUP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Model setup
|
|
2
2
|
|
|
3
|
-
PiMarg reads the active Pi or OMP model registry whenever `auto_model_route` runs. Exact selectors are deliberately absent from this document because providers rename and replace models.
|
|
3
|
+
PiMarg reads the active Pi or OMP model registry whenever `auto_model_route` runs. It is model-agnostic: whatever subscriptions or providers someone has configured are what appear in the catalog. Exact selectors are deliberately absent from this document because providers rename and replace models.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
LIVE AUTHENTICATED CATALOG
|
|
@@ -13,13 +13,15 @@ LIVE AUTHENTICATED CATALOG
|
|
|
13
13
|
└─ different ─┘ within each pair
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
If the live catalog is empty, PiMarg stops and promotes provider/subscription setup instead of guessing models.
|
|
17
|
+
|
|
16
18
|
## Roles
|
|
17
19
|
|
|
18
|
-
| Stage | Purpose | Preferred class |
|
|
20
|
+
| Stage | Purpose | Preferred class (optional ranking) |
|
|
19
21
|
|---|---|---|
|
|
20
|
-
| A | Discovery and difficult reasoning | Latest eligible Claude frontier, OpenAI frontier, or Grok frontier |
|
|
22
|
+
| A | Discovery and difficult reasoning | Latest eligible Claude frontier, OpenAI frontier, or Grok frontier when present |
|
|
23
|
+
| B | Execute the accepted plan | Claude/OpenAI mid-layer, Cursor Composer, eligible zero-metered route, or Devin SWE when present |
|
|
21
24
|
| C | Independent plan review | A different frontier model/family from A |
|
|
22
|
-
| B | Execute the accepted plan | Claude/OpenAI mid-layer, Cursor Composer, eligible zero-metered route, or Devin SWE |
|
|
23
25
|
| D | Independent implementation review | A different execution model/family from B |
|
|
24
26
|
|
|
25
27
|
“Zero-metered” means the runtime reports zero token price for that route. Subscription quotas and pool limits can still apply.
|
|
@@ -48,15 +50,48 @@ The OMP adapter reads `ctx.models.list()`, which supplies authenticated and enab
|
|
|
48
50
|
|
|
49
51
|
1. Run `/pi-marg` for the full native intake, or `/auto-models choose` to change saved models.
|
|
50
52
|
2. Confirm the work type so PiMarg knows which stages are required.
|
|
51
|
-
3.
|
|
52
|
-
4.
|
|
53
|
-
5.
|
|
54
|
-
6.
|
|
53
|
+
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).
|
|
54
|
+
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.
|
|
55
|
+
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.
|
|
56
|
+
6. Select B and D when implementation is required; recommended execution choices appear first within their lists when recognizable, but every live model remains selectable.
|
|
57
|
+
7. Resolve any A=C or B=D identity conflict before continuing.
|
|
58
|
+
8. Activate the saved selector immediately before its stage.
|
|
59
|
+
|
|
60
|
+
### Tool form (`auto_model_route`)
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
# 1) Overview: every provider, no model dump
|
|
64
|
+
auto_model_route action=catalog workType=<n>
|
|
65
|
+
|
|
66
|
+
# 2) Full model list for one authenticated provider
|
|
67
|
+
auto_model_route action=catalog workType=<n> provider=<provider-name>
|
|
68
|
+
|
|
69
|
+
# 3) Persist and activate an exact selector
|
|
70
|
+
auto_model_route action=select stage=<A|B|C|D> target=<provider>/<model-id> workType=<n>
|
|
71
|
+
```
|
|
55
72
|
|
|
56
|
-
Every authenticated and enabled runtime model remains selectable at every required stage.
|
|
73
|
+
Never present a hand-curated subset that omits a logged-in provider. Every authenticated and enabled runtime model remains selectable at every required stage. Soft preferred-class ranking controls order, not visibility.
|
|
57
74
|
|
|
58
75
|
The normalized identity check treats the same underlying model reached through different providers as the same candidate when their identifiers normalize equally.
|
|
59
76
|
|
|
77
|
+
### OMP cursor → openai-codex note
|
|
78
|
+
|
|
79
|
+
On OMP with `@oh-my-pi/pi-coding-agent` 18.1.17, switching from a `cursor` stage to an `openai-codex` stage in the **same** session can fail if prior `cursor-agent` tool ids are replayed (`call_id` longer than 64). PiMarg surfaces this as a catalog advisory when both providers are live. Workaround: start a **fresh session** that does not replay the affected tool history. The durable fix belongs in the harness, not in PiMarg selectors.
|
|
80
|
+
|
|
81
|
+
## Provider recommendation (optional ranking only)
|
|
82
|
+
|
|
83
|
+
These names are soft ranking hints for common catalogs. They are never required. If your subscriptions expose different models, PiMarg still lists every authenticated entry and lets you choose.
|
|
84
|
+
|
|
85
|
+
- Cursor Composer or a zero-metered Cursor route: economical B/D work when present in the live catalog.
|
|
86
|
+
- Claude Fable/Opus: A/C product thinking, architecture, specification, and cross-family review.
|
|
87
|
+
- Claude Sonnet: B/D mid-layer implementation or review.
|
|
88
|
+
- OpenAI Astra/Sol: A/C complex discovery and plan review.
|
|
89
|
+
- OpenAI Terra/Luna/Codex-class: B/D implementation and review, chosen by task risk and cost.
|
|
90
|
+
- Latest matching Grok 4.6-or-newer model: A/C independent frontier analysis or current web/social context.
|
|
91
|
+
- Native Devin SWE: B/D when exposed by the runtime. External Devin: bounded delegation with acceptance criteria and a quota limit.
|
|
92
|
+
|
|
93
|
+
Use Pi or OMP's own authentication and model UI as the authority for what the account can actually access. When nothing is authenticated, set up any subscription first (`/login` on OMP, or Pi provider credentials), then re-run PiMarg.
|
|
94
|
+
|
|
60
95
|
## Devin
|
|
61
96
|
|
|
62
97
|
Native Devin model entries are ordinary B/D candidates when the runtime catalog exposes them. A separate external Devin session appears only when a Devin tool is detected. Creating that session is a web mutation and requires one-use `post-web` approval.
|
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.3" src="https://img.shields.io/badge/version-1.0.3-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.3 · 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
|
|
|
@@ -2,10 +2,15 @@ import {
|
|
|
2
2
|
MODEL_STATE_TYPE,
|
|
3
3
|
STAGES,
|
|
4
4
|
buildModelCatalog,
|
|
5
|
+
candidatesForProvider,
|
|
6
|
+
emptyCatalogSetupMessage,
|
|
5
7
|
emptyModelRouteState,
|
|
6
8
|
executeModelRoute,
|
|
9
|
+
matchNumberedOption,
|
|
10
|
+
numberedOptionLabel,
|
|
7
11
|
requiredStagesForWorkType,
|
|
8
12
|
restoreModelRouteState,
|
|
13
|
+
uniqueProviders,
|
|
9
14
|
type AutoStage,
|
|
10
15
|
type ModelCandidate,
|
|
11
16
|
type ModelLike,
|
|
@@ -14,7 +19,7 @@ import {
|
|
|
14
19
|
} from "../../../core/model-routing";
|
|
15
20
|
import { normalizeCapabilityNames } from "../../../core/runtime-capabilities";
|
|
16
21
|
|
|
17
|
-
const WORK_BOUNDARIES = ["Job/client", "Personal"] as const;
|
|
22
|
+
const WORK_BOUNDARIES = ["1. Job/client", "2. Personal"] as const;
|
|
18
23
|
|
|
19
24
|
const WORK_TYPES = [
|
|
20
25
|
"1. New idea or development from scratch",
|
|
@@ -77,15 +82,33 @@ async function chooseModel(
|
|
|
77
82
|
);
|
|
78
83
|
return undefined;
|
|
79
84
|
}
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
|
|
86
|
+
const providers = uniqueProviders(eligible);
|
|
87
|
+
const providerOptions = providers.map((name, index) => ({
|
|
88
|
+
label: numberedOptionLabel(index, `${name} (${candidatesForProvider(eligible, name).length})`),
|
|
89
|
+
description: `All authenticated ${name} models for ${STAGE_PURPOSE[stage]}`,
|
|
90
|
+
}));
|
|
91
|
+
const providerAnswer = await ctx.ui.select(`Stage ${stage} provider`, providerOptions, {
|
|
92
|
+
selectionMarker: "radio",
|
|
93
|
+
helpText: `${providers.length} providers · ${eligible.length} models · ${STAGE_PURPOSE[stage]}`,
|
|
94
|
+
});
|
|
95
|
+
const provider = matchNumberedOption(
|
|
96
|
+
providerAnswer,
|
|
97
|
+
providers,
|
|
98
|
+
(name) => `${name} (${candidatesForProvider(eligible, name).length})`,
|
|
99
|
+
);
|
|
100
|
+
if (!provider) return undefined;
|
|
101
|
+
|
|
102
|
+
const providerModels = candidatesForProvider(eligible, provider);
|
|
103
|
+
const modelOptions = providerModels.map((candidate, index) => ({
|
|
104
|
+
label: numberedOptionLabel(index, optionLabel(candidate)),
|
|
82
105
|
description: optionDescription(stage, candidate),
|
|
83
106
|
}));
|
|
84
|
-
const answer = await ctx.ui.select(`Stage ${stage} model`,
|
|
107
|
+
const answer = await ctx.ui.select(`Stage ${stage} model`, modelOptions, {
|
|
85
108
|
selectionMarker: "radio",
|
|
86
|
-
helpText: `${
|
|
109
|
+
helpText: `${providerModels.length} ${provider} choices · ${STAGE_PURPOSE[stage]}`,
|
|
87
110
|
});
|
|
88
|
-
return
|
|
111
|
+
return matchNumberedOption(answer, providerModels, optionLabel);
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
function selectedState(
|
|
@@ -164,7 +187,8 @@ export default function modelRouter(pi: any) {
|
|
|
164
187
|
const boundaryLabel = await ctx.ui.select("Work boundary", [...WORK_BOUNDARIES], {
|
|
165
188
|
selectionMarker: "radio",
|
|
166
189
|
});
|
|
167
|
-
|
|
190
|
+
const boundary = matchNumberedOption(boundaryLabel, ["Job/client", "Personal"], (item) => item);
|
|
191
|
+
if (!boundary) return;
|
|
168
192
|
|
|
169
193
|
const workTypeLabel = await ctx.ui.select("Work type", [...WORK_TYPES], { selectionMarker: "radio" });
|
|
170
194
|
const workType = Number.parseInt(workTypeLabel?.match(/^([1-7])\./)?.[1] ?? "", 10);
|
|
@@ -175,7 +199,7 @@ export default function modelRouter(pi: any) {
|
|
|
175
199
|
|
|
176
200
|
const liveModels = ctx.models.list() as ModelLike[];
|
|
177
201
|
if (liveModels.length === 0) {
|
|
178
|
-
ctx.ui.notify("
|
|
202
|
+
ctx.ui.notify(emptyCatalogSetupMessage("OMP"), "error");
|
|
179
203
|
return;
|
|
180
204
|
}
|
|
181
205
|
const catalog = buildModelCatalog(liveModels, hasExternalDevin(pi));
|
|
@@ -189,7 +213,7 @@ export default function modelRouter(pi: any) {
|
|
|
189
213
|
const nextState = selectedState(workType, selected);
|
|
190
214
|
state = nextState;
|
|
191
215
|
pi.appendEntry(MODEL_STATE_TYPE, nextState);
|
|
192
|
-
pi.sendUserMessage(intakeMessage(request,
|
|
216
|
+
pi.sendUserMessage(intakeMessage(request, boundary, workTypeLabel!, skillMode, selected));
|
|
193
217
|
},
|
|
194
218
|
});
|
|
195
219
|
|
|
@@ -202,6 +226,7 @@ export default function modelRouter(pi: any) {
|
|
|
202
226
|
stage: z.enum(STAGES).optional(),
|
|
203
227
|
target: z.string().optional(),
|
|
204
228
|
workType: z.number().optional(),
|
|
229
|
+
provider: z.string().optional(),
|
|
205
230
|
}),
|
|
206
231
|
async execute(_toolCallId: string, params: ModelRouteParams, _signal: unknown, _onUpdate: unknown, ctx: any) {
|
|
207
232
|
const models = ctx.models.list() as ModelLike[];
|
|
@@ -44,6 +44,7 @@ export default function modelRouter(pi: any) {
|
|
|
44
44
|
stage: Type.Optional(Type.Union([Type.Literal("A"), Type.Literal("B"), Type.Literal("C"), Type.Literal("D")])),
|
|
45
45
|
target: Type.Optional(Type.String()),
|
|
46
46
|
workType: Type.Optional(Type.Number()),
|
|
47
|
+
provider: Type.Optional(Type.String()),
|
|
47
48
|
}),
|
|
48
49
|
async execute(_toolCallId: string, params: ModelRouteParams, _signal: unknown, _onUpdate: unknown, ctx: any) {
|
|
49
50
|
const models = availableModels(ctx);
|
|
Binary file
|
package/core/model-routing.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface ModelRouteParams {
|
|
|
46
46
|
stage?: AutoStage;
|
|
47
47
|
target?: string;
|
|
48
48
|
workType?: number;
|
|
49
|
+
provider?: string;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface ModelRoutePort {
|
|
@@ -89,7 +90,7 @@ export function requiredStagesForWorkType(workType: number): AutoStage[] {
|
|
|
89
90
|
switch (workType) {
|
|
90
91
|
case 2:
|
|
91
92
|
case 3:
|
|
92
|
-
return ["A", "
|
|
93
|
+
return ["A", "B", "C", "D"];
|
|
93
94
|
case 4:
|
|
94
95
|
return ["A", "B", "D"];
|
|
95
96
|
case 1:
|
|
@@ -102,6 +103,25 @@ export function requiredStagesForWorkType(workType: number): AutoStage[] {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
export function ascendingStages(stages: readonly AutoStage[]): AutoStage[] {
|
|
107
|
+
return STAGES.filter((stage) => stages.includes(stage));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function emptyCatalogSetupMessage(runtimeName: RuntimeName): string {
|
|
111
|
+
if (runtimeName === "OMP") {
|
|
112
|
+
return [
|
|
113
|
+
"No authenticated and enabled OMP models were found.",
|
|
114
|
+
"Set up any provider subscription with /login, verify with `omp models` or /model, then run /pi-marg again.",
|
|
115
|
+
"PiMarg picks up whatever models you configure in OMP; see MODEL-SETUP.md.",
|
|
116
|
+
].join(" ");
|
|
117
|
+
}
|
|
118
|
+
return [
|
|
119
|
+
"No authenticated and available Pi models were found.",
|
|
120
|
+
"Configure any provider subscription or API credentials, verify with `pi --list-models`, then retry.",
|
|
121
|
+
"PiMarg picks up whatever models you configure in Pi; see MODEL-SETUP.md.",
|
|
122
|
+
].join(" ");
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
export function normalizeModelIdentity(value: string): string {
|
|
106
126
|
let normalized = value
|
|
107
127
|
.toLowerCase()
|
|
@@ -191,6 +211,107 @@ export function buildModelCatalog(models: ModelLike[], hasExternalDevin: boolean
|
|
|
191
211
|
} satisfies Record<AutoStage, ModelCandidate[]>;
|
|
192
212
|
}
|
|
193
213
|
|
|
214
|
+
export function numberedOptionLabel(index: number, text: string): string {
|
|
215
|
+
return `${index + 1}. ${text}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function matchNumberedOption<T>(
|
|
219
|
+
answer: string | undefined,
|
|
220
|
+
items: readonly T[],
|
|
221
|
+
labelOf: (item: T) => string,
|
|
222
|
+
): T | undefined {
|
|
223
|
+
if (!answer) return undefined;
|
|
224
|
+
return items.find((item, index) => numberedOptionLabel(index, labelOf(item)) === answer);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function uniqueProviders(candidates: readonly ModelCandidate[]): string[] {
|
|
228
|
+
return [...new Set(candidates.map((candidate) => candidate.provider))].sort((left, right) => left.localeCompare(right));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function candidatesForProvider(
|
|
232
|
+
candidates: readonly ModelCandidate[],
|
|
233
|
+
provider: string,
|
|
234
|
+
): ModelCandidate[] {
|
|
235
|
+
return candidates.filter((candidate) => candidate.provider === provider);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function formatCatalogPresentation(
|
|
239
|
+
runtimeName: RuntimeName,
|
|
240
|
+
catalog: Record<AutoStage, ModelCandidate[]>,
|
|
241
|
+
requiredStages: AutoStage[],
|
|
242
|
+
providerFilter?: string,
|
|
243
|
+
) {
|
|
244
|
+
const notes = catalogNotes(runtimeName, catalog);
|
|
245
|
+
const orderedStages = ascendingStages(requiredStages);
|
|
246
|
+
const stages: Record<string, {
|
|
247
|
+
providers: Array<{ index: number; name: string; count: number }>;
|
|
248
|
+
providerFilter: string | null;
|
|
249
|
+
count: number;
|
|
250
|
+
options: Array<{
|
|
251
|
+
index: number;
|
|
252
|
+
selector: string;
|
|
253
|
+
label: string;
|
|
254
|
+
provider: string;
|
|
255
|
+
tier: CandidateTier;
|
|
256
|
+
choice: ModelCandidate["choice"];
|
|
257
|
+
}>;
|
|
258
|
+
}> = {};
|
|
259
|
+
const lines: string[] = [
|
|
260
|
+
`${runtimeName} model catalog`,
|
|
261
|
+
`Required stages: ${orderedStages.join(", ")}`,
|
|
262
|
+
"",
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
for (const stage of orderedStages) {
|
|
266
|
+
const all = catalog[stage];
|
|
267
|
+
const providers = uniqueProviders(all).map((name, index) => ({
|
|
268
|
+
index: index + 1,
|
|
269
|
+
name,
|
|
270
|
+
count: candidatesForProvider(all, name).length,
|
|
271
|
+
}));
|
|
272
|
+
const shown = providerFilter ? candidatesForProvider(all, providerFilter) : [];
|
|
273
|
+
const options = shown.map((candidate, index) => ({
|
|
274
|
+
index: index + 1,
|
|
275
|
+
selector: candidate.selector,
|
|
276
|
+
label: candidate.label,
|
|
277
|
+
provider: candidate.provider,
|
|
278
|
+
tier: candidate.tier,
|
|
279
|
+
choice: candidate.choice,
|
|
280
|
+
}));
|
|
281
|
+
stages[stage] = {
|
|
282
|
+
providers,
|
|
283
|
+
providerFilter: providerFilter ?? null,
|
|
284
|
+
count: providerFilter ? shown.length : all.length,
|
|
285
|
+
options,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
lines.push(`## Stage ${stage} (${all.length} models across ${providers.length} providers)`);
|
|
289
|
+
lines.push("Providers:");
|
|
290
|
+
for (const provider of providers) {
|
|
291
|
+
const marker = providerFilter === provider.name ? " ← selected" : "";
|
|
292
|
+
lines.push(` ${provider.index}. ${provider.name} (${provider.count})${marker}`);
|
|
293
|
+
}
|
|
294
|
+
if (!providerFilter) {
|
|
295
|
+
lines.push("Call catalog again with provider=<name> to list every model for that provider.");
|
|
296
|
+
} else {
|
|
297
|
+
lines.push(`Models for ${providerFilter} (${shown.length}):`);
|
|
298
|
+
for (const option of options) {
|
|
299
|
+
lines.push(` ${option.index}. ${option.label} — ${option.selector} [${option.tier}]`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
lines.push("");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const note of notes) lines.push(`- ${note}`);
|
|
306
|
+
lines.push("- Present every provider as 1,2,… then every model for the chosen provider as 1,2,… Never omit a logged-in provider.");
|
|
307
|
+
lines.push("- Select with action=select using the exact selector string.");
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
text: lines.join("\n"),
|
|
311
|
+
presentation: { runtime: runtimeName, requiredStages: orderedStages, stages, notes },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
194
315
|
export function validateDistinctSelection(
|
|
195
316
|
state: ModelRouteState,
|
|
196
317
|
stage: AutoStage,
|
|
@@ -223,14 +344,42 @@ export async function executeModelRoute(
|
|
|
223
344
|
const catalog = buildModelCatalog(port.models, port.hasExternalDevin);
|
|
224
345
|
|
|
225
346
|
if (params.action === "catalog") {
|
|
347
|
+
if (port.models.length === 0) {
|
|
348
|
+
return {
|
|
349
|
+
state: currentState,
|
|
350
|
+
response: toolResult(
|
|
351
|
+
emptyCatalogSetupMessage(port.runtimeName),
|
|
352
|
+
{ runtime: port.runtimeName, catalog, state: currentState },
|
|
353
|
+
true,
|
|
354
|
+
),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
226
357
|
const workType = Number(params.workType);
|
|
227
358
|
const requiredStages = Number.isInteger(workType) ? requiredStagesForWorkType(workType) : STAGES;
|
|
359
|
+
const providerFilter = typeof params.provider === "string" && params.provider.trim() ? params.provider.trim() : undefined;
|
|
360
|
+
if (providerFilter) {
|
|
361
|
+
const known = STAGES.some((stage) => catalog[stage].some((candidate) => candidate.provider === providerFilter));
|
|
362
|
+
if (!known) {
|
|
363
|
+
return {
|
|
364
|
+
state: currentState,
|
|
365
|
+
response: toolResult(
|
|
366
|
+
`Provider ${providerFilter} is not in the live authenticated catalog. Refresh catalog without a provider filter.`,
|
|
367
|
+
{ runtime: port.runtimeName, requiredStages, catalog, state: currentState },
|
|
368
|
+
true,
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const formatted = formatCatalogPresentation(port.runtimeName, catalog, requiredStages, providerFilter);
|
|
228
374
|
return {
|
|
229
375
|
state: currentState,
|
|
230
|
-
response: toolResult(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
376
|
+
response: toolResult(formatted.text, {
|
|
377
|
+
runtime: port.runtimeName,
|
|
378
|
+
requiredStages: formatted.presentation.requiredStages,
|
|
379
|
+
catalog,
|
|
380
|
+
presentation: formatted.presentation,
|
|
381
|
+
state: currentState,
|
|
382
|
+
}),
|
|
234
383
|
};
|
|
235
384
|
}
|
|
236
385
|
|
|
@@ -354,10 +503,14 @@ function toolResult(text: string, details: unknown, isError = false) {
|
|
|
354
503
|
};
|
|
355
504
|
}
|
|
356
505
|
|
|
506
|
+
function catalogHasProvider(catalog: Record<AutoStage, ModelCandidate[]>, provider: string): boolean {
|
|
507
|
+
return STAGES.some((stage) => catalog[stage].some((candidate) => candidate.provider === provider));
|
|
508
|
+
}
|
|
509
|
+
|
|
357
510
|
function catalogNotes(runtimeName: RuntimeName, catalog: Record<AutoStage, ModelCandidate[]>): string[] {
|
|
358
511
|
const notes = [
|
|
359
|
-
`The catalog shows every model in ${runtimeName}'s authenticated, enabled registry at call time; selectors are never guessed or
|
|
360
|
-
"A and C rank frontier fits first. B and D rank execution fits first. Every live model remains selectable for every stage.",
|
|
512
|
+
`The catalog shows every model in ${runtimeName}'s authenticated, enabled registry at call time; selectors are never guessed, pinned, or limited to a maintainer's subscriptions.`,
|
|
513
|
+
"A and C rank recognizable frontier fits first when present. B and D rank recognizable execution fits first when present. Every live model remains selectable for every stage.",
|
|
361
514
|
"Zero-metered describes the runtime's reported token price; it does not promise unlimited subscription usage.",
|
|
362
515
|
];
|
|
363
516
|
if (!catalog.A.some((candidate) => candidate.tier === "frontier")) {
|
|
@@ -366,5 +519,14 @@ function catalogNotes(runtimeName: RuntimeName, catalog: Record<AutoStage, Model
|
|
|
366
519
|
if (!catalog.B.some((candidate) => candidate.tier === "execution")) {
|
|
367
520
|
notes.push("No preferred execution matches are authenticated and no external Devin tool was detected; choose another available model.");
|
|
368
521
|
}
|
|
522
|
+
if (
|
|
523
|
+
runtimeName === "OMP"
|
|
524
|
+
&& catalogHasProvider(catalog, "cursor")
|
|
525
|
+
&& catalogHasProvider(catalog, "openai-codex")
|
|
526
|
+
) {
|
|
527
|
+
notes.push(
|
|
528
|
+
"OMP compatibility warning (@oh-my-pi/pi-coding-agent 18.1.17): mid-session switches from cursor to openai-codex can reject tool call_id values longer than 64 characters when prior cursor-agent history is replayed. Start a fresh session that does not replay the affected tool history; branching alone is insufficient if that history is retained. This is a harness limitation, not a PiMarg model-routing failure.",
|
|
529
|
+
);
|
|
530
|
+
}
|
|
369
531
|
return notes;
|
|
370
532
|
}
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -71,4 +71,4 @@ if [[ "${runtime}" == "omp" || "${runtime}" == "both" ]]; then
|
|
|
71
71
|
omp plugin install --scope user pi-marg@pi-marg-marketplace
|
|
72
72
|
fi
|
|
73
73
|
|
|
74
|
-
echo "PiMarg v1.0.
|
|
74
|
+
echo "PiMarg v1.0.3 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
|
package/scripts/validate.sh
CHANGED
|
@@ -17,7 +17,7 @@ bun -e '
|
|
|
17
17
|
const packageJson = await Bun.file("package.json").json();
|
|
18
18
|
const marketplace = await Bun.file(".omp-plugin/marketplace.json").json();
|
|
19
19
|
if (packageJson.name !== "@sangyaahub/pi-marg") throw new Error("Unexpected package name");
|
|
20
|
-
if (packageJson.version !== "1.0.
|
|
20
|
+
if (packageJson.version !== "1.0.3") throw new Error("package.json must be v1.0.3");
|
|
21
21
|
if (packageJson.license !== "MIT") throw new Error("package.json must declare MIT");
|
|
22
22
|
if (packageJson.author !== "Sangyaa") throw new Error("Unexpected package author");
|
|
23
23
|
if (!packageJson.keywords?.includes("pi-package")) throw new Error("Pi catalog keyword is missing");
|
|
@@ -18,11 +18,14 @@ Ask one intake stage at a time. A clear user prompt can support a proposed class
|
|
|
18
18
|
|
|
19
19
|
## Stage 1 — work context
|
|
20
20
|
|
|
21
|
-
Ask first:
|
|
21
|
+
Ask first with numbered options:
|
|
22
22
|
|
|
23
23
|
> Is this for your job/client, or is it personal work?
|
|
24
|
+
>
|
|
25
|
+
> 1. Job/client
|
|
26
|
+
> 2. Personal
|
|
24
27
|
|
|
25
|
-
Record exactly one:
|
|
28
|
+
Record exactly one canonical value (`job-client` or `personal`); numbering is presentation-only:
|
|
26
29
|
|
|
27
30
|
- `job-client`: Do not add Sangyaa branding, name, logo, metadata, footer, or promotional language. Use the employer/client/project brand already in scope.
|
|
28
31
|
- `personal`: Existing project branding wins. Sangyaa branding is opt-in, never automatic.
|
|
@@ -58,6 +61,14 @@ Ask:
|
|
|
58
61
|
|
|
59
62
|
Call `auto_model_route` with `action: catalog` and the confirmed work type. Present only selectors returned by the tool; do not type a remembered model ID.
|
|
60
63
|
|
|
64
|
+
Catalog presentation is provider-first:
|
|
65
|
+
|
|
66
|
+
1. Show **every** authenticated provider as numbered options `1.`, `2.`, … (including Devin when present). Never omit a logged-in provider and never curate down to Claude/Codex/Cursor only.
|
|
67
|
+
2. After the user picks a provider, call `auto_model_route` again with the same work type and `provider=<name>` so the tool lists **every** model for that provider as numbered `1.`, `2.`, …
|
|
68
|
+
3. Ask the user with those numbered labels. If the option UI cannot fit the full list, paste the numbered list into the question text and accept the number or exact selector.
|
|
69
|
+
|
|
70
|
+
Interactive `/pi-marg` uses the same provider → model two-step with numbered labels.
|
|
71
|
+
|
|
61
72
|
The four roles are:
|
|
62
73
|
|
|
63
74
|
- A — thinking/discovery using a current Claude Fable/Opus, OpenAI Astra/Sol, or Grok 4.6-or-newer frontier model.
|
|
@@ -67,7 +78,7 @@ The four roles are:
|
|
|
67
78
|
|
|
68
79
|
Ask only for stages required by the current work type. Work types 2–3 normally use A → C → B → D. Type 4 uses A → B → D and adds C only for a non-trivial plan. Types 1, 6, and 7 use A → C. Type 5 uses A → C for review and adds B → D when fixes are authorized.
|
|
69
80
|
|
|
70
|
-
Use `action: select` for each answer. The active adapter saves exact selectors in session state and immediately activates the selected runtime model. Native `devin/swe-*` selectors behave like other models. An external Devin session requires `post-web` approval before creation.
|
|
81
|
+
Use `action: select` for each answer with the **exact selector** string (for example `devin/swe-2`), not the display number alone. The active adapter saves exact selectors in session state and immediately activates the selected runtime model. Native `devin/swe-*` selectors behave like other models. An external Devin session requires `post-web` approval before creation.
|
|
71
82
|
|
|
72
83
|
## Stage 5 — native runtime and supporting capabilities
|
|
73
84
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`auto_model_route` builds its catalog from the active runtime every time it is called: Pi uses scoped or authenticated available models, while OMP uses `ctx.models.list()`. Exact versions come from that live registry, not this document.
|
|
6
6
|
|
|
7
|
+
Catalog text is compact and provider-grouped. Call `action: catalog` first to list every provider with counts, then call again with `provider=<name>` to list every model for that provider as numbered options. Both OMP and Pi adapters accept the optional `provider` parameter. Never present a hand-curated Claude/Codex/Cursor subset when other authenticated providers (including Devin) are in the catalog.
|
|
8
|
+
|
|
7
9
|
| Stage | Purpose | Eligible live groups | Normal agent and tools |
|
|
8
10
|
|---|---|---|---|
|
|
9
11
|
| A | Thinking/discovery | Claude Fable/Opus; OpenAI Astra/Sol; current Grok 4.6-or-newer | strategy skill; detected CodeGraph/LSP/AST tools, web for current facts; no implementation writes |
|
|
@@ -61,7 +63,9 @@ Call `auto_runtime_status` during intake. Follow [native-omp-runtime.md](native-
|
|
|
61
63
|
- Security: detected `security_scan` when available, otherwise repository-native dependency audit, secret scan, SAST rules, configuration review, and source-to-sink inspection. Validate findings before changing code.
|
|
62
64
|
- Current business/market facts: web research with source links; keep external posting disabled until approved.
|
|
63
65
|
|
|
64
|
-
## Provider recommendation
|
|
66
|
+
## Provider recommendation (optional ranking only)
|
|
67
|
+
|
|
68
|
+
These names are soft ranking hints. They are never required. PiMarg always picks up the live authenticated catalog for whoever is using it.
|
|
65
69
|
|
|
66
70
|
- Cursor Composer or a zero-metered Cursor route: economical B/D work when present in the live catalog.
|
|
67
71
|
- Claude Fable/Opus: A/C product thinking, architecture, specification, and cross-family review.
|
|
@@ -71,4 +75,4 @@ Call `auto_runtime_status` during intake. Follow [native-omp-runtime.md](native-
|
|
|
71
75
|
- Latest matching Grok 4.6-or-newer model: A/C independent frontier analysis or current web/social context.
|
|
72
76
|
- Native Devin SWE: B/D when exposed by the runtime. External Devin: bounded delegation with acceptance criteria and a quota limit.
|
|
73
77
|
|
|
74
|
-
Use Pi or OMP's own authentication and model UI as the authority for what the account can actually access.
|
|
78
|
+
Use Pi or OMP's own authentication and model UI as the authority for what the account can actually access. If the catalog is empty, promote subscription/provider setup (`/login` on OMP, or Pi credentials) before continuing. Present required stages in ascending A/B/C/D order.
|