@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 {
|
|
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
|
|
59
|
+
it("registers 6 events", () => {
|
|
60
60
|
const { pi, captured } = createMockPi();
|
|
61
61
|
registerSessionHooks(pi);
|
|
62
|
-
for (const ev of [
|
|
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.
|
|
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",
|