pi-pstack-adapter 0.3.0 → 0.4.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,14 @@
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.4.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.3.0...v0.4.0) (2026-10-14)
6
+
7
+
8
+ ### Features
9
+
10
+ * **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))
11
+ * **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))
12
+
5
13
  ## [0.3.0](https://github.com/elderfo/pi-pstack-adapter/compare/v0.2.0...v0.3.0) (2026-10-05)
6
14
 
7
15
 
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
@@ -172,6 +189,7 @@ settings to `.pi/pi-pstack-adapter.json`. Environment variables override both fi
172
189
  | `PISTACK_ALLOW_BOOTSTRAP` | Allows a noninteractive first download. |
173
190
  | `PISTACK_CACHE_DIR` | Changes the cache directory. |
174
191
  | `PISTACK_MODELS` | Assigns models as comma-separated `role=provider/model` pairs. |
192
+ | `PISTACK_DELEGATION` | Sets the delegation provider to try first: `t3` (default) or `pi-subagents`. The config file key is `delegation`. |
175
193
 
176
194
  ## Security and trust
177
195
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.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
 
@@ -61,6 +61,14 @@ plain words before you call it, and report a refusal as a refusal.
61
61
 
62
62
  Regeneration happens on the next Pi start. Tell the user to restart Pi, or run `/reload`.
63
63
 
64
+ ## Delegation order
65
+
66
+ Delegating workflows try T3 Code's `delegate_task` first, then pi-subagents' `subagent`. A user
67
+ who wants `subagent` first sets `"delegation": "pi-subagents"` in the adapter config file, or
68
+ `PISTACK_DELEGATION=pi-subagents`. The adapter tool has no action for this. Tell the user which
69
+ file to edit: the `config layers` row of the status report lists them. Regeneration happens on
70
+ the next Pi start.
71
+
64
72
  ## Finish
65
73
 
66
74
  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,7 +6,8 @@ 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,
@@ -26,7 +27,7 @@ import { entryFor } from "./src/registry.ts";
26
27
  import { platformSupport, renderCheck, renderStatus, shellDiagnostics, type ShellState } from "./src/status.ts";
27
28
  import { BootstrapRequiredError, resolveRefToCommit, resolveSource } from "./src/upstream.ts";
28
29
  import { normalizeRepo } from "./src/config.ts";
29
- import type { CapabilityStatus, Diagnostic, GenerationResult, ResolvedSource } from "./src/types.ts";
30
+ import type { CapabilityStatus, Diagnostic, GeneratedAgent, GenerationResult, ResolvedSource } from "./src/types.ts";
30
31
 
31
32
  const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
32
33
  const ADAPTER_VERSION = readAdapterVersion();
@@ -45,6 +46,7 @@ export default function (pi: ExtensionAPI) {
45
46
  let startupError: Error | undefined;
46
47
  let registrations: readonly RuntimeRegistration[] = [];
47
48
  let agentDiagnostics: readonly Diagnostic[] = [];
49
+ let unregisteredAgents: readonly GeneratedAgent[] = [];
48
50
  let modeActive = false;
49
51
  const commandNamespace = startupNamespace();
50
52
 
@@ -77,6 +79,7 @@ export default function (pi: ExtensionAPI) {
77
79
 
78
80
  const result = registerAgents(pi.events, state.generation.agents, state.config.namespace);
79
81
  registrations = result.registrations;
82
+ unregisteredAgents = result.unregistered;
80
83
  const diagnostics: Diagnostic[] = [...result.diagnostics];
81
84
  if (result.registrations.length > 0) {
82
85
  diagnostics.push({
@@ -100,6 +103,7 @@ export default function (pi: ExtensionAPI) {
100
103
  registerDelegationActivation(pi, {
101
104
  skills: () => state?.generation.skills ?? [],
102
105
  modeActive: () => modeActive && state !== undefined,
106
+ preferred: () => state?.config.delegation ?? DEFAULT_DELEGATION,
103
107
  });
104
108
 
105
109
  pi.on("before_agent_start", async (event) => {
@@ -202,21 +206,29 @@ export default function (pi: ExtensionAPI) {
202
206
  if (!state) return "The pstack adapter has not resolved its upstream checkout yet.";
203
207
 
204
208
  switch (params.action) {
205
- case "status":
209
+ case "status": {
210
+ const current = capabilities();
211
+ const delegation = current.find((capability) => capability.id === "delegation");
206
212
  return renderStatus({
207
213
  adapterVersion: ADAPTER_VERSION,
208
214
  config: state.config,
209
215
  source: state.source,
210
216
  upstreamVersion: state.upstreamVersion,
211
217
  generation: state.generation,
212
- capabilities: capabilities(),
218
+ capabilities: current,
213
219
  platform: platformSupport(platform()),
214
220
  platformName: platform(),
215
221
  modeActive,
216
222
  cacheRoot: state.cacheRoot,
217
223
  bootstrapped: state.bootstrapped,
218
- runtimeDiagnostics: [...agentDiagnostics, ...shellDiagnostics(probedShellState(ctx, pi.getActiveTools()))],
224
+ runtimeDiagnostics: [
225
+ ...agentDiagnostics,
226
+ // Judged now, because T3's tools can connect after the agents were registered.
227
+ ...unregisteredAgentDiagnostics(unregisteredAgents, delegation?.backend, state.generation.agentPromptsDir),
228
+ ...shellDiagnostics(probedShellState(ctx, pi.getActiveTools())),
229
+ ],
219
230
  });
231
+ }
220
232
  case "check":
221
233
  return checkSkill(params.skill ?? "", ctx);
222
234
  case "models":
@@ -231,7 +243,7 @@ export default function (pi: ExtensionAPI) {
231
243
  }
232
244
 
233
245
  function capabilities(): CapabilityStatus[] {
234
- return detectCapabilities(pi.getAllTools() as unknown as ToolDescriptor[]);
246
+ return detectCapabilities(pi.getAllTools() as unknown as ToolDescriptor[], state?.config.delegation);
235
247
  }
236
248
 
237
249
  function checkSkill(requested: string, ctx: ExtensionContext): string {
@@ -250,10 +262,12 @@ export default function (pi: ExtensionAPI) {
250
262
  const executables = probeExecutables(skill.executables, usable ? () => sessionBashContext(ctx) : undefined);
251
263
  const shell: ShellState = { ...resolved, runError: executables.shellFailure };
252
264
  const shellProblems = shellDiagnostics(shell).map((diagnostic) => `${diagnostic.message} ${diagnostic.action ?? ""}`.trim());
265
+ const current = capabilities();
253
266
  return renderCheck(
254
267
  skill,
255
268
  entryFor(skill.upstreamName, skill.generatedName.slice(prefix.length)),
256
- checkPrerequisites(skill.capabilities, capabilities(), skill.executables, executables.has, shellProblems),
269
+ checkPrerequisites(skill.capabilities, current, skill.executables, executables.has, shellProblems),
270
+ current.find((capability) => capability.id === "delegation"),
257
271
  );
258
272
  }
259
273
 
@@ -336,6 +350,7 @@ export default function (pi: ExtensionAPI) {
336
350
  }
337
351
  }
338
352
  registrations = [];
353
+ unregisteredAgents = [];
339
354
  }
340
355
  }
341
356
 
@@ -418,6 +433,7 @@ function buildState(
418
433
  namespace: config.namespace,
419
434
  cacheRoot: root,
420
435
  adapterVersion: ADAPTER_VERSION,
436
+ delegation: config.delegation,
421
437
  // A local checkout has uncommitted edits the digest cannot see, so never reuse its output.
422
438
  force: source.selection.kind === "local",
423
439
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pstack-adapter",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
package/src/activation.ts CHANGED
@@ -2,23 +2,26 @@ import { homedir } from "node:os";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
- import type { GeneratedSkill } from "./types.ts";
6
-
7
- /** The pi-subagents tool a delegation skill calls. */
8
- export const DELEGATION_TOOL = "subagent";
9
- /** The pi-subagents loader that exposes a registered but inactive `subagent` tool. */
10
- export const DELEGATION_LOADER = "subagents_enable";
5
+ import {
6
+ DEFAULT_DELEGATION,
7
+ type DelegationHost,
8
+ type ToolPresence,
9
+ callableTool,
10
+ delegationHost,
11
+ resolveDelegation,
12
+ } from "./delegation.ts";
13
+ import type { DelegationProvider, GeneratedSkill } from "./types.ts";
11
14
 
12
15
  /** The slice of Pi's tool registry that activation reads and writes. */
13
16
  export interface ToolLoadout {
14
- getAllTools(): readonly { readonly name: string }[];
17
+ getAllTools(): readonly ToolPresence[];
15
18
  getActiveTools(): string[];
16
19
  setActiveTools(toolNames: string[]): void;
17
20
  }
18
21
 
19
22
  export type ActivationResult =
20
- | { readonly kind: "already-active" }
21
- | { readonly kind: "activated" }
23
+ | { readonly kind: "already-active"; readonly backend: DelegationProvider }
24
+ | { readonly kind: "activated"; readonly backend: DelegationProvider }
22
25
  | { readonly kind: "unavailable" };
23
26
 
24
27
  export function needsDelegation(skill: GeneratedSkill): boolean {
@@ -54,37 +57,51 @@ export function skillFromReadPath(
54
57
  }
55
58
 
56
59
  /**
57
- * Adds `subagent` to the active tools, the same selection `subagents_enable` makes, so a
58
- * delegation skill never depends on the model remembering to call the loader first.
59
- * Pi applies the new loadout on the next model request.
60
+ * Adds the delegation backend that capability detection resolves to the active tools, with its
61
+ * companion tools, so a delegation skill never depends on the model loading it first. For
62
+ * pi-subagents that is the selection `subagents_enable` makes; T3's `delegate_task` may be
63
+ * deferred behind `tool_search`. Pi applies the new loadout on the next model request.
60
64
  */
61
- export function activateDelegation(loadout: ToolLoadout): ActivationResult {
62
- if (!loadout.getAllTools().some((tool) => tool.name === DELEGATION_TOOL)) return { kind: "unavailable" };
65
+ export function activateDelegation(
66
+ loadout: ToolLoadout,
67
+ preferred: DelegationProvider = DEFAULT_DELEGATION,
68
+ host: DelegationHost = delegationHost(),
69
+ ): ActivationResult {
70
+ const registered = loadout.getAllTools();
71
+ const resolution = resolveDelegation(registered, preferred, host);
72
+ if (resolution.kind === "unavailable") return { kind: "unavailable" };
73
+ const { backend, companions } = resolution;
74
+ const wanted = [backend.tool, ...companions];
63
75
  const active = loadout.getActiveTools();
64
- if (active.includes(DELEGATION_TOOL)) return { kind: "already-active" };
65
- loadout.setActiveTools([...active, DELEGATION_TOOL]);
66
- return { kind: "activated" };
76
+ const missing = wanted.filter((name) => !active.includes(name));
77
+ if (missing.length === 0) return { kind: "already-active", backend: backend.id };
78
+ loadout.setActiveTools([...active, ...missing]);
79
+ return { kind: "activated", backend: backend.id };
67
80
  }
68
81
 
69
82
  export interface ActivationSources {
70
83
  /** Generated skills, or none while the adapter has no upstream checkout. */
71
84
  readonly skills: () => readonly GeneratedSkill[];
72
85
  readonly modeActive: () => boolean;
86
+ /** The configured backend preference, read at activation time. */
87
+ readonly preferred: () => DelegationProvider;
88
+ /** Defaults to the running process's T3 Code bridge, if any. */
89
+ readonly host?: DelegationHost;
73
90
  }
74
91
 
75
92
  type ActivationHost = Pick<ExtensionAPI, "on" | "getAllTools" | "getActiveTools" | "setActiveTools">;
76
93
 
77
94
  /**
78
- * A delegation skill must not depend on the model remembering to call `subagents_enable`,
79
- * so a slash command, a model-initiated SKILL.md read, and Poteto Mode each activate
80
- * `subagent` in code.
95
+ * A delegation skill must not depend on the model remembering to load its delegation tool,
96
+ * so a slash command, a model-initiated SKILL.md read, and Poteto Mode each activate it in code.
81
97
  */
82
98
  export function registerDelegationActivation(pi: ActivationHost, sources: ActivationSources): void {
99
+ const activate = () => activateDelegation(pi, sources.preferred(), sources.host ?? delegationHost());
83
100
  // `input` runs before Pi copies the tool list for the prompt, so the first model request
84
- // already carries `subagent`. It also covers messages queued while the agent is streaming.
101
+ // already carries the tool. It also covers messages queued while the agent is streaming.
85
102
  pi.on("input", async (event) => {
86
103
  const skill = skillFromPrompt(event.text, sources.skills());
87
- if (sources.modeActive() || (skill && needsDelegation(skill))) activateDelegation(pi);
104
+ if (sources.modeActive() || (skill && needsDelegation(skill))) activate();
88
105
  });
89
106
 
90
107
  pi.on("tool_call", async (event, ctx) => {
@@ -92,12 +109,12 @@ export function registerDelegationActivation(pi: ActivationHost, sources: Activa
92
109
  const path = (event.input as { path?: unknown }).path;
93
110
  if (typeof path !== "string") return;
94
111
  const skill = skillFromReadPath(path, ctx.cwd, sources.skills());
95
- if (skill && needsDelegation(skill)) activateDelegation(pi);
112
+ if (skill && needsDelegation(skill)) activate();
96
113
  });
97
114
 
98
115
  // Backstop for prompts that reach the agent without an `input` event.
99
116
  pi.on("before_agent_start", async () => {
100
- if (sources.modeActive()) activateDelegation(pi);
117
+ if (sources.modeActive()) activate();
101
118
  });
102
119
  }
103
120
 
package/src/agents.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Diagnostic, GeneratedAgent } from "./types.ts";
1
+ import { join } from "node:path";
2
+ import { T3_DELEGATE_TOOL } from "./delegation.ts";
3
+ import type { DelegationProvider, Diagnostic, GeneratedAgent } from "./types.ts";
2
4
 
3
5
  export const RUNTIME_AGENT_REGISTER_EVENT = "pi-subagents:runtime-agent-register:v1";
4
6
 
@@ -19,6 +21,9 @@ export interface EventBus {
19
21
 
20
22
  export interface AgentRegistrationResult {
21
23
  readonly registrations: readonly RuntimeRegistration[];
24
+ /** Agents no installed extension accepted as named agents. */
25
+ readonly unregistered: readonly GeneratedAgent[];
26
+ /** Registrations a provider refused. */
22
27
  readonly diagnostics: readonly Diagnostic[];
23
28
  }
24
29
 
@@ -32,6 +37,7 @@ export function registerAgents(
32
37
  namespace: string,
33
38
  ): AgentRegistrationResult {
34
39
  const registrations: RuntimeRegistration[] = [];
40
+ const unregistered: GeneratedAgent[] = [];
35
41
  const diagnostics: Diagnostic[] = [];
36
42
 
37
43
  for (const agent of agents) {
@@ -45,12 +51,7 @@ export function registerAgents(
45
51
  };
46
52
  bus.emit(RUNTIME_AGENT_REGISTER_EVENT, request);
47
53
  if (!request.result) {
48
- diagnostics.push({
49
- level: "warning",
50
- resource: `agent:${agent.upstreamName}`,
51
- message: `No installed delegation provider accepted \`${agent.generatedName}\`, so pstack workflows cannot delegate to it.`,
52
- action: `Install a provider that owns ${RUNTIME_AGENT_REGISTER_EVENT}, for example \`pi install npm:pi-subagents\`.`,
53
- });
54
+ unregistered.push(agent);
54
55
  continue;
55
56
  }
56
57
  if (!request.result.ok) {
@@ -65,5 +66,32 @@ export function registerAgents(
65
66
  registrations.push(request.result.registration);
66
67
  }
67
68
 
68
- return { registrations, diagnostics };
69
+ return { registrations, unregistered, diagnostics };
70
+ }
71
+
72
+ /**
73
+ * T3 Code children have no named agents and load an agent from its prompt file, so a missing
74
+ * pi-subagents registration only blocks delegation when T3 is not the resolved backend.
75
+ */
76
+ export function unregisteredAgentDiagnostics(
77
+ unregistered: readonly GeneratedAgent[],
78
+ backend: DelegationProvider | undefined,
79
+ promptsDir: string,
80
+ ): Diagnostic[] {
81
+ return unregistered.map((agent) => {
82
+ const promptPath = join(promptsDir, `${agent.generatedName}.md`);
83
+ if (backend === "t3") {
84
+ return {
85
+ level: "info",
86
+ resource: `agent:${agent.upstreamName}`,
87
+ message: `\`${agent.generatedName}\` is not a named agent here. T3 Code children run it by reading ${promptPath} as their system prompt.`,
88
+ };
89
+ }
90
+ return {
91
+ level: "warning",
92
+ resource: `agent:${agent.upstreamName}`,
93
+ message: `No installed delegation provider accepted \`${agent.generatedName}\` as a named agent, so pstack workflows cannot delegate to it through \`subagent\`.`,
94
+ action: `Install a provider that owns ${RUNTIME_AGENT_REGISTER_EVENT}, for example \`pi install npm:pi-subagents\`, or run Pi inside T3 Code, whose \`${T3_DELEGATE_TOOL}\` loads the agent from ${promptPath}.`,
95
+ };
96
+ });
69
97
  }