@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.
- package/extensions/rpiv-core/built-in-workflows.test.ts +4 -4
- package/extensions/rpiv-core/index.ts +11 -3
- package/extensions/rpiv-core/register-built-in-workflows.test.ts +77 -0
- package/extensions/rpiv-core/register-built-in-workflows.ts +62 -0
- package/package.json +1 -1
- package/skills/blueprint/SKILL.md +20 -2
- package/skills/design/SKILL.md +20 -2
|
@@ -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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
103
|
+
For each dimension, classify findings into three classes:
|
|
104
104
|
|
|
105
|
-
|
|
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
|
|
package/skills/design/SKILL.md
CHANGED
|
@@ -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
|
|
104
|
+
For each dimension, classify findings into three classes:
|
|
105
105
|
|
|
106
|
-
|
|
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
|
|