@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,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package-resources.ts — Discover agent and workflow files that installed pi
|
|
3
|
+
* packages declare in their `package.json`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Pi already ships four resource types inside packages — `extensions`, `skills`,
|
|
8
|
+
* `prompts`, `themes` — resolved from a `pi` manifest key or, when there is no
|
|
9
|
+
* `pi` key at all, from convention directories. Subagents are *our* concept, so
|
|
10
|
+
* pi knows nothing about them and a package author's only option was to copy
|
|
11
|
+
* `.md` files into `.pi/agents/` on every container start (#109).
|
|
12
|
+
*
|
|
13
|
+
* ## The contract
|
|
14
|
+
*
|
|
15
|
+
* Two-sided, deliberately:
|
|
16
|
+
*
|
|
17
|
+
* 1. The package author opts in by naming the paths in `package.json`.
|
|
18
|
+
* 2. The user opts in by running `pi install`, which is already the trust
|
|
19
|
+
* decision for that package — pi executes its `extensions/` and injects its
|
|
20
|
+
* `skills/` into the system prompt with no further prompt. A declared `.md`
|
|
21
|
+
* agent is strictly less privileged than either, so it rides the boundary
|
|
22
|
+
* the user already crossed rather than inventing a second one.
|
|
23
|
+
*
|
|
24
|
+
* There is deliberately **no convention-directory fallback**. Pi scans `skills/`
|
|
25
|
+
* only when a package has no `pi` key whatsoever; we never scan an undeclared
|
|
26
|
+
* `agents/`. A package that happens to carry an `agents/` folder for some other
|
|
27
|
+
* tool must not start contributing subagents to pi because it was installed for
|
|
28
|
+
* an unrelated reason.
|
|
29
|
+
*
|
|
30
|
+
* ## Which packages are visible
|
|
31
|
+
*
|
|
32
|
+
* Only what pi itself has configured — `settings.json -> packages[]`, global and
|
|
33
|
+
* project — read through pi's own `DefaultPackageManager.listConfiguredPackages()`.
|
|
34
|
+
* That call is synchronous, pure, and never installs anything; it also resolves
|
|
35
|
+
* npm / git / local install roots, pnpm global roots and legacy fallbacks, which
|
|
36
|
+
* is why it is worth borrowing rather than reimplementing.
|
|
37
|
+
*
|
|
38
|
+
* `node_modules` is never scanned. Being a transitive dependency of the user's
|
|
39
|
+
* project means nothing here — the same rule pi applies to itself.
|
|
40
|
+
*
|
|
41
|
+
* ## Accepted manifest spellings
|
|
42
|
+
*
|
|
43
|
+
* All four mean the same thing. The `pi-subagents` top-level key exists for
|
|
44
|
+
* cross-compatibility with `nicobailon/pi-subagents`, which reads the same two
|
|
45
|
+
* shapes, so a package author writes one manifest that works on either extension.
|
|
46
|
+
*
|
|
47
|
+
* "pi": { "subagents": { "agents": ["./agents"], "workflows": ["./flows"] } }
|
|
48
|
+
* "pi": { "subagents": ["./agents"] } // shorthand for { agents }
|
|
49
|
+
* "pi-subagents": { "agents": ["./agents"] }
|
|
50
|
+
* "pi-subagents": ["./agents"] // shorthand for { agents }
|
|
51
|
+
*
|
|
52
|
+
* Adding `pi.subagents` is safe for pi itself: its `readPiManifest` reads only
|
|
53
|
+
* `extensions`/`skills`/`prompts`/`themes` and ignores every other key.
|
|
54
|
+
*/
|
|
55
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
56
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
57
|
+
import { DefaultPackageManager, getAgentDir, ProjectTrustStore, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
58
|
+
/** File extension scanned for each kind, mirroring the local roots for each. */
|
|
59
|
+
const KIND_EXTENSION = {
|
|
60
|
+
agents: ".md",
|
|
61
|
+
workflows: ".js",
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Per-file size ceiling for package-provided resources, matching Claude Code's
|
|
65
|
+
* plugin-agent limit. These files come from third-party packages and are read
|
|
66
|
+
* eagerly on every agent load; a pathological one should be skipped, not
|
|
67
|
+
* `readFileSync`'d into the session.
|
|
68
|
+
*/
|
|
69
|
+
export const MAX_PACKAGE_RESOURCE_BYTES = 1_048_576;
|
|
70
|
+
// ---- Session state ----
|
|
71
|
+
//
|
|
72
|
+
// Held here rather than threaded through every caller, matching how the rest of
|
|
73
|
+
// this extension carries session-wide settings (`setMaxSubagentDepth`,
|
|
74
|
+
// `setWorktreeIsolationEnabled`, ...). The alternative was an options bag
|
|
75
|
+
// travelling from `index.ts` through `resolveWorkflowScript`,
|
|
76
|
+
// `resolveWorkflowSource`, `readSavedWorkflow` and `listSavedWorkflows` purely
|
|
77
|
+
// so a gate could be consulted at the bottom.
|
|
78
|
+
//
|
|
79
|
+
// Both gates start `undefined`, which reads as `true`: absent settings mean
|
|
80
|
+
// every declaring package contributes, matching pi's own default for the
|
|
81
|
+
// resources it owns.
|
|
82
|
+
let agentsGate;
|
|
83
|
+
let workflowsGate;
|
|
84
|
+
/**
|
|
85
|
+
* Defaults to `false` — an extension activates before any session context
|
|
86
|
+
* exists, and reading an untrusted project's package list only to take it away
|
|
87
|
+
* again is the wrong direction to fail. `index.ts` corrects it from
|
|
88
|
+
* `ctx.isProjectTrusted()` on `session_start`.
|
|
89
|
+
*/
|
|
90
|
+
let projectTrustedState = false;
|
|
91
|
+
/** Apply the `packageAgents` setting. Drops the cache, since roots may change. */
|
|
92
|
+
export function setPackageAgentsGate(gate) {
|
|
93
|
+
agentsGate = gate;
|
|
94
|
+
invalidatePackageCache();
|
|
95
|
+
}
|
|
96
|
+
/** Apply the `packageWorkflows` setting. Drops the cache, since roots may change. */
|
|
97
|
+
export function setPackageWorkflowsGate(gate) {
|
|
98
|
+
workflowsGate = gate;
|
|
99
|
+
invalidatePackageCache();
|
|
100
|
+
}
|
|
101
|
+
export function getPackageAgentsGate() {
|
|
102
|
+
return agentsGate;
|
|
103
|
+
}
|
|
104
|
+
export function getPackageWorkflowsGate() {
|
|
105
|
+
return workflowsGate;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Seed the trust state from pi's *saved* decision, before any session exists.
|
|
109
|
+
*
|
|
110
|
+
* The correction on `session_start` is too late for one thing that matters: the
|
|
111
|
+
* `Agent` tool's description — the list of agent types the model is told about —
|
|
112
|
+
* is built once at activation, so an agent registered after it never reaches the
|
|
113
|
+
* model even though dispatch would accept it. That is exactly the shape a team
|
|
114
|
+
* ships agents in: a package named in a committed `.pi/settings.json`, which is
|
|
115
|
+
* invisible until the project is trusted.
|
|
116
|
+
*
|
|
117
|
+
* Pi keeps that decision in `<agentDir>/trust.json`, keyed by canonical path
|
|
118
|
+
* with the nearest ancestor winning, so on the second and every later session in
|
|
119
|
+
* a trusted repo the answer is already on disk. No saved decision falls back to
|
|
120
|
+
* the global `defaultProjectTrust`, which is what pi does; `"ask"` resolves to
|
|
121
|
+
* untrusted here, because the prompt has not been answered yet and guessing yes
|
|
122
|
+
* would be the wrong direction to guess.
|
|
123
|
+
*
|
|
124
|
+
* Best-effort by design: any failure leaves the state untrusted, and
|
|
125
|
+
* `session_start` still delivers pi's real answer either way.
|
|
126
|
+
*/
|
|
127
|
+
export function seedProjectTrust(cwd) {
|
|
128
|
+
try {
|
|
129
|
+
if (typeof ProjectTrustStore !== "function")
|
|
130
|
+
return;
|
|
131
|
+
const agentDir = getAgentDir();
|
|
132
|
+
const saved = new ProjectTrustStore(agentDir).get(cwd);
|
|
133
|
+
if (typeof saved === "boolean") {
|
|
134
|
+
setProjectTrusted(saved);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
138
|
+
setProjectTrusted(settings.getGlobalSettings().defaultProjectTrust === "always");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Leave it untrusted; session_start corrects it.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Record pi's project-trust answer. Drops the cache when the answer changes,
|
|
146
|
+
* since a project's `packages[]` becomes visible or invisible with it. A repeat
|
|
147
|
+
* of the same answer is a no-op — `session_start` fires on every new, resumed
|
|
148
|
+
* and forked session, and it has its own reason to invalidate.
|
|
149
|
+
*/
|
|
150
|
+
export function setProjectTrusted(trusted) {
|
|
151
|
+
if (projectTrustedState === trusted)
|
|
152
|
+
return;
|
|
153
|
+
projectTrustedState = trusted;
|
|
154
|
+
invalidatePackageCache();
|
|
155
|
+
}
|
|
156
|
+
/** Reset every session-scoped value. Tests only — the extension never un-loads. */
|
|
157
|
+
export function resetPackageState() {
|
|
158
|
+
agentsGate = undefined;
|
|
159
|
+
workflowsGate = undefined;
|
|
160
|
+
projectTrustedState = false;
|
|
161
|
+
invalidatePackageCache();
|
|
162
|
+
}
|
|
163
|
+
// ---- Manifest reading ----
|
|
164
|
+
/** A string array, or undefined for anything else. Mirrors pi's `readPiManifest` strictness. */
|
|
165
|
+
function stringArray(val) {
|
|
166
|
+
return Array.isArray(val) && val.every(e => typeof e === "string") ? val : undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Normalize one accepted spelling into `{ agents, workflows }`.
|
|
170
|
+
* A bare array is the `agents` shorthand — the only kind that existed when the
|
|
171
|
+
* shorthand was worth having, and the shape the other fork's docs show.
|
|
172
|
+
*/
|
|
173
|
+
function normalizeDeclaration(val) {
|
|
174
|
+
const asArray = stringArray(val);
|
|
175
|
+
if (asArray)
|
|
176
|
+
return { agents: asArray };
|
|
177
|
+
if (!val || typeof val !== "object" || Array.isArray(val))
|
|
178
|
+
return undefined;
|
|
179
|
+
const obj = val;
|
|
180
|
+
const agents = stringArray(obj.agents);
|
|
181
|
+
const workflows = stringArray(obj.workflows);
|
|
182
|
+
if (!agents && !workflows)
|
|
183
|
+
return undefined;
|
|
184
|
+
return { agents, workflows };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read the subagent declaration out of a parsed `package.json`.
|
|
188
|
+
*
|
|
189
|
+
* `pi.subagents` wins over the top-level `pi-subagents` when a package carries
|
|
190
|
+
* both — the pi-namespaced key is the primary spelling here, and a package that
|
|
191
|
+
* writes both almost certainly means them to be identical anyway.
|
|
192
|
+
*/
|
|
193
|
+
export function readSubagentManifest(pkg) {
|
|
194
|
+
if (!pkg || typeof pkg !== "object")
|
|
195
|
+
return undefined;
|
|
196
|
+
const obj = pkg;
|
|
197
|
+
const piKey = obj.pi;
|
|
198
|
+
if (piKey && typeof piKey === "object" && !Array.isArray(piKey)) {
|
|
199
|
+
const nested = normalizeDeclaration(piKey.subagents);
|
|
200
|
+
if (nested)
|
|
201
|
+
return nested;
|
|
202
|
+
}
|
|
203
|
+
return normalizeDeclaration(obj["pi-subagents"]);
|
|
204
|
+
}
|
|
205
|
+
/** Parse `<root>/package.json`, or undefined when it is missing or malformed. */
|
|
206
|
+
function readPackageJson(root) {
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
209
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** `@scope/foo` becomes `foo`, lowercased. The name a user types in an allowlist. */
|
|
216
|
+
export function unscopedShortName(name) {
|
|
217
|
+
const short = name.startsWith("@") ? name.slice(name.indexOf("/") + 1) : name;
|
|
218
|
+
return short.toLowerCase();
|
|
219
|
+
}
|
|
220
|
+
// ---- Path resolution ----
|
|
221
|
+
/**
|
|
222
|
+
* Canonical absolute path, resolving symlinks as far as the path exists.
|
|
223
|
+
*
|
|
224
|
+
* A plain `realpathSync` is not enough: an exclusion may name a file the package
|
|
225
|
+
* does not ship yet, and a bare `resolve()` fallback would leave it uncanonical
|
|
226
|
+
* — which on macOS means `/var/...` compared against a canonical `/private/var/...`
|
|
227
|
+
* root, so the containment check below rejects it. Canonicalizing the nearest
|
|
228
|
+
* existing ancestor and re-appending the rest keeps both sides comparable.
|
|
229
|
+
*/
|
|
230
|
+
function realOrSelf(path) {
|
|
231
|
+
const abs = resolve(path);
|
|
232
|
+
try {
|
|
233
|
+
return realpathSync(abs);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
const parent = dirname(abs);
|
|
237
|
+
return parent === abs ? abs : join(realOrSelf(parent), basename(abs));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* True when `candidate` is inside `root`. Package manifests are third-party
|
|
242
|
+
* input, so `"../../.ssh"` must not become a scanned directory — pi refuses the
|
|
243
|
+
* same shape with "Refusing to use path outside package install root".
|
|
244
|
+
*
|
|
245
|
+
* Compared on canonical paths, because `..` is not the only way out: a symlinked
|
|
246
|
+
* `agents/` pointing at `/etc` is textually inside the root, and `readdirSync`
|
|
247
|
+
* follows it. Both sides are canonicalized so a package installed *under* a
|
|
248
|
+
* symlink (`/var` → `/private/var` on macOS) still matches itself.
|
|
249
|
+
*/
|
|
250
|
+
function isInside(root, candidate) {
|
|
251
|
+
const rel = relative(realOrSelf(root), realOrSelf(candidate));
|
|
252
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Resolve one manifest entry against the package root, returning the path only
|
|
256
|
+
* when it stays inside the package and exists.
|
|
257
|
+
*
|
|
258
|
+
* Globs are deliberately not supported. Pi's own manifest expands them because
|
|
259
|
+
* it resolves four resource types across arbitrary layouts; here the entries are
|
|
260
|
+
* one or two directories, and a glob would mean walking a third-party tree to
|
|
261
|
+
* answer. A directory or a file path covers the declared use cases.
|
|
262
|
+
*/
|
|
263
|
+
function expandEntry(root, entry) {
|
|
264
|
+
const target = resolve(root, entry);
|
|
265
|
+
if (!isInside(root, target) || !existsSync(target))
|
|
266
|
+
return undefined;
|
|
267
|
+
return target;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resolve one kind's declared entries into concrete absolute paths inside the
|
|
271
|
+
* package: directories to scan, and individual files.
|
|
272
|
+
*
|
|
273
|
+
* Split into `{ dirs, files }` rather than a flat list because the agent loader
|
|
274
|
+
* takes directories (it does its own `readdirSync` and frontmatter handling),
|
|
275
|
+
* while a manifest entry naming a single file has to be surfaced on its own.
|
|
276
|
+
*
|
|
277
|
+
* `!entry` marks an exclusion. Exact paths only — pi's own `!` is gitignore-style
|
|
278
|
+
* glob negation, and globs are not supported here for the reason above.
|
|
279
|
+
*/
|
|
280
|
+
export function resolveDeclaredPaths(root, entries, kind) {
|
|
281
|
+
const dirs = [];
|
|
282
|
+
const files = [];
|
|
283
|
+
const excluded = new Set();
|
|
284
|
+
if (!entries)
|
|
285
|
+
return { dirs, files, excluded };
|
|
286
|
+
// Exclusions are collected first and stored canonically, so `!./agents/wip.md`
|
|
287
|
+
// matches the same file however the loader spelled its way to it. They do not
|
|
288
|
+
// have to exist: a package may exclude a path it ships conditionally.
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (!entry.startsWith("!"))
|
|
291
|
+
continue;
|
|
292
|
+
const target = resolve(root, entry.slice(1));
|
|
293
|
+
if (isInside(root, target))
|
|
294
|
+
excluded.add(realOrSelf(target));
|
|
295
|
+
}
|
|
296
|
+
const ext = KIND_EXTENSION[kind];
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (entry.startsWith("!"))
|
|
299
|
+
continue;
|
|
300
|
+
const path = expandEntry(root, entry);
|
|
301
|
+
if (!path || excluded.has(realOrSelf(path)))
|
|
302
|
+
continue;
|
|
303
|
+
let stat;
|
|
304
|
+
try {
|
|
305
|
+
stat = statSync(path);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (stat.isDirectory()) {
|
|
311
|
+
if (!dirs.includes(path))
|
|
312
|
+
dirs.push(path);
|
|
313
|
+
}
|
|
314
|
+
else if (stat.isFile() && path.endsWith(ext) && !files.includes(path)) {
|
|
315
|
+
files.push(path);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { dirs, files, excluded };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Build a read-only view of pi's configured packages.
|
|
322
|
+
*
|
|
323
|
+
* Feature-detected rather than assumed: the peer range `>=1.1.0` is open-ended,
|
|
324
|
+
* so a later pi that drops or renames this API contributes no packages instead
|
|
325
|
+
* of failing activation.
|
|
326
|
+
*/
|
|
327
|
+
function configuredPackages(cwd, projectTrusted) {
|
|
328
|
+
try {
|
|
329
|
+
if (typeof DefaultPackageManager !== "function")
|
|
330
|
+
return [];
|
|
331
|
+
const agentDir = getAgentDir();
|
|
332
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
|
|
333
|
+
const manager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
334
|
+
if (typeof manager.listConfiguredPackages !== "function")
|
|
335
|
+
return [];
|
|
336
|
+
return manager.listConfiguredPackages();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Every installed pi package that declares subagent resources, in pi's own
|
|
344
|
+
* order (global entries first, then project). Duplicates by root are collapsed —
|
|
345
|
+
* a package listed in both scopes is one package on disk.
|
|
346
|
+
*/
|
|
347
|
+
export function listPiPackages(cwd, projectTrusted = false) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
const out = [];
|
|
350
|
+
for (const entry of configuredPackages(cwd, projectTrusted)) {
|
|
351
|
+
const root = entry.installedPath;
|
|
352
|
+
if (!root || seen.has(root))
|
|
353
|
+
continue;
|
|
354
|
+
const manifest = readPackageJson(root);
|
|
355
|
+
const declared = manifest ? readSubagentManifest(manifest) : undefined;
|
|
356
|
+
if (!manifest || !declared)
|
|
357
|
+
continue;
|
|
358
|
+
seen.add(root);
|
|
359
|
+
const name = typeof manifest.name === "string" ? manifest.name : undefined;
|
|
360
|
+
out.push({
|
|
361
|
+
source: entry.source,
|
|
362
|
+
name,
|
|
363
|
+
shortName: name ? unscopedShortName(name) : undefined,
|
|
364
|
+
scope: entry.scope,
|
|
365
|
+
root,
|
|
366
|
+
declared,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Whether `pkg` passes the gate. `true`/omitted admits everything; an array
|
|
373
|
+
* matches case-insensitively against the unscoped short name, the full package
|
|
374
|
+
* name, or the settings source string — users reach for whichever of the three
|
|
375
|
+
* they happen to have in front of them.
|
|
376
|
+
*/
|
|
377
|
+
export function packageAllowed(pkg, gate) {
|
|
378
|
+
if (gate === false)
|
|
379
|
+
return false;
|
|
380
|
+
if (gate === undefined || gate === true)
|
|
381
|
+
return true;
|
|
382
|
+
const candidates = new Set([pkg.shortName, pkg.name?.toLowerCase(), pkg.source.toLowerCase()].filter((v) => typeof v === "string"));
|
|
383
|
+
return gate.some(allowed => candidates.has(allowed.trim().toLowerCase()));
|
|
384
|
+
}
|
|
385
|
+
/** A resolved-paths value with nothing in it. Fresh each call — callers get copies. */
|
|
386
|
+
function emptyPaths() {
|
|
387
|
+
return { dirs: [], files: [], excluded: new Set() };
|
|
388
|
+
}
|
|
389
|
+
/** A whole cache entry with nothing in it, for a gate that admits no package. */
|
|
390
|
+
function emptyEntry() {
|
|
391
|
+
return { agents: emptyPaths(), workflows: emptyPaths(), owners: [] };
|
|
392
|
+
}
|
|
393
|
+
const cache = new Map();
|
|
394
|
+
/** Drop the memoized package scan. Called on `/reload` and after settings changes. */
|
|
395
|
+
export function invalidatePackageCache() {
|
|
396
|
+
cache.clear();
|
|
397
|
+
}
|
|
398
|
+
function scan(cwd, projectTrusted, gate) {
|
|
399
|
+
const entry = emptyEntry();
|
|
400
|
+
for (const pkg of listPiPackages(cwd, projectTrusted)) {
|
|
401
|
+
if (!packageAllowed(pkg, gate))
|
|
402
|
+
continue;
|
|
403
|
+
// The root is normalized here, not at lookup time: the declared paths below
|
|
404
|
+
// go through `resolve(root, entry)`, so a root that arrived with a trailing
|
|
405
|
+
// separator would never prefix-match the very paths it produced.
|
|
406
|
+
//
|
|
407
|
+
// `name` is absent only when the manifest had no usable `name` string; the
|
|
408
|
+
// settings source (`npm:@foo/bar@1.0.0`) is the last resort, which is what
|
|
409
|
+
// pi itself falls back to when it labels a package-provided resource.
|
|
410
|
+
entry.owners.push({ root: resolve(pkg.root), name: pkg.name ?? pkg.shortName ?? pkg.source });
|
|
411
|
+
for (const kind of ["agents", "workflows"]) {
|
|
412
|
+
const resolvedPaths = resolveDeclaredPaths(pkg.root, pkg.declared[kind], kind);
|
|
413
|
+
entry[kind].dirs.push(...resolvedPaths.dirs);
|
|
414
|
+
entry[kind].files.push(...resolvedPaths.files);
|
|
415
|
+
// One flat set across packages: an exclusion is an absolute path, so it
|
|
416
|
+
// can only ever match inside the package that wrote it.
|
|
417
|
+
for (const path of resolvedPaths.excluded)
|
|
418
|
+
entry[kind].excluded.add(path);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Longest root first, so a package installed inside another's `node_modules`
|
|
422
|
+
// claims its own files rather than losing them to its host.
|
|
423
|
+
entry.owners.sort((a, b) => b.root.length - a.root.length);
|
|
424
|
+
return entry;
|
|
425
|
+
}
|
|
426
|
+
function cacheEntryFor(cwd, kind, opts) {
|
|
427
|
+
const gate = opts.gate ?? (kind === "agents" ? agentsGate : workflowsGate);
|
|
428
|
+
// An empty allowlist matches nothing, like `false` — see `sanitizePackageGate`
|
|
429
|
+
// in settings.ts for why an empty array is kept rather than dropped.
|
|
430
|
+
if (gate === false || (Array.isArray(gate) && gate.length === 0))
|
|
431
|
+
return emptyEntry();
|
|
432
|
+
const projectTrusted = opts.projectTrusted ?? projectTrustedState;
|
|
433
|
+
// `\u0000` written as an escape, not a raw byte: a literal NUL makes this
|
|
434
|
+
// whole file read as binary to grep, ripgrep and git, which silently drops
|
|
435
|
+
// it from search results.
|
|
436
|
+
const key = `${cwd}\u0000${projectTrusted ? 1 : 0}\u0000${JSON.stringify(gate ?? true)}`;
|
|
437
|
+
let hit = cache.get(key);
|
|
438
|
+
if (!hit) {
|
|
439
|
+
hit = scan(cwd, projectTrusted, gate);
|
|
440
|
+
cache.set(key, hit);
|
|
441
|
+
}
|
|
442
|
+
return hit;
|
|
443
|
+
}
|
|
444
|
+
function resolved(cwd, kind, opts) {
|
|
445
|
+
return cacheEntryFor(cwd, kind, opts)[kind];
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Name of the installed package that provided `path`, or undefined when no
|
|
449
|
+
* admitted package owns it. What `/agents` shows so a package agent can name its
|
|
450
|
+
* origin, and what a user needs in order to write a `packageAgents` allowlist.
|
|
451
|
+
*
|
|
452
|
+
* `kind` selects which gate to consult, because the two are configured
|
|
453
|
+
* separately: a package may be admitted for agents but not for workflows.
|
|
454
|
+
*
|
|
455
|
+
* A plain prefix test is enough. `resolveDeclaredPaths` builds every declared
|
|
456
|
+
* path as `resolve(root, entry)` without canonicalizing it — only the
|
|
457
|
+
* containment check and the exclusion set go through `realOrSelf` — so a
|
|
458
|
+
* `sourcePath` the loader derived from one is textually inside the root it came
|
|
459
|
+
* from. The `sep` anchor keeps `…/node_modules/foo` from claiming
|
|
460
|
+
* `…/node_modules/foo-bar`.
|
|
461
|
+
*/
|
|
462
|
+
export function packageNameForPath(path, cwd, kind = "agents", opts = {}) {
|
|
463
|
+
const abs = resolve(path);
|
|
464
|
+
return cacheEntryFor(cwd, kind, opts).owners.find(o => abs === o.root || abs.startsWith(o.root + sep))?.name;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Directories of package-declared agent files, lowest-precedence first.
|
|
468
|
+
* Fed to `loadCustomAgents` ahead of the global and project roots.
|
|
469
|
+
*/
|
|
470
|
+
export function packageAgentDirs(cwd, opts = {}) {
|
|
471
|
+
// A copy: the cache holds this array for the rest of the session, so a caller
|
|
472
|
+
// that sorted or spliced it would corrupt every later load.
|
|
473
|
+
return [...resolved(cwd, "agents", opts).dirs];
|
|
474
|
+
}
|
|
475
|
+
/** Individually declared agent `.md` files (a manifest entry naming a file, not a directory). */
|
|
476
|
+
export function packageAgentFiles(cwd, opts = {}) {
|
|
477
|
+
return [...resolved(cwd, "agents", opts).files];
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Whether a `!` entry in some package manifest excludes this path.
|
|
481
|
+
*
|
|
482
|
+
* Checked at load time rather than subtracted during resolution, because the
|
|
483
|
+
* common exclusion — `["./agents", "!./agents/wip.md"]` — names a file inside a
|
|
484
|
+
* directory that is not enumerated until the loader reads it.
|
|
485
|
+
*/
|
|
486
|
+
export function isPackageResourceExcluded(path, cwd, kind, opts = {}) {
|
|
487
|
+
const excluded = resolved(cwd, kind, opts).excluded;
|
|
488
|
+
return excluded.size > 0 && excluded.has(realOrSelf(path));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Directories of package-declared workflow scripts, appended to
|
|
492
|
+
* `savedWorkflowRoots` so a name resolves there last.
|
|
493
|
+
*/
|
|
494
|
+
export function packageWorkflowDirs(cwd, opts = {}) {
|
|
495
|
+
return [...resolved(cwd, "workflows", opts).dirs];
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Individually declared workflow scripts, as `name -> absolute path`.
|
|
499
|
+
*
|
|
500
|
+
* A name lookup wants a directory, and a declared *file* has none — but standing
|
|
501
|
+
* its parent directory in as a root would make every sibling `.js` resolvable,
|
|
502
|
+
* so `workflows: ["./one.js"]` would quietly offer the package's other scripts
|
|
503
|
+
* too, and `["./index.js"]` would offer everything at the package root. That is
|
|
504
|
+
* the opposite of the rule the whole feature rests on, that a package only
|
|
505
|
+
* contributes what it names. An exact map keeps the declaration honest, and
|
|
506
|
+
* matches how a declared agent file is already handled.
|
|
507
|
+
*
|
|
508
|
+
* Later entries lose a name clash, so a package's own directory-sourced script
|
|
509
|
+
* is not displaced by a file entry from a package resolved after it.
|
|
510
|
+
*/
|
|
511
|
+
export function packageWorkflowFiles(cwd, opts = {}) {
|
|
512
|
+
const out = new Map();
|
|
513
|
+
for (const path of resolved(cwd, "workflows", opts).files) {
|
|
514
|
+
const name = basename(path, KIND_EXTENSION.workflows);
|
|
515
|
+
if (!out.has(name))
|
|
516
|
+
out.set(name, path);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts.ts — System prompt builder for agents.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentConfig, EnvInfo } from "./types.js";
|
|
5
|
+
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
|
6
|
+
export interface PromptExtras {
|
|
7
|
+
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
|
8
|
+
memoryBlock?: string;
|
|
9
|
+
/** Preloaded skill contents to inject. */
|
|
10
|
+
skillBlocks?: {
|
|
11
|
+
name: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}[];
|
|
14
|
+
/**
|
|
15
|
+
* Parent directory the worktree copy was created from. Set only for
|
|
16
|
+
* `isolation: "worktree"` spawns — triggers the block that tells the agent
|
|
17
|
+
* to stay in the copy.
|
|
18
|
+
*/
|
|
19
|
+
worktreeBase?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Set only for a workflow's own children, and only when they have no
|
|
22
|
+
* `StructuredOutput` tool to answer through.
|
|
23
|
+
*
|
|
24
|
+
* A workflow child's final text is not read by a human — it is the value
|
|
25
|
+
* `agent()` resolves to, and the script interpolates it straight into the
|
|
26
|
+
* next stage's prompt. Without this, children answer the way every other
|
|
27
|
+
* subagent does (a report addressed to a reader), and the padding becomes
|
|
28
|
+
* input tokens for the stage downstream. Claude Code's `Workflow` tool
|
|
29
|
+
* documents this contract to the script-writing model; this is the end of it
|
|
30
|
+
* that makes the documentation true.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately NOT applied to every subagent. In pi an ordinary agent's
|
|
33
|
+
* output IS read by a human — through FleetView, the conversation viewer and
|
|
34
|
+
* `get_subagent_result` — so terse raw data would be the wrong answer there.
|
|
35
|
+
*/
|
|
36
|
+
workflowChild?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the system prompt for an agent from its config.
|
|
40
|
+
*
|
|
41
|
+
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
42
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
43
|
+
* - "append" with empty systemPrompt: pure parent clone
|
|
44
|
+
*
|
|
45
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
46
|
+
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
47
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
48
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
49
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
50
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
51
|
+
*
|
|
52
|
+
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
53
|
+
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildAgentPrompt(config: AgentConfig, cwd: string, env: EnvInfo, parentSystemPrompt?: string, extras?: PromptExtras): string;
|
|
56
|
+
/**
|
|
57
|
+
* The part of the parent's rendered system prompt that an append-mode child inherits.
|
|
58
|
+
*
|
|
59
|
+
* Pi renders a prompt as the preamble followed by tagged sections in a fixed order:
|
|
60
|
+
* `tools`, `rules`, `docs` (default preamble only), `addendum`, `project_context`,
|
|
61
|
+
* `skills`, `cwd`, then the sections extensions add during a turn. The child session renders its own `skills` (from its `skills:`
|
|
62
|
+
* setting), its own `cwd` and its own extension sections after the inherited text.
|
|
63
|
+
* Inheriting the parent's copies too listed them twice, showed the parent's skills to
|
|
64
|
+
* children configured with `skills: false` or a preload list, and left copies that
|
|
65
|
+
* section-editing extensions in the child cannot reach. It also made the result depend
|
|
66
|
+
* on timing: `getSystemPrompt()` includes the extension sections only while the
|
|
67
|
+
* parent's turn runs.
|
|
68
|
+
*
|
|
69
|
+
* Returns the text before the `skills` section, or before the `cwd` section when there
|
|
70
|
+
* are no skills. A prompt without the `cwd` section (for example, a forced prompt) is
|
|
71
|
+
* returned unchanged.
|
|
72
|
+
*
|
|
73
|
+
* @param rendered The parent's `ctx.getSystemPrompt()`.
|
|
74
|
+
* @param cwd The cwd the parent's prompt was rendered with.
|
|
75
|
+
*/
|
|
76
|
+
export declare function inheritableParentPrompt(rendered: string, cwd: string): string;
|