@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.
@@ -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.1"
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.1",
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. Select A and C when discovery/review is required; recommended frontier choices appear first.
52
- 4. Select B and D when implementation is required; recommended execution choices appear first.
53
- 5. Resolve any A=C or B=D identity conflict before continuing.
54
- 6. Activate the saved selector immediately before its stage.
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. The preferred classes above control ranking, not visibility.
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.1" src="https://img.shields.io/badge/version-1.0.1-0451F7"></a>
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.1 · MIT licensed · Pi Agent + Oh My Pi · by [Sangyaa](https://sangyaa.co)**
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
- const options = eligible.map((candidate) => ({
81
- label: optionLabel(candidate),
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`, options, {
107
+ const answer = await ctx.ui.select(`Stage ${stage} model`, modelOptions, {
85
108
  selectionMarker: "radio",
86
- helpText: `${eligible.length} authenticated and enabled choices · ${STAGE_PURPOSE[stage]}`,
109
+ helpText: `${providerModels.length} ${provider} choices · ${STAGE_PURPOSE[stage]}`,
87
110
  });
88
- return eligible.find((candidate) => optionLabel(candidate) === answer);
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
- if (!WORK_BOUNDARIES.includes(boundaryLabel)) return;
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("No authenticated and enabled OMP models were found. Configure a provider, then run /pi-marg again.", "error");
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, boundaryLabel, workTypeLabel!, skillMode, selected));
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
@@ -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", "C", "B", "D"];
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
- JSON.stringify({ runtime: port.runtimeName, requiredStages, catalog, notes: catalogNotes(port.runtimeName, catalog) }, null, 2),
232
- { runtime: port.runtimeName, requiredStages, catalog, state: currentState },
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 pinned.`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sangyaahub/pi-marg",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A governed, model-routed development path for Pi Agent and Oh My Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.1 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
74
+ echo "PiMarg v1.0.3 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
@@ -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.1") throw new Error("package.json must be v1.0.1");
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 for the user's accounts
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.