@juicesharp/rpiv-pi 1.9.1 → 1.9.2

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.
@@ -14,7 +14,6 @@ import { homedir, tmpdir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
16
  import {
17
- BUNDLED_AGENTS_DIR,
18
17
  CLEANUP_SKIP_REASON,
19
18
  cleanupPerCwdAgents,
20
19
  isSafeDestructiveOp,
@@ -22,6 +21,7 @@ import {
22
21
  summarizeCleanupSkips,
23
22
  syncBundledAgents,
24
23
  } from "./agents.js";
24
+ import { BUNDLED_AGENTS_DIR } from "./paths.js";
25
25
 
26
26
  const sha256 = (s: string | Buffer) => createHash("sha256").update(s).digest("hex");
27
27
 
@@ -26,27 +26,11 @@ import {
26
26
  unlinkSync,
27
27
  writeFileSync,
28
28
  } from "node:fs";
29
- import { dirname, isAbsolute, join, resolve, sep } from "node:path";
30
- import { fileURLToPath } from "node:url";
29
+ import { isAbsolute, join, resolve, sep } from "node:path";
31
30
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
31
+ import { BUNDLED_AGENTS_DIR } from "./paths.js";
32
32
  import { isPlainObject, toErrorMessage } from "./utils.js";
33
33
 
34
- // ---------------------------------------------------------------------------
35
- // Package-root resolution
36
- // ---------------------------------------------------------------------------
37
-
38
- /**
39
- * Resolves the rpiv-pi package root from this module's file URL.
40
- * Walks up from `extensions/rpiv-core/agents.ts` to the repo root.
41
- */
42
- export const PACKAGE_ROOT = (() => {
43
- const thisFile = fileURLToPath(import.meta.url);
44
- // extensions/rpiv-core/agents.ts -> rpiv-pi/
45
- return dirname(dirname(dirname(thisFile)));
46
- })();
47
-
48
- export const BUNDLED_AGENTS_DIR = join(PACKAGE_ROOT, "agents");
49
-
50
34
  // ---------------------------------------------------------------------------
51
35
  // Types
52
36
  // ---------------------------------------------------------------------------
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Resolved filesystem paths for rpiv-pi's own bundled resources.
3
+ *
4
+ * `PACKAGE_ROOT` is computed at module load from this file's URL. The walk-up
5
+ * is anchored to this file's location (`extensions/rpiv-core/paths.ts`) — three
6
+ * `dirname` levels reach the rpiv-pi package root. Other resource directories
7
+ * mirror the `pi.skills` / `pi.extensions` declarations in package.json.
8
+ *
9
+ * Pi's SDK does not expose a "give me my own extension root" API, so this is
10
+ * the idiomatic resolution path (see also docs/packages.md on `pi.*` manifest
11
+ * paths being relative to the package root).
12
+ */
13
+
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ export const PACKAGE_ROOT = (() => {
18
+ const thisFile = fileURLToPath(import.meta.url);
19
+ // extensions/rpiv-core/paths.ts -> rpiv-pi/
20
+ return dirname(dirname(dirname(thisFile)));
21
+ })();
22
+
23
+ export const BUNDLED_AGENTS_DIR = join(PACKAGE_ROOT, "agents");
24
+ export const BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills");
@@ -56,10 +56,17 @@ afterEach(() => {
56
56
  });
57
57
 
58
58
  describe("registerSessionHooks — event wiring", () => {
59
- it("registers 5 events", () => {
59
+ it("registers 6 events", () => {
60
60
  const { pi, captured } = createMockPi();
61
61
  registerSessionHooks(pi);
62
- for (const ev of ["session_start", "session_compact", "session_shutdown", "tool_call", "before_agent_start"]) {
62
+ for (const ev of [
63
+ "session_start",
64
+ "session_compact",
65
+ "session_shutdown",
66
+ "tool_call",
67
+ "before_agent_start",
68
+ "agent_end",
69
+ ]) {
63
70
  expect(captured.events.has(ev)).toBe(true);
64
71
  }
65
72
  });
@@ -458,7 +465,7 @@ describe("before_agent_start hook", () => {
458
465
  registerSessionHooks(pi);
459
466
  const handler = captured.events.get("before_agent_start")?.[0];
460
467
  const ctx = createMockCtx({ cwd: projectDir });
461
- const r = await handler?.({} as never, ctx as never);
468
+ const r = await handler?.({ prompt: "" } as never, ctx as never);
462
469
  expect(r).toHaveProperty("message");
463
470
  });
464
471
 
@@ -469,8 +476,59 @@ describe("before_agent_start hook", () => {
469
476
  registerSessionHooks(pi);
470
477
  const handler = captured.events.get("before_agent_start")?.[0];
471
478
  const ctx = createMockCtx({ cwd: projectDir });
472
- await handler?.({} as never, ctx as never);
473
- const second = await handler?.({} as never, ctx as never);
479
+ await handler?.({ prompt: "" } as never, ctx as never);
480
+ const second = await handler?.({ prompt: "" } as never, ctx as never);
474
481
  expect(second).toBeUndefined();
475
482
  });
483
+
484
+ it("sets status to 'rpiv: <name>' when prompt contains an owned rpiv-pi skill block", async () => {
485
+ const { pi, captured } = createMockPi({
486
+ exec: stubGitExec({ branch: "main", commit: "abc", user: "alice" }) as never,
487
+ });
488
+ registerSessionHooks(pi);
489
+ const handler = captured.events.get("before_agent_start")?.[0];
490
+ const ctx = createMockCtx({ cwd: projectDir });
491
+ const skillPrompt = `<skill name="discover" location="/some/path">\nbody\n</skill>`;
492
+ await handler?.({ prompt: skillPrompt } as never, ctx as never);
493
+ expect(ctx.ui.setStatus).toHaveBeenCalledWith("rpiv-skill", "rpiv: discover");
494
+ });
495
+
496
+ it("does not set status for a skill block whose name is not bundled with rpiv-pi", async () => {
497
+ // Foreign / user-supplied skills must not be branded as rpiv: — only names that
498
+ // match a directory under packages/rpiv-pi/skills/ get the rpiv-skill status.
499
+ const { pi, captured } = createMockPi({
500
+ exec: stubGitExec({ branch: "main", commit: "abc", user: "alice" }) as never,
501
+ });
502
+ registerSessionHooks(pi);
503
+ const handler = captured.events.get("before_agent_start")?.[0];
504
+ const ctx = createMockCtx({ cwd: projectDir });
505
+ const skillPrompt = `<skill name="not-an-rpiv-skill" location="/home/u/.pi/skills/not-an-rpiv-skill">\nbody\n</skill>`;
506
+ await handler?.({ prompt: skillPrompt } as never, ctx as never);
507
+ const setStatusCalls = (ctx.ui.setStatus as ReturnType<typeof vi.fn>).mock.calls.filter(
508
+ (c) => c[0] === "rpiv-skill",
509
+ );
510
+ expect(setStatusCalls).toHaveLength(0);
511
+ });
512
+
513
+ it("does not set status when prompt has no skill block", async () => {
514
+ const { pi, captured } = createMockPi({
515
+ exec: stubGitExec({ branch: "main", commit: "abc", user: "alice" }) as never,
516
+ });
517
+ registerSessionHooks(pi);
518
+ const handler = captured.events.get("before_agent_start")?.[0];
519
+ const ctx = createMockCtx({ cwd: projectDir });
520
+ await handler?.({ prompt: "just a normal chat message" } as never, ctx as never);
521
+ expect(ctx.ui.setStatus).not.toHaveBeenCalled();
522
+ });
523
+ });
524
+
525
+ describe("agent_end hook", () => {
526
+ it("clears the rpiv-skill status", async () => {
527
+ const { pi, captured } = createMockPi();
528
+ registerSessionHooks(pi);
529
+ const handler = captured.events.get("agent_end")?.[0];
530
+ const ctx = createMockCtx();
531
+ await handler?.({ messages: [] } as never, ctx as never);
532
+ expect(ctx.ui.setStatus).toHaveBeenCalledWith("rpiv-skill", undefined);
533
+ });
476
534
  });
@@ -8,9 +8,12 @@
8
8
  import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import {
11
+ type AgentEndEvent,
12
+ type BeforeAgentStartEvent,
11
13
  type ExtensionAPI,
12
14
  type ExtensionContext,
13
15
  isToolCallEventType,
16
+ parseSkillBlock,
14
17
  type ToolCallEvent,
15
18
  } from "@earendil-works/pi-coding-agent";
16
19
  import {
@@ -29,6 +32,7 @@ import {
29
32
  } from "./git-context.js";
30
33
  import { ARTIFACTS_SUBDIR, clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
31
34
  import { findMissingSiblings } from "./package-checks.js";
35
+ import { BUNDLED_SKILLS_DIR } from "./paths.js";
32
36
 
33
37
  const msgAgentsAdded = (n: number) => `Copied ${n} rpiv-pi agent(s) to ~/.pi/agent/agents/`;
34
38
  const msgAgentsHealed = (parts: string[]) => `Synced bundled agent(s): ${parts.join(", ")}.`;
@@ -60,7 +64,8 @@ export function registerSessionHooks(pi: ExtensionAPI): void {
60
64
  pi.on("session_compact", async (_event, ctx) => onSessionCompact(_event, ctx, pi));
61
65
  pi.on("session_shutdown", async () => onSessionShutdown());
62
66
  pi.on("tool_call", async (event, ctx) => onToolCall(event, ctx, pi));
63
- pi.on("before_agent_start", async () => onBeforeAgentStart(pi));
67
+ pi.on("before_agent_start", async (event, ctx) => onBeforeAgentStart(event, ctx, pi));
68
+ pi.on("agent_end", async (_event, ctx) => onAgentEnd(_event, ctx));
64
69
  }
65
70
 
66
71
  // ---------------------------------------------------------------------------
@@ -107,17 +112,45 @@ async function onToolCall(event: ToolCallEvent, ctx: ExtensionContext, pi: Exten
107
112
  }
108
113
 
109
114
  async function onBeforeAgentStart(
115
+ event: BeforeAgentStartEvent,
116
+ ctx: ExtensionContext,
110
117
  pi: ExtensionAPI,
111
118
  ): Promise<{ message: ReturnType<typeof buildGitContextMessage> } | undefined> {
119
+ const parsed = parseSkillBlock(event.prompt);
120
+ if (parsed && isOwnedSkill(parsed.name)) ctx.ui.setStatus("rpiv-skill", `rpiv: ${parsed.name}`);
112
121
  const content = await takeGitContextIfChanged(pi);
113
122
  if (!content) return undefined;
114
123
  return { message: buildGitContextMessage(pi, content) };
115
124
  }
116
125
 
126
+ async function onAgentEnd(_event: AgentEndEvent, ctx: ExtensionContext): Promise<void> {
127
+ ctx.ui.setStatus("rpiv-skill", undefined);
128
+ }
129
+
117
130
  // ---------------------------------------------------------------------------
118
131
  // Helpers
119
132
  // ---------------------------------------------------------------------------
120
133
 
134
+ // Allowlist of rpiv-pi's own skill names, generated at module load by reading
135
+ // the package's bundled skills/ directory (see paths.ts — matches the
136
+ // `pi.skills` manifest in package.json). Prevents the status bar from
137
+ // claiming `rpiv:` ownership of user-supplied or third-party skills.
138
+ const OWNED_SKILL_NAMES: ReadonlySet<string> = (() => {
139
+ try {
140
+ return new Set(
141
+ readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
142
+ .filter((e) => e.isDirectory())
143
+ .map((e) => e.name),
144
+ );
145
+ } catch {
146
+ return new Set<string>();
147
+ }
148
+ })();
149
+
150
+ function isOwnedSkill(name: string): boolean {
151
+ return OWNED_SKILL_NAMES.has(name);
152
+ }
153
+
121
154
  function resetInjectionState(): void {
122
155
  clearInjectionState();
123
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "A skill-based development workflow for Pi Agent. Five skills (research, design, plan, implement, validate) and the shared subagents that compose its ship-loop.",
5
5
  "keywords": [
6
6
  "pi-package",