@juicesharp/rpiv-pi 1.16.0 → 1.17.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.
@@ -36,11 +36,11 @@ import {
36
36
  type FanoutFn,
37
37
  produces,
38
38
  type RunState,
39
+ runsDir,
39
40
  runWorkflow,
40
41
  stateFilePath,
41
42
  validateWorkflow,
42
43
  type Workflow,
43
- workflowsDir,
44
44
  } from "@juicesharp/rpiv-workflow";
45
45
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
46
46
  import { rpivArtifactMdOutcome } from "./artifact-collector.js";
@@ -102,7 +102,7 @@ describe("[I2] readers must not silently drop the first row when no header is on
102
102
  it("readLastStage returns the row even when the header line is missing", async () => {
103
103
  const { readLastStage } = await import("@juicesharp/rpiv-workflow");
104
104
  const runId = "2026-05-23_13-05-38-abcd";
105
- mkdirSync(workflowsDir(tmpDir), { recursive: true });
105
+ mkdirSync(runsDir(tmpDir), { recursive: true });
106
106
  const filePath = stateFilePath(tmpDir, runId);
107
107
  const stageRow = {
108
108
  stageNumber: 1,
@@ -181,7 +181,7 @@ describe("[I7] truncated reply (stopReason=length) must not record as completed"
181
181
  });
182
182
 
183
183
  const readStages = (cwd: string): Array<Record<string, unknown>> => {
184
- const dir = join(cwd, ".rpiv", "workflows");
184
+ const dir = join(cwd, ".rpiv", "workflows", "runs");
185
185
  const files = readdirSync(dir);
186
186
  expect(files).toHaveLength(1);
187
187
  const lines = readFileSync(join(dir, files[0]!), "utf-8").trim().split("\n");
@@ -348,7 +348,7 @@ describe("[I9] phase fanout rows preserve both stage name (record key) and skill
348
348
  });
349
349
 
350
350
  const readRows = (cwd: string): Array<Record<string, unknown>> => {
351
- const dir = join(cwd, ".rpiv", "workflows");
351
+ const dir = join(cwd, ".rpiv", "workflows", "runs");
352
352
  const files = readdirSync(dir);
353
353
  expect(files).toHaveLength(1);
354
354
  const lines = readFileSync(join(dir, files[0]!), "utf-8").trim().split("\n");
@@ -13,9 +13,8 @@
13
13
  */
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
- import { registerBuiltIns } from "@juicesharp/rpiv-workflow";
17
- import { builtInWorkflows } from "./built-in-workflows.js";
18
16
  import { FLAG_DEBUG } from "./constants.js";
17
+ import { registerBuiltInWorkflows } from "./register-built-in-workflows.js";
19
18
  import { registerSessionHooks } from "./session-hooks.js";
20
19
  import { registerSetupCommand } from "./setup-command.js";
21
20
  import { registerUpdateAgentsCommand } from "./update-agents-command.js";
@@ -26,8 +25,17 @@ export default function (pi: ExtensionAPI) {
26
25
  type: "boolean",
27
26
  default: false,
28
27
  });
28
+ // These three register UNCONDITIONALLY and FIRST — they must work on a clean
29
+ // install where the rpiv-workflow sibling is absent, so the missing-sibling
30
+ // banner and /rpiv-setup are what guide the user to install it.
29
31
  registerSessionHooks(pi);
30
32
  registerUpdateAgentsCommand(pi);
31
33
  registerSetupCommand(pi);
32
- registerBuiltIns(builtInWorkflows);
34
+ // Built-in workflows feed the sibling's `/wf` command. Deferred behind a
35
+ // dynamic import so a missing sibling degrades gracefully instead of taking
36
+ // the whole extension down (see register-built-in-workflows.ts). Fire-and-
37
+ // forget: the registry is read lazily at `/wf` time, long after this settles.
38
+ registerBuiltInWorkflows().catch((err: unknown) => {
39
+ console.error("[rpiv-core] failed to register built-in workflows:", err);
40
+ });
33
41
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Regression tests for the clean-install chicken-and-egg bug: a top-level
3
+ * static `import … from "@juicesharp/rpiv-workflow"` in rpiv-core/index.ts made
4
+ * the whole extension fail to load when the (peerDependency) sibling was
5
+ * absent, suppressing the /rpiv-setup command + missing-sibling banner that
6
+ * tell the user to install it. The fix defers the dependency behind a guarded
7
+ * dynamic import; these tests pin both the happy path and the absent-sibling
8
+ * no-op.
9
+ */
10
+
11
+ import { afterEach, describe, expect, it, vi } from "vitest";
12
+ import { isModuleNotFound, registerBuiltInWorkflows } from "./register-built-in-workflows.js";
13
+
14
+ const BUILT_IN_NAMES = ["arch", "build", "polish", "ship", "vet"];
15
+
16
+ describe("isModuleNotFound", () => {
17
+ it("is true for an ERR_MODULE_NOT_FOUND error", () => {
18
+ const err = Object.assign(new Error("Cannot find package"), { code: "ERR_MODULE_NOT_FOUND" });
19
+ expect(isModuleNotFound(err)).toBe(true);
20
+ });
21
+
22
+ it("is false for other error codes and codeless errors", () => {
23
+ expect(isModuleNotFound(Object.assign(new Error("boom"), { code: "ERR_OTHER" }))).toBe(false);
24
+ expect(isModuleNotFound(new Error("no code"))).toBe(false);
25
+ });
26
+
27
+ it("is false for non-object values", () => {
28
+ expect(isModuleNotFound(null)).toBe(false);
29
+ expect(isModuleNotFound(undefined)).toBe(false);
30
+ expect(isModuleNotFound("ERR_MODULE_NOT_FOUND")).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("registerBuiltInWorkflows", () => {
35
+ it("registers the five built-in workflows when rpiv-workflow is present", async () => {
36
+ const { getBuiltIns } = await import("@juicesharp/rpiv-workflow/internal");
37
+ expect(getBuiltIns()).toEqual([]); // setup.ts beforeEach resets the registry
38
+
39
+ await registerBuiltInWorkflows();
40
+
41
+ expect(
42
+ getBuiltIns()
43
+ .map((w) => w.name)
44
+ .sort(),
45
+ ).toEqual(BUILT_IN_NAMES);
46
+ });
47
+
48
+ it("is idempotent — re-registering does not duplicate", async () => {
49
+ const { getBuiltIns } = await import("@juicesharp/rpiv-workflow/internal");
50
+ await registerBuiltInWorkflows();
51
+ await registerBuiltInWorkflows();
52
+ expect(getBuiltIns()).toHaveLength(BUILT_IN_NAMES.length);
53
+ });
54
+
55
+ describe("when the rpiv-workflow sibling is absent", () => {
56
+ afterEach(() => {
57
+ vi.doUnmock("@juicesharp/rpiv-workflow");
58
+ vi.resetModules();
59
+ });
60
+
61
+ it("no-ops without throwing and registers nothing", async () => {
62
+ vi.resetModules();
63
+ vi.doMock("@juicesharp/rpiv-workflow", () => {
64
+ throw Object.assign(new Error("Cannot find package '@juicesharp/rpiv-workflow'"), {
65
+ code: "ERR_MODULE_NOT_FOUND",
66
+ });
67
+ });
68
+
69
+ // Re-import the registrar so its internal dynamic import resolves the mock.
70
+ const fresh = await import("./register-built-in-workflows.js");
71
+ await expect(fresh.registerBuiltInWorkflows()).resolves.toBeUndefined();
72
+
73
+ const { getBuiltIns } = await import("@juicesharp/rpiv-workflow/internal");
74
+ expect(getBuiltIns()).toEqual([]);
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Guarded registration of rpiv-pi's built-in workflows into the
3
+ * `@juicesharp/rpiv-workflow` runtime registry.
4
+ *
5
+ * rpiv-workflow is a SIBLING (see siblings.ts) — a peerDependency that a clean
6
+ * `npm install @juicesharp/rpiv-pi` does NOT pull in; users add it via
7
+ * /rpiv-setup. So rpiv-core must never statically import it: a top-level
8
+ * `import … from "@juicesharp/rpiv-workflow"` makes the WHOLE extension fail to
9
+ * load when the sibling is absent, which in turn suppresses the very
10
+ * /rpiv-setup command and missing-sibling banner that tell the user to install
11
+ * it — a chicken-and-egg that strands clean installs.
12
+ *
13
+ * The dependency is therefore deferred behind a dynamic import so the entry
14
+ * point has no static edge to the peer. When rpiv-workflow is absent we simply
15
+ * skip registration: the built-ins are consumed only by the `/wf` command,
16
+ * which lives in rpiv-workflow itself, so there is nothing to lose. This keeps
17
+ * rpiv-core aligned with the "no runtime import of sibling packages" rule the
18
+ * other siblings already follow (siblings.ts header).
19
+ */
20
+
21
+ /**
22
+ * True for a Node module-resolution failure (the sibling isn't installed).
23
+ *
24
+ * Walks the `cause` chain: a clean `await import(...)` of a missing package
25
+ * rejects with `code === "ERR_MODULE_NOT_FOUND"` directly, but ESM loaders and
26
+ * tooling (vitest's mock layer, some bundlers) wrap that error, nesting the
27
+ * real code under `.cause`. Bounded against pathological self-referential
28
+ * chains.
29
+ */
30
+ export function isModuleNotFound(err: unknown): boolean {
31
+ for (
32
+ let cur: unknown = err, depth = 0;
33
+ cur != null && depth < 16;
34
+ cur = (cur as { cause?: unknown }).cause, depth++
35
+ ) {
36
+ if (typeof cur === "object" && (cur as { code?: unknown }).code === "ERR_MODULE_NOT_FOUND") {
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * Register the five built-in workflows (ship / build / arch / vet / polish)
45
+ * with the rpiv-workflow runtime, if that sibling is installed. A missing
46
+ * sibling resolves to a no-op; any other failure is re-thrown so genuine bugs
47
+ * surface rather than hiding behind the absent-sibling path.
48
+ */
49
+ export async function registerBuiltInWorkflows(): Promise<void> {
50
+ try {
51
+ // built-in-workflows.js top-level-imports the workflow DSL, so resolving
52
+ // it is enough to trigger the same module failure when the sibling is
53
+ // gone — but import the package explicitly first so the absence check is
54
+ // unambiguous and we never partially evaluate the workflow definitions.
55
+ const { registerBuiltIns } = await import("@juicesharp/rpiv-workflow");
56
+ const { builtInWorkflows } = await import("./built-in-workflows.js");
57
+ registerBuiltIns(builtInWorkflows);
58
+ } catch (err) {
59
+ if (isModuleNotFound(err)) return; // sibling absent — /rpiv-setup prompts the user
60
+ throw err;
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "1.16.0",
3
+ "version": "1.17.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",
@@ -100,9 +100,15 @@ Walk Step 2 findings, inherited research Q/As, and carried Open Questions throug
100
100
  - **Verification** — tests, assertions, risk-bearing behaviors
101
101
  - **Performance** — load paths, caching, N+1 risks
102
102
 
103
- For each dimension, classify findings as **simple decisions** (one valid option, obvious from codebase — record in Decisions with `file:line` evidence, do not ask) or **genuine ambiguities** (multiple valid options, conflicting patterns, scope questions, novel choices — queue for Step 4). Inherited research Q/As land as simple; Open Questions filter by dimension — architectural survives, implementation-detail defers.
103
+ For each dimension, classify findings into three classes:
104
104
 
105
- **Pre-validate every option** before queuing it against research constraints and runtime code behavior. Eliminate or caveat options that contradict Steps 1-2 evidence. **Coverage check**: every Step 2 file read appears in at least one decision or ambiguity; every dimension is addressed (silently-resolved valid, skipped-unchecked not).
105
+ - **simple** one valid option, obvious from codebase, no directional weight. Record in Decisions with `file:line` evidence; do not ask.
106
+ - **directional** — obvious *fit*, but the choice encodes a direction: propagates an existing pattern across new files, picks extend-vs-replace, or spreads a convention the project may be moving off. Queue for the **directional confirm** (Step 4, one batched ask).
107
+ - **genuine ambiguity** — multiple valid options, conflicting patterns, scope questions, novel choices. Queue for Step 4, one-at-a-time.
108
+
109
+ Inherited research Q/As land as simple unless directional; Open Questions filter by dimension — architectural survives, implementation-detail defers.
110
+
111
+ **Pre-validate every option** before queuing it against research constraints and runtime code behavior. Eliminate or caveat options that contradict Steps 1-2 evidence. **Coverage check**: every Step 2 file read appears in at least one decision, directional confirm, or ambiguity; every dimension is addressed (silently-resolved valid, skipped-unchecked not).
106
112
 
107
113
  ### Step 4: Developer Checkpoint
108
114
 
@@ -111,6 +117,12 @@ Use the grounded-questions-one-at-a-time pattern. Use a **❓ Question:** prefix
111
117
  - Present concrete options (not abstract choices)
112
118
  - Pull a DECISION from the developer, not confirm what you already found
113
119
 
120
+ **Directional confirms first.** Before the one-at-a-time questions, clear every **directional** finding from Step 3 in a single batched `ask_user_question` (up to 4 per call). Do not mark the "follow" option Recommended.
121
+
122
+ > Question: "About to follow {pattern X} (`file:line`, used ×N) across {the N new files} — confirm that's the direction, or moving off it?". Header: "Direction". Options: "Follow {X}" (propagate as-is); "Moving off {X}" (deliberate departure).
123
+
124
+ **Follow** records the decision as stated. **Moving off** promotes the finding to a genuine ambiguity — ask it one-at-a-time below with the alternative in view.
125
+
114
126
  **Question patterns by ambiguity type:**
115
127
 
116
128
  - **Pattern conflict**: "Found 2 patterns for {X}: {pattern A} at `file:line` and {pattern B} at `file:line`. They differ in {specific way}. Which should the new {feature} follow?"
@@ -277,6 +289,12 @@ Present a **condensed review** of the slice — NOT the full generated code. The
277
289
  2. **Signatures**: type/interface definitions, exported function signatures with parameter and return types
278
290
  3. **Key code blocks**: factory calls, wiring, non-obvious logic — the interesting parts that show the design decision in action
279
291
 
292
+ **Then, once per slice, a mandatory Fit line** (always shown, regardless of the omit list below):
293
+
294
+ > **Fit** — Reused: {existing helpers/utils/types this slice builds on, `file:line`}. New surface: {new abstractions/exports introduced}. Convention: {naming/error/logging pattern followed + source `file:line`}.
295
+
296
+ If the slice introduces a new abstraction where an existing one would serve, or reuses nothing, say so explicitly.
297
+
280
298
  **Omit**: boilerplate, import lists, full function bodies, obvious implementations.
281
299
  **MODIFY files**: focused diff (`- old` / `+ new`) with ~3 lines context. **Test files**: test case names only.
282
300
 
@@ -101,9 +101,15 @@ Walk Step 2 findings, inherited research Q/As, and carried Open Questions throug
101
101
  - **Verification** — tests, assertions, risk-bearing behaviors
102
102
  - **Performance** — load paths, caching, N+1 risks
103
103
 
104
- For each dimension, classify findings as **simple decisions** (one valid option, obvious from codebase — record in Decisions with `file:line` evidence, do not ask) or **genuine ambiguities** (multiple valid options, conflicting patterns, scope questions, novel choices — queue for Step 4). Inherited research Q/As land as simple; Open Questions filter by dimension — architectural survives, implementation-detail defers.
104
+ For each dimension, classify findings into three classes:
105
105
 
106
- **Pre-validate every option** before queuing it against research constraints and runtime code behavior. Eliminate or caveat options that contradict Steps 1-2 evidence. **Coverage check**: every Step 2 file read appears in at least one decision or ambiguity; every dimension is addressed (silently-resolved valid, skipped-unchecked not).
106
+ - **simple** one valid option, obvious from codebase, no directional weight. Record in Decisions with `file:line` evidence; do not ask.
107
+ - **directional** — obvious *fit*, but the choice encodes a direction: propagates an existing pattern across new files, picks extend-vs-replace, or spreads a convention the project may be moving off. Queue for the **directional confirm** (Step 4, one batched ask).
108
+ - **genuine ambiguity** — multiple valid options, conflicting patterns, scope questions, novel choices. Queue for Step 4, one-at-a-time.
109
+
110
+ Inherited research Q/As land as simple unless directional; Open Questions filter by dimension — architectural survives, implementation-detail defers.
111
+
112
+ **Pre-validate every option** before queuing it against research constraints and runtime code behavior. Eliminate or caveat options that contradict Steps 1-2 evidence. **Coverage check**: every Step 2 file read appears in at least one decision, directional confirm, or ambiguity; every dimension is addressed (silently-resolved valid, skipped-unchecked not).
107
113
 
108
114
  ### Step 4: Developer Checkpoint
109
115
 
@@ -112,6 +118,12 @@ Use the grounded-questions-one-at-a-time pattern. Use a **❓ Question:** prefix
112
118
  - Present concrete options (not abstract choices)
113
119
  - Pull a DECISION from the developer, not confirm what you already found
114
120
 
121
+ **Directional confirms first.** Before the one-at-a-time questions, clear every **directional** finding from Step 3 in a single batched `ask_user_question` (up to 4 per call). Do not mark the "follow" option Recommended.
122
+
123
+ > Question: "About to follow {pattern X} (`file:line`, used ×N) across {the N new files} — confirm that's the direction, or moving off it?". Header: "Direction". Options: "Follow {X}" (propagate as-is); "Moving off {X}" (deliberate departure).
124
+
125
+ **Follow** records the decision as stated. **Moving off** promotes the finding to a genuine ambiguity — ask it one-at-a-time below with the alternative in view.
126
+
115
127
  **Question patterns by ambiguity type:**
116
128
 
117
129
  - **Pattern conflict**: "Found 2 patterns for {X}: {pattern A} at `file:line` and {pattern B} at `file:line`. They differ in {specific way}. Which should the new {feature} follow?"
@@ -275,6 +287,12 @@ Present a **condensed review** of the slice — NOT the full generated code. The
275
287
  2. **Signatures**: type/interface definitions, exported function signatures with parameter and return types
276
288
  3. **Key code blocks**: factory calls, wiring, non-obvious logic — the interesting parts that show the design decision in action
277
289
 
290
+ **Then, once per slice, a mandatory Fit line** (always shown, regardless of the omit list below):
291
+
292
+ > **Fit** — Reused: {existing helpers/utils/types this slice builds on, `file:line`}. New surface: {new abstractions/exports introduced}. Convention: {naming/error/logging pattern followed + source `file:line`}.
293
+
294
+ If the slice introduces a new abstraction where an existing one would serve, or reuses nothing, say so explicitly.
295
+
278
296
  **Omit**: boilerplate, import lists, full function bodies, obvious implementations.
279
297
  **MODIFY files**: focused diff (`- old` / `+ new`) with ~3 lines context. **Test files**: test case names only.
280
298