@juicesharp/rpiv-pi 0.12.4 → 0.12.5

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/README.md CHANGED
@@ -194,6 +194,8 @@ Pi Agent discovers extensions via `"extensions": ["./extensions"]` and skills vi
194
194
 
195
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
196
 
197
+ The bundled built-in agents from `pi-subagents` (`scout`, `planner`, `oracle`, …) are hidden from both the `subagent` tool that the assistant dispatches to and the `/agents` manager overlay (and `ctrl+shift+a`). The overlay filter is best-effort — if a future `pi-subagents` release changes its manager UI, rpiv-pi will print one boot-time warning to stderr and the built-in rows will reappear in `/agents` until rpiv-pi ships an update. The assistant-side filter is unaffected by upstream changes. To re-enable a built-in agent yourself, edit `subagents.disableBuiltins` in `~/.pi/agent/settings.json` (set to `false` or delete the key) and restart Pi.
198
+
197
199
  To fully uninstall:
198
200
 
199
201
  1. Remove rpiv-pi from Pi: `pi uninstall npm:@juicesharp/rpiv-pi`
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ __resetManagerRowFilterForTests,
4
+ INSTALLED_SENTINEL,
5
+ installManagerRowFilter,
6
+ LOAD_ENTRIES_SOURCE_FRAGMENT,
7
+ type SkipReason,
8
+ } from "./hide-builtin-manager-rows.js";
9
+
10
+ interface AgentEntry {
11
+ name: string;
12
+ }
13
+ interface AgentDataFixture {
14
+ builtin: AgentEntry[];
15
+ user: AgentEntry[];
16
+ project: AgentEntry[];
17
+ }
18
+ interface ManagerLike {
19
+ agentData: AgentDataFixture;
20
+ _walkedBuiltin: string[];
21
+ }
22
+
23
+ // Synthetic constructor whose `loadEntries` mirrors the load-bearing shape
24
+ // of upstream agent-manager.ts:120-124 — references `this.agentData.builtin`
25
+ // (drift anchor) and walks it, recording names into a side-channel so tests
26
+ // can assert what would have been rendered.
27
+ function makeFixtureCtor(): new () => ManagerLike {
28
+ function FixtureCtor(this: ManagerLike) {
29
+ this.agentData = { builtin: [], user: [], project: [] };
30
+ this._walkedBuiltin = [];
31
+ }
32
+ FixtureCtor.prototype.loadEntries = function (this: ManagerLike) {
33
+ this._walkedBuiltin = [];
34
+ // Keep the literal substring so the drift guard accepts this fixture.
35
+ for (const config of this.agentData.builtin) {
36
+ this._walkedBuiltin.push(config.name);
37
+ }
38
+ };
39
+ return FixtureCtor as unknown as new () => ManagerLike;
40
+ }
41
+
42
+ describe("installManagerRowFilter", () => {
43
+ it("filters PI_SUBAGENTS_BUILTINS rows from the walked list", () => {
44
+ const Ctor = makeFixtureCtor();
45
+ const result = installManagerRowFilter(Ctor);
46
+ expect(result).toBe("installed");
47
+
48
+ const instance = new Ctor();
49
+ instance.agentData = {
50
+ builtin: [{ name: "scout" }, { name: "planner" }, { name: "general-purpose" }, { name: "codebase-locator" }],
51
+ user: [],
52
+ project: [],
53
+ };
54
+ instance._walkedBuiltin = [];
55
+ instance.constructor.prototype.loadEntries.call(instance);
56
+
57
+ expect(instance._walkedBuiltin).toEqual(["general-purpose", "codebase-locator"]);
58
+ __resetManagerRowFilterForTests();
59
+ });
60
+
61
+ it("filters on every invocation (covers refreshAgentData re-call)", () => {
62
+ const Ctor = makeFixtureCtor();
63
+ installManagerRowFilter(Ctor);
64
+ const instance = new Ctor();
65
+
66
+ instance.agentData = { builtin: [{ name: "scout" }, { name: "thoughts-locator" }], user: [], project: [] };
67
+ instance.constructor.prototype.loadEntries.call(instance);
68
+ expect(instance._walkedBuiltin).toEqual(["thoughts-locator"]);
69
+
70
+ instance.agentData = { builtin: [{ name: "oracle" }, { name: "diff-auditor" }], user: [], project: [] };
71
+ instance.constructor.prototype.loadEntries.call(instance);
72
+ expect(instance._walkedBuiltin).toEqual(["diff-auditor"]);
73
+
74
+ __resetManagerRowFilterForTests();
75
+ });
76
+
77
+ it("restores the unfiltered agentData reference after loadEntries returns", () => {
78
+ const Ctor = makeFixtureCtor();
79
+ installManagerRowFilter(Ctor);
80
+ const instance = new Ctor();
81
+ const unfiltered = {
82
+ builtin: [{ name: "scout" }, { name: "general-purpose" }],
83
+ user: [],
84
+ project: [],
85
+ };
86
+ instance.agentData = unfiltered;
87
+ instance.constructor.prototype.loadEntries.call(instance);
88
+
89
+ expect(instance.agentData).toBe(unfiltered);
90
+ expect(instance.agentData.builtin.map((c) => c.name)).toEqual(["scout", "general-purpose"]);
91
+ __resetManagerRowFilterForTests();
92
+ });
93
+
94
+ it("is idempotent — second install returns 'skipped' with reason 'already-installed'", () => {
95
+ const Ctor = makeFixtureCtor();
96
+ const first = installManagerRowFilter(Ctor);
97
+ const patched = (Ctor.prototype as { loadEntries: () => void }).loadEntries;
98
+
99
+ const onSkip = vi.fn<(reason: SkipReason) => void>();
100
+ const second = installManagerRowFilter(Ctor, { onSkip });
101
+ expect(first).toBe("installed");
102
+ expect(second).toBe("skipped");
103
+ expect(onSkip).toHaveBeenCalledWith("already-installed");
104
+ expect((Ctor.prototype as { loadEntries: () => void }).loadEntries).toBe(patched);
105
+ expect((Ctor as { [INSTALLED_SENTINEL]?: boolean })[INSTALLED_SENTINEL]).toBe(true);
106
+
107
+ __resetManagerRowFilterForTests();
108
+ });
109
+
110
+ it.each<[string, unknown, SkipReason]>([
111
+ ["missing constructor (undefined)", undefined, "missing-constructor"],
112
+ ["missing constructor (null)", null, "missing-constructor"],
113
+ ["missing prototype", { prototype: undefined }, "missing-prototype"],
114
+ ["missing loadEntries", { prototype: {} }, "missing-loadentries"],
115
+ ["loadEntries lacks drift anchor", { prototype: { loadEntries: () => 1 } }, "drift-detected"],
116
+ ])("fails soft on %s — onSkip(%s) and never throws", (_label, ctor, expectedReason) => {
117
+ const onSkip = vi.fn<(reason: SkipReason) => void>();
118
+ expect(() => installManagerRowFilter(ctor, { onSkip })).not.toThrow();
119
+ expect(onSkip).toHaveBeenCalledExactlyOnceWith(expectedReason);
120
+ });
121
+
122
+ it("survives invocation when agentData.builtin is missing", () => {
123
+ const Ctor = makeFixtureCtor();
124
+ installManagerRowFilter(Ctor);
125
+ const instance = new Ctor();
126
+ instance.agentData = { builtin: undefined as unknown as AgentEntry[], user: [], project: [] };
127
+ expect(() => instance.constructor.prototype.loadEntries.call(instance)).not.toThrow();
128
+ __resetManagerRowFilterForTests();
129
+ });
130
+ });
131
+
132
+ describe("real pi-subagents AgentManagerComponent contract", () => {
133
+ // Canary: this is the only test that touches the live upstream module.
134
+ // Failing here means upstream pi-subagents drifted between our pin and
135
+ // our release — bump the dep, update LOAD_ENTRIES_SOURCE_FRAGMENT (or
136
+ // retire the patch), and re-snapshot. At user runtime the patch fails
137
+ // soft via onSkip; this test exists so we catch drift before shipping.
138
+ it("loadEntries body still contains the load-bearing drift anchor", async () => {
139
+ // Dynamic import: matches the runtime guarded import in renderer-override.ts
140
+ // and avoids tsc resolving into the upstream .ts source via the path stub.
141
+ const mod = (await import("pi-subagents/agent-manager")) as {
142
+ AgentManagerComponent?: { prototype?: { loadEntries?: () => void } };
143
+ };
144
+ const proto = mod.AgentManagerComponent?.prototype;
145
+ const fn = proto?.loadEntries;
146
+ expect(typeof fn).toBe("function");
147
+ expect(fn?.toString()).toContain(LOAD_ENTRIES_SOURCE_FRAGMENT);
148
+ });
149
+ });
@@ -0,0 +1,129 @@
1
+ // Hides the upstream pi-subagents built-in agent rows from the `/agents`
2
+ // manager overlay (and ctrl+shift+a). Companion to hide-builtin-subagents.ts,
3
+ // which hides the same names from the LLM-facing `subagent` tool surface.
4
+ //
5
+ // Strategy: monkey-patch `AgentManagerComponent.prototype.loadEntries` — the
6
+ // single chokepoint that converts agentData → rendered AgentEntry rows. The
7
+ // constructor calls it, refreshAgentData() calls it after every in-overlay
8
+ // mutation, so one patch covers both code paths.
9
+ //
10
+ // Fail-soft contract: never throw at user runtime. Drift, missing class, or
11
+ // missing method routes through the caller-supplied `onSkip` so Pi can
12
+ // surface a single warning notify and continue booting. The /agents overlay
13
+ // silently regains the upstream rows; the LLM-facing filter (Proxy in
14
+ // renderer-override.ts) is unaffected because it lives on a different layer.
15
+
16
+ import { PI_SUBAGENTS_BUILTINS } from "./hide-builtin-subagents.js";
17
+
18
+ const BUILTIN_NAMES_SET = new Set<string>(PI_SUBAGENTS_BUILTINS);
19
+
20
+ // Drift anchor: the load-bearing substring inside upstream
21
+ // agent-manager.ts:120-124 loadEntries. If upstream rewrites the method to
22
+ // stop reading agentData.builtin (rename, inline into ctor, route through a
23
+ // helper), this fragment disappears and we skip with reason "drift-detected"
24
+ // before mutating the prototype.
25
+ export const LOAD_ENTRIES_SOURCE_FRAGMENT = "this.agentData.builtin";
26
+
27
+ export const INSTALLED_SENTINEL = Symbol.for("rpiv-pi.manager-row-filter-installed");
28
+
29
+ export type InstallResult = "installed" | "skipped";
30
+
31
+ export type SkipReason =
32
+ | "missing-constructor"
33
+ | "missing-prototype"
34
+ | "missing-loadentries"
35
+ | "drift-detected"
36
+ | "already-installed";
37
+
38
+ export interface InstallOptions {
39
+ onSkip?: (reason: SkipReason) => void;
40
+ }
41
+
42
+ interface AgentDataLike {
43
+ builtin?: Array<{ name?: unknown }>;
44
+ }
45
+
46
+ interface ManagerInstanceLike {
47
+ agentData?: AgentDataLike;
48
+ }
49
+
50
+ interface ManagerCtorLike {
51
+ prototype?: { loadEntries?: (this: ManagerInstanceLike) => void };
52
+ [INSTALLED_SENTINEL]?: boolean;
53
+ }
54
+
55
+ interface PatchRecord {
56
+ ctor: ManagerCtorLike;
57
+ original: (this: ManagerInstanceLike) => void;
58
+ }
59
+
60
+ let installedPatch: PatchRecord | undefined;
61
+
62
+ function isFilteredBuiltin(entry: { name?: unknown }): boolean {
63
+ return typeof entry.name === "string" && BUILTIN_NAMES_SET.has(entry.name);
64
+ }
65
+
66
+ export function installManagerRowFilter(managerCtor: unknown, options: InstallOptions = {}): InstallResult {
67
+ const onSkip = options.onSkip ?? (() => {});
68
+ const ctor = managerCtor as ManagerCtorLike | undefined | null;
69
+
70
+ if (!ctor) {
71
+ onSkip("missing-constructor");
72
+ return "skipped";
73
+ }
74
+ if (ctor[INSTALLED_SENTINEL] === true) {
75
+ onSkip("already-installed");
76
+ return "skipped";
77
+ }
78
+ const proto = ctor.prototype;
79
+ if (!proto) {
80
+ onSkip("missing-prototype");
81
+ return "skipped";
82
+ }
83
+ const original = proto.loadEntries;
84
+ if (typeof original !== "function") {
85
+ onSkip("missing-loadentries");
86
+ return "skipped";
87
+ }
88
+ let source: string;
89
+ try {
90
+ source = original.toString();
91
+ } catch {
92
+ onSkip("drift-detected");
93
+ return "skipped";
94
+ }
95
+ if (!source.includes(LOAD_ENTRIES_SOURCE_FRAGMENT)) {
96
+ onSkip("drift-detected");
97
+ return "skipped";
98
+ }
99
+
100
+ proto.loadEntries = function patchedLoadEntries(this: ManagerInstanceLike): void {
101
+ // Defensive clone: never mutate the agentData reference we received.
102
+ // Other manager screens (override-scope, edit) read agentData directly
103
+ // and may rely on the unfiltered shape.
104
+ const original = this.agentData;
105
+ const builtin = Array.isArray(original?.builtin) ? original.builtin : [];
106
+ const filteredBuiltin = builtin.filter((c) => !isFilteredBuiltin(c ?? {}));
107
+ const filteredAgentData = { ...(original ?? {}), builtin: filteredBuiltin };
108
+ this.agentData = filteredAgentData as AgentDataLike;
109
+ try {
110
+ (installedPatch?.original ?? (() => {})).call(this);
111
+ } finally {
112
+ // Restore the unfiltered view for downstream reads outside loadEntries.
113
+ this.agentData = original;
114
+ }
115
+ };
116
+ ctor[INSTALLED_SENTINEL] = true;
117
+ installedPatch = { ctor, original };
118
+ return "installed";
119
+ }
120
+
121
+ // Test-only: undo the prototype mutation so each test sees a fresh slate.
122
+ // Wired into test/setup.ts beforeEach. No-op when nothing is installed.
123
+ export function __resetManagerRowFilterForTests(): void {
124
+ if (!installedPatch) return;
125
+ const { ctor, original } = installedPatch;
126
+ if (ctor.prototype) ctor.prototype.loadEntries = original;
127
+ delete ctor[INSTALLED_SENTINEL];
128
+ installedPatch = undefined;
129
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Compile-time stub for `pi-subagents/agent-manager` — see `./index.d.ts`
3
+ * for the stub rationale.
4
+ *
5
+ * Surface kept opaque on purpose: we touch exactly one thing on this
6
+ * class — `prototype.loadEntries` — via a runtime prototype patch in
7
+ * hide-builtin-manager-rows.ts. Typing the full upstream class (30+
8
+ * private fields, a dozen managed screens) would force us to mirror
9
+ * internal state we never read.
10
+ */
11
+
12
+ export declare class AgentManagerComponent {
13
+ private readonly __piSubagentsAgentManagerComponent: true;
14
+ }
@@ -63,6 +63,19 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
63
63
  });
64
64
  vi.mock("pi-subagents", () => ({ default: registerSubagentExtensionMock }));
65
65
  vi.mock("pi-subagents/render", () => ({ renderSubagentResult: vi.fn() }));
66
+ // Stub AgentManagerComponent for the manager-row-filter install. Body
67
+ // includes "this.agentData.builtin" to satisfy the drift anchor — the
68
+ // install path is exercised end-to-end without touching the real upstream
69
+ // class. Per-test prototype reset is wired in test/setup.ts.
70
+ vi.mock("pi-subagents/agent-manager", () => {
71
+ class AgentManagerComponent {
72
+ agentData: { builtin: Array<{ name: string }> } = { builtin: [] };
73
+ loadEntries() {
74
+ void this.agentData.builtin;
75
+ }
76
+ }
77
+ return { AgentManagerComponent };
78
+ });
66
79
 
67
80
  import { registerSubagentsWithQuietRenderer } from "./renderer-override.js";
68
81
 
@@ -6,6 +6,7 @@
6
6
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
7
  import registerSubagentExtension from "pi-subagents";
8
8
  import { buildAgentEnumDescription } from "./agent-catalog.js";
9
+ import { installManagerRowFilter, type SkipReason } from "./hide-builtin-manager-rows.js";
9
10
  import {
10
11
  filterDisabledFromListResult,
11
12
  getCuratedSubagentDescription,
@@ -87,6 +88,33 @@ function interceptRegisterTool(pi: ExtensionAPI): ExtensionAPI {
87
88
  });
88
89
  }
89
90
 
91
+ // Fail-soft install: the /agents overlay row-filter is best-effort UI polish
92
+ // tied to pi-subagents@0.17.5 internals (AgentManagerComponent.prototype.
93
+ // loadEntries). On any drift — module moved, class renamed, method body
94
+ // changed — we log one stderr line and continue. The LLM-side filter
95
+ // (interceptRegisterTool above) lives entirely on our boundary and is
96
+ // unaffected. See hide-builtin-manager-rows.ts for skip-reason semantics.
97
+ async function tryInstallManagerRowFilter(): Promise<void> {
98
+ let mod: { AgentManagerComponent?: unknown };
99
+ try {
100
+ mod = (await import("pi-subagents/agent-manager")) as { AgentManagerComponent?: unknown };
101
+ } catch {
102
+ process.stderr.write(
103
+ "[rpiv-pi] /agents overlay built-in filter disabled: pi-subagents/agent-manager not found.\n",
104
+ );
105
+ return;
106
+ }
107
+ installManagerRowFilter(mod.AgentManagerComponent, {
108
+ onSkip: (reason: SkipReason) => {
109
+ if (reason === "already-installed") return;
110
+ process.stderr.write(
111
+ `[rpiv-pi] /agents overlay built-in filter disabled (${reason}); built-in agents will be visible until rpiv-pi updates.\n`,
112
+ );
113
+ },
114
+ });
115
+ }
116
+
90
117
  export async function registerSubagentsWithQuietRenderer(pi: ExtensionAPI): Promise<void> {
118
+ await tryInstallManagerRowFilter();
91
119
  await registerSubagentExtension(interceptRegisterTool(pi));
92
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
5
5
  "keywords": [
6
6
  "pi-package",