@sangyaahub/pi-marg 1.0.0 → 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.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.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,25 @@
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
+
15
+ ## 1.0.1 — 2026-09-10
16
+
17
+ - Added the global `pi-marg start` and `pi-marg "<work prompt>"` OMP launch commands.
18
+ - Canonicalized the npm executable mapping so npm installs the global `pi-marg` command.
19
+ - Added native interactive OMP intake for work boundary, work type, workflow skill, and required A/B/C/D model choices.
20
+ - Changed model pickers to show every authenticated and enabled runtime model while ranking stage-appropriate choices first.
21
+ - Preserved independent-review enforcement: A cannot equal C, and B cannot equal D.
22
+
3
23
  ## 1.0.0 — 2026-09-08
4
24
 
5
25
  - First PiMarg release for Pi Agent and Oh My Pi.
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.
@@ -39,22 +41,57 @@ The Pi adapter reads `ctx.scopedModels` when the session has a model allowlist;
39
41
  Use OMP `/login`, then inspect `/model` or:
40
42
 
41
43
  ```bash
42
- omp models list
44
+ omp models
43
45
  ```
44
46
 
45
- The OMP adapter reads `ctx.models.list()` and activates an exact catalog entry with `pi.setModel()`.
47
+ The OMP adapter reads `ctx.models.list()`, which supplies authenticated and enabled models, and activates an exact catalog entry with `pi.setModel()`.
46
48
 
47
49
  ## Selection workflow
48
50
 
49
- 1. Run `/auto-models choose`.
50
- 2. Request `action: catalog` with the confirmed work type.
51
- 3. Select A and C from frontier candidates.
52
- 4. Select B and D when implementation is required.
53
- 5. Resolve any A=C or B=D identity conflict before continuing.
54
- 6. Activate the saved selector immediately before its stage.
51
+ 1. Run `/pi-marg` for the full native intake, or `/auto-models choose` to change saved models.
52
+ 2. Confirm the work type so PiMarg knows which stages are required.
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
+ ```
72
+
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.
55
74
 
56
75
  The normalized identity check treats the same underlying model reached through different providers as the same candidate when their identifiers normalize equally.
57
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
+
58
95
  ## Devin
59
96
 
60
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/PUBLISHING.md CHANGED
@@ -5,7 +5,7 @@ This checklist is for maintainers. Public mutations remain subject to PiMarg's o
5
5
  ## Before a release
6
6
 
7
7
  1. Update `package.json`, `.omp-plugin/marketplace.json`, and `CHANGELOG.md` to the same version.
8
- 2. Run `./scripts/validate.sh`, `bun test tests`, and `npm pack --dry-run`.
8
+ 2. Run `./scripts/validate.sh`, `bun test tests`, `node bin/pi-marg.js --version`, and `npm pack --dry-run`.
9
9
  3. Smoke-test the package root with the supported Pi and OMP versions.
10
10
  4. Inspect the packed file list for secrets, generated debris, and missing runtime files.
11
11
 
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.0" src="https://img.shields.io/badge/version-1.0.0-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.0 · 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
 
@@ -42,7 +42,7 @@ The same model-selection and approval logic lives in `core/`. Runtime-specific c
42
42
  - Confirms job/client versus personal work; job/client mode suppresses Sangyaa branding by default.
43
43
  - Routes seven work types: idea, new feature, improvement, proactive bug fix, security, repository analysis, and business/sales analysis.
44
44
  - Uses one primary Compound Engineering, Superpowers, GSD Core, or automatically selected workflow spine.
45
- - Discovers current authenticated model selectors instead of pinning dated model names.
45
+ - Shows every current authenticated and enabled model, ranking the best fit for each stage first instead of pinning dated model names.
46
46
  - Enforces that A and C use different underlying models, and B and D use different underlying models.
47
47
  - Uses CodeGraph for repository architecture and impact when available, with LSP/AST or repository-native tools for exact code questions.
48
48
  - Adds conditional testing, debugger, browser/desktop, security, Advisor, task, and memory routes only when detected.
@@ -62,7 +62,7 @@ The same model-selection and approval logic lives in `core/`. Runtime-specific c
62
62
  | Advisor, checkpoint, security scan, Hindsight | Extension/fallback when detected | Native capability when enabled |
63
63
  | Parallel agents | Only when a compatible task extension exists | Native `task`/workflow routes when available |
64
64
 
65
- Tested locally against Pi Agent `0.85.1` and OMP `18.1.14`. Optional features are always capability-detected, so their absence produces a recorded fallback rather than a false success claim.
65
+ Tested locally against Pi Agent `0.85.1` and OMP `18.1.15`. Optional features are always capability-detected, so their absence produces a recorded fallback rather than a false success claim.
66
66
 
67
67
  ## Try the demo
68
68
 
@@ -98,6 +98,12 @@ Install PiMarg from the Pi package catalog/npm:
98
98
  pi install npm:@sangyaahub/pi-marg
99
99
  ```
100
100
 
101
+ Install the global OMP launcher:
102
+
103
+ ```bash
104
+ npm install --global @sangyaahub/pi-marg
105
+ ```
106
+
101
107
  Or install Pi and OMP directly from this GitHub repository:
102
108
 
103
109
  ```bash
@@ -106,6 +112,8 @@ omp plugin marketplace add sangyaahub/pi-marg
106
112
  omp plugin install --scope user pi-marg@pi-marg-marketplace
107
113
  ```
108
114
 
115
+ The npm command launches OMP; the OMP plugin installation supplies PiMarg's extensions, skills, commands, and rules. Install both when you want to start the complete workflow with `pi-marg` from any repository.
116
+
109
117
  ## Configure models
110
118
 
111
119
  Authenticate providers using the active runtime's normal login flow, then run:
@@ -114,16 +122,27 @@ Authenticate providers using the active runtime's normal login flow, then run:
114
122
  /auto-models choose
115
123
  ```
116
124
 
117
- The router builds choices from the live authenticated registry. 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. It does not promise that a subscription route is free or unlimited. See [MODEL-SETUP.md](MODEL-SETUP.md).
118
126
 
119
127
  ## Start a workflow
120
128
 
121
- Type a normal request or invoke:
129
+ From a terminal in the repository you want to work on, run either:
130
+
131
+ ```bash
132
+ pi-marg start
133
+ pi-marg "add CSV export to this repository"
134
+ ```
135
+
136
+ `pi-marg start` opens OMP and asks for the work prompt. Supplying a prompt starts the same guided flow immediately. Inside an existing OMP session, invoke:
122
137
 
123
138
  ```text
139
+ /pi-marg
140
+ /pi-marg add CSV export to this repository
124
141
  /auto add CSV export to this repository
125
142
  ```
126
143
 
144
+ The native picker asks for work boundary, work type, workflow skill, and every model required by that work type. It shows only models reported as authenticated and enabled by OMP, with recommended stage fits first.
145
+
127
146
  Useful entry points:
128
147
 
129
148
  - `/auto-models choose` — refresh A/B/C/D choices.
@@ -1,21 +1,153 @@
1
1
  import {
2
2
  MODEL_STATE_TYPE,
3
3
  STAGES,
4
+ buildModelCatalog,
5
+ candidatesForProvider,
6
+ emptyCatalogSetupMessage,
4
7
  emptyModelRouteState,
5
8
  executeModelRoute,
9
+ matchNumberedOption,
10
+ numberedOptionLabel,
11
+ requiredStagesForWorkType,
6
12
  restoreModelRouteState,
13
+ uniqueProviders,
14
+ type AutoStage,
15
+ type ModelCandidate,
7
16
  type ModelLike,
8
17
  type ModelRouteParams,
18
+ type ModelRouteState,
9
19
  } from "../../../core/model-routing";
20
+ import { normalizeCapabilityNames } from "../../../core/runtime-capabilities";
10
21
 
11
- function toolNames(pi: any): string[] {
12
- return (pi.getAllTools?.() ?? [])
13
- .map((tool: any) => typeof tool === "string" ? tool : String(tool?.name ?? ""))
14
- .filter(Boolean);
15
- }
22
+ const WORK_BOUNDARIES = ["1. Job/client", "2. Personal"] as const;
23
+
24
+ const WORK_TYPES = [
25
+ "1. New idea or development from scratch",
26
+ "2. New feature for an existing repository",
27
+ "3. Improve an existing feature",
28
+ "4. Pro-active bug fix",
29
+ "5. Security review or fixes",
30
+ "6. Thoughts about an existing repository or features",
31
+ "7. Business, sales, or other non-technical analysis",
32
+ ] as const;
33
+
34
+ const SKILL_MODES = [
35
+ "1. Compound Engineering",
36
+ "2. Superpowers",
37
+ "3. GSD Core",
38
+ "4. PiMarg chooses the best available skill",
39
+ ] as const;
40
+
41
+ const STAGE_PURPOSE: Record<AutoStage, string> = {
42
+ A: "thinking/discovery",
43
+ B: "implementation/execution",
44
+ C: "plan second-eye review",
45
+ D: "implementation second-eye review",
46
+ };
16
47
 
17
48
  function hasExternalDevin(pi: any): boolean {
18
- return toolNames(pi).some((name) => /(?:^|[_-])devin(?:[_-]|$)/i.test(name));
49
+ return normalizeCapabilityNames(pi.getAllTools?.() ?? [])
50
+ .some((name) => /(?:^|[_-])devin(?:[_-]|$)/i.test(name));
51
+ }
52
+
53
+ function commandPrompt(args: string): string {
54
+ const trimmed = args.trim();
55
+ if (trimmed.toLowerCase() === "start") return "";
56
+ return trimmed;
57
+ }
58
+
59
+ function optionLabel(candidate: ModelCandidate): string {
60
+ return `${candidate.label} — ${candidate.selector}`;
61
+ }
62
+
63
+ function optionDescription(stage: AutoStage, candidate: ModelCandidate): string {
64
+ const preferredTier = stage === "A" || stage === "C" ? "frontier" : "execution";
65
+ const fit = candidate.tier === preferredTier ? "Recommended fit" : "Available";
66
+ return `${fit} for ${STAGE_PURPOSE[stage]}${candidate.billingNote ? ` · ${candidate.billingNote}` : ""}`;
67
+ }
68
+
69
+ async function chooseModel(
70
+ ctx: any,
71
+ stage: AutoStage,
72
+ candidates: ModelCandidate[],
73
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
74
+ ): Promise<ModelCandidate | undefined> {
75
+ const opposite: Record<AutoStage, AutoStage> = { A: "C", C: "A", B: "D", D: "B" };
76
+ const other = selected[opposite[stage]];
77
+ const eligible = candidates.filter((candidate) => !other || candidate.identity !== other.identity);
78
+ if (eligible.length === 0) {
79
+ ctx.ui.notify(
80
+ "Paired stages require two distinct authenticated model identities. Configure another model, then run /pi-marg again.",
81
+ "error",
82
+ );
83
+ return undefined;
84
+ }
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)),
105
+ description: optionDescription(stage, candidate),
106
+ }));
107
+ const answer = await ctx.ui.select(`Stage ${stage} model`, modelOptions, {
108
+ selectionMarker: "radio",
109
+ helpText: `${providerModels.length} ${provider} choices · ${STAGE_PURPOSE[stage]}`,
110
+ });
111
+ return matchNumberedOption(answer, providerModels, optionLabel);
112
+ }
113
+
114
+ function selectedState(
115
+ workType: number,
116
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
117
+ ): ModelRouteState {
118
+ const selectedAt = new Date().toISOString();
119
+ return {
120
+ version: 1,
121
+ workType,
122
+ selections: Object.fromEntries(
123
+ Object.entries(selected).map(([stage, candidate]) => [stage, { ...candidate, selectedAt }]),
124
+ ) as ModelRouteState["selections"],
125
+ };
126
+ }
127
+
128
+ function intakeMessage(
129
+ request: string,
130
+ boundary: string,
131
+ workType: string,
132
+ skillMode: string,
133
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
134
+ ): string {
135
+ const models = STAGES
136
+ .filter((stage) => selected[stage])
137
+ .map((stage) => `- Stage ${stage} (${STAGE_PURPOSE[stage]}): ${selected[stage]!.selector}`)
138
+ .join("\n");
139
+ return [
140
+ "Start PiMarg for the following request:",
141
+ request,
142
+ "",
143
+ "Interactive intake is already confirmed; do not ask these questions again:",
144
+ `- Boundary: ${boundary}`,
145
+ `- Work type: ${workType}`,
146
+ `- Workflow skill: ${skillMode}`,
147
+ models,
148
+ "",
149
+ "Load auto-mode-router, record these choices in the session ledger, activate each saved model before its stage, and continue the workflow. Ask interactively only for information that is still missing.",
150
+ ].join("\n");
19
151
  }
20
152
 
21
153
  export default function modelRouter(pi: any) {
@@ -30,6 +162,61 @@ export default function modelRouter(pi: any) {
30
162
  pi.on("session_branch", restore);
31
163
  pi.on("session_tree", restore);
32
164
 
165
+ pi.registerCommand("pi-marg", {
166
+ description: "Start PiMarg with interactive intake, or pass a work prompt.",
167
+ async handler(args: string, ctx: any) {
168
+ if (typeof ctx.isIdle === "function" && ctx.isIdle() === false) {
169
+ ctx.ui.notify("PiMarg intake can start after the current turn finishes.", "warning");
170
+ return;
171
+ }
172
+
173
+ let request = commandPrompt(args);
174
+ if (!ctx.hasUI) {
175
+ if (!request) {
176
+ ctx.ui.notify("PiMarg interactive intake requires an OMP TUI session.", "error");
177
+ return;
178
+ }
179
+ pi.sendUserMessage(`Start PiMarg for this request:\n${request}`);
180
+ return;
181
+ }
182
+ if (!request) {
183
+ request = (await ctx.ui.input("PiMarg", "What would you like to work on?"))?.trim() ?? "";
184
+ }
185
+ if (!request) return;
186
+
187
+ const boundaryLabel = await ctx.ui.select("Work boundary", [...WORK_BOUNDARIES], {
188
+ selectionMarker: "radio",
189
+ });
190
+ const boundary = matchNumberedOption(boundaryLabel, ["Job/client", "Personal"], (item) => item);
191
+ if (!boundary) return;
192
+
193
+ const workTypeLabel = await ctx.ui.select("Work type", [...WORK_TYPES], { selectionMarker: "radio" });
194
+ const workType = Number.parseInt(workTypeLabel?.match(/^([1-7])\./)?.[1] ?? "", 10);
195
+ if (!Number.isInteger(workType)) return;
196
+
197
+ const skillMode = await ctx.ui.select("Workflow skill", [...SKILL_MODES], { selectionMarker: "radio" });
198
+ if (!skillMode) return;
199
+
200
+ const liveModels = ctx.models.list() as ModelLike[];
201
+ if (liveModels.length === 0) {
202
+ ctx.ui.notify(emptyCatalogSetupMessage("OMP"), "error");
203
+ return;
204
+ }
205
+ const catalog = buildModelCatalog(liveModels, hasExternalDevin(pi));
206
+ const selected: Partial<Record<AutoStage, ModelCandidate>> = {};
207
+ for (const stage of requiredStagesForWorkType(workType)) {
208
+ const choice = await chooseModel(ctx, stage, catalog[stage], selected);
209
+ if (!choice) return;
210
+ selected[stage] = choice;
211
+ }
212
+
213
+ const nextState = selectedState(workType, selected);
214
+ state = nextState;
215
+ pi.appendEntry(MODEL_STATE_TYPE, nextState);
216
+ pi.sendUserMessage(intakeMessage(request, boundary, workTypeLabel!, skillMode, selected));
217
+ },
218
+ });
219
+
33
220
  pi.registerTool({
34
221
  name: "auto_model_route",
35
222
  label: "PiMarg Model Router",
@@ -39,6 +226,7 @@ export default function modelRouter(pi: any) {
39
226
  stage: z.enum(STAGES).optional(),
40
227
  target: z.string().optional(),
41
228
  workType: z.number().optional(),
229
+ provider: z.string().optional(),
42
230
  }),
43
231
  async execute(_toolCallId: string, params: ModelRouteParams, _signal: unknown, _onUpdate: unknown, ctx: any) {
44
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/bin/pi-marg.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, realpathSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const VERSION = JSON.parse(
8
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
9
+ ).version;
10
+
11
+ const HELP = `PiMarg ${VERSION}
12
+
13
+ Usage:
14
+ pi-marg start
15
+ pi-marg "<work prompt>"
16
+ pi-marg start "<work prompt>"
17
+
18
+ Starts Oh My Pi and invokes PiMarg's interactive workflow.`;
19
+
20
+ export function parsePiMargArgs(args) {
21
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
22
+ return { action: "help", prompt: "" };
23
+ }
24
+ if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
25
+ return { action: "version", prompt: "" };
26
+ }
27
+ const promptParts = args[0]?.toLowerCase() === "start" ? args.slice(1) : args;
28
+ return { action: "launch", prompt: promptParts.join(" ").trim() };
29
+ }
30
+
31
+ export function buildOmpInvocation(parsed) {
32
+ if (parsed.action !== "launch") return [];
33
+ return [parsed.prompt ? `/pi-marg ${parsed.prompt}` : "/pi-marg"];
34
+ }
35
+
36
+ export function runPiMarg(args = process.argv.slice(2)) {
37
+ const parsed = parsePiMargArgs(args);
38
+ if (parsed.action === "help") {
39
+ console.log(HELP);
40
+ return 0;
41
+ }
42
+ if (parsed.action === "version") {
43
+ console.log(VERSION);
44
+ return 0;
45
+ }
46
+
47
+ const executable = process.env.PIMARG_OMP_BIN || "omp";
48
+ const result = spawnSync(executable, buildOmpInvocation(parsed), {
49
+ cwd: process.cwd(),
50
+ env: process.env,
51
+ stdio: "inherit",
52
+ shell: false,
53
+ });
54
+ if (result.error) {
55
+ if (result.error.code === "ENOENT") {
56
+ console.error("PiMarg could not find the `omp` command. Install Oh My Pi and make sure `omp` is on PATH.");
57
+ return 127;
58
+ }
59
+ console.error(`PiMarg could not start OMP: ${result.error.message}`);
60
+ return 1;
61
+ }
62
+ return result.status ?? (result.signal ? 1 : 0);
63
+ }
64
+
65
+ function isMainModule() {
66
+ if (!process.argv[1]) return false;
67
+ try {
68
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ if (isMainModule()) {
75
+ process.exitCode = runPiMarg();
76
+ }
@@ -1,5 +1,5 @@
1
1
  export type AutoStage = "A" | "B" | "C" | "D";
2
- export type CandidateTier = "frontier" | "execution";
2
+ export type CandidateTier = "frontier" | "execution" | "general";
3
3
  export type RuntimeName = "OMP" | "Pi";
4
4
 
5
5
  export interface ModelLike {
@@ -25,6 +25,7 @@ export interface ModelCandidate {
25
25
  | "included-cursor"
26
26
  | "devin-swe"
27
27
  | "included-devin"
28
+ | "runtime-available"
28
29
  | "devin-external";
29
30
  access: "runtime-model" | "external-agent";
30
31
  billingNote?: string;
@@ -45,6 +46,7 @@ export interface ModelRouteParams {
45
46
  stage?: AutoStage;
46
47
  target?: string;
47
48
  workType?: number;
49
+ provider?: string;
48
50
  }
49
51
 
50
52
  export interface ModelRoutePort {
@@ -88,7 +90,7 @@ export function requiredStagesForWorkType(workType: number): AutoStage[] {
88
90
  switch (workType) {
89
91
  case 2:
90
92
  case 3:
91
- return ["A", "C", "B", "D"];
93
+ return ["A", "B", "C", "D"];
92
94
  case 4:
93
95
  return ["A", "B", "D"];
94
96
  case 1:
@@ -101,6 +103,25 @@ export function requiredStagesForWorkType(workType: number): AutoStage[] {
101
103
  }
102
104
  }
103
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
+
104
125
  export function normalizeModelIdentity(value: string): string {
105
126
  let normalized = value
106
127
  .toLowerCase()
@@ -130,7 +151,7 @@ export function normalizeModelIdentity(value: string): string {
130
151
  return normalized;
131
152
  }
132
153
 
133
- export function classifyModel(model: ModelLike): ModelCandidate | undefined {
154
+ export function classifyModel(model: ModelLike): ModelCandidate {
134
155
  const selector = `${model.provider}/${model.id}`;
135
156
  const haystack = `${model.provider} ${model.id} ${model.name ?? ""}`;
136
157
 
@@ -156,17 +177,20 @@ export function classifyModel(model: ModelLike): ModelCandidate | undefined {
156
177
  billingNote: "The runtime reports zero metered token cost; Devin plan quotas or pool limits may still apply.",
157
178
  };
158
179
  }
159
- return undefined;
180
+ return candidate(model, selector, "runtime-available", "general");
160
181
  }
161
182
 
162
183
  export function buildModelCatalog(models: ModelLike[], hasExternalDevin: boolean) {
163
- const candidates = models
164
- .map(classifyModel)
165
- .filter((item): item is ModelCandidate => Boolean(item))
166
- .sort(compareCandidates);
184
+ const candidates = [...new Map(models.map((model) => {
185
+ const item = classifyModel(model);
186
+ return [item.selector, item] as const;
187
+ })).values()];
188
+
189
+ const frontier = [...candidates].sort((left, right) => compareForStage(left, right, "frontier"));
190
+ const execution = [...candidates].sort((left, right) => compareForStage(left, right, "execution"));
167
191
 
168
192
  if (hasExternalDevin) {
169
- candidates.push({
193
+ execution.push({
170
194
  selector: "devin/external-session",
171
195
  identity: "devin-external-session",
172
196
  label: "Devin external session",
@@ -176,11 +200,116 @@ export function buildModelCatalog(models: ModelLike[], hasExternalDevin: boolean
176
200
  access: "external-agent",
177
201
  billingNote: "Uses Devin quota rather than a runtime model slot; session creation requires post-web approval.",
178
202
  });
203
+ execution.sort((left, right) => compareForStage(left, right, "execution"));
179
204
  }
180
205
 
181
- const frontier = candidates.filter((item) => item.tier === "frontier");
182
- const execution = candidates.filter((item) => item.tier === "execution");
183
- return { A: frontier, B: execution, C: frontier, D: execution } satisfies Record<AutoStage, ModelCandidate[]>;
206
+ return {
207
+ A: frontier,
208
+ B: execution,
209
+ C: frontier,
210
+ D: execution,
211
+ } satisfies Record<AutoStage, ModelCandidate[]>;
212
+ }
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
+ };
184
313
  }
185
314
 
186
315
  export function validateDistinctSelection(
@@ -215,14 +344,42 @@ export async function executeModelRoute(
215
344
  const catalog = buildModelCatalog(port.models, port.hasExternalDevin);
216
345
 
217
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
+ }
218
357
  const workType = Number(params.workType);
219
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);
220
374
  return {
221
375
  state: currentState,
222
- response: toolResult(
223
- JSON.stringify({ runtime: port.runtimeName, requiredStages, catalog, notes: catalogNotes(port.runtimeName, catalog) }, null, 2),
224
- { runtime: port.runtimeName, requiredStages, catalog, state: currentState },
225
- ),
376
+ response: toolResult(formatted.text, {
377
+ runtime: port.runtimeName,
378
+ requiredStages: formatted.presentation.requiredStages,
379
+ catalog,
380
+ presentation: formatted.presentation,
381
+ state: currentState,
382
+ }),
226
383
  };
227
384
  }
228
385
 
@@ -328,6 +485,16 @@ function compareCandidates(left: ModelCandidate, right: ModelCandidate): number
328
485
  return left.selector.localeCompare(right.selector);
329
486
  }
330
487
 
488
+ function compareForStage(
489
+ left: ModelCandidate,
490
+ right: ModelCandidate,
491
+ preferredTier: Exclude<CandidateTier, "general">,
492
+ ): number {
493
+ const leftRank = left.tier === preferredTier ? 0 : left.tier === "general" ? 1 : 2;
494
+ const rightRank = right.tier === preferredTier ? 0 : right.tier === "general" ? 1 : 2;
495
+ return leftRank - rightRank || compareCandidates(left, right);
496
+ }
497
+
331
498
  function toolResult(text: string, details: unknown, isError = false) {
332
499
  return {
333
500
  content: [{ type: "text" as const, text }],
@@ -336,13 +503,30 @@ function toolResult(text: string, details: unknown, isError = false) {
336
503
  };
337
504
  }
338
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
+
339
510
  function catalogNotes(runtimeName: RuntimeName, catalog: Record<AutoStage, ModelCandidate[]>): string[] {
340
511
  const notes = [
341
- `The catalog is built from ${runtimeName}'s authenticated model registry at call time; selectors are never guessed or pinned.`,
342
- "A and C use frontier candidates. B and D use execution candidates, including eligible Cursor or Devin routes when the runtime exposes them.",
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.",
343
514
  "Zero-metered describes the runtime's reported token price; it does not promise unlimited subscription usage.",
344
515
  ];
345
- if (catalog.A.length === 0) notes.push("No frontier matches are authenticated. Connect a provider, then refresh.");
346
- if (catalog.B.length === 0) notes.push("No execution matches are authenticated and no external Devin tool was detected.");
516
+ if (!catalog.A.some((candidate) => candidate.tier === "frontier")) {
517
+ notes.push("No preferred frontier matches are authenticated; choose another available model or connect a provider.");
518
+ }
519
+ if (!catalog.B.some((candidate) => candidate.tier === "execution")) {
520
+ notes.push("No preferred execution matches are authenticated and no external Devin tool was detected; choose another available model.");
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
+ }
347
531
  return notes;
348
532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sangyaahub/pi-marg",
3
- "version": "1.0.0",
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",
@@ -24,6 +24,9 @@
24
24
  "multi-agent",
25
25
  "sangyaa"
26
26
  ],
27
+ "bin": {
28
+ "pi-marg": "bin/pi-marg.js"
29
+ },
27
30
  "files": [
28
31
  ".mcp.json",
29
32
  ".omp-plugin",
@@ -40,6 +43,7 @@
40
43
  "adapters",
41
44
  "agents",
42
45
  "assets",
46
+ "bin",
43
47
  "commands",
44
48
  "core",
45
49
  "rules",
@@ -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.0 installed for ${runtime}. Start a fresh runtime session, then run /auto-runtime status."
74
+ echo "PiMarg v1.0.3 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
@@ -17,10 +17,13 @@ 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.0") throw new Error("package.json must be v1.0.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");
24
+ if (packageJson.bin?.["pi-marg"] !== "bin/pi-marg.js") throw new Error("PiMarg CLI bin mapping is missing");
25
+ if (!packageJson.files?.includes("bin")) throw new Error("PiMarg CLI is excluded from npm files");
26
+ if (!existsSync("bin/pi-marg.js")) throw new Error("PiMarg CLI entry point is missing");
24
27
  if (marketplace.name !== "pi-marg-marketplace") throw new Error("Unexpected marketplace name");
25
28
  if (marketplace.metadata?.version !== packageJson.version) throw new Error("Marketplace version mismatch");
26
29
  if (marketplace.plugins?.[0]?.version !== packageJson.version) throw new Error("Plugin version mismatch");
@@ -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.