@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.
- package/.omp-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +20 -0
- package/MODEL-SETUP.md +49 -12
- package/PUBLISHING.md +1 -1
- package/README.md +25 -6
- package/adapters/omp/extensions/model-router.ts +194 -6
- package/adapters/pi/extensions/model-router.ts +1 -0
- package/assets/pi-marg-banner.png +0 -0
- package/bin/pi-marg.js +76 -0
- package/core/model-routing.ts +204 -20
- package/package.json +5 -1
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +4 -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,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
|
|
44
|
+
omp models
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
The OMP adapter reads `ctx.models.list()
|
|
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.
|
|
51
|
-
3.
|
|
52
|
-
4.
|
|
53
|
-
5.
|
|
54
|
-
6.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
+
}
|
package/core/model-routing.ts
CHANGED
|
@@ -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", "
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
|
342
|
-
"A and C
|
|
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.
|
|
346
|
-
|
|
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.
|
|
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",
|
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,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.
|
|
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
|
|
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.
|