pi-pstack-adapter 0.3.0 → 0.5.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 CHANGED
@@ -2,6 +2,21 @@
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.5.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.4.0...v0.5.0) (2026-10-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * **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))
11
+
12
+ ## [0.4.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.3.0...v0.4.0) (2026-10-14)
13
+
14
+
15
+ ### Features
16
+
17
+ * **delegation:** delegate through T3 Code's delegate_task, with pi-subagents as fallback ([#11](https://github.com/elderfo/pi-pstack-adapter/issues/11)) ([4f9c8e8](https://github.com/elderfo/pi-pstack-adapter/commit/4f9c8e8bef690885c37cb0d215473db11a5e9049))
18
+ * **pstack:** certify pstack 0.15.23 with in-your-own-words, ketchup, and poteto-help ([#9](https://github.com/elderfo/pi-pstack-adapter/issues/9)) ([73bb412](https://github.com/elderfo/pi-pstack-adapter/commit/73bb4125fcd470e5f8990e1953e7de80a05a0485))
19
+
5
20
  ## [0.3.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.2.0...v0.3.0) (2026-10-05)
6
21
 
7
22
 
package/README.md CHANGED
@@ -46,17 +46,34 @@ counts as Linux.
46
46
 
47
47
  ### Add optional providers for delegating workflows
48
48
 
49
- Some workflows hand work to subagents or ask you multiple-choice questions. Install these two
50
- packages to enable them:
49
+ Some workflows hand work to subagents or ask you multiple-choice questions. Two providers can run
50
+ the subagents:
51
+
52
+ - **T3 Code.** When Pi runs inside [T3 Code](https://github.com/pingdotgg/t3code), workflows
53
+ delegate through T3's `delegate_task` tool. Each child appears in T3, can run on any provider
54
+ and model T3 offers, and can work in its own worktree. You install nothing extra.
55
+ - **pi-subagents.** Outside T3 Code, install `pi-subagents`. The upstream agents register as
56
+ `pistack-comment-sicko` and `pistack-poteto-agent`.
57
+
58
+ For multiple-choice questions, install `pi-ask-user`:
51
59
 
52
60
  ```bash
53
61
  pi install npm:pi-subagents
54
62
  pi install npm:pi-ask-user
55
63
  ```
56
64
 
57
- With `pi-subagents` installed, the upstream agents register as `pistack-comment-sicko` and
58
- `pistack-poteto-agent`. The adapter turns on the `subagent` tool when a delegating workflow
59
- starts, whether you run its `/skill:` command or the agent opens the skill itself. While Poteto
65
+ The adapter uses T3 Code first and falls back to `pi-subagents`. To reverse that order, set
66
+ `"delegation": "pi-subagents"` in the adapter config, or set `PISTACK_DELEGATION=pi-subagents`.
67
+ Restart Pi or run `/reload` after changing it. `/pistack-status` shows the order and the
68
+ provider the session resolved.
69
+
70
+ The adapter counts T3's tools, including `task_status` and `orchestrator_capabilities`, only
71
+ when T3 Code's own bridge registered them. A tool that another extension or MCP server registers
72
+ under the same name is ignored, and `/pistack-check <skill>` names the one tool to delegate with.
73
+
74
+ When a delegating workflow starts, the adapter turns on that provider's tool, even if you turned
75
+ it off in the tool selector. This happens
76
+ whether you run the workflow's `/skill:` command or the agent opens the skill itself. While Poteto
60
77
  Mode is on, the adapter turns it on for every prompt.
61
78
 
62
79
  To find missing providers or executables before you start a workflow, run
@@ -133,6 +150,20 @@ In a noninteractive session, set roles with `PISTACK_MODELS`:
133
150
  PISTACK_MODELS='feature=provider/model,bug-fix=provider/other-model' pi
134
151
  ```
135
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
+
136
167
  ## Run Pi noninteractively
137
168
 
138
169
  A noninteractive session, such as `pi --print`, cannot answer the download question. To allow
@@ -171,7 +202,8 @@ settings to `.pi/pi-pstack-adapter.json`. Environment variables override both fi
171
202
  | `PISTACK_UPSTREAM_PLUGIN_PATH` | Selects the plugin directory inside the checkout. |
172
203
  | `PISTACK_ALLOW_BOOTSTRAP` | Allows a noninteractive first download. |
173
204
  | `PISTACK_CACHE_DIR` | Changes the cache directory. |
174
- | `PISTACK_MODELS` | Assigns models as comma-separated `role=provider/model` pairs. |
205
+ | `PISTACK_MODELS` | Assigns models as comma-separated `role=provider/model[:thinking]` pairs. |
206
+ | `PISTACK_DELEGATION` | Sets the delegation provider to try first: `t3` (default) or `pi-subagents`. The config file key is `delegation`. |
175
207
 
176
208
  ## Security and trust
177
209
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.5.0
@@ -4,7 +4,7 @@ Upstream pstack ships a `setup-pstack` skill that writes `~/.cursor/rules/pstack
4
4
  a Cursor-only always-applied rule full of Cursor model slugs. Pi has neither that file nor
5
5
  those models, so this adapter replaces the body with the Pi equivalent.
6
6
 
7
- Two things are configurable. Model roles, and the upstream source.
7
+ Three things are configurable. Model roles, the upstream source, and the delegation order.
8
8
 
9
9
  ## Model roles
10
10
 
@@ -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. For each answered role, call the adapter tool with `action: "set_model"`, the role, and the
38
- chosen `provider/model` id. Pass `scope: "project"` when the user wants the choice to apply
39
- only to this repository.
40
- 4. Read back `action: "status"` and show the user what was written.
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 environment wins
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
@@ -61,6 +95,14 @@ plain words before you call it, and report a refusal as a refusal.
61
95
 
62
96
  Regeneration happens on the next Pi start. Tell the user to restart Pi, or run `/reload`.
63
97
 
98
+ ## Delegation order
99
+
100
+ Delegating workflows try T3 Code's `delegate_task` first, then pi-subagents' `subagent`. A user
101
+ who wants `subagent` first sets `"delegation": "pi-subagents"` in the adapter config file, or
102
+ `PISTACK_DELEGATION=pi-subagents`. The adapter tool has no action for this. Tell the user which
103
+ file to edit: the `config layers` row of the status report lists them. Regeneration happens on
104
+ the next Pi start.
105
+
64
106
  ## Finish
65
107
 
66
108
  End by reporting, in your own words, which roles were set, which still inherit, the resolved
@@ -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.9` at `e43c7ee26e0038c6c1fa8380dd34ce86ff94cb2a` in
7
+ Certified revision: pstack `0.15.23` at `dbf49813a39c1a5389c5e28cb8e4c1c2379e2454` in
8
8
  `https://github.com/cursor/plugins`.
9
9
 
10
10
  ## Platforms
@@ -21,21 +21,37 @@ 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 at the certified revision, on Windows 11 with Git for Windows 2.53, Node 22,
25
- and Pi 1.0.2:
26
-
27
- - The unit suite passes, and `npm run test:isolated` passes against the adapter commit that
28
- introduced Windows support. Pi registers all 51 skills, and a later run reuses the cache
29
- without the network. CI runs the unit suite on Windows for every change.
30
- - Every upstream helper script ran in Git Bash: `show-me-your-work/scripts/log.sh`,
31
- `poteto-mode/scripts/check-plan.mjs`, `worktree-audit.sh`, `watch-pr`, and `orch.ts`.
24
+ Windows evidence at the certified revision, pstack `0.15.23`, on Windows 11 with Git for
25
+ Windows 2.53, Node 22, Bun 1.3, and Pi 1.2.0:
26
+
27
+ - The unit suite and the type check pass. `npm run test:isolated` passes from Git Bash: Pi
28
+ registers all 54 skills, and a later run reuses the cache without the network. CI runs the
29
+ unit suite on Windows for every change.
30
+ - The certification probe passes on an LF export of the tree: no diagnostics, 52 bodies
31
+ byte-identical, and none mismatched. The adapter's cache checkout matches that export byte for
32
+ byte.
33
+ - `in-your-own-words`, `ketchup`, and `poteto-help` ran end to end in the isolated install, with
34
+ no `subagent` or `ask_user` tool. `ketchup` reported a commit and a pending decision with
35
+ Mermaid diagrams and changed nothing. `poteto-help` answered with `/pistack-mode on` and read
36
+ the guide from the `plugin directory` row, whose Windows path it resolved.
37
+ - The status report's `plugin directory` and `generated skills` rows give native Windows paths.
38
+ `/pistack-check` clears `ketchup`, and still reports `uptime` missing for `benchmark-checklist`.
39
+ - The helpers changed in `0.15.23` ran in Git Bash. `worktree-audit.sh` handled `-b <ref>`,
40
+ `--help`, an unknown base, and the `origin/HEAD` fallback, and classified a linked worktree.
41
+ `check-plan.mjs` ran on Node. Upstream's helper suite passed 61 of 63 tests under Bun. The two
42
+ failures fake the Graphite `gt` CLI with a shell script, which Bun cannot spawn on Windows.
43
+ Both fail at `0.15.9` too, and the adapter does not gate on `gt`.
44
+ - At pstack `0.15.9`, `show-me-your-work/scripts/log.sh`, `watch-pr`, and `orch.ts` also ran
45
+ in Git Bash. None of them changed in a way that adds an executable or a path assumption.
32
46
  - Git for Windows bundles `git`, `curl`, `nproc`, and the POSIX tools the skill bodies call,
33
47
  but not `uptime`, so `benchmark-checklist` reports it missing. `gh`, `tailscale`, and `sudo`
34
48
  are separate installs on Windows, as on the other platforms.
35
49
  - The adapter checks out upstream with `core.autocrlf=false`. Git for Windows enables
36
50
  `autocrlf` system-wide, which would otherwise rewrite every upstream file to CRLF. A cache
37
51
  written before that setting existed is repaired on the next start, or refused with a recovery
38
- step when it cannot be written.
52
+ step when it cannot be written. `git archive` follows the same setting, so a certification
53
+ export on Windows needs `-c core.autocrlf=false`. Without it, 155 upstream text files at
54
+ `0.15.23` come out as CRLF and no longer match what Pi loads.
39
55
 
40
56
  A session that replaces the `bash` tool with `powershell` cannot run the skills.
41
57
  `/pistack-status` reports it, and `/pistack-check` refuses to clear any workflow.
@@ -66,11 +82,12 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
66
82
 
67
83
  ## Registry
68
84
 
69
- ### native (30)
85
+ ### native (31)
70
86
 
71
87
  | upstream skill | Pi skill | capabilities | executables | why |
72
88
  | --- | --- | --- | --- | --- |
73
89
  | `bro` | `/skill:pistack-bro` | - | - | - |
90
+ | `in-your-own-words` | `/skill:pistack-in-your-own-words` | - | - | - |
74
91
  | `principle-attack-the-premise` | `/skill:pistack-principle-attack-the-premise` | - | - | - |
75
92
  | `principle-boundary-discipline` | `/skill:pistack-principle-boundary-discipline` | - | - | - |
76
93
  | `principle-build-the-lever` | `/skill:pistack-principle-build-the-lever` | - | - | - |
@@ -101,11 +118,12 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
101
118
  | `typescript-best-practices` | `/skill:pistack-typescript-best-practices` | - | - | - |
102
119
  | `unslop` | `/skill:pistack-unslop` | - | - | - |
103
120
 
104
- ### adapted (1)
121
+ ### adapted (2)
105
122
 
106
123
  | upstream skill | Pi skill | capabilities | executables | why |
107
124
  | --- | --- | --- | --- | --- |
108
125
  | `create-verification-skill` | `/skill:pistack-create-verification-skill` | - | - | Only the generated skill's output path needs remapping to Pi's skill directory. |
126
+ | `ketchup` | `/skill:pistack-ketchup` | - | - | Read-only. Pi has no `/visualize` skill or image generation, so its visuals are Mermaid diagrams and screenshots that already exist. It checks PR and CI state only for work the chat touched. |
109
127
 
110
128
  ### dependency-gated (13)
111
129
 
@@ -120,16 +138,17 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
120
138
  | `how` | `/skill:pistack-how` | `delegation` | - | Every path spawns explorer or explainer subagents; nothing runs inline. |
121
139
  | `interrogate` | `/skill:pistack-interrogate` | `delegation` | `git` | Multi-model adversarial review is entirely one subagent per configured model. |
122
140
  | `maintain-verification-skill` | `/skill:pistack-maintain-verification-skill` | `delegation` | - | Source wave launches one read-only subagent per feature file. |
123
- | `no-comments` | `/skill:pistack-no-comments` | `delegation` | `git` | Step 1 must spawn the named Comment Sicko agent; the skill only triages its report. |
141
+ | `no-comments` | `/skill:pistack-no-comments` | `delegation` | `git` | Step 1 must spawn the comment-sicko agent; the skill only triages its report. |
124
142
  | `show-me-your-work` | `/skill:pistack-show-me-your-work` | `delegation` | - | The mandatory cross-model review of the trail requires spawning a different-family subagent. |
125
143
  | `teach` | `/skill:pistack-teach` | `delegation` | - | It runs the how and why skills, both of which need subagents. |
126
144
  | `why` | `/skill:pistack-why` | `delegation` | `git`, `gh` | Default posture spawns one investigator subagent per evidence category plus a synthesizer. |
127
145
 
128
- ### experimental (5)
146
+ ### experimental (6)
129
147
 
130
148
  | upstream skill | Pi skill | capabilities | executables | why |
131
149
  | --- | --- | --- | --- | --- |
132
150
  | `automate-me` | `/skill:pistack-automate-me` | `delegation`, `structured-question` | `git`, `gh` | Mining step depends on Cursor transcript files and the built-in create-skill skill, with no Pi equivalent. |
151
+ | `poteto-help` | `/skill:pistack-poteto-help` | - | - | Its answers describe Cursor setup, the slash menu, and Custom Modes. A request for work runs poteto-mode, which is experimental. It asks a multiple-choice question only when the need is unclear. Its README and guide links resolve under the status report's plugin directory. |
133
152
  | `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. |
134
153
  | `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. |
135
154
  | `reflect` | `/skill:pistack-reflect` | `delegation` | - | Transcript mining and create-skill routing are Cursor-specific, though the digest fallback partly works. |
@@ -145,12 +164,40 @@ Lowering a tier needs one reproduction of the failure and a note saying what bro
145
164
 
146
165
  | upstream agent | Pi agent | notes |
147
166
  | --- | --- | --- |
148
- | `comment-sicko` | `pistack-comment-sicko` | Display name `Comment Sicko` is preserved in the description. Needs `git` to scope a diff. |
149
- | `poteto-agent` | `pistack-poteto-agent` | Upstream `is_background: true` has no runtime equivalent; the caller chooses `async` on the `subagent` call. |
167
+ | `comment-sicko` | `pistack-comment-sicko` | Upstream names it `comment-sicko` since pstack 0.15.23, so the registered description starts with that name. Needs `git` to scope a diff. |
168
+ | `poteto-agent` | `pistack-poteto-agent` | Upstream `is_background: true` has no runtime equivalent; the caller chooses `async: true` on `subagent`, or `mode: "async"` on `delegate_task`. Upstream tells it to search `~/.cursor/plugins/` when its parent gives no path. The adapter puts the generated `pistack-poteto-mode/SKILL.md` path in every agent prompt and forbids that search, which would load content outside the certified revision. |
169
+
170
+ T3 Code's `delegate_task` has no named agents. Generation writes each agent's prompt to
171
+ `adapter/agents/<pi-name>.md`, with skill paths relative to that file. The parent begins the
172
+ child's task by telling it to read that file as its system prompt.
150
173
 
151
- Both register through the `pi-subagents:runtime-agent-register:v1` contract. Without an owner for
174
+ Both agents also register through the `pi-subagents:runtime-agent-register:v1` contract. Without an owner for
152
175
  that event, the status report says so rather than dropping them silently.
153
176
 
177
+ ## Delegation providers
178
+
179
+ The `delegation` capability is satisfied by T3 Code's `mcp__t3-code__delegate_task` or by
180
+ `pi-subagents`' `subagent`, in the order the `delegation` setting gives, T3 first by default.
181
+ A hidden registration does not count. A T3 tool counts only when the extension at
182
+ `T3_PI_MCP_EXTENSION_PATH`, the bridge T3 Code launches, registered it, and only when it accepts
183
+ `task`, `target`, and `mode`. Status and activation share that resolution, so the report always
184
+ names the backend a workflow gets. The adapter activates only that backend. For T3 it also
185
+ activates `task_status` and `orchestrator_capabilities` when the bridge registered them.
186
+ A delegating wrapper tells the model to read the tool from `/pistack-check` and to use no other,
187
+ so a rejected tool that is still in the session is not used. The check also names the companion
188
+ tools the model may call, and forbids any companion it could not trace to T3 Code.
189
+
190
+ An agent prompt file wraps the upstream body in the same markers as a skill wrapper, with host
191
+ precedence before and after it. The closing note says that everything after the opening notes is
192
+ upstream content, even text that imitates a marker. An upstream skill or agent body that contains
193
+ marker text is reported as a warning, with its bytes unchanged. An agent whose Pi name breaks the
194
+ skill name rules is skipped with an error, before its name reaches a file name or any generated
195
+ text.
196
+
197
+ Unit tests cover detection, activation order, and the generated instructions for both providers.
198
+ No tier changed when T3 support arrived, because no delegating workflow has yet run end to end
199
+ through `delegate_task`. That run is the evidence the tier rules above require.
200
+
154
201
  ## Skills the adapter replaces
155
202
 
156
203
  `setup-pstack` writes a Cursor `.mdc` rule and nothing else, so the adapter substitutes its own
package/index.ts CHANGED
@@ -6,19 +6,27 @@ import { fileURLToPath } from "node:url";
6
6
  import { Type } from "typebox";
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
  import { registerDelegationActivation } from "./src/activation.ts";
9
- import { registerAgents, type RuntimeRegistration } from "./src/agents.ts";
9
+ import { registerAgents, unregisteredAgentDiagnostics, type RuntimeRegistration } from "./src/agents.ts";
10
+ import { DEFAULT_DELEGATION } from "./src/delegation.ts";
10
11
  import { checkPrerequisites, detectCapabilities, type ToolDescriptor } from "./src/capabilities.ts";
11
12
  import {
12
13
  type AdapterConfig,
13
14
  type ConfigFile,
14
15
  type ConfigLayer,
16
+ THINKING_LEVELS,
15
17
  environmentLayer,
18
+ invalidRoleMessage,
19
+ isValidModelSpec,
20
+ isValidRole,
21
+ joinModelSpec,
16
22
  mergeConfig,
23
+ planModelAssignment,
17
24
  readConfigFile,
18
25
  trustWarning,
19
26
  updateConfigFile,
20
27
  } from "./src/config.ts";
21
28
  import { generate } from "./src/generate.ts";
29
+ import { safeEcho } from "./src/echo.ts";
22
30
  import { MODE_ENTRY_TYPE, modeFromEntries, modeInstruction } from "./src/mode.ts";
23
31
  import { CONFIG_FILE_NAME, agentDir, cacheRoot } from "./src/paths.ts";
24
32
  import { bashToolContext, probeExecutables } from "./src/executables.ts";
@@ -26,7 +34,7 @@ import { entryFor } from "./src/registry.ts";
26
34
  import { platformSupport, renderCheck, renderStatus, shellDiagnostics, type ShellState } from "./src/status.ts";
27
35
  import { BootstrapRequiredError, resolveRefToCommit, resolveSource } from "./src/upstream.ts";
28
36
  import { normalizeRepo } from "./src/config.ts";
29
- import type { CapabilityStatus, Diagnostic, GenerationResult, ResolvedSource } from "./src/types.ts";
37
+ import type { CapabilityStatus, Diagnostic, GeneratedAgent, GenerationResult, ResolvedSource } from "./src/types.ts";
30
38
 
31
39
  const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
32
40
  const ADAPTER_VERSION = readAdapterVersion();
@@ -45,6 +53,7 @@ export default function (pi: ExtensionAPI) {
45
53
  let startupError: Error | undefined;
46
54
  let registrations: readonly RuntimeRegistration[] = [];
47
55
  let agentDiagnostics: readonly Diagnostic[] = [];
56
+ let unregisteredAgents: readonly GeneratedAgent[] = [];
48
57
  let modeActive = false;
49
58
  const commandNamespace = startupNamespace();
50
59
 
@@ -54,9 +63,8 @@ export default function (pi: ExtensionAPI) {
54
63
  startupError = undefined;
55
64
  modeActive = modeFromEntries(ctx.sessionManager.getBranch() as never[]);
56
65
 
57
- const config = loadConfig(ctx);
58
66
  try {
59
- state = await bringUpUpstream(config, ctx);
67
+ state = await bringUpUpstream(loadConfig(ctx), ctx);
60
68
  } catch (error) {
61
69
  startupError = error as Error;
62
70
  if (ctx.hasUI) ctx.ui.notify(`pstack adapter: ${(error as Error).message}`, "error");
@@ -77,6 +85,7 @@ export default function (pi: ExtensionAPI) {
77
85
 
78
86
  const result = registerAgents(pi.events, state.generation.agents, state.config.namespace);
79
87
  registrations = result.registrations;
88
+ unregisteredAgents = result.unregistered;
80
89
  const diagnostics: Diagnostic[] = [...result.diagnostics];
81
90
  if (result.registrations.length > 0) {
82
91
  diagnostics.push({
@@ -100,6 +109,7 @@ export default function (pi: ExtensionAPI) {
100
109
  registerDelegationActivation(pi, {
101
110
  skills: () => state?.generation.skills ?? [],
102
111
  modeActive: () => modeActive && state !== undefined,
112
+ preferred: () => state?.config.delegation ?? DEFAULT_DELEGATION,
103
113
  });
104
114
 
105
115
  pi.on("before_agent_start", async (event) => {
@@ -131,6 +141,11 @@ export default function (pi: ExtensionAPI) {
131
141
  skill: Type.Optional(Type.String({ description: "Skill name for `check`, with or without the namespace prefix" })),
132
142
  role: Type.Optional(Type.String({ description: "pstack model role for `set_model`" })),
133
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
+ ),
134
149
  repo: Type.Optional(Type.String({ description: "Upstream repository URL for `set_source`" })),
135
150
  ref: Type.Optional(Type.String({ description: "Branch, tag, or commit for `set_source`" })),
136
151
  localPath: Type.Optional(Type.String({ description: "Local pstack checkout for `set_source`" })),
@@ -191,6 +206,7 @@ export default function (pi: ExtensionAPI) {
191
206
  skill?: string;
192
207
  role?: string;
193
208
  model?: string;
209
+ thinking?: string;
194
210
  repo?: string;
195
211
  ref?: string;
196
212
  localPath?: string;
@@ -202,27 +218,35 @@ export default function (pi: ExtensionAPI) {
202
218
  if (!state) return "The pstack adapter has not resolved its upstream checkout yet.";
203
219
 
204
220
  switch (params.action) {
205
- case "status":
221
+ case "status": {
222
+ const current = capabilities();
223
+ const delegation = current.find((capability) => capability.id === "delegation");
206
224
  return renderStatus({
207
225
  adapterVersion: ADAPTER_VERSION,
208
226
  config: state.config,
209
227
  source: state.source,
210
228
  upstreamVersion: state.upstreamVersion,
211
229
  generation: state.generation,
212
- capabilities: capabilities(),
230
+ capabilities: current,
213
231
  platform: platformSupport(platform()),
214
232
  platformName: platform(),
215
233
  modeActive,
216
234
  cacheRoot: state.cacheRoot,
217
235
  bootstrapped: state.bootstrapped,
218
- runtimeDiagnostics: [...agentDiagnostics, ...shellDiagnostics(probedShellState(ctx, pi.getActiveTools()))],
236
+ runtimeDiagnostics: [
237
+ ...agentDiagnostics,
238
+ // Judged now, because T3's tools can connect after the agents were registered.
239
+ ...unregisteredAgentDiagnostics(unregisteredAgents, delegation?.backend, state.generation.agentPromptsDir),
240
+ ...shellDiagnostics(probedShellState(ctx, pi.getActiveTools())),
241
+ ],
219
242
  });
243
+ }
220
244
  case "check":
221
245
  return checkSkill(params.skill ?? "", ctx);
222
246
  case "models":
223
247
  return renderModels(ctx);
224
248
  case "set_model":
225
- return setModel(params.role, params.model, params.scope ?? "user", ctx);
249
+ return setModel(params.role, params.model, params.thinking, params.scope ?? "user", ctx);
226
250
  case "set_source":
227
251
  return setSource(params, params.scope ?? "user", ctx);
228
252
  default:
@@ -231,7 +255,7 @@ export default function (pi: ExtensionAPI) {
231
255
  }
232
256
 
233
257
  function capabilities(): CapabilityStatus[] {
234
- return detectCapabilities(pi.getAllTools() as unknown as ToolDescriptor[]);
258
+ return detectCapabilities(pi.getAllTools() as unknown as ToolDescriptor[], state?.config.delegation);
235
259
  }
236
260
 
237
261
  function checkSkill(requested: string, ctx: ExtensionContext): string {
@@ -242,7 +266,7 @@ export default function (pi: ExtensionAPI) {
242
266
  (candidate) => candidate.generatedName === requested || candidate.generatedName === `${prefix}${bare}`,
243
267
  );
244
268
  if (!skill) {
245
- return `No pstack skill named ${JSON.stringify(requested)}. Run the adapter tool with action "status" for the full list.`;
269
+ return `No pstack skill named ${safeEcho(requested)}. Run the adapter tool with action "status" for the full list.`;
246
270
  }
247
271
  const resolved = shellState(ctx, pi.getActiveTools());
248
272
  // An inactive bash tool or an unresolved shell is already a blocker, so do not spawn anything.
@@ -250,37 +274,48 @@ export default function (pi: ExtensionAPI) {
250
274
  const executables = probeExecutables(skill.executables, usable ? () => sessionBashContext(ctx) : undefined);
251
275
  const shell: ShellState = { ...resolved, runError: executables.shellFailure };
252
276
  const shellProblems = shellDiagnostics(shell).map((diagnostic) => `${diagnostic.message} ${diagnostic.action ?? ""}`.trim());
277
+ const current = capabilities();
253
278
  return renderCheck(
254
279
  skill,
255
280
  entryFor(skill.upstreamName, skill.generatedName.slice(prefix.length)),
256
- checkPrerequisites(skill.capabilities, capabilities(), skill.executables, executables.has, shellProblems),
281
+ checkPrerequisites(skill.capabilities, current, skill.executables, executables.has, shellProblems),
282
+ current.find((capability) => capability.id === "delegation"),
257
283
  );
258
284
  }
259
285
 
260
286
  function renderModels(ctx: ExtensionContext): string {
261
287
  const scoped = ctx.scopedModels ?? [];
262
288
  const models = scoped.length > 0 ? scoped.map((entry) => entry.model) : ctx.modelRegistry.getAvailable();
263
- const ids = models.map((model) => `${model.provider}/${model.id}`).sort();
289
+ const ids = models.map((model) => `${model.provider}/${model.id}`).filter(isValidModelSpec).sort();
264
290
  if (ids.length === 0) return "No models are available in this Pi installation.";
265
291
  return `Available model ids.\n\n${ids.map((id) => `- \`${id}\``).join("\n")}`;
266
292
  }
267
293
 
268
- function setModel(role: string | undefined, model: string | undefined, scope: "user" | "project", ctx: ExtensionContext): string {
294
+ function setModel(
295
+ role: string | undefined,
296
+ model: string | undefined,
297
+ thinking: string | undefined,
298
+ scope: "user" | "project",
299
+ ctx: ExtensionContext,
300
+ ): string {
269
301
  if (!role) return "set_model needs a `role`.";
270
302
  if (!model) return "set_model needs a `model`, or `inherit` to clear the role.";
271
- const clearing = model.trim().toLowerCase() === "inherit";
272
- if (!clearing && !ctx.modelRegistry.getAvailable().some((candidate) => `${candidate.provider}/${candidate.id}` === model)) {
273
- return `No model \`${model}\` is available in this Pi installation. Call this tool with action "models" for real ids, or pass "inherit".`;
274
- }
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;
275
309
  const path = configPath(scope, ctx);
276
- const current = readConfigFile(path)?.values.models ?? {};
277
- const models = { ...current };
278
- if (clearing) delete models[role];
279
- else models[role] = model;
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);
280
314
  updateConfigFile(path, { models });
281
- return clearing
282
- ? `Role \`${role}\` now inherits the parent session model. Written to ${path}.`
283
- : `Role \`${role}\` now uses \`${model}\`. Written to ${path}.`;
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}.`;
284
319
  }
285
320
 
286
321
  async function setSource(
@@ -336,6 +371,7 @@ export default function (pi: ExtensionAPI) {
336
371
  }
337
372
  }
338
373
  registrations = [];
374
+ unregisteredAgents = [];
339
375
  }
340
376
  }
341
377
 
@@ -418,6 +454,7 @@ function buildState(
418
454
  namespace: config.namespace,
419
455
  cacheRoot: root,
420
456
  adapterVersion: ADAPTER_VERSION,
457
+ delegation: config.delegation,
421
458
  // A local checkout has uncommitted edits the digest cannot see, so never reuse its output.
422
459
  force: source.selection.kind === "local",
423
460
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pstack-adapter",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Run Cursor's official pstack plugin inside Pi from an unmodified upstream checkout",
5
5
  "type": "module",
6
6
  "license": "MIT",