feat(agents): discover subagents and workflows declared by installed pi packages - #262
Open
tintinweb wants to merge 4 commits into
Open
feat(agents): discover subagents and workflows declared by installed pi packages#262tintinweb wants to merge 4 commits into
tintinweb wants to merge 4 commits into
Conversation
…nt the feature Follow-up to the feature commit; all of it is still unreleased. - package-resources.ts held two literal NUL bytes as cache-key separators, so the whole module read as binary and ripgrep silently skipped it. - Eject dropped `ext:` tool selectors, `persist_session` and `session_dir` from a package agent. The ejected copy shadows the package file, so the running agent silently lost tools. Only reachable since package agents became ejectable — a built-in default carries none of those fields. - `strictAgentFiles` aborted activation over an unparseable `.md` inside an installed package. Strict mode now covers only files the user owns. - `!` manifest exclusions were inert against a file inside an included directory, which is the only form that does anything. Now applied at load time, for agents and workflows. - `"packageAgents": "my-pkg"` was dropped as malformed and fell back to the `true` default, silently admitting every package. Read as a one-entry list. - A symlinked `agents/` pointing out of the package passed the root check. Containment now compares canonical paths. - A workflow declared as a single file promoted its parent directory to a name-resolution root, exposing scripts the package never declared. Declared files now resolve through an exact name map. - Project trust is seeded at activation from pi's saved decision. The Agent tool description is built before `session_start`, so a package named in a committed `.pi/settings.json` was dispatchable but never advertised to the model — the team-distribution case from #109. - Carry the manifest declaration on PiPackage instead of re-reading it. Adds docs/packages.md and trims the README section to a summary that links it.
tintinweb
marked this pull request as ready for review
August 27, 2026 13:26
Owner
Author
|
the square should be package based but I see it is not in the agent types list 👍 edit: should probably also list which package it's coming from :D |
Owner
Author
The `▪` package badge shipped without a legend key, so a package agent rendered a glyph nothing on screen explained — and the badge says only that *some* package won, not which one, which is also the string a user needs for a `packageAgents` allowlist. - package-resources: the memoized scan now records each admitted package's normalized root and display name, longest root first, so a package nested in another's node_modules claims its own files. New `packageNameForPath()` traces a loaded `sourcePath` back to it. No extra fs work; inherits the existing gate/trust keying and cache invalidation. - agent-source-badge (new): `sourceIndicator`/`sourceLegend`/ `rowDescription`, extracted from the `/agents` closure the way agent-file-toggle.ts was, since `registerCommand` is mocked in every wiring test. The legend is now derived from the badges actually on screen, so a glyph can't ship without its key again — and a roster whose only custom agent is disabled no longer renders `✕•` with no key, nor a global-only roster a `•` that appears nowhere. - /agents: a package row prefixes the highlighted-row description with the full `@scope/name`; the detail menu is titled `<name> — <package>`. No new column, row, or grouping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Closes #109, except for the package-scoped runtime names that issue also proposes (
basic-package.package-researcher) — see Behavior and compatibility. Every number and state below re-verified against this branch's head (ce776bd) on 2026-08-28.Summary
A reusable subagent cannot travel with the pi package that needs it. Agents are discovered from
.pi/agents/,.agents/agents/and the global agent dir only, so a package that ships anagents/directory is inert until something copies those files into one of the three. That is a real setup step, not a formality:git statusand/agentsboth present package content as project content.pi updatechanges the package's agents and the stale copies keep loading.The same gap exists for saved workflow scripts, which resolve names against the same three roots.
What changed
One idea: a package declares its agent and workflow paths in
package.json, and this extension reads those paths for packages pi itself has installed — nothing else.{ "pi": { "subagents": { "agents": ["./agents"], "workflows": ["./workflows"] } } }Then
pi installis the whole setup, at either scope:-l(--local) is pi's own install flag; without it the package lands in the user's globalsettings.json. A local-path install is not copied, so an edit to an agent file is live on the nextAgentcall — the fastest authoring loop. Project scope is the shape a team distributes agents in: commit the package to.pi/settings.jsonand everyone who trusts the repo gets them.Consequences:
src/package-resources.ts. Enumerates installed packages via pi's ownDefaultPackageManager.listConfiguredPackages()(feature-detected — a host without it contributes no packages instead of failing activation), reads eachpackage.json, and resolves declared entries to absolute paths. A directory is scanned, a file path is taken as that file,!entryexcludes, and anything resolving outside the package root is refused. No globs.node_modulesis never scanned. Onlypackages[]from pi's global and projectsettings.json. A transitive dependency of the user's project contributes nothing, matching pi. A package configured only by an untrusted project stays invisible until the project is trusted —session_startreadsctx.isProjectTrusted()and reloads, because agents register at activation, long before any context exists.skills/for a package with nopikey at all; this never scans an undeclaredagents/. Declaration is the package author's half of the opt-in.pi.subagentsas an object or an array shorthand, and the top-levelpi-subagentskey in both shapes. The top-level key is whatnicobailon/pi-subagentsreads, so one manifest serves both extensions. Addingpi.subagentsis inert for pi itself, whosereadPiManifestignores unknown keys.custom-agents.ts). Same tier pi gives package-provided skills and Claude Code gives plugin agents. Package workflow dirs are appended last tosavedWorkflowRoots.packageAgentsandpackageWorkflows, eachtrue(default),false, or a list of package names matched case-insensitively against the unscoped short name, the full name, or the settings source string. Separate because an agent is markdown handed to a model and a workflow is.jsthis extension executes. Both appear in/agents → Settings; the row is on/off, and toggling it says in the toast that it replaced a hand-written allowlist./agents. Package agents carry a▪badge and offer only Eject and Disable — their files live under pi's install root, where an edit is lost on the nextpi update. Both write a local file that shadows the original.locateAgentFilenow takes the agent'ssourceand refuses to hand back a package path. An agent its own author shipped withenabled: falseoffers Eject alone: only the package can take that back, and the ejected copy carries noenabled:line, so it lands enabled.docs/packages.md— authoring, entry syntax, precedence, the/agentsbehaviour, the gate and troubleshooting.README.md's Shipping agents in a pi package section is a summary that links it, anddocs/workflows.mddocuments the package root in the saved-workflow resolution order.CHANGELOG.mdhas anUnreleased → Addedentry.Related work
States re-checked 2026-08-28.
savedWorkflowRoots, which this appends a package tier to.pi-subagentsmanifest key; accepted here for manifest compatibility. No code shared.#248 (
requiredExtensions) and #239 (isolationBackend) — both still open — each add a settings key in the same four regions ofsrc/settings.tsthis PR edits: the interface,SettingsAppliers,sanitizeandapplySettings, plustest/settings.test.ts. Textual overlap only, a rebase and not a design conflict: the three keys are independent. #261 (open) sharessrc/index.tsandsrc/types.tsbut not the regions touched here. Nothing open touchescustom-agents.ts,package-resources.tsorworkflow/saved.ts.Behavior and compatibility
node_modules, and no existing agent or workflow changes precedence relative to another.extensions/and injects itsskills/into the system prompt with no further prompt, so a declared.mdagent is strictly less privileged than what the same package already runs. The gate exists for narrowing that, not as a second install step. SetpackageAgents/packageWorkflowstofalseto opt out entirely.Explorereplaces it. That matches the priority order Support package-owned subagent discovery #109 proposes and the existing rule for custom agents, but it is a name a package can take.reviewercollide, and the later load wins, as it already does for two local files claiming one name. Thebasic-package.package-researcherform in Support package-owned subagent discovery #109 is not implemented, and:remains rejected in aname:.packageAgentsandpackageWorkflowsinsubagents.json;AgentConfig.sourcegains"package". An empty allowlist ([]) is kept rather than dropped, and matches nothing — dropping it would silently restore thetruedefault.session_startnow callsreloadCustomAgents()and drops the package cache unconditionally, so api installin another terminal is picked up by the next session or/reloadwithout restarting pi.Performance
npm run bench:ab -- 221df02, 3 rounds, fastest round's median sample, on a machine with no packages configured — the path every existing setup takes. The base is this branch's merge base, not currentmaster: master has since picked up #264 (conversation-viewer render cost), which would show in the delta as if it were this PR's doing.loadCustomAgentsis the path this PR actually changes, and it is flat. Across all 38 benchmarks only two moved more than 2%: the spawn row above at -5.8%, andFleetList.update > 1 agentsat -16.4% — which is 250ns → 209ns, at the timer's resolution rather than a real effect. Both are improvements and neither is on a path this branch touches; the harness's own guidance is to treat anything under ~5% as noise unless it reproduces.Not measured: the cost with packages actually installed. By construction it is one settings read plus one
package.jsonper package on the first load per (cwd, trust, gate) key, then cached for the session — but no benchmark covers it, and the bench fixture has no packages to configure.Testing
At
ce776bd:The lint warning is
test/workflow-tool-description.test.ts:155(noTemplateCurlyInString), a file this branch does not touch. It is not pre-existing on master any more: master silenced it with abiome-ignorein4f572ea, which this branch has not picked up — it is 7 commits behind master (0.18.2here vs0.19.0there). A rebase clears it;npm run linton master is clean.New coverage — 121 cases, all passing (
npx vitest runon the six files: 6 files, 341 passed):test/package-resources.test.ts(new, 43) — the four manifest spellings and their precedence, non-string and malformed input, directory vs file vs!exclusion, path escape outside the package root and via symlink, allowlist matching on all three name forms, the gate/trust cache keys.test/package-agents-wiring.test.ts(new, 25) — boots the realsrc/index.tsagainst a package on disk declared in a real pisettings.json, covering the activation-then-trust ordering: a project-configured package is invisible untilsession_startreports the project trusted, plus the saved-trust anddefaultProjectTrustpaths.test/workflow-saved.test.ts(new, 25) — resolution order with package roots appended, and that a package script still needsexport const meta =to count.test/custom-agents.test.ts(79 → 97) — package tier loads lowest, local files take the name back, the 1 MB ceiling and non-regular-file skip,!exclusions at load time.test/settings.test.ts(87 → 94) — gate sanitizing, including[]kept rather than dropped.test/agent-file-toggle.test.ts(54 → 57) —locateAgentFilerefuses a package path and falls through to the shadowing local file.Not covered:
settings.jsonand lets pi's own package manager resolve them, so registry download, version resolution and pnpm/legacy global roots are exercised only by pi's code, not ours./agentsmenu paths are not driven through the UI: Eject and Disable on a package agent are covered at thelocateAgentFilelevel, and the settings-row toggle at the applier level, not throughctx.ui.DefaultPackageManagerfeature-detection fallback is unit-tested, but no CI job runs against a peer version that actually lackslistConfiguredPackages().