@sangyaahub/pi-marg 1.0.0 → 1.0.1

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.1"
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.1",
18
18
  "author": {
19
19
  "name": "Sangyaa",
20
20
  "email": "contact@sangyaa.co"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1 — 2026-09-10
4
+
5
+ - Added the global `pi-marg start` and `pi-marg "<work prompt>"` OMP launch commands.
6
+ - Canonicalized the npm executable mapping so npm installs the global `pi-marg` command.
7
+ - Added native interactive OMP intake for work boundary, work type, workflow skill, and required A/B/C/D model choices.
8
+ - Changed model pickers to show every authenticated and enabled runtime model while ranking stage-appropriate choices first.
9
+ - Preserved independent-review enforcement: A cannot equal C, and B cannot equal D.
10
+
3
11
  ## 1.0.0 — 2026-09-08
4
12
 
5
13
  - First PiMarg release for Pi Agent and Oh My Pi.
package/MODEL-SETUP.md CHANGED
@@ -39,20 +39,22 @@ The Pi adapter reads `ctx.scopedModels` when the session has a model allowlist;
39
39
  Use OMP `/login`, then inspect `/model` or:
40
40
 
41
41
  ```bash
42
- omp models list
42
+ omp models
43
43
  ```
44
44
 
45
- The OMP adapter reads `ctx.models.list()` and activates an exact catalog entry with `pi.setModel()`.
45
+ The OMP adapter reads `ctx.models.list()`, which supplies authenticated and enabled models, and activates an exact catalog entry with `pi.setModel()`.
46
46
 
47
47
  ## Selection workflow
48
48
 
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.
49
+ 1. Run `/pi-marg` for the full native intake, or `/auto-models choose` to change saved models.
50
+ 2. Confirm the work type so PiMarg knows which stages are required.
51
+ 3. Select A and C when discovery/review is required; recommended frontier choices appear first.
52
+ 4. Select B and D when implementation is required; recommended execution choices appear first.
53
53
  5. Resolve any A=C or B=D identity conflict before continuing.
54
54
  6. Activate the saved selector immediately before its stage.
55
55
 
56
+ Every authenticated and enabled runtime model remains selectable at every required stage. The preferred classes above control ranking, not visibility.
57
+
56
58
  The normalized identity check treats the same underlying model reached through different providers as the same candidate when their identifiers normalize equally.
57
59
 
58
60
  ## Devin
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.1" src="https://img.shields.io/badge/version-1.0.1-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.1 · 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,130 @@
1
1
  import {
2
2
  MODEL_STATE_TYPE,
3
3
  STAGES,
4
+ buildModelCatalog,
4
5
  emptyModelRouteState,
5
6
  executeModelRoute,
7
+ requiredStagesForWorkType,
6
8
  restoreModelRouteState,
9
+ type AutoStage,
10
+ type ModelCandidate,
7
11
  type ModelLike,
8
12
  type ModelRouteParams,
13
+ type ModelRouteState,
9
14
  } from "../../../core/model-routing";
15
+ import { normalizeCapabilityNames } from "../../../core/runtime-capabilities";
10
16
 
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
- }
17
+ const WORK_BOUNDARIES = ["Job/client", "Personal"] as const;
18
+
19
+ const WORK_TYPES = [
20
+ "1. New idea or development from scratch",
21
+ "2. New feature for an existing repository",
22
+ "3. Improve an existing feature",
23
+ "4. Pro-active bug fix",
24
+ "5. Security review or fixes",
25
+ "6. Thoughts about an existing repository or features",
26
+ "7. Business, sales, or other non-technical analysis",
27
+ ] as const;
28
+
29
+ const SKILL_MODES = [
30
+ "1. Compound Engineering",
31
+ "2. Superpowers",
32
+ "3. GSD Core",
33
+ "4. PiMarg chooses the best available skill",
34
+ ] as const;
35
+
36
+ const STAGE_PURPOSE: Record<AutoStage, string> = {
37
+ A: "thinking/discovery",
38
+ B: "implementation/execution",
39
+ C: "plan second-eye review",
40
+ D: "implementation second-eye review",
41
+ };
16
42
 
17
43
  function hasExternalDevin(pi: any): boolean {
18
- return toolNames(pi).some((name) => /(?:^|[_-])devin(?:[_-]|$)/i.test(name));
44
+ return normalizeCapabilityNames(pi.getAllTools?.() ?? [])
45
+ .some((name) => /(?:^|[_-])devin(?:[_-]|$)/i.test(name));
46
+ }
47
+
48
+ function commandPrompt(args: string): string {
49
+ const trimmed = args.trim();
50
+ if (trimmed.toLowerCase() === "start") return "";
51
+ return trimmed;
52
+ }
53
+
54
+ function optionLabel(candidate: ModelCandidate): string {
55
+ return `${candidate.label} — ${candidate.selector}`;
56
+ }
57
+
58
+ function optionDescription(stage: AutoStage, candidate: ModelCandidate): string {
59
+ const preferredTier = stage === "A" || stage === "C" ? "frontier" : "execution";
60
+ const fit = candidate.tier === preferredTier ? "Recommended fit" : "Available";
61
+ return `${fit} for ${STAGE_PURPOSE[stage]}${candidate.billingNote ? ` · ${candidate.billingNote}` : ""}`;
62
+ }
63
+
64
+ async function chooseModel(
65
+ ctx: any,
66
+ stage: AutoStage,
67
+ candidates: ModelCandidate[],
68
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
69
+ ): Promise<ModelCandidate | undefined> {
70
+ const opposite: Record<AutoStage, AutoStage> = { A: "C", C: "A", B: "D", D: "B" };
71
+ const other = selected[opposite[stage]];
72
+ const eligible = candidates.filter((candidate) => !other || candidate.identity !== other.identity);
73
+ if (eligible.length === 0) {
74
+ ctx.ui.notify(
75
+ "Paired stages require two distinct authenticated model identities. Configure another model, then run /pi-marg again.",
76
+ "error",
77
+ );
78
+ return undefined;
79
+ }
80
+ const options = eligible.map((candidate) => ({
81
+ label: optionLabel(candidate),
82
+ description: optionDescription(stage, candidate),
83
+ }));
84
+ const answer = await ctx.ui.select(`Stage ${stage} model`, options, {
85
+ selectionMarker: "radio",
86
+ helpText: `${eligible.length} authenticated and enabled choices · ${STAGE_PURPOSE[stage]}`,
87
+ });
88
+ return eligible.find((candidate) => optionLabel(candidate) === answer);
89
+ }
90
+
91
+ function selectedState(
92
+ workType: number,
93
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
94
+ ): ModelRouteState {
95
+ const selectedAt = new Date().toISOString();
96
+ return {
97
+ version: 1,
98
+ workType,
99
+ selections: Object.fromEntries(
100
+ Object.entries(selected).map(([stage, candidate]) => [stage, { ...candidate, selectedAt }]),
101
+ ) as ModelRouteState["selections"],
102
+ };
103
+ }
104
+
105
+ function intakeMessage(
106
+ request: string,
107
+ boundary: string,
108
+ workType: string,
109
+ skillMode: string,
110
+ selected: Partial<Record<AutoStage, ModelCandidate>>,
111
+ ): string {
112
+ const models = STAGES
113
+ .filter((stage) => selected[stage])
114
+ .map((stage) => `- Stage ${stage} (${STAGE_PURPOSE[stage]}): ${selected[stage]!.selector}`)
115
+ .join("\n");
116
+ return [
117
+ "Start PiMarg for the following request:",
118
+ request,
119
+ "",
120
+ "Interactive intake is already confirmed; do not ask these questions again:",
121
+ `- Boundary: ${boundary}`,
122
+ `- Work type: ${workType}`,
123
+ `- Workflow skill: ${skillMode}`,
124
+ models,
125
+ "",
126
+ "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.",
127
+ ].join("\n");
19
128
  }
20
129
 
21
130
  export default function modelRouter(pi: any) {
@@ -30,6 +139,60 @@ export default function modelRouter(pi: any) {
30
139
  pi.on("session_branch", restore);
31
140
  pi.on("session_tree", restore);
32
141
 
142
+ pi.registerCommand("pi-marg", {
143
+ description: "Start PiMarg with interactive intake, or pass a work prompt.",
144
+ async handler(args: string, ctx: any) {
145
+ if (typeof ctx.isIdle === "function" && ctx.isIdle() === false) {
146
+ ctx.ui.notify("PiMarg intake can start after the current turn finishes.", "warning");
147
+ return;
148
+ }
149
+
150
+ let request = commandPrompt(args);
151
+ if (!ctx.hasUI) {
152
+ if (!request) {
153
+ ctx.ui.notify("PiMarg interactive intake requires an OMP TUI session.", "error");
154
+ return;
155
+ }
156
+ pi.sendUserMessage(`Start PiMarg for this request:\n${request}`);
157
+ return;
158
+ }
159
+ if (!request) {
160
+ request = (await ctx.ui.input("PiMarg", "What would you like to work on?"))?.trim() ?? "";
161
+ }
162
+ if (!request) return;
163
+
164
+ const boundaryLabel = await ctx.ui.select("Work boundary", [...WORK_BOUNDARIES], {
165
+ selectionMarker: "radio",
166
+ });
167
+ if (!WORK_BOUNDARIES.includes(boundaryLabel)) return;
168
+
169
+ const workTypeLabel = await ctx.ui.select("Work type", [...WORK_TYPES], { selectionMarker: "radio" });
170
+ const workType = Number.parseInt(workTypeLabel?.match(/^([1-7])\./)?.[1] ?? "", 10);
171
+ if (!Number.isInteger(workType)) return;
172
+
173
+ const skillMode = await ctx.ui.select("Workflow skill", [...SKILL_MODES], { selectionMarker: "radio" });
174
+ if (!skillMode) return;
175
+
176
+ const liveModels = ctx.models.list() as ModelLike[];
177
+ if (liveModels.length === 0) {
178
+ ctx.ui.notify("No authenticated and enabled OMP models were found. Configure a provider, then run /pi-marg again.", "error");
179
+ return;
180
+ }
181
+ const catalog = buildModelCatalog(liveModels, hasExternalDevin(pi));
182
+ const selected: Partial<Record<AutoStage, ModelCandidate>> = {};
183
+ for (const stage of requiredStagesForWorkType(workType)) {
184
+ const choice = await chooseModel(ctx, stage, catalog[stage], selected);
185
+ if (!choice) return;
186
+ selected[stage] = choice;
187
+ }
188
+
189
+ const nextState = selectedState(workType, selected);
190
+ state = nextState;
191
+ pi.appendEntry(MODEL_STATE_TYPE, nextState);
192
+ pi.sendUserMessage(intakeMessage(request, boundaryLabel, workTypeLabel!, skillMode, selected));
193
+ },
194
+ });
195
+
33
196
  pi.registerTool({
34
197
  name: "auto_model_route",
35
198
  label: "PiMarg Model Router",
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;
@@ -130,7 +131,7 @@ export function normalizeModelIdentity(value: string): string {
130
131
  return normalized;
131
132
  }
132
133
 
133
- export function classifyModel(model: ModelLike): ModelCandidate | undefined {
134
+ export function classifyModel(model: ModelLike): ModelCandidate {
134
135
  const selector = `${model.provider}/${model.id}`;
135
136
  const haystack = `${model.provider} ${model.id} ${model.name ?? ""}`;
136
137
 
@@ -156,17 +157,20 @@ export function classifyModel(model: ModelLike): ModelCandidate | undefined {
156
157
  billingNote: "The runtime reports zero metered token cost; Devin plan quotas or pool limits may still apply.",
157
158
  };
158
159
  }
159
- return undefined;
160
+ return candidate(model, selector, "runtime-available", "general");
160
161
  }
161
162
 
162
163
  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);
164
+ const candidates = [...new Map(models.map((model) => {
165
+ const item = classifyModel(model);
166
+ return [item.selector, item] as const;
167
+ })).values()];
168
+
169
+ const frontier = [...candidates].sort((left, right) => compareForStage(left, right, "frontier"));
170
+ const execution = [...candidates].sort((left, right) => compareForStage(left, right, "execution"));
167
171
 
168
172
  if (hasExternalDevin) {
169
- candidates.push({
173
+ execution.push({
170
174
  selector: "devin/external-session",
171
175
  identity: "devin-external-session",
172
176
  label: "Devin external session",
@@ -176,11 +180,15 @@ export function buildModelCatalog(models: ModelLike[], hasExternalDevin: boolean
176
180
  access: "external-agent",
177
181
  billingNote: "Uses Devin quota rather than a runtime model slot; session creation requires post-web approval.",
178
182
  });
183
+ execution.sort((left, right) => compareForStage(left, right, "execution"));
179
184
  }
180
185
 
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[]>;
186
+ return {
187
+ A: frontier,
188
+ B: execution,
189
+ C: frontier,
190
+ D: execution,
191
+ } satisfies Record<AutoStage, ModelCandidate[]>;
184
192
  }
185
193
 
186
194
  export function validateDistinctSelection(
@@ -328,6 +336,16 @@ function compareCandidates(left: ModelCandidate, right: ModelCandidate): number
328
336
  return left.selector.localeCompare(right.selector);
329
337
  }
330
338
 
339
+ function compareForStage(
340
+ left: ModelCandidate,
341
+ right: ModelCandidate,
342
+ preferredTier: Exclude<CandidateTier, "general">,
343
+ ): number {
344
+ const leftRank = left.tier === preferredTier ? 0 : left.tier === "general" ? 1 : 2;
345
+ const rightRank = right.tier === preferredTier ? 0 : right.tier === "general" ? 1 : 2;
346
+ return leftRank - rightRank || compareCandidates(left, right);
347
+ }
348
+
331
349
  function toolResult(text: string, details: unknown, isError = false) {
332
350
  return {
333
351
  content: [{ type: "text" as const, text }],
@@ -338,11 +356,15 @@ function toolResult(text: string, details: unknown, isError = false) {
338
356
 
339
357
  function catalogNotes(runtimeName: RuntimeName, catalog: Record<AutoStage, ModelCandidate[]>): string[] {
340
358
  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.",
359
+ `The catalog shows every model in ${runtimeName}'s authenticated, enabled registry at call time; selectors are never guessed or pinned.`,
360
+ "A and C rank frontier fits first. B and D rank execution fits first. Every live model remains selectable for every stage.",
343
361
  "Zero-metered describes the runtime's reported token price; it does not promise unlimited subscription usage.",
344
362
  ];
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.");
363
+ if (!catalog.A.some((candidate) => candidate.tier === "frontier")) {
364
+ notes.push("No preferred frontier matches are authenticated; choose another available model or connect a provider.");
365
+ }
366
+ if (!catalog.B.some((candidate) => candidate.tier === "execution")) {
367
+ notes.push("No preferred execution matches are authenticated and no external Devin tool was detected; choose another available model.");
368
+ }
347
369
  return notes;
348
370
  }
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.1",
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.1 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.1") throw new Error("package.json must be v1.0.1");
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");