@leing2021/super-pi 0.30.6 → 0.31.0
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/README.md
CHANGED
|
@@ -98,6 +98,8 @@ Configure in `.pi/settings.json`:
|
|
|
98
98
|
|
|
99
99
|
Model and thinking level switch automatically — no manual `/model` needed.
|
|
100
100
|
|
|
101
|
+
Routing triggers on explicit `/skill:` commands **and** when the agent reads a stage's `SKILL.md` on its own. If no strategy is configured, routing is a no-op — the session model is never touched. Project-level settings take precedence; missing keys fall back to global `~/.pi/agent/settings.json`.
|
|
102
|
+
|
|
101
103
|
## Design Philosophy & Acknowledgements
|
|
102
104
|
|
|
103
105
|
**80% planning and review, 20% execution.**
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
4
|
-
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent"
|
|
5
2
|
import { Type } from "typebox"
|
|
6
3
|
import { createArtifactHelperTool, type ArtifactType } from "./tools/artifact-helper"
|
|
7
4
|
import { createAskUserQuestionTool, CUSTOM_SENTINEL } from "./tools/ask-user-question"
|
|
@@ -19,14 +16,7 @@ import { createContextHandoffTool } from "./tools/context-handoff"
|
|
|
19
16
|
import { filterBashOutput } from "./tools/bash-output-filter"
|
|
20
17
|
import { filterReadOutput } from "./tools/read-output-filter"
|
|
21
18
|
import { COMPACTION_FOCUS_INSTRUCTIONS } from "./tools/compaction-optimizer"
|
|
22
|
-
|
|
23
|
-
const PIPELINE_STAGE_KEYS = new Set([
|
|
24
|
-
"01-brainstorm",
|
|
25
|
-
"02-plan",
|
|
26
|
-
"03-work",
|
|
27
|
-
"04-review",
|
|
28
|
-
"05-learn",
|
|
29
|
-
])
|
|
19
|
+
import { applyStageStrategies, parseStageSkillName, parseStageSkillPath } from "./utils/stage-routing"
|
|
30
20
|
|
|
31
21
|
/**
|
|
32
22
|
* Module-level promise chain that serializes interactive `ask_user_question`
|
|
@@ -92,91 +82,6 @@ function buildAskUserQuestionUi(ctx: any): import("./tools/ask-user-question").A
|
|
|
92
82
|
return { input, select }
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
interface StrategySettings {
|
|
96
|
-
modelStrategy?: Record<string, string>
|
|
97
|
-
thinkingStrategy?: Record<string, string>
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Read settings from two locations (config dir honors pi's `CONFIG_DIR_NAME`,
|
|
102
|
-
* which defaults to `.pi` but is user-configurable since pi 0.79.7):
|
|
103
|
-
* 1. Project-level: {cwd}/{CONFIG_DIR_NAME}/settings.json (highest priority)
|
|
104
|
-
* 2. Global-level: ~/{CONFIG_DIR_NAME}/agent/settings.json (fallback)
|
|
105
|
-
*
|
|
106
|
-
* Project-level takes precedence; global-level is used as fallback.
|
|
107
|
-
*/
|
|
108
|
-
async function readSettings(cwd: string): Promise<StrategySettings | null> {
|
|
109
|
-
const agentHome = process.env.HOME || "~"
|
|
110
|
-
// Try project-level first
|
|
111
|
-
const projectPath = path.join(cwd, CONFIG_DIR_NAME, "settings.json")
|
|
112
|
-
try {
|
|
113
|
-
const content = await readFile(projectPath, "utf8")
|
|
114
|
-
const projectSettings = JSON.parse(content) as StrategySettings
|
|
115
|
-
// If project has modelStrategy or thinkingStrategy, use it
|
|
116
|
-
if (projectSettings.modelStrategy || projectSettings.thinkingStrategy) {
|
|
117
|
-
return projectSettings
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
// Project settings not found, continue to global
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Fallback to global-level
|
|
124
|
-
const globalPath = path.join(agentHome, CONFIG_DIR_NAME, "agent", "settings.json")
|
|
125
|
-
try {
|
|
126
|
-
const content = await readFile(globalPath, "utf8")
|
|
127
|
-
return JSON.parse(content) as StrategySettings
|
|
128
|
-
} catch {
|
|
129
|
-
// Global settings not found either
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Try ~/{CONFIG_DIR_NAME}/settings.json as another fallback
|
|
133
|
-
const altGlobalPath = path.join(agentHome, CONFIG_DIR_NAME, "settings.json")
|
|
134
|
-
try {
|
|
135
|
-
const content = await readFile(altGlobalPath, "utf8")
|
|
136
|
-
return JSON.parse(content) as StrategySettings
|
|
137
|
-
} catch {
|
|
138
|
-
return null
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function parseStageSkillName(text: string): string | null {
|
|
143
|
-
const trimmed = text.trim()
|
|
144
|
-
const match = trimmed.match(/^\/skill:([^\s]+)/)
|
|
145
|
-
if (!match) {
|
|
146
|
-
return null
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const skillName = match[1]
|
|
150
|
-
return PIPELINE_STAGE_KEYS.has(skillName) ? skillName : null
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function parseModelRef(
|
|
154
|
-
modelRef: string,
|
|
155
|
-
currentProvider?: string,
|
|
156
|
-
): { provider: string, id: string } | null {
|
|
157
|
-
const trimmed = modelRef.trim()
|
|
158
|
-
if (!trimmed) {
|
|
159
|
-
return null
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const slashIndex = trimmed.indexOf("/")
|
|
163
|
-
if (slashIndex > 0 && slashIndex < trimmed.length - 1) {
|
|
164
|
-
return {
|
|
165
|
-
provider: trimmed.slice(0, slashIndex),
|
|
166
|
-
id: trimmed.slice(slashIndex + 1),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!currentProvider) {
|
|
171
|
-
return null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
provider: currentProvider,
|
|
176
|
-
id: trimmed,
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
85
|
const artifactHelperParams = Type.Object({
|
|
181
86
|
repoRoot: Type.String({ description: "Repository root where workflow artifacts should be created" }),
|
|
182
87
|
artifactType: Type.Union([
|
|
@@ -360,78 +265,31 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
360
265
|
return { action: "continue" as const }
|
|
361
266
|
}
|
|
362
267
|
|
|
363
|
-
|
|
364
|
-
const modelStrategy = settings?.modelStrategy
|
|
365
|
-
const thinkingStrategy = settings?.thinkingStrategy
|
|
366
|
-
// Notification guard: only notify in interactive (TUI) or RPC modes.
|
|
367
|
-
// For interactive input capability checks (askUserQuestion), use ctx.hasUI directly.
|
|
368
|
-
const shouldNotify = ctx.mode === "tui" || ctx.mode === "rpc"
|
|
369
|
-
|
|
370
|
-
// Model switching
|
|
371
|
-
if (modelStrategy) {
|
|
372
|
-
const targetModelRef = modelStrategy[stageKey] ?? modelStrategy.default
|
|
373
|
-
if (targetModelRef) {
|
|
374
|
-
const parsed = parseModelRef(targetModelRef, ctx.model?.provider)
|
|
375
|
-
if (parsed) {
|
|
376
|
-
// Skip if already using the same model
|
|
377
|
-
if (ctx.model?.provider !== parsed.provider || ctx.model?.id !== parsed.id) {
|
|
378
|
-
const model = ctx.modelRegistry.find(parsed.provider, parsed.id)
|
|
379
|
-
if (model) {
|
|
380
|
-
const switched = await pi.setModel(model)
|
|
381
|
-
if (switched) {
|
|
382
|
-
if (shouldNotify) {
|
|
383
|
-
ctx.ui.notify(`Switched model for ${stageKey}: ${model.provider}/${model.id}`, "info")
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
if (shouldNotify) {
|
|
387
|
-
ctx.ui.notify(`No API key for ${stageKey}: ${model.provider}/${model.id}`, "warning")
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} else if (shouldNotify) {
|
|
391
|
-
ctx.ui.notify(`Model not found for ${stageKey}: ${targetModelRef}`, "warning")
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} else if (shouldNotify) {
|
|
395
|
-
ctx.ui.notify(`Invalid modelStrategy for ${stageKey}: ${targetModelRef}`, "warning")
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Thinking level switching
|
|
401
|
-
if (thinkingStrategy) {
|
|
402
|
-
const targetThinking = thinkingStrategy[stageKey] ?? thinkingStrategy.default
|
|
403
|
-
if (targetThinking) {
|
|
404
|
-
const levelMap: Record<string, ReturnType<ExtensionAPI["getThinkingLevel"]>> = {
|
|
405
|
-
off: "off",
|
|
406
|
-
minimal: "minimal",
|
|
407
|
-
low: "low",
|
|
408
|
-
medium: "medium",
|
|
409
|
-
high: "high",
|
|
410
|
-
xhigh: "xhigh",
|
|
411
|
-
max: "max",
|
|
412
|
-
"0": "low",
|
|
413
|
-
"1": "medium",
|
|
414
|
-
"2": "high",
|
|
415
|
-
}
|
|
416
|
-
const rawThinking = targetThinking.toLowerCase()
|
|
417
|
-
const knownLevel = levelMap[rawThinking]
|
|
418
|
-
const normalized = knownLevel ?? "medium"
|
|
419
|
-
if (!knownLevel && shouldNotify) {
|
|
420
|
-
ctx.ui.notify(`Unknown thinking level for ${stageKey}: ${targetThinking}, falling back to medium`, "warning")
|
|
421
|
-
}
|
|
422
|
-
const currentLevel = pi.getThinkingLevel()
|
|
423
|
-
if (currentLevel !== normalized) {
|
|
424
|
-
pi.setThinkingLevel(normalized)
|
|
425
|
-
if (shouldNotify) {
|
|
426
|
-
ctx.ui.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
268
|
+
await applyStageStrategies(pi, ctx, stageKey)
|
|
431
269
|
|
|
432
270
|
return { action: "continue" as const }
|
|
433
271
|
})
|
|
434
272
|
|
|
273
|
+
// Model-initiated skill invocation: when the agent reads a pipeline
|
|
274
|
+
// stage's SKILL.md (instead of the user typing /skill:), route the
|
|
275
|
+
// stage's model/thinking strategies the same way. Reads of other paths
|
|
276
|
+
// are untouched.
|
|
277
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
278
|
+
if (event.toolName !== "read") {
|
|
279
|
+
return undefined
|
|
280
|
+
}
|
|
281
|
+
const filePath = (event.input as { path?: unknown } | undefined)?.path
|
|
282
|
+
if (typeof filePath !== "string") {
|
|
283
|
+
return undefined
|
|
284
|
+
}
|
|
285
|
+
const stageKey = parseStageSkillPath(filePath)
|
|
286
|
+
if (!stageKey) {
|
|
287
|
+
return undefined
|
|
288
|
+
}
|
|
289
|
+
await applyStageStrategies(pi, ctx, stageKey)
|
|
290
|
+
return undefined
|
|
291
|
+
})
|
|
292
|
+
|
|
435
293
|
const artifactHelper = createArtifactHelperTool()
|
|
436
294
|
const askUserQuestion = createAskUserQuestionTool()
|
|
437
295
|
const workflowState = createWorkflowStateTool()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
4
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent"
|
|
5
|
+
|
|
6
|
+
export const PIPELINE_STAGE_KEYS = new Set([
|
|
7
|
+
"01-brainstorm",
|
|
8
|
+
"02-plan",
|
|
9
|
+
"03-work",
|
|
10
|
+
"04-review",
|
|
11
|
+
"05-learn",
|
|
12
|
+
"06-next",
|
|
13
|
+
"07-worktree",
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
interface StrategySettings {
|
|
17
|
+
modelStrategy?: Record<string, string>
|
|
18
|
+
thinkingStrategy?: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Structural subset of the extension context used by stage routing.
|
|
23
|
+
* Keeps the routing helpers testable without full ExtensionContext mocks.
|
|
24
|
+
*/
|
|
25
|
+
interface StageRoutingContext {
|
|
26
|
+
cwd: string
|
|
27
|
+
mode?: string
|
|
28
|
+
hasUI?: boolean
|
|
29
|
+
model?: { provider?: string, id?: string }
|
|
30
|
+
modelRegistry?: { find(provider: string, id: string): unknown }
|
|
31
|
+
ui?: { notify(message: string, level?: string): void }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Model/thinking strategies only matter where the user can see the switch
|
|
36
|
+
* notification; silent hosts would swallow failures.
|
|
37
|
+
*/
|
|
38
|
+
function shouldNotifyRouting(ctx: StageRoutingContext): boolean {
|
|
39
|
+
return ctx.mode === "tui" || ctx.mode === "rpc"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function applyModelStrategy(
|
|
43
|
+
pi: ExtensionAPI,
|
|
44
|
+
ctx: StageRoutingContext,
|
|
45
|
+
stageKey: string,
|
|
46
|
+
strategy: Record<string, string> | undefined,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
if (!strategy) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const notify = (message: string, level: "info" | "warning") => {
|
|
52
|
+
if (shouldNotifyRouting(ctx)) {
|
|
53
|
+
ctx.ui?.notify(message, level)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const targetModelRef = strategy[stageKey] ?? strategy.default
|
|
58
|
+
if (!targetModelRef) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseModelRef(targetModelRef, ctx.model?.provider)
|
|
62
|
+
if (!parsed) {
|
|
63
|
+
notify(`Invalid modelStrategy for ${stageKey}: ${targetModelRef}`, "warning")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// Skip if already using the same model (idempotent re-reads).
|
|
67
|
+
if (ctx.model?.provider === parsed.provider && ctx.model?.id === parsed.id) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const model = ctx.modelRegistry?.find(parsed.provider, parsed.id)
|
|
71
|
+
if (!model) {
|
|
72
|
+
notify(`Model not found for ${stageKey}: ${targetModelRef}`, "warning")
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const switched = await pi.setModel(model as Parameters<ExtensionAPI["setModel"]>[0])
|
|
76
|
+
if (switched) {
|
|
77
|
+
notify(`Switched model for ${stageKey}: ${parsed.provider}/${parsed.id}`, "info")
|
|
78
|
+
} else {
|
|
79
|
+
notify(`No API key for ${stageKey}: ${parsed.provider}/${parsed.id}`, "warning")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyThinkingStrategy(
|
|
84
|
+
pi: ExtensionAPI,
|
|
85
|
+
ctx: StageRoutingContext,
|
|
86
|
+
stageKey: string,
|
|
87
|
+
strategy: Record<string, string> | undefined,
|
|
88
|
+
): void {
|
|
89
|
+
if (!strategy) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
const targetThinking = strategy[stageKey] ?? strategy.default
|
|
93
|
+
if (!targetThinking) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const levelMap: Record<string, ReturnType<ExtensionAPI["getThinkingLevel"]>> = {
|
|
97
|
+
off: "off",
|
|
98
|
+
minimal: "minimal",
|
|
99
|
+
low: "low",
|
|
100
|
+
medium: "medium",
|
|
101
|
+
high: "high",
|
|
102
|
+
xhigh: "xhigh",
|
|
103
|
+
max: "max",
|
|
104
|
+
"0": "low",
|
|
105
|
+
"1": "medium",
|
|
106
|
+
"2": "high",
|
|
107
|
+
}
|
|
108
|
+
const rawThinking = targetThinking.toLowerCase()
|
|
109
|
+
const knownLevel = levelMap[rawThinking]
|
|
110
|
+
const normalized = knownLevel ?? "medium"
|
|
111
|
+
if (!knownLevel && shouldNotifyRouting(ctx)) {
|
|
112
|
+
ctx.ui?.notify(`Unknown thinking level for ${stageKey}: ${targetThinking}, falling back to medium`, "warning")
|
|
113
|
+
}
|
|
114
|
+
if (pi.getThinkingLevel() !== normalized) {
|
|
115
|
+
pi.setThinkingLevel(normalized)
|
|
116
|
+
if (shouldNotifyRouting(ctx)) {
|
|
117
|
+
ctx.ui?.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Apply model + thinking strategies for a pipeline stage. Shared by the
|
|
124
|
+
* `input` hook (explicit `/skill:` commands) and the `tool_call` hook
|
|
125
|
+
* (model-initiated SKILL.md reads).
|
|
126
|
+
*/
|
|
127
|
+
export async function applyStageStrategies(
|
|
128
|
+
pi: ExtensionAPI,
|
|
129
|
+
ctx: StageRoutingContext,
|
|
130
|
+
stageKey: string,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const settings = await readSettings(ctx.cwd)
|
|
133
|
+
await applyModelStrategy(pi, ctx, stageKey, settings?.modelStrategy)
|
|
134
|
+
applyThinkingStrategy(pi, ctx, stageKey, settings?.thinkingStrategy)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read settings from two locations (config dir honors pi's `CONFIG_DIR_NAME`,
|
|
139
|
+
* which defaults to `.pi` but is user-configurable since pi 0.79.7):
|
|
140
|
+
* 1. Project-level: {cwd}/{CONFIG_DIR_NAME}/settings.json (highest priority)
|
|
141
|
+
* 2. Global-level: ~/{CONFIG_DIR_NAME}/agent/settings.json (fallback)
|
|
142
|
+
*
|
|
143
|
+
* Project-level takes precedence; global-level is used as fallback.
|
|
144
|
+
*/
|
|
145
|
+
async function readSettings(cwd: string): Promise<StrategySettings | null> {
|
|
146
|
+
const agentHome = process.env.HOME || "~"
|
|
147
|
+
// Try project-level first
|
|
148
|
+
const projectPath = path.join(cwd, CONFIG_DIR_NAME, "settings.json")
|
|
149
|
+
try {
|
|
150
|
+
const content = await readFile(projectPath, "utf8")
|
|
151
|
+
const projectSettings = JSON.parse(content) as StrategySettings
|
|
152
|
+
// If project has modelStrategy or thinkingStrategy, use it
|
|
153
|
+
if (projectSettings.modelStrategy || projectSettings.thinkingStrategy) {
|
|
154
|
+
return projectSettings
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Project settings not found, continue to global
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback to global-level
|
|
161
|
+
const globalPath = path.join(agentHome, CONFIG_DIR_NAME, "agent", "settings.json")
|
|
162
|
+
try {
|
|
163
|
+
const content = await readFile(globalPath, "utf8")
|
|
164
|
+
return JSON.parse(content) as StrategySettings
|
|
165
|
+
} catch {
|
|
166
|
+
// Global settings not found either
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try ~/{CONFIG_DIR_NAME}/settings.json as another fallback
|
|
170
|
+
const altGlobalPath = path.join(agentHome, CONFIG_DIR_NAME, "settings.json")
|
|
171
|
+
try {
|
|
172
|
+
const content = await readFile(altGlobalPath, "utf8")
|
|
173
|
+
return JSON.parse(content) as StrategySettings
|
|
174
|
+
} catch {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Detect a pipeline stage SKILL.md path, e.g.
|
|
181
|
+
* `.../skills/03-work/SKILL.md`. Returns the stage key or null.
|
|
182
|
+
*/
|
|
183
|
+
export function parseStageSkillPath(filePath: string): string | null {
|
|
184
|
+
// Accept absolute paths, `./`-prefixed, and bare relative paths like
|
|
185
|
+
// `skills/03-work/SKILL.md` (the read tool allows relative paths).
|
|
186
|
+
const match = filePath.match(/(?:^|[\\/])skills[\\/]([^\\/]+)[\\/]SKILL\.md$/)
|
|
187
|
+
if (!match) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
const stageKey = match[1]
|
|
191
|
+
return PIPELINE_STAGE_KEYS.has(stageKey) ? stageKey : null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function parseStageSkillName(text: string): string | null {
|
|
195
|
+
const trimmed = text.trim()
|
|
196
|
+
const match = trimmed.match(/^\/skill:([^\s]+)/)
|
|
197
|
+
if (!match) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const skillName = match[1]
|
|
202
|
+
return PIPELINE_STAGE_KEYS.has(skillName) ? skillName : null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseModelRef(
|
|
206
|
+
modelRef: string,
|
|
207
|
+
currentProvider?: string,
|
|
208
|
+
): { provider: string, id: string } | null {
|
|
209
|
+
const trimmed = modelRef.trim()
|
|
210
|
+
if (!trimmed) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const slashIndex = trimmed.indexOf("/")
|
|
215
|
+
if (slashIndex > 0 && slashIndex < trimmed.length - 1) {
|
|
216
|
+
return {
|
|
217
|
+
provider: trimmed.slice(0, slashIndex),
|
|
218
|
+
id: trimmed.slice(slashIndex + 1),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!currentProvider) {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
provider: currentProvider,
|
|
228
|
+
id: trimmed,
|
|
229
|
+
}
|
|
230
|
+
}
|
package/package.json
CHANGED
|
@@ -4,8 +4,12 @@ Use these rules in all Phase 1 skills: `01-brainstorm` → `02-plan` → `03-wor
|
|
|
4
4
|
|
|
5
5
|
## Start of skill: model routing
|
|
6
6
|
|
|
7
|
-
Model routing is handled automatically by the ce-core extension
|
|
8
|
-
|
|
7
|
+
Model routing is handled automatically by the ce-core extension, with two triggers:
|
|
8
|
+
|
|
9
|
+
1. **Explicit command**: the user types `/skill:01-brainstorm` through `/skill:07-worktree` (the `input` hook).
|
|
10
|
+
2. **Model-initiated skill load**: the agent reads a stage's `SKILL.md` (e.g. `.../skills/03-work/SKILL.md`) without an explicit command (the `tool_call` hook).
|
|
11
|
+
|
|
12
|
+
On either trigger the extension:
|
|
9
13
|
|
|
10
14
|
1. Reads `.pi/settings.json` from the project root.
|
|
11
15
|
2. Parses `modelStrategy[stageKey]` or falls back to `modelStrategy.default`.
|