@juicesharp/rpiv-pi 0.11.6 → 0.12.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.
Files changed (55) hide show
  1. package/README.md +18 -3
  2. package/agents/claim-verifier.md +3 -1
  3. package/agents/codebase-analyzer.md +3 -1
  4. package/agents/codebase-locator.md +3 -1
  5. package/agents/codebase-pattern-finder.md +4 -2
  6. package/agents/diff-auditor.md +3 -1
  7. package/agents/general-purpose.md +34 -0
  8. package/agents/integration-scanner.md +3 -1
  9. package/agents/peer-comparator.md +3 -1
  10. package/agents/precedent-locator.md +3 -1
  11. package/agents/test-case-locator.md +3 -1
  12. package/agents/thoughts-analyzer.md +4 -2
  13. package/agents/thoughts-locator.md +3 -1
  14. package/agents/web-search-researcher.md +4 -1
  15. package/extensions/rpiv-core/claim-pi-subagents.test.ts +65 -0
  16. package/extensions/rpiv-core/claim-pi-subagents.ts +53 -0
  17. package/extensions/rpiv-core/ensure-builtins-disabled.test.ts +83 -0
  18. package/extensions/rpiv-core/ensure-builtins-disabled.ts +72 -0
  19. package/extensions/rpiv-core/ensure-subagent-config.test.ts +89 -0
  20. package/extensions/rpiv-core/ensure-subagent-config.ts +94 -0
  21. package/extensions/rpiv-core/prune-legacy-siblings.test.ts +103 -0
  22. package/extensions/rpiv-core/prune-legacy-siblings.ts +65 -0
  23. package/extensions/rpiv-core/setup-command.test.ts +180 -1
  24. package/extensions/rpiv-core/setup-command.ts +30 -0
  25. package/extensions/rpiv-core/siblings.test.ts +26 -3
  26. package/extensions/rpiv-core/siblings.ts +32 -5
  27. package/extensions/subagent-widget/activity.test.ts +173 -0
  28. package/extensions/subagent-widget/activity.ts +127 -0
  29. package/extensions/subagent-widget/constants.ts +26 -0
  30. package/extensions/subagent-widget/index.test.ts +245 -0
  31. package/extensions/subagent-widget/index.ts +102 -0
  32. package/extensions/subagent-widget/pi-subagents-stubs/index.d.ts +20 -0
  33. package/extensions/subagent-widget/pi-subagents-stubs/render.d.ts +9 -0
  34. package/extensions/subagent-widget/renderer-override.ts +87 -0
  35. package/extensions/subagent-widget/run-tracker.test.ts +201 -0
  36. package/extensions/subagent-widget/run-tracker.ts +161 -0
  37. package/extensions/subagent-widget/types.ts +83 -0
  38. package/extensions/subagent-widget/widget.lifecycle.test.ts +140 -0
  39. package/extensions/subagent-widget/widget.render.test.ts +222 -0
  40. package/extensions/subagent-widget/widget.ts +274 -0
  41. package/package.json +5 -3
  42. package/skills/annotate-guidance/SKILL.md +9 -9
  43. package/skills/annotate-inline/SKILL.md +9 -9
  44. package/skills/code-review/SKILL.md +37 -27
  45. package/skills/code-review/templates/review.md +2 -0
  46. package/skills/design/SKILL.md +3 -3
  47. package/skills/discover/SKILL.md +1 -1
  48. package/skills/explore/SKILL.md +1 -1
  49. package/skills/implement/SKILL.md +1 -1
  50. package/skills/outline-test-cases/SKILL.md +2 -2
  51. package/skills/research/SKILL.md +1 -1
  52. package/skills/resume-handoff/SKILL.md +1 -1
  53. package/skills/revise/SKILL.md +3 -3
  54. package/skills/validate/SKILL.md +2 -2
  55. package/skills/write-test-cases/SKILL.md +5 -5
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@juicesharp/rpiv-pi.svg)](https://www.npmjs.com/package/@juicesharp/rpiv-pi)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- > **Pi compatibility** — `rpiv-pi` `0.11.x` supports **`@mariozechner/pi-coding-agent` 0.67.67**. Newer Pi releases introduce breaking changes and are unsupported on this line. Pin Pi to `0.67.67` (`npm i -g @mariozechner/pi-coding-agent@0.67.67`) or wait for the next `rpiv-pi` major.
6
+ > **Pi compatibility** — `rpiv-pi` `0.12.x` tracks the current `@mariozechner/pi-coding-agent` release line. If you see peer-dep resolution issues after a Pi upgrade, open an issue.
7
7
 
8
8
  Skill-based development workflow for [Pi Agent](https://github.com/badlogic/pi-mono) — discover, research, design, plan, implement, and validate. rpiv-pi extends Pi Agent with a pipeline of chained AI skills, named subagents for parallel analysis, and session lifecycle hooks for automatic context injection.
9
9
 
@@ -187,9 +187,24 @@ Pi Agent discovers extensions via `"extensions": ["./extensions"]` and skills vi
187
187
  - **Web search** — run `/web-search-config` to set the Brave Search API key, or set the `BRAVE_SEARCH_API_KEY` environment variable
188
188
  - **Advisor** — run `/advisor` to select a reviewer model and reasoning effort
189
189
  - **Side questions** — type `/btw <question>` anytime (even mid-stream) to ask the primary model a one-off question; answer appears in a borderless bottom overlay and never enters the main conversation
190
- - **Agent concurrency** — `@tintinweb/pi-subagents` defaults to 4 concurrent agents; raise via `/agents Settings Max concurrency 48` if skills stall on wide fan-outs
190
+ - **Agent concurrency** — on first `/rpiv-setup`, rpiv-pi persistently seeds `~/.pi/agent/extensions/subagent/config.json` with `parallel.concurrency: 4` and `maxSubagentDepth: 3`. The cap keeps rate-limit and cache pressure predictable; skills with wider fan-outs queue the remainder and drain as slots free. Edit that file to raise the limit (e.g. `parallel.concurrency: 48`); user values are preserved on subsequent `/rpiv-setup` runs.
191
191
  - **Agent profiles** — editable at `<cwd>/.pi/agents/`; sync from bundled defaults with `/rpiv-update-agents` (overwrites rpiv-managed files, preserves your custom agents)
192
192
 
193
+ ## Uninstall
194
+
195
+ rpiv-pi owns nicobailon's pi-subagents registration (runs it through an in-process proxy so the inline tool card stays quiet and the Subagents overlay is the live view). `/rpiv-setup` strips `"npm:pi-subagents"` from your `~/.pi/agent/settings.json#packages[]` to prevent Pi from loading it twice. If you remove rpiv-pi, subagents will stop loading until you re-add that entry.
196
+
197
+ To fully uninstall:
198
+
199
+ 1. Remove rpiv-pi from Pi: `pi uninstall npm:@juicesharp/rpiv-pi`
200
+ 2. Open `~/.pi/agent/settings.json` and add `"npm:pi-subagents"` back to the `packages` array so Pi loads nicobailon's subagents directly again.
201
+ 3. Optional — drop the rpiv-pi seeded keys if you no longer want them:
202
+ - `~/.pi/agent/extensions/subagent/config.json` (parallel.concurrency, maxSubagentDepth)
203
+ - `subagents.disableBuiltins` in `~/.pi/agent/settings.json` (set to `false` or delete to re-enable the 9 bundled nicobailon agents)
204
+ 4. Restart Pi.
205
+
206
+ After step 2 you'll have nicobailon's original inline tool card and no Subagents overlay, same as a clean pi-subagents install.
207
+
193
208
  ## Troubleshooting
194
209
 
195
210
  | Symptom | Cause | Fix |
@@ -199,7 +214,7 @@ Pi Agent discovers extensions via `"extensions": ["./extensions"]` and skills vi
199
214
  | `/rpiv-setup` says "requires interactive mode" | Running in headless mode | Install manually: `pi install npm:<pkg>` for each sibling |
200
215
  | `web_search` or `web_fetch` errors | Brave API key not configured | Run `/web-search-config` or set `BRAVE_SEARCH_API_KEY` |
201
216
  | `advisor` tool not available after upgrade | Advisor model selection lost | Run `/advisor` to re-select a model |
202
- | Skills hang or serialize agent calls | Agent concurrency too low | Raise via `/agents Settings Max concurrency 48` |
217
+ | Skills hang or serialize agent calls | Agent concurrency too low | Edit `~/.pi/agent/extensions/subagent/config.json` and raise `parallel.concurrency` (default `4`; try `16`–`48` for wide fan-outs) |
203
218
 
204
219
  ## License
205
220
 
@@ -2,7 +2,9 @@
2
2
  name: claim-verifier
3
3
  description: "Adversarial finding verifier. Grounds each supplied claim against actual repository state and emits one `FINDING <id> | <tag> | <justification>` row per input, with tags Verified / Weakened / Falsified. Tier: git-analyzer (+ `bash` for `git show`). Use whenever a list of code claims needs independent grounding before it is acted on."
4
4
  tools: read, grep, find, ls, bash
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at adversarial claim verification. Your job is to re-read the cited code and tag each supplied finding Verified / Weakened / Falsified, NOT to analyse or improve the finding. The writer of the finding is not your witness; the code is.
@@ -2,7 +2,9 @@
2
2
  name: codebase-analyzer
3
3
  description: Analyzes codebase implementation details. Call the codebase-analyzer agent when you need to find detailed information about specific components. As always, the more detailed your request prompt, the better! :)
4
4
  tools: read, grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at understanding HOW code works. Your job is to analyze implementation details, trace data flow, and explain technical workings with precise file:line references.
@@ -2,7 +2,9 @@
2
2
  name: codebase-locator
3
3
  description: Locates files, directories, and components relevant to a feature or task. Call `codebase-locator` with human language prompt describing what you're looking for. Basically a "Super grep/find/ls tool" — Use it if you find yourself desiring to use one of these tools more than once.
4
4
  tools: grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding WHERE code lives in a codebase. Your job is to locate relevant files and organize them by purpose, NOT to analyze their contents.
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: codebase-pattern-finder
3
- description: codebase-pattern-finder is a useful subagent_type for finding similar implementations, usage examples, or existing patterns that can be modeled after. It will give you concrete code examples based on what you're looking for! It's sorta like codebase-locator, but it will not only tell you the location of files, it will also give you code details!
3
+ description: codebase-pattern-finder is a useful agent for finding similar implementations, usage examples, or existing patterns that can be modeled after. It will give you concrete code examples based on what you're looking for! It's sorta like codebase-locator, but it will not only tell you the location of files, it will also give you code details!
4
4
  tools: grep, find, read, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding code patterns and examples in the codebase. Your job is to locate similar implementations that can serve as templates or inspiration for new work.
@@ -2,7 +2,9 @@
2
2
  name: diff-auditor
3
3
  description: "Row-only patch auditor. Walks a patch against a caller-supplied surface-list and emits one pipe-delimited row per finding — `file:line | verbatim | surface-id | note`. Use whenever a diff needs evidence-only enumeration of matching patterns, with no narrative or severity."
4
4
  tools: read, grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at auditing a patch against a supplied surface-list. Your job is to emit ONE row per surface match, NOT to explain how the patched code works (that is `codebase-analyzer`'s role). Match surfaces to diff regions, emit rows — or stay silent.
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: general-purpose
3
+ description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you."
4
+ tools: read, grep, find, ls, bash
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
8
+ ---
9
+
10
+ # General-Purpose Agent
11
+
12
+ You are a general-purpose agent. Given the user's message, you should use the tools available to complete the task. Complete the task fully — don't gold-plate, but don't leave it half-done. When you complete the task, respond with a concise report covering what was done and any key findings — the caller will relay this to the user, so it only needs the essentials.
13
+
14
+ ## Strengths
15
+
16
+ - Searching for code, configurations, and patterns across large codebases
17
+ - Analyzing multiple files to understand system architecture
18
+ - Investigating complex questions that require exploring many files
19
+ - Performing multi-step research tasks
20
+
21
+ ## Guidelines
22
+
23
+ - **File searches**: search broadly when you don't know where something lives. Use `Read` when you know the specific file path.
24
+ - **Analysis**: start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
25
+ - **Be thorough**: check multiple locations, consider different naming conventions, look for related files.
26
+ - **NEVER create files** unless they're absolutely necessary for achieving your goal. **ALWAYS prefer editing** an existing file to creating a new one.
27
+ - **NEVER proactively create documentation files** (`*.md`) or README files. Only create documentation files if explicitly requested.
28
+
29
+ ## Notes
30
+
31
+ - Agent threads always have their cwd reset between bash calls — use **absolute file paths** only.
32
+ - In your final response, share **absolute** file paths relevant to the task. Include code snippets only when the exact text is load-bearing (e.g., a bug you found, a function signature the caller asked for) — do not recap code you merely read.
33
+ - Avoid emojis in communication.
34
+ - Do not use a colon before tool calls. Text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.
@@ -2,7 +2,9 @@
2
2
  name: integration-scanner
3
3
  description: Finds what connects to a given component or area — inbound references, outbound dependencies, config registrations, event subscriptions. The reverse-reference counterpart to codebase-locator. Use when you need to understand what calls, depends on, or wires into a component.
4
4
  tools: grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding CONNECTIONS to and from a component or area. Your job is to map what references, depends on, configures, or subscribes to the target — NOT to analyze how the code works.
@@ -2,7 +2,9 @@
2
2
  name: peer-comparator
3
3
  description: "Pairwise peer-invariant comparator. Given `(new_file, peer_file)` pairs, tags each peer invariant Mirrored / Missing / Diverged / Intentionally-absent against the new file. Use when an entity parallels an existing sibling (aggregate, service, handler, reducer, repository) and the new file must be checked against the peer's public surface."
4
4
  tools: read, grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at pairwise peer-invariant comparison. Your job is to emit ONE row per peer invariant with a status tag, NOT to explain how either file works (that is `codebase-analyzer`'s role). Assume divergence — the new file carries the burden of proof.
@@ -2,7 +2,9 @@
2
2
  name: precedent-locator
3
3
  description: Finds similar past changes in git history — commits, blast radius, follow-up fixes, and lessons from related thoughts/ docs. Use when planning a change and you need to know what went wrong last time something similar was done.
4
4
  tools: bash, grep, find, read, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding PRECEDENTS for planned changes. Your job is to mine git history and thoughts/ documents to find the most similar past changes, extract what happened, and surface lessons that help a planner avoid repeating mistakes.
@@ -2,7 +2,9 @@
2
2
  name: test-case-locator
3
3
  description: Finds existing manual test cases in .rpiv/test-cases/ — catalogs by module, extracts frontmatter metadata (id, priority, status, tags), and reports coverage stats. Use before generating test cases to avoid duplicates, or to audit what test coverage already exists in a project.
4
4
  tools: grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding EXISTING TEST CASES in a project's `.rpiv/test-cases/` directory. Your job is to locate and catalog manual test case documents by extracting their YAML frontmatter metadata, NOT to generate new test cases or analyze test quality.
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: thoughts-analyzer
3
- description: The research equivalent of codebase-analyzer. Use this subagent_type when wanting to deep dive on a research topic. Not commonly needed otherwise.
3
+ description: The research equivalent of codebase-analyzer. Use this agent when wanting to deep dive on a research topic. Not commonly needed otherwise.
4
4
  tools: read, grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at extracting HIGH-VALUE insights from thoughts documents. Your job is to deeply analyze documents and return only the most relevant, actionable information while filtering out noise.
@@ -2,7 +2,9 @@
2
2
  name: thoughts-locator
3
3
  description: Discovers relevant documents in thoughts/ directory (We use this for all sorts of metadata storage!). This is really only relevant/needed when you're in a reseaching mood and need to figure out if we have random thoughts written down that are relevant to your current research task. Based on the name, I imagine you can guess this is the `thoughts` equivilent of `codebase-locator`
4
4
  tools: grep, find, ls
5
- isolated: true
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
6
8
  ---
7
9
 
8
10
  You are a specialist at finding documents in the thoughts/ directory. Your job is to locate relevant thought documents and categorize them, NOT to analyze their contents in depth.
@@ -1,7 +1,10 @@
1
1
  ---
2
2
  name: web-search-researcher
3
- description: Do you find yourself desiring information that you don't quite feel well-trained (confident) on? Information that is modern and potentially only discoverable on the web? Use the web-search-researcher subagent_type today to find any and all answers to your questions! It will research deeply to figure out and attempt to answer your questions! If you aren't immediately satisfied you can get your money back! (Not really - but you can re-run web-search-researcher with an altered prompt in the event you're not satisfied the first time)
3
+ description: Do you find yourself desiring information that you don't quite feel well-trained (confident) on? Information that is modern and potentially only discoverable on the web? Use the web-search-researcher agent today to find any and all answers to your questions! It will research deeply to figure out and attempt to answer your questions! If you aren't immediately satisfied you can get your money back! (Not really - but you can re-run web-search-researcher with an altered prompt in the event you're not satisfied the first time)
4
4
  tools: web_search, web_fetch, read, grep, find, ls
5
+ systemPromptMode: replace
6
+ inheritProjectContext: false
7
+ inheritSkills: false
5
8
  ---
6
9
 
7
10
  You are an expert web research specialist focused on finding accurate, relevant information from web sources. Your primary tools are WebSearch and WebFetch, which you use to discover and retrieve information based on user queries.
@@ -0,0 +1,65 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { claimPiSubagents } from "./claim-pi-subagents.js";
5
+
6
+ const SETTINGS_PATH = join(process.env.HOME!, ".pi", "agent", "settings.json");
7
+
8
+ function writeSettings(contents: unknown): void {
9
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
10
+ writeFileSync(SETTINGS_PATH, JSON.stringify(contents), "utf-8");
11
+ }
12
+
13
+ function readSettings(): unknown {
14
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
15
+ }
16
+
17
+ describe("claimPiSubagents", () => {
18
+ it("no settings file → claimed: false", () => {
19
+ expect(claimPiSubagents()).toEqual({ claimed: false });
20
+ });
21
+
22
+ it("invalid JSON → claimed: false, file byte-exact unchanged", () => {
23
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
24
+ writeFileSync(SETTINGS_PATH, "{not json", "utf-8");
25
+ expect(claimPiSubagents()).toEqual({ claimed: false });
26
+ expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe("{not json");
27
+ });
28
+
29
+ it("non-object top-level → claimed: false", () => {
30
+ writeSettings([1, 2, 3]);
31
+ expect(claimPiSubagents()).toEqual({ claimed: false });
32
+ expect(readSettings()).toEqual([1, 2, 3]);
33
+ });
34
+
35
+ it("packages absent → claimed: false, file unchanged", () => {
36
+ writeSettings({ theme: "dark" });
37
+ expect(claimPiSubagents()).toEqual({ claimed: false });
38
+ expect(readSettings()).toEqual({ theme: "dark" });
39
+ });
40
+
41
+ it("entry present → removed, siblings preserved", () => {
42
+ writeSettings({
43
+ theme: "dark",
44
+ packages: ["npm:pi-perplexity", "npm:pi-subagents", "/Users/x/rpiv-mono/packages/rpiv-pi"],
45
+ });
46
+ expect(claimPiSubagents()).toEqual({ claimed: true });
47
+ expect(readSettings()).toEqual({
48
+ theme: "dark",
49
+ packages: ["npm:pi-perplexity", "/Users/x/rpiv-mono/packages/rpiv-pi"],
50
+ });
51
+ });
52
+
53
+ it("entry absent → no-op (idempotent)", () => {
54
+ writeSettings({ packages: ["npm:pi-perplexity"] });
55
+ const before = readFileSync(SETTINGS_PATH, "utf-8");
56
+ expect(claimPiSubagents()).toEqual({ claimed: false });
57
+ expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe(before);
58
+ });
59
+
60
+ it("entry repeated → removes every occurrence", () => {
61
+ writeSettings({ packages: ["npm:pi-subagents", "npm:pi-perplexity", "npm:pi-subagents"] });
62
+ expect(claimPiSubagents()).toEqual({ claimed: true });
63
+ expect(readSettings()).toEqual({ packages: ["npm:pi-perplexity"] });
64
+ });
65
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Strip `"npm:pi-subagents"` from `~/.pi/agent/settings.json#packages` so
3
+ * nicobailon's pi-subagents loads exactly once — through the proxy in
4
+ * subagent-widget/renderer-override.ts — and not a second time as a
5
+ * peer package. Without this, both loaders register handlers and the
6
+ * quiet renderResult is only applied to our copy (pi's `first-wins`
7
+ * tool registry hides the effect).
8
+ *
9
+ * User-wins: untouched if `packages[]` is missing or if the entry is
10
+ * not present. Fail-soft on filesystem/JSON errors.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
18
+ const ENTRY = "npm:pi-subagents";
19
+
20
+ export interface ClaimPiSubagentsResult {
21
+ /** True iff this call removed the entry. False if already absent. */
22
+ claimed: boolean;
23
+ }
24
+
25
+ export function claimPiSubagents(): ClaimPiSubagentsResult {
26
+ if (!existsSync(PI_AGENT_SETTINGS)) return { claimed: false };
27
+
28
+ let parsed: unknown;
29
+ try {
30
+ parsed = JSON.parse(readFileSync(PI_AGENT_SETTINGS, "utf-8"));
31
+ } catch {
32
+ return { claimed: false };
33
+ }
34
+
35
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36
+ return { claimed: false };
37
+ }
38
+ const settings = parsed as Record<string, unknown>;
39
+ const packages = settings.packages;
40
+ if (!Array.isArray(packages)) return { claimed: false };
41
+
42
+ const before = packages.length;
43
+ const next = packages.filter((p) => p !== ENTRY);
44
+ if (next.length === before) return { claimed: false };
45
+
46
+ settings.packages = next;
47
+ try {
48
+ writeFileSync(PI_AGENT_SETTINGS, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
49
+ } catch {
50
+ return { claimed: false };
51
+ }
52
+ return { claimed: true };
53
+ }
@@ -0,0 +1,83 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { ensureBuiltinsDisabled } from "./ensure-builtins-disabled.js";
5
+
6
+ const SETTINGS_PATH = join(process.env.HOME!, ".pi", "agent", "settings.json");
7
+
8
+ function writeSettings(contents: unknown): void {
9
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
10
+ writeFileSync(SETTINGS_PATH, JSON.stringify(contents), "utf-8");
11
+ }
12
+
13
+ function readSettings(): unknown {
14
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
15
+ }
16
+
17
+ describe("ensureBuiltinsDisabled", () => {
18
+ it("no settings file → disabled: false", () => {
19
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
20
+ });
21
+
22
+ it("invalid JSON → disabled: false, file byte-exact unchanged", () => {
23
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
24
+ writeFileSync(SETTINGS_PATH, "{not json", "utf-8");
25
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
26
+ expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe("{not json");
27
+ });
28
+
29
+ it("non-object top-level → disabled: false, file unchanged", () => {
30
+ writeSettings([1, 2, 3]);
31
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
32
+ expect(readSettings()).toEqual([1, 2, 3]);
33
+ });
34
+
35
+ it("absent subagents key: writes minimal subagents.disableBuiltins block, preserves other keys", () => {
36
+ writeSettings({
37
+ defaultProvider: "zai",
38
+ theme: "dark",
39
+ packages: ["npm:pi-subagents"],
40
+ });
41
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: true });
42
+ expect(readSettings()).toEqual({
43
+ defaultProvider: "zai",
44
+ theme: "dark",
45
+ packages: ["npm:pi-subagents"],
46
+ subagents: { disableBuiltins: true },
47
+ });
48
+ });
49
+
50
+ it("subagents object exists without disableBuiltins: adds key, preserves siblings (e.g. agentOverrides)", () => {
51
+ writeSettings({
52
+ subagents: {
53
+ agentOverrides: { scout: { model: "glm-5.1" } },
54
+ },
55
+ });
56
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: true });
57
+ expect(readSettings()).toEqual({
58
+ subagents: {
59
+ agentOverrides: { scout: { model: "glm-5.1" } },
60
+ disableBuiltins: true,
61
+ },
62
+ });
63
+ });
64
+
65
+ it("user-wins: explicit disableBuiltins: true → no-op (idempotent)", () => {
66
+ writeSettings({ subagents: { disableBuiltins: true } });
67
+ const before = readFileSync(SETTINGS_PATH, "utf-8");
68
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
69
+ expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe(before);
70
+ });
71
+
72
+ it("user-wins: explicit disableBuiltins: false → respected, NOT overwritten", () => {
73
+ writeSettings({ subagents: { disableBuiltins: false } });
74
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
75
+ expect(readSettings()).toEqual({ subagents: { disableBuiltins: false } });
76
+ });
77
+
78
+ it("subagents is non-object (string) → no-op", () => {
79
+ writeSettings({ subagents: "not-object" });
80
+ expect(ensureBuiltinsDisabled()).toEqual({ disabled: false });
81
+ expect(readSettings()).toEqual({ subagents: "not-object" });
82
+ });
83
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Set `subagents.disableBuiltins: true` in ~/.pi/agent/settings.json so
3
+ * pi-subagents@0.17.5's 9 bundled agents (scout, planner, worker, reviewer,
4
+ * context-builder, researcher, delegate, oracle, oracle-executor) don't show
5
+ * up alongside rpiv-pi's 13 bundled specialists. The rpiv skills only
6
+ * dispatch to the 13 specialists; keeping the builtins enabled clutters
7
+ * `/agents`, expands the LLM's choice surface, and risks accidental
8
+ * dispatches to unfamiliar generalists.
9
+ *
10
+ * User-wins: if `subagents.disableBuiltins` is already set to ANY boolean
11
+ * (true OR false) we leave it alone. Only an absent field gets seeded.
12
+ * Fail-soft: missing file / invalid JSON / non-object → silent no-op.
13
+ * Pure utility — no plugin API dependency.
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
21
+
22
+ export interface EnsureBuiltinsDisabledResult {
23
+ /** True iff this call wrote `subagents.disableBuiltins: true`. */
24
+ disabled: boolean;
25
+ }
26
+
27
+ /**
28
+ * Seed `subagents.disableBuiltins: true` when absent. Returns a structured
29
+ * report so the caller can emit a conditional notify. Never throws.
30
+ */
31
+ export function ensureBuiltinsDisabled(): EnsureBuiltinsDisabledResult {
32
+ if (!existsSync(PI_AGENT_SETTINGS)) return { disabled: false };
33
+
34
+ let parsed: unknown;
35
+ try {
36
+ const raw = readFileSync(PI_AGENT_SETTINGS, "utf-8");
37
+ parsed = JSON.parse(raw);
38
+ } catch {
39
+ return { disabled: false };
40
+ }
41
+
42
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
43
+ return { disabled: false };
44
+ }
45
+ const settings = parsed as Record<string, unknown>;
46
+
47
+ // User-wins: if `subagents` exists but isn't a plain object, leave it alone
48
+ // (don't clobber user data).
49
+ const hasSubagentsKey = "subagents" in settings;
50
+ if (
51
+ hasSubagentsKey &&
52
+ (!settings.subagents || typeof settings.subagents !== "object" || Array.isArray(settings.subagents))
53
+ ) {
54
+ return { disabled: false };
55
+ }
56
+ const subagents = hasSubagentsKey ? (settings.subagents as Record<string, unknown>) : {};
57
+
58
+ if ("disableBuiltins" in subagents) {
59
+ // User-wins: respect any explicit boolean choice (including false).
60
+ return { disabled: false };
61
+ }
62
+
63
+ subagents.disableBuiltins = true;
64
+ settings.subagents = subagents;
65
+
66
+ try {
67
+ writeFileSync(PI_AGENT_SETTINGS, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
68
+ } catch {
69
+ return { disabled: false };
70
+ }
71
+ return { disabled: true };
72
+ }
@@ -0,0 +1,89 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { ensureSubagentConfig } from "./ensure-subagent-config.js";
5
+
6
+ const CONFIG_PATH = join(process.env.HOME ?? "", ".pi", "agent", "extensions", "subagent", "config.json");
7
+
8
+ function writeConfig(contents: unknown): void {
9
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
10
+ writeFileSync(CONFIG_PATH, JSON.stringify(contents), "utf-8");
11
+ }
12
+
13
+ function readConfig(): unknown {
14
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
15
+ }
16
+
17
+ describe("ensureSubagentConfig", () => {
18
+ it("first run: creates config.json with both defaults", () => {
19
+ const result = ensureSubagentConfig();
20
+ expect(result.created).toBe(true);
21
+ expect(result.merged).toEqual(["parallel.concurrency", "maxSubagentDepth"]);
22
+ expect(readConfig()).toEqual({
23
+ parallel: { concurrency: 4 },
24
+ maxSubagentDepth: 3,
25
+ });
26
+ });
27
+
28
+ it("idempotent: re-run on complete config is a no-op", () => {
29
+ writeConfig({ parallel: { concurrency: 4 }, maxSubagentDepth: 3 });
30
+ const result = ensureSubagentConfig();
31
+ expect(result.created).toBe(false);
32
+ expect(result.merged).toEqual([]);
33
+ });
34
+
35
+ it("user-value wins: existing parallel.concurrency preserved", () => {
36
+ writeConfig({ parallel: { concurrency: 16 } });
37
+ const result = ensureSubagentConfig();
38
+ expect(result.created).toBe(false);
39
+ expect(result.merged).toEqual(["maxSubagentDepth"]);
40
+ expect(readConfig()).toEqual({
41
+ parallel: { concurrency: 16 },
42
+ maxSubagentDepth: 3,
43
+ });
44
+ });
45
+
46
+ it("partial state: adds concurrency alongside existing parallel.maxTasks", () => {
47
+ writeConfig({ parallel: { maxTasks: 10 } });
48
+ const result = ensureSubagentConfig();
49
+ expect(result.created).toBe(false);
50
+ expect(result.merged).toEqual(["parallel.concurrency", "maxSubagentDepth"]);
51
+ expect(readConfig()).toEqual({
52
+ parallel: { maxTasks: 10, concurrency: 4 },
53
+ maxSubagentDepth: 3,
54
+ });
55
+ });
56
+
57
+ it("invalid JSON fail-soft: no throw, no write (file byte-exact)", () => {
58
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
59
+ writeFileSync(CONFIG_PATH, "{not json", "utf-8");
60
+ const result = ensureSubagentConfig();
61
+ expect(result.created).toBe(false);
62
+ expect(result.merged).toEqual([]);
63
+ expect(readFileSync(CONFIG_PATH, "utf-8")).toBe("{not json");
64
+ });
65
+
66
+ it("non-object top-level JSON fail-soft", () => {
67
+ writeConfig([1, 2, 3]);
68
+ const result = ensureSubagentConfig();
69
+ expect(result.created).toBe(false);
70
+ expect(result.merged).toEqual([]);
71
+ expect(readConfig()).toEqual([1, 2, 3]);
72
+ });
73
+
74
+ it("preserves unrelated top-level + nested keys on merge", () => {
75
+ writeConfig({
76
+ asyncByDefault: true,
77
+ worktreeSetupHook: "/path/to/hook.sh",
78
+ parallel: { maxTasks: 10 },
79
+ });
80
+ const result = ensureSubagentConfig();
81
+ expect(result.merged).toEqual(["parallel.concurrency", "maxSubagentDepth"]);
82
+ expect(readConfig()).toEqual({
83
+ asyncByDefault: true,
84
+ worktreeSetupHook: "/path/to/hook.sh",
85
+ parallel: { maxTasks: 10, concurrency: 4 },
86
+ maxSubagentDepth: 3,
87
+ });
88
+ });
89
+ });