@ymy88/pi-subagents 0.0.0-stage → 0.24.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 +904 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +1170 -2
- package/SECURITY.md +95 -0
- package/dist/abortable.d.ts +12 -0
- package/dist/abortable.js +42 -0
- package/dist/agent-color.d.ts +35 -0
- package/dist/agent-color.js +123 -0
- package/dist/agent-file-toggle.d.ts +133 -0
- package/dist/agent-file-toggle.js +287 -0
- package/dist/agent-manager.d.ts +552 -0
- package/dist/agent-manager.js +1466 -0
- package/dist/agent-record-entry.d.ts +96 -0
- package/dist/agent-record-entry.js +121 -0
- package/dist/agent-runner.d.ts +310 -0
- package/dist/agent-runner.js +1195 -0
- package/dist/agent-source-badge.d.ts +50 -0
- package/dist/agent-source-badge.js +74 -0
- package/dist/agent-types.d.ts +119 -0
- package/dist/agent-types.js +286 -0
- package/dist/child-context.d.ts +2 -0
- package/dist/child-context.js +12 -0
- package/dist/completion-outbox.d.ts +63 -0
- package/dist/completion-outbox.js +104 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +72 -0
- package/dist/cross-extension-rpc.js +153 -0
- package/dist/custom-agents.d.ts +77 -0
- package/dist/custom-agents.js +396 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +121 -0
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +4111 -0
- package/dist/invocation-config.d.ts +108 -0
- package/dist/invocation-config.js +83 -0
- package/dist/memory.d.ts +53 -0
- package/dist/memory.js +165 -0
- package/dist/mention-clone.d.ts +79 -0
- package/dist/mention-clone.js +155 -0
- package/dist/mention.d.ts +81 -0
- package/dist/mention.js +131 -0
- package/dist/model-resolver.d.ts +36 -0
- package/dist/model-resolver.js +95 -0
- package/dist/model-scope.d.ts +49 -0
- package/dist/model-scope.js +48 -0
- package/dist/nested-tools.d.ts +57 -0
- package/dist/nested-tools.js +303 -0
- package/dist/output-file.d.ts +43 -0
- package/dist/output-file.js +153 -0
- package/dist/package-resources.d.ts +240 -0
- package/dist/package-resources.js +519 -0
- package/dist/prompts.d.ts +76 -0
- package/dist/prompts.js +121 -0
- package/dist/schedule-store.d.ts +38 -0
- package/dist/schedule-store.js +155 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +359 -0
- package/dist/settings.d.ts +398 -0
- package/dist/settings.js +282 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +61 -0
- package/dist/status-note.js +85 -0
- package/dist/structured-output.d.ts +61 -0
- package/dist/structured-output.js +112 -0
- package/dist/types.d.ts +391 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-mention.d.ts +82 -0
- package/dist/ui/agent-mention.js +187 -0
- package/dist/ui/agent-widget.d.ts +219 -0
- package/dist/ui/agent-widget.js +585 -0
- package/dist/ui/conversation-viewer.d.ts +177 -0
- package/dist/ui/conversation-viewer.js +905 -0
- package/dist/ui/fleet-list.d.ts +211 -0
- package/dist/ui/fleet-list.js +489 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +94 -0
- package/dist/ui/select-item.d.ts +27 -0
- package/dist/ui/select-item.js +34 -0
- package/dist/ui/viewer-keys.d.ts +22 -0
- package/dist/ui/viewer-keys.js +18 -0
- package/dist/ui/workflow-card.d.ts +175 -0
- package/dist/ui/workflow-card.js +332 -0
- package/dist/ui/workflow-dialog.d.ts +305 -0
- package/dist/ui/workflow-dialog.js +843 -0
- package/dist/ui/workflow-menu.d.ts +60 -0
- package/dist/ui/workflow-menu.js +147 -0
- package/dist/usage.d.ts +135 -0
- package/dist/usage.js +120 -0
- package/dist/workflow/collisions.d.ts +95 -0
- package/dist/workflow/collisions.js +88 -0
- package/dist/workflow/entry.d.ts +32 -0
- package/dist/workflow/entry.js +29 -0
- package/dist/workflow/host.d.ts +62 -0
- package/dist/workflow/host.js +419 -0
- package/dist/workflow/journal.d.ts +97 -0
- package/dist/workflow/journal.js +120 -0
- package/dist/workflow/json-schema.d.ts +52 -0
- package/dist/workflow/json-schema.js +112 -0
- package/dist/workflow/meta.d.ts +67 -0
- package/dist/workflow/meta.js +317 -0
- package/dist/workflow/progress.d.ts +224 -0
- package/dist/workflow/progress.js +361 -0
- package/dist/workflow/runtime.d.ts +334 -0
- package/dist/workflow/runtime.js +830 -0
- package/dist/workflow/saved.d.ts +102 -0
- package/dist/workflow/saved.js +247 -0
- package/dist/workflow/task.d.ts +136 -0
- package/dist/workflow/task.js +207 -0
- package/dist/workflow/tool-description.d.ts +38 -0
- package/dist/workflow/tool-description.js +199 -0
- package/dist/workflow/worker-source.d.ts +47 -0
- package/dist/workflow/worker-source.js +778 -0
- package/dist/worktree.d.ts +52 -0
- package/dist/worktree.js +164 -0
- package/dist/xml.d.ts +10 -0
- package/dist/xml.js +12 -0
- package/docs/packages.md +211 -0
- package/docs/rpc.md +192 -0
- package/docs/workflows.md +447 -0
- package/examples/agent-tool-description.md +42 -0
- package/examples/workflows/compose.js +51 -0
- package/examples/workflows/fan-out-audit.js +47 -0
- package/examples/workflows/gated-fix.js +60 -0
- package/examples/workflows/lib/count-child.js +27 -0
- package/examples/workflows/review-panel.js +63 -0
- package/examples/workflows/structured-findings.js +78 -0
- package/package.json +74 -4
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +297 -0
- package/src/agent-manager.ts +1735 -0
- package/src/agent-record-entry.ts +153 -0
- package/src/agent-runner.ts +1468 -0
- package/src/agent-source-badge.ts +75 -0
- package/src/agent-types.ts +346 -0
- package/src/child-context.ts +15 -0
- package/src/completion-outbox.ts +116 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +221 -0
- package/src/custom-agents.ts +452 -0
- package/src/default-agents.ts +125 -0
- package/src/enabled-models.ts +180 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +4433 -0
- package/src/invocation-config.ts +155 -0
- package/src/memory.ts +179 -0
- package/src/mention-clone.ts +195 -0
- package/src/mention.ts +141 -0
- package/src/model-resolver.ts +118 -0
- package/src/model-scope.ts +70 -0
- package/src/nested-tools.ts +426 -0
- package/src/output-file.ts +164 -0
- package/src/package-resources.ts +652 -0
- package/src/prompts.ts +171 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +386 -0
- package/src/settings.ts +650 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +90 -0
- package/src/structured-output.ts +130 -0
- package/src/types.ts +404 -0
- package/src/ui/agent-mention.ts +216 -0
- package/src/ui/agent-widget.ts +661 -0
- package/src/ui/conversation-viewer.ts +928 -0
- package/src/ui/fleet-list.ts +559 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/select-item.ts +45 -0
- package/src/ui/viewer-keys.ts +43 -0
- package/src/ui/workflow-card.ts +470 -0
- package/src/ui/workflow-dialog.ts +1115 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +167 -0
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +467 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +129 -0
- package/src/workflow/meta.ts +325 -0
- package/src/workflow/progress.ts +551 -0
- package/src/workflow/runtime.ts +1219 -0
- package/src/workflow/saved.ts +259 -0
- package/src/workflow/task.ts +302 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +205 -0
- package/src/xml.ts +13 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree.ts — Git worktree isolation for agents.
|
|
3
|
+
*
|
|
4
|
+
* Creates a temporary git worktree so the agent works on an isolated copy of the repo.
|
|
5
|
+
* On completion, if no changes were made, the worktree is cleaned up.
|
|
6
|
+
* If changes exist, a branch is created and returned in the result.
|
|
7
|
+
*
|
|
8
|
+
* Every git call goes through `pi.exec` (async) rather than `execFileSync`: a
|
|
9
|
+
* worktree copy can take seconds, and a session that spawns several isolated
|
|
10
|
+
* agents at once would otherwise serialize them all on the TUI's event loop.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
export interface WorktreeInfo {
|
|
14
|
+
/** Absolute path to the worktree directory (the copied repo's root). */
|
|
15
|
+
path: string;
|
|
16
|
+
/** Branch name created for this worktree (if changes exist). */
|
|
17
|
+
branch: string;
|
|
18
|
+
/** Commit SHA that the worktree was created from. */
|
|
19
|
+
baseSha: string;
|
|
20
|
+
/**
|
|
21
|
+
* Where the agent should work inside the worktree: the equivalent of the
|
|
22
|
+
* cwd the worktree was created from. Equals `path` when that cwd was the
|
|
23
|
+
* repo root; points at the copied subdirectory when it was deeper (e.g. a
|
|
24
|
+
* monorepo package), so the requested scoping survives isolation.
|
|
25
|
+
*/
|
|
26
|
+
workPath: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function setWorktreeIsolationEnabled(enabled: boolean): void;
|
|
29
|
+
export declare function isWorktreeIsolationEnabled(): boolean;
|
|
30
|
+
export interface WorktreeCleanupResult {
|
|
31
|
+
/** Whether changes were found in the worktree. */
|
|
32
|
+
hasChanges: boolean;
|
|
33
|
+
/** Branch name if changes were committed. */
|
|
34
|
+
branch?: string;
|
|
35
|
+
/** Worktree path if it was kept. */
|
|
36
|
+
path?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a temporary git worktree for an agent.
|
|
40
|
+
* Returns the worktree path, or undefined if not in a git repo.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createWorktree(pi: ExtensionAPI, cwd: string, agentId: string): Promise<WorktreeInfo | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Clean up a worktree after agent completion.
|
|
45
|
+
* - If no changes: remove worktree entirely.
|
|
46
|
+
* - If changes exist: create a branch, commit changes, return branch info.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cleanupWorktree(pi: ExtensionAPI, cwd: string, worktree: WorktreeInfo, agentDescription: string): Promise<WorktreeCleanupResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Prune any orphaned worktrees (crash recovery).
|
|
51
|
+
*/
|
|
52
|
+
export declare function pruneWorktrees(pi: ExtensionAPI, cwd: string): Promise<void>;
|
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree.ts — Git worktree isolation for agents.
|
|
3
|
+
*
|
|
4
|
+
* Creates a temporary git worktree so the agent works on an isolated copy of the repo.
|
|
5
|
+
* On completion, if no changes were made, the worktree is cleaned up.
|
|
6
|
+
* If changes exist, a branch is created and returned in the result.
|
|
7
|
+
*
|
|
8
|
+
* Every git call goes through `pi.exec` (async) rather than `execFileSync`: a
|
|
9
|
+
* worktree copy can take seconds, and a session that spawns several isolated
|
|
10
|
+
* agents at once would otherwise serialize them all on the TUI's event loop.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join, relative } from "node:path";
|
|
16
|
+
/**
|
|
17
|
+
* Project-wide switch for worktree isolation (`worktreeIsolation` in
|
|
18
|
+
* subagents.json). Default `true` — unchanged behaviour.
|
|
19
|
+
*
|
|
20
|
+
* The `"off"` isolation value gives a model a legal way to decline a worktree,
|
|
21
|
+
* but it still depends on the model choosing it. This is the deterministic half
|
|
22
|
+
* of the same fix: on a large repo where every worktree costs real time and
|
|
23
|
+
* disk (#184), turning it off means no caller can create one, whatever it
|
|
24
|
+
* passes.
|
|
25
|
+
*/
|
|
26
|
+
let worktreeIsolationEnabled = true;
|
|
27
|
+
export function setWorktreeIsolationEnabled(enabled) {
|
|
28
|
+
worktreeIsolationEnabled = enabled;
|
|
29
|
+
}
|
|
30
|
+
export function isWorktreeIsolationEnabled() {
|
|
31
|
+
return worktreeIsolationEnabled;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run git and return its trimmed stdout, throwing on failure so callers keep
|
|
35
|
+
* the try/catch control flow `execFileSync` gave them.
|
|
36
|
+
*
|
|
37
|
+
* `pi.exec` never rejects — it reports failure in the result — and a command
|
|
38
|
+
* killed by its timeout comes back as `killed` with an exit code of 0, so both
|
|
39
|
+
* have to be checked to reproduce `execFileSync`'s "throws on anything but a
|
|
40
|
+
* clean exit".
|
|
41
|
+
*/
|
|
42
|
+
async function git(pi, cwd, args, timeout) {
|
|
43
|
+
const result = await pi.exec("git", args, { cwd, timeout });
|
|
44
|
+
if (result.killed || result.code !== 0) {
|
|
45
|
+
throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed (exit ${result.code})`);
|
|
46
|
+
}
|
|
47
|
+
return result.stdout.trim();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a temporary git worktree for an agent.
|
|
51
|
+
* Returns the worktree path, or undefined if not in a git repo.
|
|
52
|
+
*/
|
|
53
|
+
export async function createWorktree(pi, cwd, agentId) {
|
|
54
|
+
// Verify we're in a git repo with at least one commit (HEAD must exist)
|
|
55
|
+
let baseSha;
|
|
56
|
+
let subdir;
|
|
57
|
+
try {
|
|
58
|
+
await git(pi, cwd, ["rev-parse", "--is-inside-work-tree"], 5000);
|
|
59
|
+
baseSha = await git(pi, cwd, ["rev-parse", "HEAD"], 5000);
|
|
60
|
+
// Where cwd sits inside the repo ("" at the root): the agent must work at
|
|
61
|
+
// the same subdirectory inside the copy, or a monorepo-package cwd would
|
|
62
|
+
// silently widen to the whole repo. realpath both sides — git emits
|
|
63
|
+
// resolved paths while cwd may arrive through a symlink (macOS /tmp).
|
|
64
|
+
const topLevel = await git(pi, cwd, ["rev-parse", "--show-toplevel"], 5000);
|
|
65
|
+
subdir = relative(realpathSync(topLevel), realpathSync(cwd));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const branch = `pi-agent-${agentId}`;
|
|
71
|
+
const suffix = randomUUID().slice(0, 8);
|
|
72
|
+
const worktreePath = join(tmpdir(), `pi-agent-${agentId}-${suffix}`);
|
|
73
|
+
try {
|
|
74
|
+
// Create detached worktree at HEAD
|
|
75
|
+
await git(pi, cwd, ["worktree", "add", "--detach", worktreePath, "HEAD"], 30000);
|
|
76
|
+
return { path: worktreePath, branch, baseSha, workPath: subdir ? join(worktreePath, subdir) : worktreePath };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// If worktree creation fails, return undefined (agent runs in normal cwd)
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clean up a worktree after agent completion.
|
|
85
|
+
* - If no changes: remove worktree entirely.
|
|
86
|
+
* - If changes exist: create a branch, commit changes, return branch info.
|
|
87
|
+
*/
|
|
88
|
+
export async function cleanupWorktree(pi, cwd, worktree, agentDescription) {
|
|
89
|
+
if (!existsSync(worktree.path)) {
|
|
90
|
+
return { hasChanges: false };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
// Check for uncommitted changes in the worktree
|
|
94
|
+
const status = await git(pi, worktree.path, ["status", "--porcelain"], 10000);
|
|
95
|
+
if (status) {
|
|
96
|
+
// Changes exist — stage, commit, and create a branch
|
|
97
|
+
await git(pi, worktree.path, ["add", "-A"], 10000);
|
|
98
|
+
// Truncate description for commit message (no shell sanitization needed — pi.exec uses argv)
|
|
99
|
+
const safeDesc = agentDescription.slice(0, 200);
|
|
100
|
+
const commitMsg = `pi-agent: ${safeDesc}`;
|
|
101
|
+
await git(pi, worktree.path, ["commit", "--no-verify", "-m", commitMsg], 10000);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const currentSha = await git(pi, worktree.path, ["rev-parse", "HEAD"], 5000);
|
|
105
|
+
if (currentSha === worktree.baseSha) {
|
|
106
|
+
// No changes — remove worktree
|
|
107
|
+
await removeWorktree(pi, cwd, worktree.path);
|
|
108
|
+
return { hasChanges: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Create a branch pointing to the worktree's HEAD.
|
|
112
|
+
// If the branch already exists, append a suffix to avoid overwriting previous work.
|
|
113
|
+
let branchName = worktree.branch;
|
|
114
|
+
try {
|
|
115
|
+
await git(pi, worktree.path, ["branch", branchName], 5000);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Branch already exists — use a unique suffix
|
|
119
|
+
branchName = `${worktree.branch}-${Date.now()}`;
|
|
120
|
+
await git(pi, worktree.path, ["branch", branchName], 5000);
|
|
121
|
+
}
|
|
122
|
+
// Update branch name in worktree info for the caller
|
|
123
|
+
worktree.branch = branchName;
|
|
124
|
+
// Remove the worktree (branch persists in main repo)
|
|
125
|
+
await removeWorktree(pi, cwd, worktree.path);
|
|
126
|
+
return {
|
|
127
|
+
hasChanges: true,
|
|
128
|
+
branch: worktree.branch,
|
|
129
|
+
path: worktree.path,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Best effort cleanup on error
|
|
134
|
+
try {
|
|
135
|
+
await removeWorktree(pi, cwd, worktree.path);
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
return { hasChanges: false };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Force-remove a worktree.
|
|
143
|
+
*/
|
|
144
|
+
async function removeWorktree(pi, cwd, worktreePath) {
|
|
145
|
+
try {
|
|
146
|
+
await git(pi, cwd, ["worktree", "remove", "--force", worktreePath], 10000);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// If git worktree remove fails, try pruning
|
|
150
|
+
try {
|
|
151
|
+
await git(pi, cwd, ["worktree", "prune"], 5000);
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Prune any orphaned worktrees (crash recovery).
|
|
158
|
+
*/
|
|
159
|
+
export async function pruneWorktrees(pi, cwd) {
|
|
160
|
+
try {
|
|
161
|
+
await git(pi, cwd, ["worktree", "prune"], 5000);
|
|
162
|
+
}
|
|
163
|
+
catch { /* ignore */ }
|
|
164
|
+
}
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xml.ts — escaping for the `<task-notification>` payloads.
|
|
3
|
+
*
|
|
4
|
+
* A module of its own because both notification builders need it and they sit
|
|
5
|
+
* on opposite sides of a dependency edge: the agent one lives in `index.ts`,
|
|
6
|
+
* which imports `workflow/task.ts`, so the workflow one cannot reach back for
|
|
7
|
+
* it without a cycle.
|
|
8
|
+
*/
|
|
9
|
+
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
10
|
+
export declare function escapeXml(s: string): string;
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xml.ts — escaping for the `<task-notification>` payloads.
|
|
3
|
+
*
|
|
4
|
+
* A module of its own because both notification builders need it and they sit
|
|
5
|
+
* on opposite sides of a dependency edge: the agent one lives in `index.ts`,
|
|
6
|
+
* which imports `workflow/task.ts`, so the workflow one cannot reach back for
|
|
7
|
+
* it without a cycle.
|
|
8
|
+
*/
|
|
9
|
+
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
10
|
+
export function escapeXml(s) {
|
|
11
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
12
|
+
}
|
package/docs/packages.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Shipping subagents in a pi package
|
|
2
|
+
|
|
3
|
+
A pi package can declare its own subagents and workflow scripts. Installing the package is then the whole setup — no copying `.md` files into every project's `.pi/agents/`, and no sync script running on container start ([#109](https://github.com/tintinweb/pi-subagents/issues/109)).
|
|
4
|
+
|
|
5
|
+
This guide is for both sides of that: authoring a package that ships agents, and controlling which packages a machine accepts them from.
|
|
6
|
+
|
|
7
|
+
For where locally-authored agents live and what their frontmatter accepts, see [`README.md`](../README.md#custom-agents).
|
|
8
|
+
|
|
9
|
+
## The idea
|
|
10
|
+
|
|
11
|
+
Pi already ships four resource types inside packages — `extensions`, `skills`, `prompts`, `themes` — declared under a `pi` key in `package.json` and installed with `pi install`. Subagents are this extension's concept, so pi knows nothing about them, and until now a package author's only option was to tell users to copy files.
|
|
12
|
+
|
|
13
|
+
The mechanism here is the same shape as pi's, with one deliberate difference: **a package must declare its agents by name.** Pi falls back to scanning conventional directories (`skills/`, `prompts/`, …) when a package has no `pi` key at all. This extension never does. A package that happens to carry an `agents/` folder for some other tool contributes nothing until it says so — that is the author's half of the agreement, and installing the package is the user's.
|
|
14
|
+
|
|
15
|
+
## Authoring
|
|
16
|
+
|
|
17
|
+
### 1. Declare the paths
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"name": "my-subagents",
|
|
22
|
+
"version": "1.0.0",
|
|
23
|
+
"keywords": ["pi-package"],
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": ["./src/index.ts"],
|
|
26
|
+
"subagents": {
|
|
27
|
+
"agents": ["./agents"],
|
|
28
|
+
"workflows": ["./workflows"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`keywords: ["pi-package"]` is pi's discoverability convention, not a requirement. The `extensions` entry is only there to show the keys coexisting — a package can ship agents and nothing else.
|
|
35
|
+
|
|
36
|
+
Adding `pi.subagents` is safe for pi itself: its manifest reader looks at `extensions`, `skills`, `prompts` and `themes` and ignores every other key.
|
|
37
|
+
|
|
38
|
+
### 2. Put the files there
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
my-subagents/
|
|
42
|
+
├── package.json
|
|
43
|
+
├── agents/
|
|
44
|
+
│ ├── researcher.md
|
|
45
|
+
│ └── reviewer.md
|
|
46
|
+
└── workflows/
|
|
47
|
+
└── audit.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
An agent file is an ordinary agent `.md` — the same frontmatter a `.pi/agents/` file takes, documented in [`README.md`](../README.md#custom-agents). A workflow file is an ordinary saved workflow, and still has to carry its `export const meta = …` declaration to count as one ([`workflows.md`](workflows.md)).
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
---
|
|
54
|
+
name: pkg-researcher
|
|
55
|
+
description: Research with sources, for the my-subagents toolchain
|
|
56
|
+
tools: read, grep, ext:my-mcp/search
|
|
57
|
+
model: claude-haiku-4-5
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
You research things. Cite what you read.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Ship it
|
|
64
|
+
|
|
65
|
+
Include the directories in the published tarball — npm's `files` field, or an `.npmignore` that does not exclude them. Then:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pi install npm:my-subagents # published
|
|
69
|
+
pi install git:github.com/me/repo # straight from git
|
|
70
|
+
pi install /path/to/my-subagents # local checkout, for development
|
|
71
|
+
pi install ./my-subagents -l # project scope, into .pi/settings.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A local-path install is not copied, so an edit to the agent file is live on the next `Agent` call — the fastest authoring loop.
|
|
75
|
+
|
|
76
|
+
### Entry syntax
|
|
77
|
+
|
|
78
|
+
Entries are paths relative to the package root.
|
|
79
|
+
|
|
80
|
+
| Entry | Meaning |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `"./agents"` | A directory. Scanned for `*.md` (`*.js` for workflows), non-recursively. |
|
|
83
|
+
| `"./agents/one.md"` | A single file, taken as-is. |
|
|
84
|
+
| `"!./agents/wip.md"` | An exclusion. Applies to a file inside an included directory, which is the only form that does anything useful. |
|
|
85
|
+
|
|
86
|
+
The exclusion may name a path the package does not ship yet, so a file added in a later version is still skipped rather than silently appearing.
|
|
87
|
+
|
|
88
|
+
Nothing outside the package root is read. The containment check runs on canonical paths, so `../../.ssh` is refused and a symlinked `agents/` pointing at `/etc` does not get around it either.
|
|
89
|
+
|
|
90
|
+
Glob patterns are deliberately **not** supported. Pi expands globs because it resolves four resource types across arbitrary layouts; here the entries are one or two directories, and answering a glob would mean walking a third-party tree.
|
|
91
|
+
|
|
92
|
+
### Accepted spellings
|
|
93
|
+
|
|
94
|
+
All four mean the same thing. The top-level `pi-subagents` key is the one [`nicobailon/pi-subagents`](https://github.com/nicobailon/pi-subagents) reads, so a single manifest works with either extension.
|
|
95
|
+
|
|
96
|
+
```jsonc
|
|
97
|
+
"pi": { "subagents": { "agents": ["./agents"], "workflows": ["./flows"] } }
|
|
98
|
+
"pi": { "subagents": ["./agents"] } // shorthand for { "agents": [...] }
|
|
99
|
+
"pi-subagents": { "agents": ["./agents"] }
|
|
100
|
+
"pi-subagents": ["./agents"] // shorthand for { "agents": [...] }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`pi.subagents` wins if a package carries both.
|
|
104
|
+
|
|
105
|
+
### Naming
|
|
106
|
+
|
|
107
|
+
A package agent registers under its frontmatter `name:`, with no namespace prefix. There is no `mypkg:researcher` form (yet — the `:` character stays reserved for it, so agent names may not contain one).
|
|
108
|
+
|
|
109
|
+
That makes a name a shared space, so prefix anything that could collide: `acme-researcher` rather than `researcher`. Two packages claiming one name is last-load-wins with a warning naming the file that took over, and *any* local file claiming it takes it from both.
|
|
110
|
+
|
|
111
|
+
## Operating
|
|
112
|
+
|
|
113
|
+
### Which packages are read
|
|
114
|
+
|
|
115
|
+
Only what pi itself has installed — the `packages[]` array in pi's `settings.json`, global (`~/.pi/agent/settings.json`) and project (`.pi/settings.json`), resolved through pi's own package manager so npm, git and local sources all land on the right install root.
|
|
116
|
+
|
|
117
|
+
`node_modules` is never scanned. Being a transitive dependency of your project means nothing here, exactly as it means nothing to pi.
|
|
118
|
+
|
|
119
|
+
A package listed only by a project's settings stays invisible until you trust the project, matching pi — an untrusted `.pi/settings.json` is not read at all. This is the shape a team distributes agents in: commit the package to `.pi/settings.json`, and everyone who trusts the repo gets them.
|
|
120
|
+
|
|
121
|
+
One wrinkle on the *very first* session in a repo, before pi has saved a trust decision. The `Agent` tool's description — the list of agent types the model is told about — is built when the extension loads, which is before the trust prompt is answered. A project-scoped package's agents are registered and can be spawned by name that session, but the model is not told they exist until the next one. Once the decision is saved (or `defaultProjectTrust` is `"always"`), every later session is correct from the first turn.
|
|
122
|
+
|
|
123
|
+
### Precedence
|
|
124
|
+
|
|
125
|
+
Lowest to highest:
|
|
126
|
+
|
|
127
|
+
| | Source |
|
|
128
|
+
|---|---|
|
|
129
|
+
| 1 (lowest) | Built-in agents (`general-purpose`, `Explore`, `Plan`) |
|
|
130
|
+
| 2 | Installed pi packages |
|
|
131
|
+
| 3 | `$PI_CODING_AGENT_DIR/agents/*.md` (global) |
|
|
132
|
+
| 4 | `.agents/agents/*.md` (project workspace) |
|
|
133
|
+
| 5 (highest) | `.pi/agents/*.md` (project) |
|
|
134
|
+
|
|
135
|
+
A package can offer a `reviewer`; it can never take the name from a `reviewer` of yours. This mirrors pi's own precedence for package-provided skills and Claude Code's for plugin agents.
|
|
136
|
+
|
|
137
|
+
Saved workflows resolve first-hit-wins over the same ordering, so package directories are searched last.
|
|
138
|
+
|
|
139
|
+
One consequence worth knowing: a package agent *does* outrank a built-in. A package could ship a `general-purpose` — which is also the default `fallbackSubagent` — and take over unspecified delegation. Narrow the gate below if that matters for your setup.
|
|
140
|
+
|
|
141
|
+
### In `/agents`
|
|
142
|
+
|
|
143
|
+
A package agent is listed with a `▪` badge and is read-only: its file lives under pi's install root, where an edit is lost on the next `pi update` and a delete is undone by the next `pi install`.
|
|
144
|
+
|
|
145
|
+
The badge says a package won, not which one. The declaring package's name — the full `@scope/name`, which is also what you can write into a [`packageAgents` allowlist](#turning-it-off) — is prefixed to the description line under the highlighted row, and titles the agent's action menu.
|
|
146
|
+
|
|
147
|
+
- **Eject** copies it into your project or personal agent directory as an editable file that shadows the original. The copy carries the agent's full configuration, including `ext:` tool selectors and `persist_session`.
|
|
148
|
+
- **Disable** writes an `enabled: false` stub at project or personal scope, which shadows the package definition without touching it. Deleting that stub restores the package agent.
|
|
149
|
+
|
|
150
|
+
An agent the *package author* shipped with `enabled: false` shows as disabled and offers no Enable — only the package can take that back. Ejecting it is the way out: the copy is written without an `enabled:` line, so the ejected agent is on.
|
|
151
|
+
|
|
152
|
+
### Turning it off
|
|
153
|
+
|
|
154
|
+
`packageAgents` and `packageWorkflows` in [`subagents.json`](../README.md#persistent-settings) both default to `true`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"packageAgents": ["my-subagents"],
|
|
159
|
+
"packageWorkflows": false
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Value | Effect |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `true` (or absent) | Every declaring package contributes. |
|
|
166
|
+
| `false` | None do. |
|
|
167
|
+
| `["a", "b"]` | Only those packages. |
|
|
168
|
+
|
|
169
|
+
A list entry matches the unscoped short name (`@acme/tools` → `tools`), the full package name, or the settings source string, case-insensitively. A bare string is read as a one-entry list, so a `"my-pkg"` typo narrows the gate rather than silently widening it.
|
|
170
|
+
|
|
171
|
+
The two are separate on purpose: an agent is markdown handed to a model, a workflow is JavaScript this extension executes, so package agents can be accepted while package workflow scripts are refused.
|
|
172
|
+
|
|
173
|
+
Both appear in `/agents → Settings` as on/off rows. An allowlist is hand-written in `subagents.json`; toggling the row replaces it, and the toast says what it replaced.
|
|
174
|
+
|
|
175
|
+
### Why on by default
|
|
176
|
+
|
|
177
|
+
Installing a package is already the trust decision. Pi executes an installed package's `extensions/` and puts its `skills/` descriptions in the system prompt, with no per-resource prompt — [pi's own docs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) say so plainly: *"Pi packages run with full system access."*
|
|
178
|
+
|
|
179
|
+
A declared `.md` agent is strictly less privileged than what the same package already runs, so it rides the boundary the user crossed at `pi install` rather than inventing a second one. The gate exists for narrowing that anyway, not as a second install step.
|
|
180
|
+
|
|
181
|
+
## Troubleshooting
|
|
182
|
+
|
|
183
|
+
**The agent does not appear.**
|
|
184
|
+
|
|
185
|
+
1. `pi list` — is the package in pi's settings at all? Discovery reads that array, not `node_modules`, and not a `pi -e` source: a `--extension` install is resolved for that one run and never written to `packages[]`, so its agents stay invisible even though its extensions and skills load.
|
|
186
|
+
2. Does `package.json` carry `pi.subagents` (or `pi-subagents`)? A `pi` key that declares only `extensions` contributes no agents; the conventional `agents/` directory is not scanned.
|
|
187
|
+
3. Is the file `*.md` directly inside a declared directory? The scan is not recursive.
|
|
188
|
+
4. Does the frontmatter parse, and does it have a `name:`? A broken file is skipped with a `[pi-subagents] Skipping agent file …` warning on stderr.
|
|
189
|
+
5. Is `packageAgents` set to `false` or to a list that does not include it? Check `/agents → Settings → Package agents`.
|
|
190
|
+
6. Is the package configured only in an untrusted project's `.pi/settings.json`? Trust the project, or install it globally.
|
|
191
|
+
|
|
192
|
+
**It appears but a different one runs.** A local file of the same name outranks it. `/agents` shows the badge of whichever won — `•` project, `◦` global, `▪` package.
|
|
193
|
+
|
|
194
|
+
**Edits to the package file do nothing.** Agents reload on every `Agent` call, so a local-path install picks up edits immediately. An npm or git install is a copy under `~/.pi/agent/{npm,git}/`; edit the source and reinstall, or install the local path during development.
|
|
195
|
+
|
|
196
|
+
**A file over 1 MB is skipped.** Package files are third-party content re-read on every spawn, so there is a per-file ceiling, matching Claude Code's for plugin agents. Local files are unbounded.
|
|
197
|
+
|
|
198
|
+
**`strictAgentFiles` and a broken package file.** Strict mode fails startup over an unparseable file in *your* `.pi/agents/`, which is the point of it. It deliberately does not extend to files inside installed packages — those are not yours to fix, and one bad `.md` in a dependency should not stop pi from starting.
|
|
199
|
+
|
|
200
|
+
## Reference
|
|
201
|
+
|
|
202
|
+
| | |
|
|
203
|
+
|---|---|
|
|
204
|
+
| Manifest keys | `pi.subagents.agents`, `pi.subagents.workflows`, `pi-subagents.agents`, `pi-subagents.workflows`, plus the bare-array shorthand for each |
|
|
205
|
+
| Settings | `packageAgents`, `packageWorkflows` — `boolean \| string[]`, default `true` |
|
|
206
|
+
| Agent file layout | `<declared dir>/*.md`, non-recursive |
|
|
207
|
+
| Workflow file layout | `<declared dir>/*.js`, must carry `export const meta = …` |
|
|
208
|
+
| Per-file size ceiling | 1 MiB |
|
|
209
|
+
| Discovery source | pi's `settings.json → packages[]`, global and project |
|
|
210
|
+
| Precedence | Above built-ins, below every local agent |
|
|
211
|
+
| Implementation | [`src/package-resources.ts`](../src/package-resources.ts) |
|