pi-pstack-adapter 0.4.0 → 0.6.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/CHANGELOG.md +14 -0
- package/README.md +15 -1
- package/VERSION +1 -1
- package/adapter-skills/setup.md +39 -5
- package/docs/compatibility.md +10 -4
- package/index.ts +38 -17
- package/package.json +1 -1
- package/src/certified.ts +3 -3
- package/src/config.ts +173 -8
- package/src/echo.ts +9 -0
- package/src/generate.ts +18 -5
- package/src/namespace.ts +3 -1
- package/src/registry.ts +1 -0
- package/src/status.ts +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. release-please writes new entries from Conventional Commit PR titles.
|
|
4
4
|
|
|
5
|
+
## [0.6.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.5.0...v0.6.0) (2026-10-17)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **pstack:** certify pstack 0.15.25 with sharpen ([#15](https://github.com/elderfo/pi-pstack-adapter/issues/15)) ([bb09bbb](https://github.com/elderfo/pi-pstack-adapter/commit/bb09bbb963c528666c79176072da002957a93827))
|
|
11
|
+
|
|
12
|
+
## [0.5.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.4.0...v0.5.0) (2026-10-15)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **models:** add a per-role thinking level ([#12](https://github.com/elderfo/pi-pstack-adapter/issues/12)) ([ae99d0c](https://github.com/elderfo/pi-pstack-adapter/commit/ae99d0cb9ab6c516a6847eb47ea44a0b5ec7c141))
|
|
18
|
+
|
|
5
19
|
## [0.4.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.3.0...v0.4.0) (2026-10-14)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -150,6 +150,20 @@ In a noninteractive session, set roles with `PISTACK_MODELS`:
|
|
|
150
150
|
PISTACK_MODELS='feature=provider/model,bug-fix=provider/other-model' pi
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
Add a thinking level to a role with a `:level` suffix. The levels are `off`, `minimal`, `low`,
|
|
154
|
+
`medium`, `high`, `xhigh`, and `max`:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
PISTACK_MODELS='feature=provider/model:medium,judgment=provider/other-model:high' pi
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Role names are lowercase letters, digits, and hyphens. A model value is printable ASCII with no spaces or markup characters, at most 128 characters. A config entry that breaks these rules stops the adapter at startup with an error that names the file, so fix the key there and restart Pi.
|
|
161
|
+
|
|
162
|
+
A role without a level passes none, so the delegation backend applies its own default. A role you leave unset inherits the
|
|
163
|
+
session model and the session thinking level, so a session at high thinking makes every
|
|
164
|
+
inheriting delegate expensive. The `pstack_adapter` tool's `set_model` action takes the same
|
|
165
|
+
level in its `thinking` parameter. A level the model does not offer is clamped by Pi at run time.
|
|
166
|
+
|
|
153
167
|
## Run Pi noninteractively
|
|
154
168
|
|
|
155
169
|
A noninteractive session, such as `pi --print`, cannot answer the download question. To allow
|
|
@@ -188,7 +202,7 @@ settings to `.pi/pi-pstack-adapter.json`. Environment variables override both fi
|
|
|
188
202
|
| `PISTACK_UPSTREAM_PLUGIN_PATH` | Selects the plugin directory inside the checkout. |
|
|
189
203
|
| `PISTACK_ALLOW_BOOTSTRAP` | Allows a noninteractive first download. |
|
|
190
204
|
| `PISTACK_CACHE_DIR` | Changes the cache directory. |
|
|
191
|
-
| `PISTACK_MODELS` | Assigns models as comma-separated `role=provider/model` pairs. |
|
|
205
|
+
| `PISTACK_MODELS` | Assigns models as comma-separated `role=provider/model[:thinking]` pairs. |
|
|
192
206
|
| `PISTACK_DELEGATION` | Sets the delegation provider to try first: `t3` (default) or `pi-subagents`. The config file key is `delegation`. |
|
|
193
207
|
|
|
194
208
|
## Security and trust
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.6.0
|
package/adapter-skills/setup.md
CHANGED
|
@@ -27,6 +27,13 @@ Roles used by pstack workflows.
|
|
|
27
27
|
The status report lists the same roles, so read it back rather than trusting this table if the
|
|
28
28
|
adapter has been upgraded.
|
|
29
29
|
|
|
30
|
+
Each role also has a thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or
|
|
31
|
+
`max`. Upstream pstack calls this a reasoning budget and encodes it in the model slug. A role with
|
|
32
|
+
no thinking level passes none, so the delegation backend applies its own default. A role left on
|
|
33
|
+
`inherit` uses the parent session's model and thinking level, so a parent at high thinking makes
|
|
34
|
+
every inheriting child expensive. Model choice often affects cost more than thinking level does,
|
|
35
|
+
so consider a cheaper model for the bulk roles as well.
|
|
36
|
+
|
|
30
37
|
Steps.
|
|
31
38
|
|
|
32
39
|
1. Call the adapter tool with `action: "models"` to list the provider and model ids this Pi
|
|
@@ -34,13 +41,40 @@ Steps.
|
|
|
34
41
|
2. Ask the user which model each role should use. Offer the listed ids, and offer
|
|
35
42
|
`inherit` for any role they do not care about. Use the structured question tool when it is
|
|
36
43
|
available; otherwise ask in plain text. Only ids from step 1 are accepted.
|
|
37
|
-
3.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
3. Ask for a thinking level. Do not assume the user wants none. Skip this step only when every
|
|
45
|
+
role is on `inherit`. It applies to every role that got a model in step 2. A role on
|
|
46
|
+
`inherit` cannot have a level, so tell the user that and say that those roles follow the
|
|
47
|
+
parent session's level. Ask one single-select question first. Put the whole choice in each option
|
|
48
|
+
title so the user can pick without opening anything else, for example "Balanced: code roles
|
|
49
|
+
medium, judgment high, strongest-judgment xhigh (suggested)".
|
|
50
|
+
|
|
51
|
+
| choice | code roles (`feature`, `refactoring`, `bug-fix`, `perf-issue`, `hillclimb`) | `judgment` | `strongest-judgment` |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| Balanced (suggest this first) | `medium` | `high` | `xhigh` |
|
|
54
|
+
| Low cost | `low` | `medium` | `high` |
|
|
55
|
+
| Maximum quality | `high` | `xhigh` | `max` |
|
|
56
|
+
| Backend default | none | none | none |
|
|
57
|
+
| Custom | ask once per column, as a single-select of level names | | |
|
|
58
|
+
|
|
59
|
+
These are starting points, not measurements. They keep the heaviest reasoning on the roles
|
|
60
|
+
that make the hardest calls. Offer Custom for a user who wants different levels. Ask once for
|
|
61
|
+
the code roles, once for `judgment`, and once for `strongest-judgment`. Ask a role on its own
|
|
62
|
+
only when the user says it should differ from its group. Backend default passes no level and
|
|
63
|
+
leaves the choice to the delegation backend. A Custom user who wants thinking disabled picks
|
|
64
|
+
`off`.
|
|
65
|
+
A model may not offer every level. A level it lacks is clamped when it runs, so say that
|
|
66
|
+
instead of asking again. The tool rejects any level other than `off` on a model that has no reasoning support.
|
|
67
|
+
When it does, set that role with no level and tell the user which role has none.
|
|
68
|
+
4. For each role, call the adapter tool with `action: "set_model"`, the role, the chosen
|
|
69
|
+
`provider/model` id, and `thinking` when step 3 picked a level. Pass `scope: "project"` when
|
|
70
|
+
the user wants the choice to apply only to this repository. Calling `set_model` for a role
|
|
71
|
+
again without `thinking` clears a level set earlier, so always pass it again.
|
|
72
|
+
5. Read back `action: "status"` and show the user what was written, including the thinking
|
|
73
|
+
column.
|
|
41
74
|
|
|
42
75
|
For a scripted environment, set `PISTACK_MODELS` instead, for example
|
|
43
|
-
`PISTACK_MODELS=feature=anthropic/claude-sonnet-4-5,bug-fix=openai/gpt-5`. The
|
|
76
|
+
`PISTACK_MODELS=feature=anthropic/claude-sonnet-4-5:medium,bug-fix=openai/gpt-5:high`. The
|
|
77
|
+
optional `:level` suffix sets the thinking level. The environment wins
|
|
44
78
|
over both config files, so CI never needs this dialog.
|
|
45
79
|
|
|
46
80
|
## Upstream source
|
package/docs/compatibility.md
CHANGED
|
@@ -4,7 +4,7 @@ The compatibility registry in `src/registry.ts` assigns one support tier to ever
|
|
|
4
4
|
certified pstack revision. Generation reads it, the wrapper frontmatter publishes it, and
|
|
5
5
|
`/pistack-status` reports it.
|
|
6
6
|
|
|
7
|
-
Certified revision: pstack `0.15.
|
|
7
|
+
Certified revision: pstack `0.15.25` at `101b274dd991f694e1f7024be2157e5f3adf07de` in
|
|
8
8
|
`https://github.com/cursor/plugins`.
|
|
9
9
|
|
|
10
10
|
## Platforms
|
|
@@ -21,8 +21,9 @@ executables in the shell the skills run in, with Pi's `shellCommandPrefix` appli
|
|
|
21
21
|
that matters most, because Git Bash adds `/usr/bin` and `/mingw64/bin` to the `PATH` Pi
|
|
22
22
|
started with. A shell that is missing, inactive, or cannot run a command blocks every workflow.
|
|
23
23
|
|
|
24
|
-
Windows evidence
|
|
25
|
-
|
|
24
|
+
Windows evidence was last gathered at pstack `0.15.23`, on Windows 11 with Git for Windows 2.53,
|
|
25
|
+
Node 22, Bun 1.3, and Pi 1.2.0. CI runs the unit suite on Windows at `0.15.25`; the rest of this
|
|
26
|
+
list has not been rerun there:
|
|
26
27
|
|
|
27
28
|
- The unit suite and the type check pass. `npm run test:isolated` passes from Git Bash: Pi
|
|
28
29
|
registers all 54 skills, and a later run reuses the cache without the network. CI runs the
|
|
@@ -41,6 +42,10 @@ Windows 2.53, Node 22, Bun 1.3, and Pi 1.2.0:
|
|
|
41
42
|
`check-plan.mjs` ran on Node. Upstream's helper suite passed 61 of 63 tests under Bun. The two
|
|
42
43
|
failures fake the Graphite `gt` CLI with a shell script, which Bun cannot spawn on Windows.
|
|
43
44
|
Both fail at `0.15.9` too, and the adapter does not gate on `gt`.
|
|
45
|
+
- At `0.15.25`, `orch.ts` resolves the stack frontier through `gh`, or `origin` with
|
|
46
|
+
`--forge origin`, instead of `gt`. `poteto-mode` already requires `gh`. `worktree-audit.sh`
|
|
47
|
+
reads PR state through `origin` when it is installed, otherwise `gh`, and reports no PR state
|
|
48
|
+
when neither works. Neither helper has run on Windows at `0.15.25`.
|
|
44
49
|
- At pstack `0.15.9`, `show-me-your-work/scripts/log.sh`, `watch-pr`, and `orch.ts` also ran
|
|
45
50
|
in Git Bash. None of them changed in a way that adds an executable or a path assumption.
|
|
46
51
|
- Git for Windows bundles `git`, `curl`, `nproc`, and the POSIX tools the skill bodies call,
|
|
@@ -143,7 +148,7 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
|
|
|
143
148
|
| `teach` | `/skill:pistack-teach` | `delegation` | - | It runs the how and why skills, both of which need subagents. |
|
|
144
149
|
| `why` | `/skill:pistack-why` | `delegation` | `git`, `gh` | Default posture spawns one investigator subagent per evidence category plus a synthesizer. |
|
|
145
150
|
|
|
146
|
-
### experimental (
|
|
151
|
+
### experimental (7)
|
|
147
152
|
|
|
148
153
|
| upstream skill | Pi skill | capabilities | executables | why |
|
|
149
154
|
| --- | --- | --- | --- | --- |
|
|
@@ -152,6 +157,7 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
|
|
|
152
157
|
| `poteto-mode` | `/skill:pistack-poteto-mode` | `delegation`, `structured-question` | `git`, `gh` | Sticky-mode hosting plus references to Cursor built-in and plugin skills only partially map to Pi. |
|
|
153
158
|
| `recall` | `/skill:pistack-recall` | `delegation` | `git`, `gh` | Core corpus is Cursor's on-disk transcript JSONL, so only the git/gh and shared-record half survives. |
|
|
154
159
|
| `reflect` | `/skill:pistack-reflect` | `delegation` | - | Transcript mining and create-skill routing are Cursor-specific, though the digest fallback partly works. |
|
|
160
|
+
| `sharpen` | `/skill:pistack-sharpen` | - | - | New in pstack 0.15.25 and not yet run end to end in Pi. Its body is prompt-only, with no tool, delegation, or executable, and it runs only when you type it. |
|
|
155
161
|
| `swarm` | `/skill:pistack-swarm` | `delegation` | - | Cloud background workers and cloud_base_branch have no Pi mapping; only local fan-out approximates it. |
|
|
156
162
|
|
|
157
163
|
### unsupported (1)
|
package/index.ts
CHANGED
|
@@ -13,13 +13,20 @@ import {
|
|
|
13
13
|
type AdapterConfig,
|
|
14
14
|
type ConfigFile,
|
|
15
15
|
type ConfigLayer,
|
|
16
|
+
THINKING_LEVELS,
|
|
16
17
|
environmentLayer,
|
|
18
|
+
invalidRoleMessage,
|
|
19
|
+
isValidModelSpec,
|
|
20
|
+
isValidRole,
|
|
21
|
+
joinModelSpec,
|
|
17
22
|
mergeConfig,
|
|
23
|
+
planModelAssignment,
|
|
18
24
|
readConfigFile,
|
|
19
25
|
trustWarning,
|
|
20
26
|
updateConfigFile,
|
|
21
27
|
} from "./src/config.ts";
|
|
22
28
|
import { generate } from "./src/generate.ts";
|
|
29
|
+
import { safeEcho } from "./src/echo.ts";
|
|
23
30
|
import { MODE_ENTRY_TYPE, modeFromEntries, modeInstruction } from "./src/mode.ts";
|
|
24
31
|
import { CONFIG_FILE_NAME, agentDir, cacheRoot } from "./src/paths.ts";
|
|
25
32
|
import { bashToolContext, probeExecutables } from "./src/executables.ts";
|
|
@@ -56,9 +63,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
56
63
|
startupError = undefined;
|
|
57
64
|
modeActive = modeFromEntries(ctx.sessionManager.getBranch() as never[]);
|
|
58
65
|
|
|
59
|
-
const config = loadConfig(ctx);
|
|
60
66
|
try {
|
|
61
|
-
state = await bringUpUpstream(
|
|
67
|
+
state = await bringUpUpstream(loadConfig(ctx), ctx);
|
|
62
68
|
} catch (error) {
|
|
63
69
|
startupError = error as Error;
|
|
64
70
|
if (ctx.hasUI) ctx.ui.notify(`pstack adapter: ${(error as Error).message}`, "error");
|
|
@@ -135,6 +141,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
141
|
skill: Type.Optional(Type.String({ description: "Skill name for `check`, with or without the namespace prefix" })),
|
|
136
142
|
role: Type.Optional(Type.String({ description: "pstack model role for `set_model`" })),
|
|
137
143
|
model: Type.Optional(Type.String({ description: "`provider/model` id, or `inherit` to clear the role" })),
|
|
144
|
+
thinking: Type.Optional(
|
|
145
|
+
Type.String({
|
|
146
|
+
description: `Thinking level for \`set_model\`: ${THINKING_LEVELS.join(", ")}. Omit to pass no level, which also clears a level set earlier.`,
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
138
149
|
repo: Type.Optional(Type.String({ description: "Upstream repository URL for `set_source`" })),
|
|
139
150
|
ref: Type.Optional(Type.String({ description: "Branch, tag, or commit for `set_source`" })),
|
|
140
151
|
localPath: Type.Optional(Type.String({ description: "Local pstack checkout for `set_source`" })),
|
|
@@ -195,6 +206,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
195
206
|
skill?: string;
|
|
196
207
|
role?: string;
|
|
197
208
|
model?: string;
|
|
209
|
+
thinking?: string;
|
|
198
210
|
repo?: string;
|
|
199
211
|
ref?: string;
|
|
200
212
|
localPath?: string;
|
|
@@ -234,7 +246,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
246
|
case "models":
|
|
235
247
|
return renderModels(ctx);
|
|
236
248
|
case "set_model":
|
|
237
|
-
return setModel(params.role, params.model, params.scope ?? "user", ctx);
|
|
249
|
+
return setModel(params.role, params.model, params.thinking, params.scope ?? "user", ctx);
|
|
238
250
|
case "set_source":
|
|
239
251
|
return setSource(params, params.scope ?? "user", ctx);
|
|
240
252
|
default:
|
|
@@ -254,7 +266,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
254
266
|
(candidate) => candidate.generatedName === requested || candidate.generatedName === `${prefix}${bare}`,
|
|
255
267
|
);
|
|
256
268
|
if (!skill) {
|
|
257
|
-
return `No pstack skill named ${
|
|
269
|
+
return `No pstack skill named ${safeEcho(requested)}. Run the adapter tool with action "status" for the full list.`;
|
|
258
270
|
}
|
|
259
271
|
const resolved = shellState(ctx, pi.getActiveTools());
|
|
260
272
|
// An inactive bash tool or an unresolved shell is already a blocker, so do not spawn anything.
|
|
@@ -274,27 +286,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
286
|
function renderModels(ctx: ExtensionContext): string {
|
|
275
287
|
const scoped = ctx.scopedModels ?? [];
|
|
276
288
|
const models = scoped.length > 0 ? scoped.map((entry) => entry.model) : ctx.modelRegistry.getAvailable();
|
|
277
|
-
const ids = models.map((model) => `${model.provider}/${model.id}`).sort();
|
|
289
|
+
const ids = models.map((model) => `${model.provider}/${model.id}`).filter(isValidModelSpec).sort();
|
|
278
290
|
if (ids.length === 0) return "No models are available in this Pi installation.";
|
|
279
291
|
return `Available model ids.\n\n${ids.map((id) => `- \`${id}\``).join("\n")}`;
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
function setModel(
|
|
294
|
+
function setModel(
|
|
295
|
+
role: string | undefined,
|
|
296
|
+
model: string | undefined,
|
|
297
|
+
thinking: string | undefined,
|
|
298
|
+
scope: "user" | "project",
|
|
299
|
+
ctx: ExtensionContext,
|
|
300
|
+
): string {
|
|
283
301
|
if (!role) return "set_model needs a `role`.";
|
|
284
302
|
if (!model) return "set_model needs a `model`, or `inherit` to clear the role.";
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
303
|
+
if (!isValidRole(role)) return invalidRoleMessage(role);
|
|
304
|
+
const available = ctx.modelRegistry
|
|
305
|
+
.getAvailable()
|
|
306
|
+
.map((candidate) => ({ id: `${candidate.provider}/${candidate.id}`, reasoning: candidate.reasoning }));
|
|
307
|
+
const plan = planModelAssignment(model, thinking, available);
|
|
308
|
+
if (plan.kind === "rejected") return plan.message;
|
|
289
309
|
const path = configPath(scope, ctx);
|
|
290
|
-
const
|
|
291
|
-
const models = { ...
|
|
292
|
-
if (
|
|
293
|
-
else models[role] =
|
|
310
|
+
const stored = readConfigFile(path)?.values.models;
|
|
311
|
+
const models = typeof stored === "object" && stored !== null && !Array.isArray(stored) ? { ...stored } : {};
|
|
312
|
+
if (plan.kind === "inherit") delete models[role];
|
|
313
|
+
else models[role] = joinModelSpec(plan.spec);
|
|
294
314
|
updateConfigFile(path, { models });
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
315
|
+
if (plan.kind === "inherit") return `Role \`${role}\` now inherits the parent session model and thinking. Written to ${path}.`;
|
|
316
|
+
return plan.spec.thinking === undefined
|
|
317
|
+
? `Role \`${role}\` now uses \`${plan.spec.model}\` at the model's default thinking. Written to ${path}.`
|
|
318
|
+
: `Role \`${role}\` now uses \`${plan.spec.model}\` with thinking \`${plan.spec.thinking}\`. Written to ${path}.`;
|
|
298
319
|
}
|
|
299
320
|
|
|
300
321
|
async function setSource(
|
package/package.json
CHANGED
package/src/certified.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
export const CERTIFIED = {
|
|
6
6
|
repo: "https://github.com/cursor/plugins",
|
|
7
7
|
ref: "main",
|
|
8
|
-
commit: "
|
|
8
|
+
commit: "101b274dd991f694e1f7024be2157e5f3adf07de",
|
|
9
9
|
pluginPath: "pstack",
|
|
10
|
-
pstackVersion: "0.15.
|
|
11
|
-
skillCount:
|
|
10
|
+
pstackVersion: "0.15.25",
|
|
11
|
+
skillCount: 54,
|
|
12
12
|
agentCount: 2,
|
|
13
13
|
} as const;
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { CERTIFIED } from "./certified.ts";
|
|
4
|
+
import { safeEcho } from "./echo.ts";
|
|
4
5
|
import { DEFAULT_NAMESPACE, validateNamespace } from "./namespace.ts";
|
|
5
6
|
import { expandHome } from "./paths.ts";
|
|
6
7
|
import { DEFAULT_DELEGATION, DELEGATION_PROVIDERS, isDelegationProvider } from "./delegation.ts";
|
|
@@ -66,8 +67,9 @@ export function readConfigFile(path: string): ConfigLayer | undefined {
|
|
|
66
67
|
let parsed: unknown;
|
|
67
68
|
try {
|
|
68
69
|
parsed = JSON.parse(raw);
|
|
69
|
-
} catch
|
|
70
|
-
|
|
70
|
+
} catch {
|
|
71
|
+
// The parser's message quotes raw file text, which can reach the agent. Say only where to look.
|
|
72
|
+
throw new Error(`${path} is not valid JSON. Fix the syntax in that file and restart Pi.`);
|
|
71
73
|
}
|
|
72
74
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
73
75
|
throw new Error(`${path} must contain a JSON object.`);
|
|
@@ -87,7 +89,139 @@ export function updateConfigFile(path: string, patch: ConfigFile): ConfigFile {
|
|
|
87
89
|
return next;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
/** `
|
|
92
|
+
/** Thinking levels Pi accepts as a `:level` suffix on a model id. */
|
|
93
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
94
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
95
|
+
|
|
96
|
+
export function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
97
|
+
return (THINKING_LEVELS as readonly string[]).includes(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ModelSpec {
|
|
101
|
+
readonly model: string;
|
|
102
|
+
readonly thinking?: ThinkingLevel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Splits `provider/model[:thinking]`. Only a known level is a suffix, so a model id that
|
|
107
|
+
* itself contains a colon, such as `ollama/qwen:7b`, stays whole. Pi parses ids the same way.
|
|
108
|
+
*/
|
|
109
|
+
export function splitModelSpec(spec: string): ModelSpec {
|
|
110
|
+
const separator = spec.lastIndexOf(":");
|
|
111
|
+
if (separator > 0) {
|
|
112
|
+
const suffix = spec.slice(separator + 1).toLowerCase();
|
|
113
|
+
if (isThinkingLevel(suffix)) return { model: spec.slice(0, separator), thinking: suffix };
|
|
114
|
+
}
|
|
115
|
+
return { model: spec };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function joinModelSpec(spec: ModelSpec): string {
|
|
119
|
+
return spec.thinking === undefined ? spec.model : `${spec.model}:${spec.thinking}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface AvailableModel {
|
|
123
|
+
readonly id: string;
|
|
124
|
+
readonly reasoning?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type ModelAssignment =
|
|
128
|
+
| { readonly kind: "inherit" }
|
|
129
|
+
| { readonly kind: "assign"; readonly spec: ModelSpec }
|
|
130
|
+
| { readonly kind: "rejected"; readonly message: string };
|
|
131
|
+
|
|
132
|
+
/** Validates a `set_model` request. `available` holds `provider/id` entries from the registry. */
|
|
133
|
+
export function planModelAssignment(
|
|
134
|
+
model: string,
|
|
135
|
+
thinking: string | undefined,
|
|
136
|
+
available: readonly AvailableModel[],
|
|
137
|
+
): ModelAssignment {
|
|
138
|
+
if (model.trim().toLowerCase() === "inherit") {
|
|
139
|
+
if (thinking?.trim()) {
|
|
140
|
+
return {
|
|
141
|
+
kind: "rejected",
|
|
142
|
+
message:
|
|
143
|
+
"A role that inherits the parent session has no thinking level of its own. Pass a `provider/model` with `thinking`, or drop `thinking`.",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { kind: "inherit" };
|
|
147
|
+
}
|
|
148
|
+
const whole = model.trim();
|
|
149
|
+
// A registry id that itself ends in a level word, such as `x/model:low`, wins over a split.
|
|
150
|
+
const exact = available.some((candidate) => candidate.id === whole);
|
|
151
|
+
const spec = exact ? { model: whole } : splitModelSpec(whole);
|
|
152
|
+
const requested = thinking?.trim().toLowerCase() || undefined;
|
|
153
|
+
if (requested !== undefined && !isThinkingLevel(requested)) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "rejected",
|
|
156
|
+
message: `Unknown thinking level \`${safeEcho(thinking).replaceAll('"', "")}\`. Use one of ${THINKING_LEVELS.map((level) => `\`${level}\``).join(", ")}.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (requested !== undefined && spec.thinking !== undefined && spec.thinking !== requested) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "rejected",
|
|
162
|
+
message: `\`model\` names thinking \`${spec.thinking}\` but \`thinking\` says \`${requested}\`. Pass one of them.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const level = requested !== undefined && isThinkingLevel(requested) ? requested : spec.thinking;
|
|
166
|
+
if (!isValidModelSpec(spec.model)) return { kind: "rejected", message: invalidModelMessage(spec.model) };
|
|
167
|
+
if (exact && requested === undefined && splitModelSpec(whole).thinking !== undefined) {
|
|
168
|
+
// Stored whole, `x/model:low` would read back as model `x/model` at thinking `low`.
|
|
169
|
+
return {
|
|
170
|
+
kind: "rejected",
|
|
171
|
+
message: `Model \`${whole}\` ends in a thinking-level word, so a plain assignment would be misread. Pass \`thinking\` explicitly, for example \`high\`.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const known = available.find((candidate) => candidate.id === spec.model);
|
|
175
|
+
if (!known) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "rejected",
|
|
178
|
+
message: `No model \`${spec.model}\` is available in this Pi installation. Call this tool with action "models" for real ids, or pass "inherit".`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (level !== undefined && level !== "off" && known.reasoning === false) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "rejected",
|
|
184
|
+
message: `Model \`${spec.model}\` does not support thinking. Drop \`thinking\`, or pick a reasoning model.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const assigned: ModelSpec = { model: spec.model, thinking: level };
|
|
188
|
+
if (!isValidModelSpec(joinModelSpec(assigned))) return { kind: "rejected", message: invalidModelMessage(joinModelSpec(assigned)) };
|
|
189
|
+
return { kind: "assign", spec: assigned };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Role names reach generated markdown and status tables, so they stay plain kebab-case. */
|
|
193
|
+
const ROLE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
194
|
+
/**
|
|
195
|
+
* A model spec is also rendered into markdown the agent reads, so it is printable ASCII with the
|
|
196
|
+
* markup characters removed. An allow-list of ASCII keeps invisible text (tag characters, zero-width
|
|
197
|
+
* and bidi controls) out; catalogue ids such as OpenRouter's `~anthropic/...` aliases are ASCII.
|
|
198
|
+
* A trailing colon is never a valid id or level.
|
|
199
|
+
*/
|
|
200
|
+
const PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
|
|
201
|
+
const MARKUP_CHARACTERS = /[`|<>[\]()\\*]/;
|
|
202
|
+
|
|
203
|
+
const MAX_MODEL_SPEC_LENGTH = 128;
|
|
204
|
+
|
|
205
|
+
export function isValidModelSpec(model: string): boolean {
|
|
206
|
+
const plain = (value: string) => PRINTABLE_ASCII.test(value) && !MARKUP_CHARACTERS.test(value) && !value.endsWith(":");
|
|
207
|
+
if (model.length > MAX_MODEL_SPEC_LENGTH || !plain(model)) return false;
|
|
208
|
+
// `x/model::high` splits to `x/model:`, which is no model id.
|
|
209
|
+
return plain(splitModelSpec(model).model);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isValidRole(role: string): boolean {
|
|
213
|
+
return ROLE_PATTERN.test(role);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function invalidRoleMessage(role: string): string {
|
|
217
|
+
return `Invalid role name ${safeEcho(role)}. Use lowercase letters, digits, and hyphens, starting with a letter, for example "bug-fix".`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function invalidModelMessage(model: unknown): string {
|
|
221
|
+
return `Invalid model value ${typeof model === "string" ? `(${model.length} characters)` : safeEcho(model)}. Use provider/model or provider/model:thinking with no spaces or markup characters, at most 128 characters.`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** `role=provider/model[:thinking],role2=provider/model2` */
|
|
91
225
|
export function parseModelRoles(value: string): Record<string, string> {
|
|
92
226
|
const roles: Record<string, string> = {};
|
|
93
227
|
for (const pair of value.split(",")) {
|
|
@@ -95,9 +229,15 @@ export function parseModelRoles(value: string): Record<string, string> {
|
|
|
95
229
|
if (trimmed === "") continue;
|
|
96
230
|
const separator = trimmed.indexOf("=");
|
|
97
231
|
if (separator <= 0) {
|
|
98
|
-
throw new Error(`Invalid model role assignment ${
|
|
232
|
+
throw new Error(`Invalid model role assignment ${safeEcho(trimmed)}. Use role=provider/model or role=provider/model:thinking.`);
|
|
99
233
|
}
|
|
100
|
-
|
|
234
|
+
const role = trimmed.slice(0, separator).trim();
|
|
235
|
+
const model = trimmed.slice(separator + 1).trim();
|
|
236
|
+
if (!isValidRole(role)) throw new Error(invalidRoleMessage(role));
|
|
237
|
+
// An empty value is an unset variable that expanded to nothing, not a model.
|
|
238
|
+
if (model === "") continue;
|
|
239
|
+
if (!isValidModelSpec(model)) throw new Error(invalidModelMessage(model));
|
|
240
|
+
roles[role] = model;
|
|
101
241
|
}
|
|
102
242
|
return roles;
|
|
103
243
|
}
|
|
@@ -111,7 +251,13 @@ export function environmentLayer(env: NodeJS.ProcessEnv): ConfigLayer {
|
|
|
111
251
|
if (env[ENV.pluginPath]) values.pluginPath = env[ENV.pluginPath];
|
|
112
252
|
if (env[ENV.cacheDir]) values.cacheDir = env[ENV.cacheDir];
|
|
113
253
|
if (env[ENV.allowBootstrap]) values.allowBootstrap = !NEGATIVE.has((env[ENV.allowBootstrap] as string).trim().toLowerCase());
|
|
114
|
-
if (env[ENV.models])
|
|
254
|
+
if (env[ENV.models]) {
|
|
255
|
+
try {
|
|
256
|
+
values.models = parseModelRoles(env[ENV.models] as string);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new Error(`${ENV.models}: ${(error as Error).message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
115
261
|
if (env[ENV.delegation]) values.delegation = (env[ENV.delegation] as string).trim().toLowerCase();
|
|
116
262
|
return { label: "environment", values };
|
|
117
263
|
}
|
|
@@ -134,7 +280,7 @@ export function mergeConfig(layers: readonly ConfigLayer[]): AdapterConfig {
|
|
|
134
280
|
merged.ref = undefined;
|
|
135
281
|
}
|
|
136
282
|
if (key === "models") {
|
|
137
|
-
merged.models = { ...merged.models, ...(value
|
|
283
|
+
merged.models = { ...merged.models, ...validatedModels(value, layer.label) };
|
|
138
284
|
} else {
|
|
139
285
|
(merged as Record<string, unknown>)[key] = value;
|
|
140
286
|
}
|
|
@@ -162,6 +308,25 @@ export function mergeConfig(layers: readonly ConfigLayer[]): AdapterConfig {
|
|
|
162
308
|
};
|
|
163
309
|
}
|
|
164
310
|
|
|
311
|
+
/** A bad `models` entry fails with the layer that holds it, so status can still be reached after a fix. */
|
|
312
|
+
function validatedModels(value: unknown, label: string): Record<string, string> {
|
|
313
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
314
|
+
throw new Error(`${label} sets \`models\` to something other than an object of role to provider/model.`);
|
|
315
|
+
}
|
|
316
|
+
const models: Record<string, string> = {};
|
|
317
|
+
for (const [role, model] of Object.entries(value)) {
|
|
318
|
+
if (!isValidRole(role)) {
|
|
319
|
+
throw new Error(`${label}: ${invalidRoleMessage(role)} Edit that key in \`models\` in the named file, then restart Pi.`);
|
|
320
|
+
}
|
|
321
|
+
if (model === "") continue;
|
|
322
|
+
if (typeof model !== "string" || !isValidModelSpec(model)) {
|
|
323
|
+
throw new Error(`${label}: role ${safeEcho(role)}. ${invalidModelMessage(model)}`);
|
|
324
|
+
}
|
|
325
|
+
Object.defineProperty(models, role, { value: model, enumerable: true, writable: true, configurable: true });
|
|
326
|
+
}
|
|
327
|
+
return models;
|
|
328
|
+
}
|
|
329
|
+
|
|
165
330
|
/**
|
|
166
331
|
* The certified commit is the default, not a floor. Naming a ref or a repository without a
|
|
167
332
|
* commit means the user asked for that ref, so the pin must get out of the way and trust
|
|
@@ -179,7 +344,7 @@ function validateDelegation(value: string | undefined, layers: readonly ConfigLa
|
|
|
179
344
|
if (isDelegationProvider(value)) return value;
|
|
180
345
|
const owner = [...layers].reverse().find((layer) => layer.values.delegation === value);
|
|
181
346
|
throw new Error(
|
|
182
|
-
`${owner?.label ?? "config"} sets \`delegation\` to ${
|
|
347
|
+
`${owner?.label ?? "config"} sets \`delegation\` to ${safeEcho(value)}. Use one of ${DELEGATION_PROVIDERS.map((id) => `"${id}"`).join(", ")}, or remove it to prefer "${DEFAULT_DELEGATION}".`,
|
|
183
348
|
);
|
|
184
349
|
}
|
|
185
350
|
|
package/src/echo.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rejected input is attacker-shaped text that can reach the agent through an error message, so echo
|
|
3
|
+
* only a short, plain, quoted form of it. Non-strings are reported by type alone.
|
|
4
|
+
*/
|
|
5
|
+
export function safeEcho(value: unknown): string {
|
|
6
|
+
if (typeof value !== "string") return `(a value of type ${value === null ? "null" : typeof value})`;
|
|
7
|
+
const cleaned = value.slice(0, 24).replace(/[^A-Za-z0-9._/:~@+-]/g, "?");
|
|
8
|
+
return `"${cleaned}${value.length > 24 ? "..." : ""}"`;
|
|
9
|
+
}
|
package/src/generate.ts
CHANGED
|
@@ -483,9 +483,18 @@ function t3Section(namespace: string): string {
|
|
|
483
483
|
path of \`agents/${namespace}-poteto-agent.md\` beside this file. The \`agent prompts\` row of
|
|
484
484
|
the status report gives that directory.
|
|
485
485
|
- Model: a role configured as \`provider/model\` becomes
|
|
486
|
-
\`target: { providerInstanceId: "pi", model: "provider/model" }\`.
|
|
487
|
-
\`
|
|
488
|
-
\`${T3_CAPABILITIES_TOOL}\`
|
|
486
|
+
\`target: { providerInstanceId: "pi", model: "provider/model" }\`. Read the thinking column of
|
|
487
|
+
the \`model roles\` table in the \`pstack_adapter\` \`status\` report. When it names a level, add it to
|
|
488
|
+
\`target.options\` under the option id that \`${T3_CAPABILITIES_TOOL}\` lists for that model's
|
|
489
|
+
thinking setting. For Pi models the id is \`thinking\`, as in \`options: { thinking: "<level>" }\`.
|
|
490
|
+
A role whose thinking reads "backend default" omits \`options\`, so the backend applies its own
|
|
491
|
+
default. An unconfigured role omits \`target\`, so the child inherits the parent model and
|
|
492
|
+
thinking.
|
|
493
|
+
- If the model does not list the level, use the highest level it lists that is below the requested
|
|
494
|
+
one, and tell the user. If it lists none below the requested level, or lists no thinking option,
|
|
495
|
+
omit \`options\` and tell the user. If the level is \`off\` and the model does not list \`off\`, omit
|
|
496
|
+
\`options\` and tell the user, because the closest listed level would turn thinking on.
|
|
497
|
+
For a different model family, call \`${T3_CAPABILITIES_TOOL}\` and use a provider and model it lists.
|
|
489
498
|
- Background: \`mode: "async"\`. A finished child wakes this thread, so end the turn instead of
|
|
490
499
|
polling. Call \`${T3_STATUS_TOOL}\` only when you need a result mid-turn.
|
|
491
500
|
- Call \`${T3_CAPABILITIES_TOOL}\` or \`${T3_STATUS_TOOL}\` only when the \`check\` report allows it.
|
|
@@ -508,7 +517,10 @@ function subagentSection(namespace: string): string {
|
|
|
508
517
|
|
|
509
518
|
- Agent names are namespaced, for example \`${namespace}-poteto-agent\`. Call
|
|
510
519
|
\`${SUBAGENT_TOOL}({ action: "list" })\` to see them.
|
|
511
|
-
- Model: pass a role's \`provider/model\` as \`model\`.
|
|
520
|
+
- Model: pass a role's \`provider/model\` as \`model\`. When the thinking column of the \`model roles\`
|
|
521
|
+
table in the \`pstack_adapter\` \`status\` report names a level for the role, append it as
|
|
522
|
+
\`provider/model:<level>\`. A role whose thinking reads "backend default" gets no suffix, so
|
|
523
|
+
pi-subagents applies its own default. An unconfigured role omits \`model\`.
|
|
512
524
|
- Background: \`async: true\`.`;
|
|
513
525
|
}
|
|
514
526
|
|
|
@@ -524,7 +536,8 @@ that names a Cursor tool, path, or model.
|
|
|
524
536
|
| \`run_in_background: true\` | \`mode: "async"\` on \`delegate_task\`, or \`async: true\` on \`subagent\`. |
|
|
525
537
|
| \`AskUserQuestion\`, \`AskQuestion\` | the \`ask_user\` tool. |
|
|
526
538
|
| \`TodoWrite\`, \`TaskCreate\`, \`TaskUpdate\` | no Pi equivalent. Keep an uncommitted \`todo.md\` checklist instead. |
|
|
527
|
-
| \`claude-opus-5-5-xhigh\`, \`grok-4.7-xhigh-fast\`, other Cursor model slugs | the role models configured by \`/skill:${namespace}-setup\`. Call the \`pstack_adapter\` tool with \`action: "status"\` to read the current role table. An unconfigured role inherits the parent model, so omit \`target\` or \`model\`. |
|
|
539
|
+
| \`claude-opus-5-5-xhigh\`, \`grok-4.7-xhigh-fast\`, other Cursor model slugs | the role models configured by \`/skill:${namespace}-setup\`. Call the \`pstack_adapter\` tool with \`action: "status"\` to read the current role table. An effort word in a slug, such as \`-xhigh\` or \`-max\`, is not a model id. The role table's thinking column carries that level, and \`-fast\` has no equivalent. An unconfigured role inherits the parent model and thinking, so omit \`target\` or \`model\`. |
|
|
540
|
+
| "pick each subagent's reasoning level and fast or standard speed", a role's starting level from the \`/setup-pstack\` budget | the thinking column of the \`model roles\` table is a role's starting level. When it names a level, you may move it along \`off\`, \`minimal\`, \`low\`, \`medium\`, \`high\`, \`xhigh\`, \`max\` as the body asks, then pass it by the backend rules in [Delegation](#delegation). A role whose thinking reads "backend default", and an unconfigured role, keep their setting, because they have no level to move. Fast or standard speed has no Pi equivalent, so leave it out. If the backend rejects the moved level, use the role's configured level and say so. |
|
|
528
541
|
| a role line valued \`auto\` or \`inherit-parent\` | an unconfigured role. Omit \`target\` or \`model\` so the child runs on the parent model. |
|
|
529
542
|
| \`~/.cursor/rules/pstack-models.mdc\` | adapter configuration written by \`/skill:${namespace}-setup\`. |
|
|
530
543
|
| role lines \`feature, refactoring\`, \`bug-fix\`, \`perf-issue\`, \`hillclimb\`, \`judgment and prose\`, \`hardest tasks\` | adapter roles \`feature\` or \`refactoring\`, \`bug-fix\`, \`perf-issue\`, \`hillclimb\`, \`judgment\`, and \`strongest-judgment\`. |
|
package/src/namespace.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { safeEcho } from "./echo.ts";
|
|
2
|
+
|
|
1
3
|
export const DEFAULT_NAMESPACE = "pistack";
|
|
2
4
|
|
|
3
5
|
const NAMESPACE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
@@ -5,7 +7,7 @@ const NAMESPACE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
5
7
|
export function validateNamespace(value: string): string {
|
|
6
8
|
if (!NAMESPACE_PATTERN.test(value)) {
|
|
7
9
|
throw new Error(
|
|
8
|
-
`Invalid pstack adapter namespace ${
|
|
10
|
+
`Invalid pstack adapter namespace ${safeEcho(value)}. Use lowercase letters, digits, and single hyphens.`,
|
|
9
11
|
);
|
|
10
12
|
}
|
|
11
13
|
return value;
|
package/src/registry.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const SKILL_REGISTRY: Readonly<Record<string, RegistryEntry>> = {
|
|
|
51
51
|
"recall": { tier: "experimental", capabilities: ["delegation"], executables: ["git", "gh"], note: "Core corpus is Cursor's on-disk transcript JSONL, so only the git/gh and shared-record half survives." },
|
|
52
52
|
"reflect": { tier: "experimental", capabilities: ["delegation"], executables: [], note: "Transcript mining and create-skill routing are Cursor-specific, though the digest fallback partly works." },
|
|
53
53
|
"setup-pstack": { tier: "native", capabilities: [], executables: [], rename: "setup", replacement: "setup.md", note: "The upstream body only writes a Cursor .mdc rule, so the adapter replaces it with a Pi setup workflow." },
|
|
54
|
+
"sharpen": { tier: "experimental", capabilities: [], executables: [], note: "New in pstack 0.15.25 and not yet run end to end in Pi. Its body is prompt-only, with no tool, delegation, or executable, and it runs only when you type it." },
|
|
54
55
|
"show-me-your-work": { tier: "dependency-gated", capabilities: ["delegation"], executables: [], note: "The mandatory cross-model review of the trail requires spawning a different-family subagent." },
|
|
55
56
|
"swarm": { tier: "experimental", capabilities: ["delegation"], executables: [], note: "Cloud background workers and cloud_base_branch have no Pi mapping; only local fan-out approximates it." },
|
|
56
57
|
"tdd": { tier: "native", capabilities: [], executables: [] },
|
package/src/status.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CERTIFIED } from "./certified.ts";
|
|
2
2
|
import { CAPABILITY_LABELS, MODEL_ROLES, TIER_ORDER } from "./registry.ts";
|
|
3
3
|
import type { AdapterConfig } from "./config.ts";
|
|
4
|
-
import { trustWarning } from "./config.ts";
|
|
4
|
+
import { splitModelSpec, trustWarning } from "./config.ts";
|
|
5
5
|
import { DELEGATION_BACKENDS, delegationOrder } from "./delegation.ts";
|
|
6
6
|
import type { PrerequisiteReport } from "./capabilities.ts";
|
|
7
7
|
import type {
|
|
@@ -88,14 +88,17 @@ export function renderStatus(input: StatusInput): string {
|
|
|
88
88
|
|
|
89
89
|
lines.push(`## Model roles`);
|
|
90
90
|
lines.push("");
|
|
91
|
-
lines.push(`| role | model |`);
|
|
92
|
-
lines.push(`| --- | --- |`);
|
|
93
|
-
for (const role of MODEL_ROLES) {
|
|
94
|
-
lines.push(`| ${role} | ${config.models[role] ?? "inherit (parent session model)"} |`);
|
|
95
|
-
}
|
|
91
|
+
lines.push(`| role | model | thinking |`);
|
|
92
|
+
lines.push(`| --- | --- | --- |`);
|
|
96
93
|
const extra = Object.keys(config.models).filter((role) => !MODEL_ROLES.includes(role)).sort();
|
|
97
|
-
for (const role of extra) {
|
|
98
|
-
|
|
94
|
+
for (const role of [...MODEL_ROLES, ...extra]) {
|
|
95
|
+
const configured = config.models[role];
|
|
96
|
+
if (configured === undefined) {
|
|
97
|
+
lines.push(`| ${role} | inherit (parent session model) | inherit (parent session thinking) |`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const spec = splitModelSpec(configured);
|
|
101
|
+
lines.push(`| ${role} | ${spec.model} | ${spec.thinking ?? "backend default"} |`);
|
|
99
102
|
}
|
|
100
103
|
lines.push("");
|
|
101
104
|
|