@juicesharp/rpiv-pi 1.8.2 → 1.9.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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ Skill-based development workflow for [Pi Agent](https://github.com/badlogic/pi-m
21
21
 
22
22
  - **A pipeline of chained AI skills** - discover → research → design → plan → implement → validate, each producing a reviewable artifact under `.rpiv/artifacts/`.
23
23
  - **Named subagents for parallel analysis** - `codebase-analyzer`, `codebase-locator`, `codebase-pattern-finder`, `claim-verifier`, and 8 more, dispatched automatically by skills.
24
- - **Session lifecycle hooks** - agent profiles, guidance files, and pipeline directories scaffold themselves on first launch.
24
+ - **Session lifecycle hooks** - agent profiles and guidance files install themselves on first launch.
25
25
 
26
26
  ## Prerequisites
27
27
 
@@ -93,7 +93,7 @@ pi install npm:@juicesharp/rpiv-pi
93
93
  On first Pi Agent session start, rpiv-pi automatically:
94
94
  - Copies agent profiles to `~/.pi/agent/agents/` (user-global, shared across all projects)
95
95
  - Detects outdated or removed agents on subsequent starts
96
- - Migrates legacy pipeline-artifact content into `.rpiv/artifacts/` (one-way) and scaffolds the artifact directories
96
+ - Migrates legacy pipeline-artifact content into `.rpiv/artifacts/` (one-way) when an old `thoughts/shared/` tree is found; otherwise `.rpiv/artifacts/` is created lazily by the first skill that writes an artifact
97
97
  - Shows a warning if any sibling plugins are missing
98
98
 
99
99
  ## Usage
@@ -66,23 +66,13 @@ describe("registerSessionHooks — event wiring", () => {
66
66
  });
67
67
 
68
68
  describe("session_start hook — migration", () => {
69
- it("scaffolds .rpiv/artifacts/ dirs on fresh project", async () => {
69
+ it("does NOT create .rpiv/artifacts/ on fresh project (no migration source) — issue #31", async () => {
70
70
  const { pi, captured } = createMockPi({ exec: stubGitExec({}) as never });
71
71
  registerSessionHooks(pi);
72
72
  const handler = captured.events.get("session_start")?.[0];
73
73
  const ctx = createMockCtx({ cwd: projectDir, hasUI: true });
74
74
  await handler?.({ reason: "startup" } as never, ctx as never);
75
- for (const d of [
76
- ".rpiv/artifacts/discover",
77
- ".rpiv/artifacts/research",
78
- ".rpiv/artifacts/designs",
79
- ".rpiv/artifacts/plans",
80
- ".rpiv/artifacts/handoffs",
81
- ".rpiv/artifacts/reviews",
82
- ".rpiv/artifacts/solutions",
83
- ]) {
84
- expect(existsSync(join(projectDir, d))).toBe(true);
85
- }
75
+ expect(existsSync(join(projectDir, ".rpiv", "artifacts"))).toBe(false);
86
76
  });
87
77
 
88
78
  it("migrates thoughts/shared/ to .rpiv/artifacts/ with content preservation", async () => {
@@ -124,6 +114,42 @@ describe("session_start hook — migration", () => {
124
114
  expect(existsSync(join(projectDir, "thoughts"))).toBe(true);
125
115
  });
126
116
 
117
+ it("does NOT create .rpiv/artifacts/ when thoughts/shared/ exists but is empty", async () => {
118
+ // Edge case: thoughts/shared/ pre-exists (created by tool, partial migration, etc.) but holds no entries.
119
+ // Migration must not leak an empty .rpiv/artifacts/ tree, and must not delete the empty source.
120
+ mkdirSync(join(projectDir, "thoughts", "shared"), { recursive: true });
121
+
122
+ const { pi, captured } = createMockPi({ exec: stubGitExec({}) as never });
123
+ registerSessionHooks(pi);
124
+ const handler = captured.events.get("session_start")?.[0];
125
+ const ctx = createMockCtx({ cwd: projectDir, hasUI: true });
126
+ await handler?.({ reason: "startup" } as never, ctx as never);
127
+
128
+ expect(existsSync(join(projectDir, ".rpiv", "artifacts"))).toBe(false);
129
+ expect(existsSync(join(projectDir, "thoughts", "shared"))).toBe(true);
130
+ });
131
+
132
+ it("preserves loose files at thoughts/shared/ root (copies them, not just subdirectories)", async () => {
133
+ // Regression: prior implementation filtered to directories only, dropping loose .md files
134
+ // at the shared/ root on rmSync. Now cpSync copies both files and directories.
135
+ const oldShared = join(projectDir, "thoughts", "shared");
136
+ mkdirSync(oldShared, { recursive: true });
137
+ writeFileSync(join(oldShared, "loose.md"), "loose content");
138
+ const oldResearch = join(oldShared, "research");
139
+ mkdirSync(oldResearch, { recursive: true });
140
+ writeFileSync(join(oldResearch, "nested.md"), "nested content");
141
+
142
+ const { pi, captured } = createMockPi({ exec: stubGitExec({}) as never });
143
+ registerSessionHooks(pi);
144
+ const handler = captured.events.get("session_start")?.[0];
145
+ const ctx = createMockCtx({ cwd: projectDir, hasUI: true });
146
+ await handler?.({ reason: "startup" } as never, ctx as never);
147
+
148
+ expect(existsSync(join(projectDir, ".rpiv", "artifacts", "loose.md"))).toBe(true);
149
+ expect(existsSync(join(projectDir, ".rpiv", "artifacts", "research", "nested.md"))).toBe(true);
150
+ expect(existsSync(join(projectDir, "thoughts"))).toBe(false);
151
+ });
152
+
127
153
  it("no-ops when thoughts/shared/ does not exist (fresh project)", async () => {
128
154
  const { pi, captured } = createMockPi({ exec: stubGitExec({}) as never });
129
155
  registerSessionHooks(pi);
@@ -131,8 +157,8 @@ describe("session_start hook — migration", () => {
131
157
  const ctx = createMockCtx({ cwd: projectDir, hasUI: true });
132
158
  await handler?.({ reason: "startup" } as never, ctx as never);
133
159
 
134
- // Artifact dirs created but no thoughts/ tree exists
135
- expect(existsSync(join(projectDir, ".rpiv", "artifacts"))).toBe(true);
160
+ // No migration source no .rpiv/artifacts/ tree, no thoughts/ tree
161
+ expect(existsSync(join(projectDir, ".rpiv", "artifacts"))).toBe(false);
136
162
  expect(existsSync(join(projectDir, "thoughts"))).toBe(false);
137
163
  });
138
164
 
@@ -30,17 +30,6 @@ import {
30
30
  import { ARTIFACTS_SUBDIR, clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
31
31
  import { findMissingSiblings } from "./package-checks.js";
32
32
 
33
- const ARTIFACTS_ROOT = `.rpiv/${ARTIFACTS_SUBDIR}`;
34
- const ARTIFACTS_DIRS = [
35
- `${ARTIFACTS_ROOT}/discover`,
36
- `${ARTIFACTS_ROOT}/research`,
37
- `${ARTIFACTS_ROOT}/designs`,
38
- `${ARTIFACTS_ROOT}/plans`,
39
- `${ARTIFACTS_ROOT}/handoffs`,
40
- `${ARTIFACTS_ROOT}/reviews`,
41
- `${ARTIFACTS_ROOT}/solutions`,
42
- ] as const;
43
-
44
33
  const msgAgentsAdded = (n: number) => `Copied ${n} rpiv-pi agent(s) to ~/.pi/agent/agents/`;
45
34
  const msgAgentsHealed = (parts: string[]) => `Synced bundled agent(s): ${parts.join(", ")}.`;
46
35
  const msgAgentsDrift = (parts: string[]) => `${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`;
@@ -135,46 +124,40 @@ function resetInjectionState(): void {
135
124
 
136
125
  function migrateThoughtsToArtifacts(cwd: string): void {
137
126
  const oldShared = join(cwd, "thoughts", "shared");
138
- const newArtifacts = join(cwd, ".rpiv", ARTIFACTS_SUBDIR);
139
-
140
- // Phase 1: Migrate existing thoughts/shared/ → .rpiv/artifacts/
141
- if (existsSync(oldShared)) {
142
- try {
143
- mkdirSync(newArtifacts, { recursive: true });
144
-
145
- const entries = readdirSync(oldShared, { withFileTypes: true }).filter((d) => d.isDirectory());
146
-
147
- for (const entry of entries) {
148
- const src = join(oldShared, entry.name);
149
- const dest = join(newArtifacts, entry.name);
150
- cpSync(src, dest, { recursive: true, errorOnExist: false, force: true });
151
- if (!existsSync(dest)) {
152
- console.warn(`[rpiv-pi] migration: failed to copy ${src} → ${dest}`);
153
- return; // abort — don't delete source if copy failed
154
- }
127
+ if (!existsSync(oldShared)) return;
128
+
129
+ try {
130
+ const entries = readdirSync(oldShared, { withFileTypes: true });
131
+ if (entries.length === 0) return; // empty source — nothing to copy, leave on disk
132
+
133
+ const newArtifacts = join(cwd, ".rpiv", ARTIFACTS_SUBDIR);
134
+ mkdirSync(newArtifacts, { recursive: true });
135
+
136
+ for (const entry of entries) {
137
+ const src = join(oldShared, entry.name);
138
+ const dest = join(newArtifacts, entry.name);
139
+ cpSync(src, dest, { recursive: true, errorOnExist: false, force: true });
140
+ if (!existsSync(dest)) {
141
+ console.warn(`[rpiv-pi] migration: failed to copy ${src} → ${dest}`);
142
+ return; // abort — don't delete source if copy failed
155
143
  }
144
+ }
156
145
 
157
- // All copies verified — safe to remove source
158
- rmSync(oldShared, { recursive: true, force: true });
159
-
160
- // Remove thoughts/ root only if empty (preserves thoughts/me/ etc.)
161
- const thoughtsRoot = join(cwd, "thoughts");
162
- try {
163
- if (readdirSync(thoughtsRoot).length === 0) {
164
- rmSync(thoughtsRoot, { recursive: true, force: true });
165
- }
166
- } catch {
167
- // thoughts/ already gone or unreadable — not an error
146
+ // All copies verified — safe to remove source
147
+ rmSync(oldShared, { recursive: true, force: true });
148
+
149
+ // Remove thoughts/ root only if empty (preserves thoughts/me/ etc.)
150
+ const thoughtsRoot = join(cwd, "thoughts");
151
+ try {
152
+ if (readdirSync(thoughtsRoot).length === 0) {
153
+ rmSync(thoughtsRoot, { recursive: true, force: true });
168
154
  }
169
- } catch (e) {
170
- console.warn(`[rpiv-pi] migration: ${e instanceof Error ? e.message : String(e)}`);
171
- // Never crash session_start — migration is best-effort
155
+ } catch {
156
+ // thoughts/ already gone or unreadable not an error
172
157
  }
173
- }
174
-
175
- // Phase 2: Ensure artifact directories exist (mirrors old scaffolding)
176
- for (const dir of ARTIFACTS_DIRS) {
177
- mkdirSync(join(cwd, dir), { recursive: true });
158
+ } catch (e) {
159
+ console.warn(`[rpiv-pi] migration: ${e instanceof Error ? e.message : String(e)}`);
160
+ // Never crash session_start migration is best-effort
178
161
  }
179
162
  }
180
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
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",
@@ -164,7 +164,7 @@ Compile interview output into the FRD. The interview's logical order (problem
164
164
  - Timestamp: run `date +"%Y-%m-%dT%H:%M:%S%z"` — raw for `date:` and `last_updated:`, first 19 chars (`T`→`_`, `:`→`-`) for filename slug.
165
165
  - Interviewer: from the User in the injected git context (fallback: `unknown`).
166
166
 
167
- 2. **Write the FRD** using the Write tool. Frontmatter `status: complete`. All template sections present and filled. The directory `.rpiv/artifacts/discover/` is pre-scaffolded by `session-hooks.ts` — no `mkdir -p` needed in the skill.
167
+ 2. **Write the FRD** using the Write tool. Frontmatter `status: complete`. All template sections present and filled. The Write tool creates parent directories automatically — no `mkdir -p` needed in the skill.
168
168
 
169
169
  3. **Present and chain**:
170
170
  ```