@sangyaahub/pi-marg 1.0.6 → 1.0.8
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 +11 -0
- package/MODEL-SETUP.md +1 -1
- package/README.md +2 -2
- package/adapters/omp/extensions/model-router.ts +28 -0
- package/adapters/omp/extensions/provider-setup.ts +27 -0
- package/adapters/pi/extensions/model-router.ts +39 -0
- package/agents/auto-intake.md +2 -2
- package/core/model-routing.ts +163 -2
- package/core/provider-config.ts +22 -0
- package/package.json +1 -1
- package/rules/auto-mode.md +1 -0
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +1 -1
- package/skills/auto-mode-router/SKILL.md +1 -1
- package/skills/auto-mode-router/references/intake-and-state.md +28 -17
|
@@ -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.8"
|
|
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.8",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Sangyaa",
|
|
20
20
|
"email": "contact@sangyaa.co"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## 1.0.8 — 2026-10-26
|
|
3
|
+
|
|
4
|
+
- A provider reporting 95% usage pauses while its stage context is kept. The saved high then low backup resumes the stage only when it is a different provider, fits the same stage, and is itself under 95%. Hard quota errors keep the existing failover.
|
|
5
|
+
- Provider lines in the catalog show remaining usage when the host reports `percentUsed`, `percentRemaining`, or `used`/`limit`, and say usage remaining is unknown otherwise.
|
|
6
|
+
- An empty OMP catalog can add the optional Kimi Code preset through the same custom-provider writer. It stores the `KIMI_API_KEY` env var name, writes no API key, and is not needed for a logged-in (OAuth) `kimi-code` provider.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## 1.0.7 — 2026-10-03
|
|
10
|
+
|
|
11
|
+
- Stages 1–3 of Auto Mode intake now use the host selection tool (`ask` on OMP) instead of asking the user to type numbers in chat.
|
|
12
|
+
- The `auto-intake` agent can call `ask`, so the selection window is available when intake runs as a subagent.
|
|
2
13
|
|
|
3
14
|
## 1.0.6 — 2026-10-03
|
|
4
15
|
|
package/MODEL-SETUP.md
CHANGED
|
@@ -16,7 +16,7 @@ LIVE AUTHENTICATED CATALOG
|
|
|
16
16
|
high backup ──► low backup
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If the live catalog is empty, `/pi-marg` offers
|
|
19
|
+
If the live catalog is empty, `/pi-marg` offers four actions: sign in with `/login`, add a custom OpenAI-compatible provider, add the optional Kimi Code preset, or cancel. The custom path writes `apiKey: ENV_VAR_NAME` into `~/.omp/agent/models.yml` and the secret only into `~/.omp/agent/.env`. The Kimi preset uses that same writer with provider id `kimi-code`, display name `Kimi Code`, base URL `https://api.kimi.com/coding/v1`, env var `KIMI_API_KEY`, and the model ids from OMP's Kimi Code provider rule. It does not write an API key. Cancelling writes neither file. A logged-in Kimi provider is listed with every other authenticated provider; the preset is not required for that.
|
|
20
20
|
|
|
21
21
|
## Roles
|
|
22
22
|
|
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.8" src="https://img.shields.io/badge/version-1.0.8-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.8 · 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
|
|
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
executeAutomaticFailover,
|
|
11
11
|
executeModelRoute,
|
|
12
12
|
failedModelSelector,
|
|
13
|
+
chooseUsageResume,
|
|
14
|
+
executeUsageResume,
|
|
13
15
|
findTerminalUsageLimitFailure,
|
|
14
16
|
formatFailoverRecoveryMessage,
|
|
17
|
+
formatUsagePauseMessage,
|
|
18
|
+
providerReachedUsagePause,
|
|
19
|
+
usageSnapshotsFromMessages,
|
|
15
20
|
hasConfiguredFallback,
|
|
16
21
|
modelFailureKey,
|
|
17
22
|
modelToolUsageLimitFailure,
|
|
@@ -370,6 +375,29 @@ export default function modelRouter(pi: any) {
|
|
|
370
375
|
|
|
371
376
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
372
377
|
if (event?.willContinue || event?.isTerminal === false) return;
|
|
378
|
+
const liveModels = (ctx.models?.list?.() ?? []) as ModelLike[];
|
|
379
|
+
const current = ctx.models?.current?.() ?? ctx.model;
|
|
380
|
+
const snapshots = usageSnapshotsFromMessages(event?.messages ?? [], liveModels);
|
|
381
|
+
const pause = typeof current?.provider === "string" ? providerReachedUsagePause(snapshots, current.provider) : undefined;
|
|
382
|
+
if (pause && typeof current?.id === "string" && hasConfiguredFallback(state)) {
|
|
383
|
+
const pausedSelector = `${current.provider}/${current.id}`;
|
|
384
|
+
const resume = chooseUsageResume(state, snapshots, current.provider);
|
|
385
|
+
if (resume) {
|
|
386
|
+
const execution = await executeUsageResume(state, pausedSelector, resume, {
|
|
387
|
+
runtimeName: "OMP",
|
|
388
|
+
models: liveModels,
|
|
389
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
390
|
+
activate: (model) => pi.setModel(model),
|
|
391
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
392
|
+
});
|
|
393
|
+
state = execution.state;
|
|
394
|
+
if (execution.switched) {
|
|
395
|
+
ctx.ui?.notify?.(`PiMarg paused ${pausedSelector} at ${pause.percentUsed}% usage and resumed on ${execution.switched.selection.selector}.`, "warning");
|
|
396
|
+
pi.sendUserMessage(formatUsagePauseMessage(pausedSelector, pause, execution.switched.selection.selector), { deliverAs: "aside" });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
373
401
|
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
374
402
|
const failure = failed
|
|
375
403
|
? {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
kimiCodeProviderPreset,
|
|
5
6
|
probeModelIds,
|
|
6
7
|
validateCustomProvider,
|
|
7
8
|
writeCustomProvider,
|
|
@@ -17,6 +18,7 @@ import { agentDirectory } from "../../../core/user-route";
|
|
|
17
18
|
const SETUP_ITEMS = [
|
|
18
19
|
"Sign in to a provider",
|
|
19
20
|
"Add a custom OpenAI-compatible provider",
|
|
21
|
+
"Add the Kimi Code preset",
|
|
20
22
|
"Cancel",
|
|
21
23
|
] as const;
|
|
22
24
|
|
|
@@ -157,9 +159,34 @@ export async function addCustomProvider(ctx: any, home: string | undefined): Pro
|
|
|
157
159
|
return { status: "ready", models };
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
export async function addKimiCodePreset(ctx: any, home: string | undefined): Promise<CatalogSetup> {
|
|
163
|
+
const preset = kimiCodeProviderPreset();
|
|
164
|
+
try {
|
|
165
|
+
const directory = agentDirectory("OMP", home);
|
|
166
|
+
writeCustomProvider({
|
|
167
|
+
modelsPath: join(directory, "models.yml"),
|
|
168
|
+
envPath: join(directory, ".env"),
|
|
169
|
+
}, preset);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Could not write the Kimi Code preset.", "error");
|
|
172
|
+
return { status: "cancelled" };
|
|
173
|
+
}
|
|
174
|
+
ctx.ui.notify(
|
|
175
|
+
`Kimi Code was added to models.yml. Set ${preset.envVar} in the environment; the API key was not stored.`,
|
|
176
|
+
"info",
|
|
177
|
+
);
|
|
178
|
+
if (typeof ctx.models?.reload === "function") {
|
|
179
|
+
await ctx.models.reload();
|
|
180
|
+
}
|
|
181
|
+
const models = (ctx.models.list() ?? []) as ModelLike[];
|
|
182
|
+
if (models.length === 0) return { status: "restart" };
|
|
183
|
+
return { status: "ready", models };
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
export async function configureEmptyCatalog(ctx: any, home: string | undefined): Promise<CatalogSetup> {
|
|
161
187
|
const choice = await askItems(ctx, "Model setup", SETUP_ITEMS);
|
|
162
188
|
if (!choice || choice === "Cancel") return { status: "cancelled" };
|
|
189
|
+
if (choice === "Add the Kimi Code preset") return addKimiCodePreset(ctx, home);
|
|
163
190
|
if (choice === "Sign in to a provider") {
|
|
164
191
|
if (typeof ctx.openLogin === "function") await ctx.openLogin();
|
|
165
192
|
else ctx.ui.notify("Sign in with /login, verify with `omp models`, then run /pi-marg again.", "warning");
|
|
@@ -7,13 +7,20 @@ import {
|
|
|
7
7
|
executeAutomaticFailover,
|
|
8
8
|
executeModelRoute,
|
|
9
9
|
failedModelSelector,
|
|
10
|
+
chooseUsageResume,
|
|
11
|
+
executeUsageResume,
|
|
10
12
|
findTerminalUsageLimitFailure,
|
|
11
13
|
formatFailoverRecoveryMessage,
|
|
14
|
+
formatUsagePauseMessage,
|
|
15
|
+
providerReachedUsagePause,
|
|
16
|
+
usageSnapshotsFromMessages,
|
|
12
17
|
hasConfiguredFallback,
|
|
13
18
|
modelFailureKey,
|
|
14
19
|
modelToolUsageLimitFailure,
|
|
15
20
|
type ModelLike,
|
|
16
21
|
type ModelRouteParams,
|
|
22
|
+
type ModelSelection,
|
|
23
|
+
type ProviderUsageSnapshot,
|
|
17
24
|
} from "../../../core/model-routing";
|
|
18
25
|
import { loadRuntimeRoute, rememberRuntimeRoute } from "../../../core/user-route";
|
|
19
26
|
|
|
@@ -36,6 +43,7 @@ export default function modelRouter(pi: any) {
|
|
|
36
43
|
let state = emptyModelRouteState();
|
|
37
44
|
let recoveryToken: symbol | undefined;
|
|
38
45
|
let sessionGeneration = 0;
|
|
46
|
+
let pendingUsagePause: { selector: string; snapshot: ProviderUsageSnapshot; resume: ModelSelection } | undefined;
|
|
39
47
|
let pendingModelFailure: { selector: string; key: string } | undefined;
|
|
40
48
|
let pendingToolFailure: { selector: string; key: string } | undefined;
|
|
41
49
|
const routeHome = (ctx: any) => typeof ctx?.agentHome === "string" ? ctx.agentHome : undefined;
|
|
@@ -50,6 +58,7 @@ export default function modelRouter(pi: any) {
|
|
|
50
58
|
const restore = (_event: unknown, ctx: any) => {
|
|
51
59
|
sessionGeneration += 1;
|
|
52
60
|
recoveryToken = undefined;
|
|
61
|
+
pendingUsagePause = undefined;
|
|
53
62
|
pendingModelFailure = undefined;
|
|
54
63
|
pendingToolFailure = undefined;
|
|
55
64
|
state = loadRuntimeRoute("Pi", ctx.sessionManager.getBranch(), routeHome(ctx));
|
|
@@ -128,6 +137,17 @@ export default function modelRouter(pi: any) {
|
|
|
128
137
|
};
|
|
129
138
|
|
|
130
139
|
pi.on("agent_end", (event: any, ctx: any) => {
|
|
140
|
+
const liveModels = availableModels(ctx);
|
|
141
|
+
const current = ctx.model as ModelLike | undefined;
|
|
142
|
+
const snapshots = usageSnapshotsFromMessages(event?.messages ?? [], liveModels);
|
|
143
|
+
const pause = current?.provider ? providerReachedUsagePause(snapshots, current.provider) : undefined;
|
|
144
|
+
if (pause && current?.id && hasConfiguredFallback(state)) {
|
|
145
|
+
const pausedSelector = `${current.provider}/${current.id}`;
|
|
146
|
+
const resume = chooseUsageResume(state, snapshots, current.provider);
|
|
147
|
+
if (resume) {
|
|
148
|
+
pendingUsagePause = { selector: pausedSelector, snapshot: pause, resume };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
131
151
|
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
132
152
|
pendingModelFailure = failed
|
|
133
153
|
? { selector: failedModelSelector(failed, ctx.model), key: modelFailureKey(failed) }
|
|
@@ -139,6 +159,25 @@ export default function modelRouter(pi: any) {
|
|
|
139
159
|
});
|
|
140
160
|
|
|
141
161
|
pi.on("agent_settled", async (_event: unknown, ctx: any) => {
|
|
162
|
+
const usagePause = pendingUsagePause;
|
|
163
|
+
pendingUsagePause = undefined;
|
|
164
|
+
if (usagePause && !pendingModelFailure && !pendingToolFailure) {
|
|
165
|
+
const execution = await executeUsageResume(state, usagePause.selector, usagePause.resume, {
|
|
166
|
+
runtimeName: "Pi",
|
|
167
|
+
models: availableModels(ctx),
|
|
168
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
169
|
+
activate: (model) => pi.setModel(model),
|
|
170
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
171
|
+
});
|
|
172
|
+
state = execution.state;
|
|
173
|
+
if (execution.switched) {
|
|
174
|
+
ctx.ui?.notify?.(`PiMarg paused ${usagePause.selector} at ${usagePause.snapshot.percentUsed}% usage and resumed on ${execution.switched.selection.selector}.`, "warning");
|
|
175
|
+
pi.sendUserMessage(formatUsagePauseMessage(usagePause.selector, usagePause.snapshot, execution.switched.selection.selector), { deliverAs: "followUp" });
|
|
176
|
+
pendingModelFailure = undefined;
|
|
177
|
+
pendingToolFailure = undefined;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
142
181
|
const failure = pendingModelFailure ?? pendingToolFailure;
|
|
143
182
|
pendingModelFailure = undefined;
|
|
144
183
|
pendingToolFailure = undefined;
|
package/agents/auto-intake.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
name: auto-intake
|
|
3
3
|
description: Classify an Auto Mode request without modifying the project.
|
|
4
4
|
model: ["@smol", "@default"]
|
|
5
|
-
tools: [read, grep, glob, ls, auto_model_route, auto_runtime_status]
|
|
5
|
+
tools: [read, grep, glob, ls, ask, auto_model_route, auto_runtime_status]
|
|
6
6
|
blocking: true
|
|
7
7
|
autoloadSkills: [auto-mode-router]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
Complete only the intake and availability checks from `auto-mode-router`. Ask in the required order: work context, work type, skill mode, live A/B/C/D selections, high and low runtime backups, then add-ons. Call `auto_runtime_status` before recommending runtime capabilities. Return the proposed `[AUTO-MODE]` state. Do not plan, implement, or change project files.
|
|
10
|
+
Complete only the intake and availability checks from `auto-mode-router`. Ask in the required order: work context, work type, skill mode, live A/B/C/D selections, high and low runtime backups, then add-ons. Ask work context, work type, and skill mode with the host selection tool, one stage at a time. Call `auto_runtime_status` before recommending runtime capabilities. Return the proposed `[AUTO-MODE]` state. Do not plan, implement, or change project files.
|
package/core/model-routing.ts
CHANGED
|
@@ -3,13 +3,29 @@ export type FallbackLevel = "high" | "low";
|
|
|
3
3
|
export type CandidateTier = "frontier" | "execution" | "general";
|
|
4
4
|
export type RuntimeName = "OMP" | "Pi";
|
|
5
5
|
|
|
6
|
+
export interface ModelUsage {
|
|
7
|
+
percentUsed?: number;
|
|
8
|
+
percentRemaining?: number;
|
|
9
|
+
used?: number;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface ModelLike {
|
|
7
14
|
provider: string;
|
|
8
15
|
id: string;
|
|
9
16
|
name?: string;
|
|
10
17
|
cost?: Record<string, unknown>;
|
|
18
|
+
usage?: ModelUsage;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProviderUsageSnapshot {
|
|
22
|
+
provider: string;
|
|
23
|
+
percentUsed: number;
|
|
24
|
+
percentRemaining: number;
|
|
11
25
|
}
|
|
12
26
|
|
|
27
|
+
export const USAGE_PAUSE_PERCENT = 95;
|
|
28
|
+
|
|
13
29
|
export interface ModelCandidate {
|
|
14
30
|
selector: string;
|
|
15
31
|
identity: string;
|
|
@@ -158,6 +174,108 @@ export function emptyModelRouteState(): ModelRouteState {
|
|
|
158
174
|
return { version: 2, selections: {}, fallbacks: {} };
|
|
159
175
|
}
|
|
160
176
|
|
|
177
|
+
|
|
178
|
+
function finitePercent(value: unknown): number | undefined {
|
|
179
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
180
|
+
return Math.min(100, Math.max(0, value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function percentUsedFromUsage(usage: ModelUsage | undefined): number | undefined {
|
|
184
|
+
if (!usage) return undefined;
|
|
185
|
+
const used = finitePercent(usage.percentUsed);
|
|
186
|
+
if (used !== undefined) return used;
|
|
187
|
+
const remaining = finitePercent(usage.percentRemaining);
|
|
188
|
+
if (remaining !== undefined) return 100 - remaining;
|
|
189
|
+
if (typeof usage.used === "number" && typeof usage.limit === "number" && usage.limit > 0 && Number.isFinite(usage.used)) {
|
|
190
|
+
return finitePercent((usage.used / usage.limit) * 100);
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function providerUsageSnapshots(models: readonly ModelLike[]): ProviderUsageSnapshot[] {
|
|
196
|
+
const byProvider = new Map<string, number>();
|
|
197
|
+
for (const model of models) {
|
|
198
|
+
const percentUsed = percentUsedFromUsage(model.usage);
|
|
199
|
+
if (percentUsed === undefined) continue;
|
|
200
|
+
const current = byProvider.get(model.provider);
|
|
201
|
+
if (current === undefined || percentUsed > current) byProvider.set(model.provider, percentUsed);
|
|
202
|
+
}
|
|
203
|
+
return [...byProvider.entries()]
|
|
204
|
+
.map(([provider, percentUsed]) => ({
|
|
205
|
+
provider,
|
|
206
|
+
percentUsed,
|
|
207
|
+
percentRemaining: Math.max(0, 100 - percentUsed),
|
|
208
|
+
}))
|
|
209
|
+
.sort((left, right) => left.provider.localeCompare(right.provider));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function usageSnapshotsFromMessages(
|
|
213
|
+
messages: readonly unknown[],
|
|
214
|
+
models: readonly ModelLike[] = [],
|
|
215
|
+
): ProviderUsageSnapshot[] {
|
|
216
|
+
const reported: ModelLike[] = [...models];
|
|
217
|
+
for (const message of messages) {
|
|
218
|
+
const record = asRecord(message);
|
|
219
|
+
if (!record || typeof record.provider !== "string") continue;
|
|
220
|
+
const usage = asRecord(record.usage);
|
|
221
|
+
if (!usage) continue;
|
|
222
|
+
reported.push({
|
|
223
|
+
provider: record.provider,
|
|
224
|
+
id: typeof record.model === "string" ? record.model : "message",
|
|
225
|
+
usage: {
|
|
226
|
+
percentUsed: typeof usage.percentUsed === "number" ? usage.percentUsed : undefined,
|
|
227
|
+
percentRemaining: typeof usage.percentRemaining === "number" ? usage.percentRemaining : undefined,
|
|
228
|
+
used: typeof usage.used === "number" ? usage.used : undefined,
|
|
229
|
+
limit: typeof usage.limit === "number" ? usage.limit : undefined,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return providerUsageSnapshots(reported);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function providerReachedUsagePause(
|
|
237
|
+
snapshots: readonly ProviderUsageSnapshot[],
|
|
238
|
+
provider: string,
|
|
239
|
+
): ProviderUsageSnapshot | undefined {
|
|
240
|
+
return snapshots.find((snapshot) => snapshot.provider === provider && snapshot.percentUsed >= USAGE_PAUSE_PERCENT);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function fallbackFitsStage(stage: AutoStage | undefined, selection: ModelSelection): boolean {
|
|
244
|
+
if (!stage || selection.tier === "general") return true;
|
|
245
|
+
if (stage === "A" || stage === "C") return selection.tier === "frontier";
|
|
246
|
+
return selection.tier === "execution";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function chooseUsageResume(
|
|
250
|
+
state: ModelRouteState,
|
|
251
|
+
snapshots: readonly ProviderUsageSnapshot[],
|
|
252
|
+
pausedProvider: string,
|
|
253
|
+
): ModelSelection | undefined {
|
|
254
|
+
for (const level of FALLBACK_LEVELS) {
|
|
255
|
+
const selection = state.fallbacks[level];
|
|
256
|
+
if (!selection || selection.access !== "runtime-model") continue;
|
|
257
|
+
if (selection.provider === pausedProvider) continue;
|
|
258
|
+
if (!fallbackFitsStage(state.activeStage, selection)) continue;
|
|
259
|
+
const usage = snapshots.find((snapshot) => snapshot.provider === selection.provider);
|
|
260
|
+
if (usage && usage.percentUsed >= USAGE_PAUSE_PERCENT) continue;
|
|
261
|
+
return selection;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function formatUsagePauseMessage(
|
|
267
|
+
pausedSelector: string,
|
|
268
|
+
snapshot: ProviderUsageSnapshot,
|
|
269
|
+
resumeSelector: string,
|
|
270
|
+
): string {
|
|
271
|
+
return [
|
|
272
|
+
`[PiMarg usage pause] ${pausedSelector} is at ${snapshot.percentUsed}% usage (${snapshot.percentRemaining}% remaining).`,
|
|
273
|
+
"Paused and kept the current stage context.",
|
|
274
|
+
`Resume on ${resumeSelector}, a saved backup of the same stage capability.`,
|
|
275
|
+
].join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
161
279
|
export function isFallbackLevel(value: unknown): value is FallbackLevel {
|
|
162
280
|
return typeof value === "string" && FALLBACK_LEVELS.includes(value as FallbackLevel);
|
|
163
281
|
}
|
|
@@ -471,6 +589,7 @@ export function formatCatalogPresentation(
|
|
|
471
589
|
catalog: Record<AutoStage, ModelCandidate[]>,
|
|
472
590
|
requiredStages: AutoStage[],
|
|
473
591
|
providerFilter?: string,
|
|
592
|
+
usage: readonly ProviderUsageSnapshot[] = [],
|
|
474
593
|
) {
|
|
475
594
|
const notes = catalogNotes(runtimeName, catalog);
|
|
476
595
|
const orderedStages = ascendingStages(requiredStages);
|
|
@@ -520,7 +639,9 @@ export function formatCatalogPresentation(
|
|
|
520
639
|
lines.push("Providers:");
|
|
521
640
|
for (const provider of providers) {
|
|
522
641
|
const marker = providerFilter === provider.name ? " ← selected" : "";
|
|
523
|
-
|
|
642
|
+
const snapshot = usage.find((item) => item.provider === provider.name);
|
|
643
|
+
const remaining = snapshot ? ` — ${snapshot.percentRemaining}% remaining` : " — usage remaining unknown";
|
|
644
|
+
lines.push(` ${provider.index}. ${provider.name} (${provider.count})${remaining}${marker}`);
|
|
524
645
|
}
|
|
525
646
|
if (!providerFilter) {
|
|
526
647
|
lines.push("Call catalog again with provider=<name> to list every model for that provider.");
|
|
@@ -583,6 +704,45 @@ export function restoreModelRouteState(entries: readonly any[]): ModelRouteState
|
|
|
583
704
|
return restored;
|
|
584
705
|
}
|
|
585
706
|
|
|
707
|
+
|
|
708
|
+
export async function executeUsageResume(
|
|
709
|
+
currentState: ModelRouteState,
|
|
710
|
+
pausedSelector: string,
|
|
711
|
+
selection: ModelSelection,
|
|
712
|
+
port: ModelRoutePort,
|
|
713
|
+
): Promise<AutomaticFailoverExecution> {
|
|
714
|
+
const level = (FALLBACK_LEVELS.find((item) => currentState.fallbacks[item]?.selector === selection.selector) ?? "high");
|
|
715
|
+
const model = port.models.find((item) => `${item.provider}/${item.id}` === selection.selector);
|
|
716
|
+
if (!model) return { state: currentState, exhausted: true, stateChanged: false };
|
|
717
|
+
let activated: boolean | void;
|
|
718
|
+
try {
|
|
719
|
+
activated = await port.activate(model);
|
|
720
|
+
} catch {
|
|
721
|
+
return { state: currentState, exhausted: false, stateChanged: false };
|
|
722
|
+
}
|
|
723
|
+
if (activated === false) return { state: currentState, exhausted: false, stateChanged: false };
|
|
724
|
+
const nextState: ModelRouteState = {
|
|
725
|
+
...currentState,
|
|
726
|
+
version: 2,
|
|
727
|
+
activeFallback: level,
|
|
728
|
+
lastFailover: {
|
|
729
|
+
stage: currentState.activeStage,
|
|
730
|
+
failedSelector: pausedSelector,
|
|
731
|
+
fallbackLevel: level,
|
|
732
|
+
activatedSelector: selection.selector,
|
|
733
|
+
switchedAt: port.now?.() ?? new Date().toISOString(),
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
const persistenceError = persistFailoverState(port, nextState);
|
|
737
|
+
return {
|
|
738
|
+
state: nextState,
|
|
739
|
+
switched: { level, selection },
|
|
740
|
+
exhausted: false,
|
|
741
|
+
stateChanged: true,
|
|
742
|
+
...(persistenceError ? { persistenceError } : {}),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
586
746
|
export async function executeAutomaticFailover(
|
|
587
747
|
currentState: ModelRouteState,
|
|
588
748
|
failedSelector: string,
|
|
@@ -780,7 +940,8 @@ export async function executeModelRoute(
|
|
|
780
940
|
};
|
|
781
941
|
}
|
|
782
942
|
}
|
|
783
|
-
const
|
|
943
|
+
const usage = providerUsageSnapshots(port.models);
|
|
944
|
+
const formatted = formatCatalogPresentation(port.runtimeName, catalog, requiredStages, providerFilter, usage);
|
|
784
945
|
return {
|
|
785
946
|
state: currentState,
|
|
786
947
|
response: toolResult(formatted.text, {
|
package/core/provider-config.ts
CHANGED
|
@@ -15,6 +15,28 @@ export interface ProviderFiles {
|
|
|
15
15
|
envPath: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const KIMI_CODE_MODEL_IDS = [
|
|
19
|
+
"kimi-for-coding",
|
|
20
|
+
"k3",
|
|
21
|
+
"k3-256k",
|
|
22
|
+
"kimi-for-coding-highspeed",
|
|
23
|
+
"kimi-k2",
|
|
24
|
+
"kimi-k2.5",
|
|
25
|
+
"kimi-k2-turbo-preview",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/** Optional Kimi Code models.yml input. It names the env var and does not carry an API key. */
|
|
29
|
+
export function kimiCodeProviderPreset(): CustomProviderInput {
|
|
30
|
+
return {
|
|
31
|
+
providerId: "kimi-code",
|
|
32
|
+
displayName: "Kimi Code",
|
|
33
|
+
baseUrl: "https://api.kimi.com/coding/v1",
|
|
34
|
+
envVar: "KIMI_API_KEY",
|
|
35
|
+
modelIds: [...KIMI_CODE_MODEL_IDS],
|
|
36
|
+
allowLocalHttp: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
const PROVIDER_ID = /^[a-z0-9_-]+$/;
|
|
19
41
|
const ENV_VAR = /^[A-Z][A-Z0-9_]*$/;
|
|
20
42
|
const MODEL_ID = /^[A-Za-z0-9._:-]+$/;
|
package/package.json
CHANGED
package/rules/auto-mode.md
CHANGED
|
@@ -10,6 +10,7 @@ For every new substantive user task, load and follow the `auto-mode-router` skil
|
|
|
10
10
|
- Do not restart intake for acknowledgements, clarifications, approval replies, or follow-ups that belong to the active task.
|
|
11
11
|
- If the conversation already contains an active Auto Mode session record, reuse it. Start a new intake only when the user starts a materially different task or invokes `/auto-reset`.
|
|
12
12
|
- The first intake question is always whether the work is for a job/client or personal.
|
|
13
|
+
- Ask work context, work type, and skill mode through the host selection tool (`ask` on OMP). One stage at a time. Do not ask the user to type a number.
|
|
13
14
|
- Confirm one of work types 1–7, then confirm skill mode 1–4 before execution.
|
|
14
15
|
- After skill mode, use the live `auto_model_route` catalog and record required A/B/C/D choices. A and C must use different underlying models; B and D must use different underlying models.
|
|
15
16
|
- Activate the saved model before each A/B/C/D stage. Never guess or pin a version absent from the active Pi or OMP registry.
|
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.8 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
|
package/scripts/validate.sh
CHANGED
|
@@ -45,7 +45,7 @@ bun -e '
|
|
|
45
45
|
const packageJson = await Bun.file("package.json").json();
|
|
46
46
|
const marketplace = await Bun.file(".omp-plugin/marketplace.json").json();
|
|
47
47
|
if (packageJson.name !== "@sangyaahub/pi-marg") throw new Error("Unexpected package name");
|
|
48
|
-
if (packageJson.version !== "1.0.
|
|
48
|
+
if (packageJson.version !== "1.0.8") throw new Error("package.json must be v1.0.8");
|
|
49
49
|
if (packageJson.license !== "MIT") throw new Error("package.json must declare MIT");
|
|
50
50
|
if (packageJson.author !== "Sangyaa") throw new Error("Unexpected package author");
|
|
51
51
|
if (!packageJson.keywords?.includes("pi-package")) throw new Error("Pi catalog keyword is missing");
|
|
@@ -7,7 +7,7 @@ description: Classify a new work request, select one development-method skill sp
|
|
|
7
7
|
|
|
8
8
|
Turn a user request into one controlled workflow.
|
|
9
9
|
|
|
10
|
-
1. Read [intake-and-state.md](references/intake-and-state.md) and complete the intake stages in order.
|
|
10
|
+
1. Read [intake-and-state.md](references/intake-and-state.md) and complete the intake stages in order. Ask work context, work type, and skill mode with the host selection tool, one stage at a time.
|
|
11
11
|
2. After the user selects mode 1–4, read [routing-matrix.md](references/routing-matrix.md) and select exactly one primary skill spine.
|
|
12
12
|
3. Read [session-ledger.md](references/session-ledger.md), create the neutral Markdown ledger, and identify the selected spine's authoritative planning artifact.
|
|
13
13
|
4. Read [models-and-tools.md](references/models-and-tools.md). Call `auto_model_route` with `action: catalog` and the confirmed work type, present the live choices, and save each required A/B/C/D selection. The runtime rejects A=C and B=D. Then save two distinct runtime-only recovery models with `action: select_fallback`: high first, low second.
|
|
@@ -16,16 +16,24 @@ CONTEXT ─► TYPE ─► SKILL MODE ─► MODELS ─► RUNTIME ─► ADD-ON
|
|
|
16
16
|
|
|
17
17
|
Ask one intake stage at a time. A clear user prompt can support a proposed classification, but it does not remove the required confirmation.
|
|
18
18
|
|
|
19
|
+
## How to ask
|
|
20
|
+
|
|
21
|
+
Stages 1–3 are selection questions. Call the host's blocking question tool when that tool is already in the current tool list. On OMP the tool is `ask`. Elsewhere, use the listed equivalent (`AskUserQuestion`, `ask_user_question`, or `vscode_askquestions`). Do not probe for a tool that is not listed.
|
|
22
|
+
|
|
23
|
+
Put each choice in `options` as a short label with no leading number. Set `recommended` to the inferred option when the tool accepts it. Then stop and wait. Do not print the choices as a numbered chat list, and do not ask for a typed number or a combined reply such as `2 / 7 / 4`.
|
|
24
|
+
|
|
25
|
+
Work type has seven options. Pass all seven in that one question. The ask tool's usual "2–5 options" note is for ordinary clarifications; it does not apply to this fixed list, and it is not a reason to drop a type or switch to typed numbers.
|
|
26
|
+
|
|
27
|
+
If no question tool is listed, or a real call returns an error, render one numbered list in chat and accept the number. The live model catalog in stage 4 always uses that numbered path, because a full provider or model list does not fit the selection UI.
|
|
28
|
+
|
|
19
29
|
## Stage 1 — work context
|
|
20
30
|
|
|
21
|
-
Ask
|
|
31
|
+
Ask with the question tool:
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
> 1. Job/client
|
|
26
|
-
> 2. Personal
|
|
33
|
+
- Question: Is this for your job/client, or is it personal work?
|
|
34
|
+
- Options: `Job/client`, `Personal`
|
|
27
35
|
|
|
28
|
-
Record exactly one canonical value (`job-client` or `personal`)
|
|
36
|
+
Record exactly one canonical value (`job-client` or `personal`):
|
|
29
37
|
|
|
30
38
|
- `job-client`: Do not add Sangyaa branding, name, logo, metadata, footer, or promotional language. Use the employer/client/project brand already in scope.
|
|
31
39
|
- `personal`: Existing project branding wins. Sangyaa branding is opt-in, never automatic.
|
|
@@ -34,23 +42,26 @@ Do not start code, research, or planning before this answer unless the user is a
|
|
|
34
42
|
|
|
35
43
|
## Stage 2 — work type
|
|
36
44
|
|
|
37
|
-
Infer the most likely type,
|
|
45
|
+
Infer the most likely type, name it in the question, mark that option recommended, and ask with the question tool. If ambiguous, show only the relevant choices. These labels are the options; the number is the recorded type id, not a chat prefix.
|
|
38
46
|
|
|
39
|
-
1. New idea or development from scratch
|
|
40
|
-
2. New feature for an existing repository
|
|
41
|
-
3. Improve an existing feature in an existing repository
|
|
42
|
-
4. Proactive bug discovery or bug fix in an existing repository
|
|
43
|
-
5. Security review or security fixes
|
|
44
|
-
6. Thoughts, explanation, or another question about an existing repository or feature
|
|
45
|
-
7. Business, sales, or other non-technical analysis
|
|
47
|
+
1. `New idea or development from scratch` — brainstorming and analysis.
|
|
48
|
+
2. `New feature for an existing repository`
|
|
49
|
+
3. `Improve an existing feature in an existing repository`
|
|
50
|
+
4. `Proactive bug discovery or bug fix in an existing repository`
|
|
51
|
+
5. `Security review or security fixes`
|
|
52
|
+
6. `Thoughts, explanation, or another question about an existing repository or feature`
|
|
53
|
+
7. `Business, sales, or other non-technical analysis`
|
|
46
54
|
|
|
47
55
|
If one request contains multiple types, pick the type that owns the immediate outcome and record later types as follow-on phases. Do not open multiple workflow spines.
|
|
48
56
|
|
|
49
57
|
## Stage 3 — skill mode
|
|
50
58
|
|
|
51
|
-
Ask:
|
|
59
|
+
Ask with the question tool. Question: Which method should own this task? Options:
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
1. `Compound Engineering`
|
|
62
|
+
2. `Superpowers`
|
|
63
|
+
3. `GSD Core`
|
|
64
|
+
4. `Choose the best one for me`
|
|
54
65
|
|
|
55
66
|
- Mode 1: use the Compound Engineering mapping for the confirmed work type.
|
|
56
67
|
- Mode 2: use the Superpowers mapping.
|
|
@@ -65,7 +76,7 @@ Catalog presentation is provider-first:
|
|
|
65
76
|
|
|
66
77
|
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
78
|
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
|
|
79
|
+
3. Ask in chat with those numbered labels, and accept the number or the exact selector. Do not put a full provider or model list into the selection tool; it truncates and hides logged-in providers.
|
|
69
80
|
|
|
70
81
|
Interactive `/pi-marg` uses the same provider → model two-step with numbered labels.
|
|
71
82
|
|