@juicesharp/rpiv-pi 1.14.7 → 1.16.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.
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { appendFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import { isAbsolute, join } from "node:path";
|
|
30
|
-
import { createMockSessionChain, mockAssistantMessage } from "@juicesharp/rpiv-test-utils";
|
|
30
|
+
import { createMockPi, createMockSessionChain, mockAssistantMessage } from "@juicesharp/rpiv-test-utils";
|
|
31
31
|
import {
|
|
32
32
|
acts,
|
|
33
33
|
defineRoute,
|
|
@@ -599,3 +599,267 @@ describe("[Q4] vet workflow", () => {
|
|
|
599
599
|
});
|
|
600
600
|
});
|
|
601
601
|
});
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// polish — iterate-driven per-review-phase blueprint + latest-pass implement.
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
describe("polish workflow", () => {
|
|
608
|
+
describe("structural validation", () => {
|
|
609
|
+
it("validates with zero errors", () => {
|
|
610
|
+
expect(validateWorkflow(findWorkflow("polish")).filter((i) => i.severity === "error")).toEqual([]);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("all stages are reachable from start", () => {
|
|
614
|
+
const issues = validateWorkflow(findWorkflow("polish"));
|
|
615
|
+
expect(
|
|
616
|
+
issues.filter((i) => /unreachable/.test(i.message)),
|
|
617
|
+
"polish has unreachable stages",
|
|
618
|
+
).toEqual([]);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("blueprint is an iterate stage and implement is a fanout stage (the two co-exist)", () => {
|
|
622
|
+
const wf = findWorkflow("polish");
|
|
623
|
+
expect(typeof wf.stages.blueprint?.iterate).toBe("function");
|
|
624
|
+
expect(wf.stages.blueprint?.kind).toBe("produces");
|
|
625
|
+
expect(typeof wf.stages.implement?.fanout).toBe("function");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("code-review carries CODE_REVIEW_SCHEMA and gates to commit | blueprint", () => {
|
|
629
|
+
const wf = findWorkflow("polish");
|
|
630
|
+
expect(wf.stages["code-review"]?.outputSchema).toBeDefined();
|
|
631
|
+
const edge = wf.edges["code-review"];
|
|
632
|
+
if (typeof edge !== "function") throw new Error("code-review edge is not an EdgeFn");
|
|
633
|
+
expect([...(edge.targets ?? [])].sort()).toEqual(["blueprint", "commit"]);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("integration", () => {
|
|
638
|
+
let tmpDir: string;
|
|
639
|
+
beforeEach(() => {
|
|
640
|
+
tmpDir = mkdtempSync(join(tmpdir(), "rpiv-polish-"));
|
|
641
|
+
});
|
|
642
|
+
afterEach(() => {
|
|
643
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const write = (relPath: string, content: string) => {
|
|
647
|
+
const parts = relPath.split("/");
|
|
648
|
+
mkdirSync(join(tmpDir, ...parts.slice(0, -1)), { recursive: true });
|
|
649
|
+
writeFileSync(join(tmpDir, relPath), content);
|
|
650
|
+
};
|
|
651
|
+
const plan = (phase = 1) => `---\ntopic: t\n---\n## Phase ${phase}: do the thing\nbody\n`;
|
|
652
|
+
const review2 = "# Arch Review\n\n### Phase 1 — Alpha\nbody\n### Phase 2 — Beta\nbody\n";
|
|
653
|
+
const review1 = "# Arch Review\n\n### Phase 1 — Alpha\nbody\n";
|
|
654
|
+
const cr = (blockers: number) => `---\nblockers_count: ${blockers}\n---\n`;
|
|
655
|
+
const impl = (m: string) => ({ branch: [mockAssistantMessage(m)] });
|
|
656
|
+
|
|
657
|
+
it("happy path: one blueprint pass per review phase, each fed the prior plans; implement fans out the plans", async () => {
|
|
658
|
+
write(".rpiv/artifacts/architecture-reviews/rev.md", review2);
|
|
659
|
+
write(".rpiv/artifacts/plans/plan-1.md", plan());
|
|
660
|
+
write(".rpiv/artifacts/plans/plan-2.md", plan());
|
|
661
|
+
write(".rpiv/artifacts/validation/val.md", "");
|
|
662
|
+
write(".rpiv/artifacts/reviews/cr.md", cr(0));
|
|
663
|
+
|
|
664
|
+
const chain = createMockSessionChain({
|
|
665
|
+
cwd: tmpDir,
|
|
666
|
+
steps: [
|
|
667
|
+
impl("wrote .rpiv/artifacts/architecture-reviews/rev.md"),
|
|
668
|
+
impl("wrote .rpiv/artifacts/plans/plan-1.md"),
|
|
669
|
+
impl("wrote .rpiv/artifacts/plans/plan-2.md"),
|
|
670
|
+
impl("phase done"),
|
|
671
|
+
impl("phase done"),
|
|
672
|
+
impl("wrote .rpiv/artifacts/validation/val.md"),
|
|
673
|
+
impl("wrote .rpiv/artifacts/reviews/cr.md"),
|
|
674
|
+
impl("committed"),
|
|
675
|
+
],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const result = await runWorkflow(chain.ctx, { workflow: findWorkflow("polish"), input: "x" });
|
|
679
|
+
|
|
680
|
+
expect(result.success).toBe(true);
|
|
681
|
+
// arch-review + blueprint×2 + implement×2 + validate + code-review + commit
|
|
682
|
+
expect(result.stagesCompleted).toBe(8);
|
|
683
|
+
// blueprint pulled one unit per review phase; phase 2 saw phase 1's plan.
|
|
684
|
+
expect(chain.sentMessages[1]).toBe(
|
|
685
|
+
"/skill:blueprint .rpiv/artifacts/architecture-reviews/rev.md Implement Phase 1: Alpha",
|
|
686
|
+
);
|
|
687
|
+
expect(chain.sentMessages[2]).toBe(
|
|
688
|
+
"/skill:blueprint .rpiv/artifacts/architecture-reviews/rev.md Implement Phase 2: Beta\n" +
|
|
689
|
+
"Prior phase plans (read first; build on them, don't duplicate): .rpiv/artifacts/plans/plan-1.md",
|
|
690
|
+
);
|
|
691
|
+
// implement fanned out the `## Phase N:` heading of each accumulated plan.
|
|
692
|
+
expect(chain.sentMessages.filter((m) => m.startsWith("/skill:implement"))).toEqual([
|
|
693
|
+
"/skill:implement .rpiv/artifacts/plans/plan-1.md Phase 1",
|
|
694
|
+
"/skill:implement .rpiv/artifacts/plans/plan-2.md Phase 1",
|
|
695
|
+
]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("validate receives EVERY plan from the latest blueprint pass in one /skill:validate call", async () => {
|
|
699
|
+
write(".rpiv/artifacts/architecture-reviews/rev.md", review2);
|
|
700
|
+
write(".rpiv/artifacts/plans/plan-1.md", plan());
|
|
701
|
+
write(".rpiv/artifacts/plans/plan-2.md", plan());
|
|
702
|
+
write(".rpiv/artifacts/validation/val.md", "");
|
|
703
|
+
write(".rpiv/artifacts/reviews/cr.md", cr(0));
|
|
704
|
+
|
|
705
|
+
const chain = createMockSessionChain({
|
|
706
|
+
cwd: tmpDir,
|
|
707
|
+
steps: [
|
|
708
|
+
impl("wrote .rpiv/artifacts/architecture-reviews/rev.md"),
|
|
709
|
+
impl("wrote .rpiv/artifacts/plans/plan-1.md"),
|
|
710
|
+
impl("wrote .rpiv/artifacts/plans/plan-2.md"),
|
|
711
|
+
impl("phase done"),
|
|
712
|
+
impl("phase done"),
|
|
713
|
+
impl("wrote .rpiv/artifacts/validation/val.md"),
|
|
714
|
+
impl("wrote .rpiv/artifacts/reviews/cr.md"),
|
|
715
|
+
impl("committed"),
|
|
716
|
+
],
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const result = await runWorkflow(chain.ctx, { workflow: findWorkflow("polish"), input: "x" });
|
|
720
|
+
|
|
721
|
+
expect(result.success).toBe(true);
|
|
722
|
+
// The single validate session is handed ALL accumulated plans — not just
|
|
723
|
+
// the rolling-primary (last) plan — so every phase gets validated.
|
|
724
|
+
expect(chain.sentMessages.filter((m) => m.startsWith("/skill:validate"))).toEqual([
|
|
725
|
+
"/skill:validate .rpiv/artifacts/plans/plan-1.md .rpiv/artifacts/plans/plan-2.md",
|
|
726
|
+
]);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("corrective loop: implement consumes only the LATEST blueprint pass, never re-implementing a stale plan", async () => {
|
|
730
|
+
write(".rpiv/artifacts/architecture-reviews/rev.md", review1);
|
|
731
|
+
for (const n of [1, 2, 3]) write(`.rpiv/artifacts/plans/plan-${n}.md`, plan());
|
|
732
|
+
for (const n of [1, 2, 3]) write(`.rpiv/artifacts/validation/val-${n}.md`, "");
|
|
733
|
+
for (const n of [1, 2, 3]) write(`.rpiv/artifacts/reviews/cr-${n}.md`, cr(1)); // always blockers → loop
|
|
734
|
+
|
|
735
|
+
const chain = createMockSessionChain({
|
|
736
|
+
cwd: tmpDir,
|
|
737
|
+
steps: [
|
|
738
|
+
impl("wrote .rpiv/artifacts/architecture-reviews/rev.md"),
|
|
739
|
+
// pass 0
|
|
740
|
+
impl("wrote .rpiv/artifacts/plans/plan-1.md"),
|
|
741
|
+
impl("phase done"),
|
|
742
|
+
impl("wrote .rpiv/artifacts/validation/val-1.md"),
|
|
743
|
+
impl("wrote .rpiv/artifacts/reviews/cr-1.md"),
|
|
744
|
+
// pass 1 (backward jump 1)
|
|
745
|
+
impl("wrote .rpiv/artifacts/plans/plan-2.md"),
|
|
746
|
+
impl("phase done"),
|
|
747
|
+
impl("wrote .rpiv/artifacts/validation/val-2.md"),
|
|
748
|
+
impl("wrote .rpiv/artifacts/reviews/cr-2.md"),
|
|
749
|
+
// pass 2 (backward jump 2)
|
|
750
|
+
impl("wrote .rpiv/artifacts/plans/plan-3.md"),
|
|
751
|
+
impl("phase done"),
|
|
752
|
+
impl("wrote .rpiv/artifacts/validation/val-3.md"),
|
|
753
|
+
impl("wrote .rpiv/artifacts/reviews/cr-3.md"),
|
|
754
|
+
// 3rd code-review's gate → blueprint = backward jump 3 > 2 → halt
|
|
755
|
+
],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const result = await runWorkflow(chain.ctx, { workflow: findWorkflow("polish"), input: "x" });
|
|
759
|
+
|
|
760
|
+
expect(result.success).toBe(false);
|
|
761
|
+
expect(result.error).toMatch(/backward-jump limit exceeded/i);
|
|
762
|
+
// Each implement round saw ONLY that pass's plan — the latest-pass slice
|
|
763
|
+
// dropped the stale generations, so no plan is implemented twice.
|
|
764
|
+
expect(chain.sentMessages.filter((m) => m.startsWith("/skill:implement"))).toEqual([
|
|
765
|
+
"/skill:implement .rpiv/artifacts/plans/plan-1.md Phase 1",
|
|
766
|
+
"/skill:implement .rpiv/artifacts/plans/plan-2.md Phase 1",
|
|
767
|
+
"/skill:implement .rpiv/artifacts/plans/plan-3.md Phase 1",
|
|
768
|
+
]);
|
|
769
|
+
// validate shares the same latest-pass slice — each round validates only
|
|
770
|
+
// that pass's plan, never a stale generation.
|
|
771
|
+
expect(chain.sentMessages.filter((m) => m.startsWith("/skill:validate"))).toEqual([
|
|
772
|
+
"/skill:validate .rpiv/artifacts/plans/plan-1.md",
|
|
773
|
+
"/skill:validate .rpiv/artifacts/plans/plan-2.md",
|
|
774
|
+
"/skill:validate .rpiv/artifacts/plans/plan-3.md",
|
|
775
|
+
]);
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
// design-to-code — the prompt-dispatch worked example. NOT registered in
|
|
782
|
+
// builtInWorkflows: it names `frontend-design` (a separate plugin skill, not
|
|
783
|
+
// bundled by rpiv-pi) and rides the unexercised continue path. Kept here as a
|
|
784
|
+
// validated example proving the spec's three-dispatch chain is well-formed.
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
describe("design-to-code example (prompt dispatch)", () => {
|
|
788
|
+
const designToCode = defineWorkflow({
|
|
789
|
+
name: "design-to-code",
|
|
790
|
+
description: "Discover a spec, design in the same session, then implement from conversation context.",
|
|
791
|
+
start: "discover",
|
|
792
|
+
stages: {
|
|
793
|
+
// skill dispatch, fresh — writes a spec artifact, opens the session
|
|
794
|
+
discover: produces({ outcome: rpivArtifactMdOutcome }),
|
|
795
|
+
// skill dispatch, continue — reasons in-session, produces no tracked artifact
|
|
796
|
+
design: acts({ skill: "frontend-design", sessionPolicy: "continue" }),
|
|
797
|
+
// prompt dispatch, continue — a focused instruction leaning on context
|
|
798
|
+
implement: acts({ prompt: "Implement the design spec discussed above.", sessionPolicy: "continue" }),
|
|
799
|
+
},
|
|
800
|
+
edges: { discover: "design", design: "implement", implement: "stop" },
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("validates with zero errors and zero warnings", () => {
|
|
804
|
+
expect(validateWorkflow(designToCode)).toEqual([]);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it("all stages are reachable from start", () => {
|
|
808
|
+
expect(validateWorkflow(designToCode).filter((i) => /unreachable/.test(i.message))).toEqual([]);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("resolves all three dispatch types in one chain", () => {
|
|
812
|
+
// discover → skill dispatch (no prompt, no run)
|
|
813
|
+
expect(designToCode.stages.discover?.prompt).toBeUndefined();
|
|
814
|
+
expect(designToCode.stages.discover?.run).toBeUndefined();
|
|
815
|
+
// design → skill dispatch in a continued session
|
|
816
|
+
expect(designToCode.stages.design?.skill).toBe("frontend-design");
|
|
817
|
+
expect(designToCode.stages.design?.sessionPolicy).toBe("continue");
|
|
818
|
+
// implement → prompt dispatch in a continued session
|
|
819
|
+
expect(typeof designToCode.stages.implement?.prompt).toBe("string");
|
|
820
|
+
expect(designToCode.stages.implement?.sessionPolicy).toBe("continue");
|
|
821
|
+
expect(designToCode.stages.implement?.skill).toBeUndefined();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it("runs the skill → continue-skill → continue-prompt chain end-to-end in one session", async () => {
|
|
825
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "rpiv-d2c-"));
|
|
826
|
+
try {
|
|
827
|
+
// discover's spec must exist on disk (rpivArtifactMdOutcome reads frontmatter).
|
|
828
|
+
mkdirSync(join(tmpDir, ".rpiv", "artifacts", "research"), { recursive: true });
|
|
829
|
+
writeFileSync(join(tmpDir, ".rpiv/artifacts/research/spec.md"), "");
|
|
830
|
+
|
|
831
|
+
// Shared mutable branch: discover reads it; each continue send grows it.
|
|
832
|
+
const sharedBranch: unknown[] = [mockAssistantMessage("wrote .rpiv/artifacts/research/spec.md")];
|
|
833
|
+
const chain = createMockSessionChain({
|
|
834
|
+
cwd: tmpDir,
|
|
835
|
+
steps: [{ branch: sharedBranch }],
|
|
836
|
+
pi: createMockPi({ skills: ["discover", "frontend-design"] }).pi,
|
|
837
|
+
});
|
|
838
|
+
chain.sendUserMessageFn.mockImplementation((content: unknown) => {
|
|
839
|
+
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
840
|
+
chain.sentMessages.push(text);
|
|
841
|
+
if (text.startsWith("/skill:frontend-design")) sharedBranch.push(mockAssistantMessage("design reasoning"));
|
|
842
|
+
else if (text === "Implement the design spec discussed above.")
|
|
843
|
+
sharedBranch.push(mockAssistantMessage("implemented"));
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const result = await runWorkflow(chain.ctx, {
|
|
847
|
+
workflow: designToCode,
|
|
848
|
+
input: "build a dashboard",
|
|
849
|
+
host: chain.pi,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
expect(result.success).toBe(true);
|
|
853
|
+
// discover (fresh) + design (continue) + implement (continue prompt)
|
|
854
|
+
expect(result.stagesCompleted).toBe(3);
|
|
855
|
+
expect(chain.ctx.newSession).toHaveBeenCalledTimes(1);
|
|
856
|
+
expect(chain.sentMessages).toEqual([
|
|
857
|
+
"/skill:discover build a dashboard",
|
|
858
|
+
"/skill:frontend-design .rpiv/artifacts/research/spec.md",
|
|
859
|
+
"Implement the design spec discussed above.",
|
|
860
|
+
]);
|
|
861
|
+
} finally {
|
|
862
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
});
|
|
@@ -14,18 +14,24 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
|
-
import { isAbsolute, join } from "node:path";
|
|
17
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
18
18
|
import {
|
|
19
|
+
type Artifact,
|
|
19
20
|
acts,
|
|
20
21
|
defineRoute,
|
|
21
22
|
defineWorkflow,
|
|
22
23
|
eq,
|
|
23
24
|
type FanoutFn,
|
|
25
|
+
type FanoutUnit,
|
|
24
26
|
gate,
|
|
25
27
|
gitCommitOutcome,
|
|
26
28
|
gt,
|
|
27
29
|
handleToString,
|
|
30
|
+
type IterateFn,
|
|
31
|
+
type Output,
|
|
32
|
+
type PromptFn,
|
|
28
33
|
produces,
|
|
34
|
+
type RunState,
|
|
29
35
|
typeboxSchema,
|
|
30
36
|
type Workflow,
|
|
31
37
|
} from "@juicesharp/rpiv-workflow";
|
|
@@ -225,8 +231,152 @@ const vetWorkflow = defineWorkflow({
|
|
|
225
231
|
},
|
|
226
232
|
});
|
|
227
233
|
|
|
234
|
+
// ===========================================================================
|
|
235
|
+
// polish — architecture-review → blueprint (iterate, per review phase) →
|
|
236
|
+
// implement → validate → code-review → (blueprint loop) | commit
|
|
237
|
+
// For a large architecture review that can't be planned in one pass:
|
|
238
|
+
// plan each review phase sequentially, each plan building on the
|
|
239
|
+
// ones before it, then implement/validate/review the lot.
|
|
240
|
+
// ===========================================================================
|
|
241
|
+
|
|
242
|
+
/** `### Phase N — name` headings define the review's dependency-ordered phases. */
|
|
243
|
+
const REVIEW_PHASE_RE = /^### Phase (\d+) — (.+)$/gm;
|
|
244
|
+
|
|
245
|
+
/** Latest `fs`-handle artifact most recently published under `name` (undefined if none). */
|
|
246
|
+
const latestFsArtifact = (state: Readonly<RunState>, name: string): Artifact | undefined =>
|
|
247
|
+
state.named[name]?.at(-1)?.artifacts.find((a) => a.handle.kind === "fs");
|
|
248
|
+
|
|
249
|
+
/** Resolve a workflow-relative path against `cwd`. */
|
|
250
|
+
const resolveCwd = (path: string, cwd: string): string => (isAbsolute(path) ? path : join(cwd, path));
|
|
251
|
+
|
|
252
|
+
/** Number of `### Phase N —` headings in the latest architecture review (0 if none). */
|
|
253
|
+
const reviewPhaseCount = (state: Readonly<RunState>, cwd: string): number => {
|
|
254
|
+
const review = latestFsArtifact(state, "architecture-reviews");
|
|
255
|
+
if (review?.handle.kind !== "fs") return 0;
|
|
256
|
+
return [...readFileSync(resolveCwd(review.handle.path, cwd), "utf-8").matchAll(REVIEW_PHASE_RE)].length;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The plans from the most recent blueprint pass. blueprint's iterate stage
|
|
261
|
+
* pushes one `Output` per review phase into `state.named["plans"]`; on a
|
|
262
|
+
* corrective loop it re-plans every phase, so keep only the last `phaseCount`
|
|
263
|
+
* (the review's phase count) and drop the stale generation. Shared by the
|
|
264
|
+
* implement fanout and the validate prompt so both see the same plan set.
|
|
265
|
+
*/
|
|
266
|
+
const latestPlans = (state: Readonly<RunState>, cwd: string): readonly Output[] => {
|
|
267
|
+
const plans = state.named.plans ?? [];
|
|
268
|
+
const phaseCount = reviewPhaseCount(state, cwd);
|
|
269
|
+
return phaseCount > 0 && plans.length > phaseCount ? plans.slice(-phaseCount) : plans;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Per-review-phase blueprint generator (the `iterate` dual of PHASE_FANOUT).
|
|
274
|
+
* One blueprint pass per review phase, each seeing the plans already produced
|
|
275
|
+
* so it builds on them instead of duplicating. blueprint writes its own
|
|
276
|
+
* natural `.rpiv/artifacts/plans/<slug>_<topic>.md` file — the iterate stage's
|
|
277
|
+
* `plans` collector captures whatever path it announces, so no output-path
|
|
278
|
+
* plumbing is needed (this is exactly the per-phase invocation the
|
|
279
|
+
* architecture-review skill documents as its next step).
|
|
280
|
+
*/
|
|
281
|
+
const REVIEW_PHASE_ITERATE: IterateFn = ({ artifact, state, accumulated, cwd }) => {
|
|
282
|
+
// Source the review from the named registry — robust to corrective re-entry,
|
|
283
|
+
// where the rolling primary is the latest code-review doc, not the review.
|
|
284
|
+
const review = latestFsArtifact(state, "architecture-reviews") ?? artifact;
|
|
285
|
+
if (review?.handle.kind !== "fs") return null;
|
|
286
|
+
const phases = [...readFileSync(resolveCwd(review.handle.path, cwd), "utf-8").matchAll(REVIEW_PHASE_RE)];
|
|
287
|
+
const i = accumulated.length;
|
|
288
|
+
if (i >= phases.length) return null; // every phase planned → terminate
|
|
289
|
+
const phaseName = phases[i]![2]!.trim();
|
|
290
|
+
|
|
291
|
+
const prior = accumulated
|
|
292
|
+
.flatMap((o) => o.artifacts)
|
|
293
|
+
.filter((a) => a.handle.kind === "fs")
|
|
294
|
+
.map((a) => handleToString(a.handle));
|
|
295
|
+
// On a corrective pass the latest code-review is in `reviews`; fold its blockers in.
|
|
296
|
+
const feedback = latestFsArtifact(state, "reviews");
|
|
297
|
+
|
|
298
|
+
let prompt = `${handleToString(review.handle)} Implement Phase ${phases[i]![1]}: ${phaseName}`;
|
|
299
|
+
if (prior.length) prompt += `\nPrior phase plans (read first; build on them, don't duplicate): ${prior.join(", ")}`;
|
|
300
|
+
if (feedback?.handle.kind === "fs")
|
|
301
|
+
prompt += `\nAddress the blockers in the latest code review: ${handleToString(feedback.handle)}`;
|
|
302
|
+
return { prompt, label: `phase ${i + 1}/${phases.length} — ${phaseName}`, id: `phase-${phases[i]![1]}` };
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Fan implement out over the `## Phase N:` headings of EVERY plan in the latest
|
|
307
|
+
* blueprint pass (see `latestPlans` for the corrective-loop dedup). This is the
|
|
308
|
+
* dedup the design's deterministic-filename scheme bought — done here over the
|
|
309
|
+
* accumulation instead, so blueprint keeps its natural timestamped filenames.
|
|
310
|
+
*/
|
|
311
|
+
const PLANS_PHASE_FANOUT: FanoutFn = ({ state, cwd }) => {
|
|
312
|
+
const units: FanoutUnit[] = [];
|
|
313
|
+
for (const out of latestPlans(state, cwd)) {
|
|
314
|
+
for (const a of out.artifacts) {
|
|
315
|
+
if (a.handle.kind !== "fs") continue;
|
|
316
|
+
const abs = resolveCwd(a.handle.path, cwd);
|
|
317
|
+
for (const m of readFileSync(abs, "utf-8").matchAll(/^## Phase (\d+):/gm)) {
|
|
318
|
+
units.push({
|
|
319
|
+
prompt: `${handleToString(a.handle)} Phase ${m[1]}`,
|
|
320
|
+
label: `${basename(a.handle.path)} P${m[1]}`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (units.length > MAX_PHASES) {
|
|
326
|
+
throw new Error(`PLANS_PHASE_FANOUT: ${units.length} phases exceeds MAX_PHASES (${MAX_PHASES})`);
|
|
327
|
+
}
|
|
328
|
+
return units;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Hand the single validate session EVERY plan from the latest blueprint pass
|
|
333
|
+
* (`latestPlans`). The runner's default rolling-primary — and a plain
|
|
334
|
+
* `reads: ["plans"]`, which only reads `.at(-1)` — would point validate at the
|
|
335
|
+
* LAST plan alone, leaving earlier phases unvalidated. A `prompt` stage owns
|
|
336
|
+
* its whole message, so the `/skill:validate` prefix is explicit.
|
|
337
|
+
*/
|
|
338
|
+
const VALIDATE_PLANS_PROMPT: PromptFn = ({ state, cwd }) => {
|
|
339
|
+
const paths = latestPlans(state, cwd)
|
|
340
|
+
.flatMap((o) => o.artifacts)
|
|
341
|
+
.filter((a) => a.handle.kind === "fs")
|
|
342
|
+
.map((a) => handleToString(a.handle));
|
|
343
|
+
return `/skill:validate ${paths.join(" ")}`;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const polishWorkflow = defineWorkflow({
|
|
347
|
+
name: "polish",
|
|
348
|
+
description:
|
|
349
|
+
"Architecture-review-driven polish: review → per-phase blueprint (sequential, accumulating) → implement → validate → code-review → commit. Best when a large architecture review can't be planned in one pass and each phase's plan must build on the ones before it.",
|
|
350
|
+
start: "architecture-review",
|
|
351
|
+
stages: {
|
|
352
|
+
"architecture-review": produces({ outcome: rpivBucketOutcome("architecture-reviews") }),
|
|
353
|
+
blueprint: produces({ outcome: rpivBucketOutcome("plans"), iterate: REVIEW_PHASE_ITERATE }),
|
|
354
|
+
implement: acts({ fanout: PLANS_PHASE_FANOUT }),
|
|
355
|
+
validate: produces({ outcome: rpivBucketOutcome("validation"), prompt: VALIDATE_PLANS_PROMPT }),
|
|
356
|
+
"code-review": produces({ outcome: rpivBucketOutcome("reviews"), outputSchema: CODE_REVIEW_SCHEMA }),
|
|
357
|
+
commit: acts({ outcome: gitCommitOutcome }),
|
|
358
|
+
},
|
|
359
|
+
edges: {
|
|
360
|
+
"architecture-review": "blueprint",
|
|
361
|
+
blueprint: "implement",
|
|
362
|
+
implement: "validate",
|
|
363
|
+
validate: "code-review",
|
|
364
|
+
// Backward edge: code-review → blueprint re-plans (implement needs a plan).
|
|
365
|
+
// The iterate stage re-runs over every review phase; bounded by the
|
|
366
|
+
// runner's default maxBackwardJumps (2 → up to 3 review iterations).
|
|
367
|
+
"code-review": gate("blockers_count", { commit: eq(0), blueprint: gt(0) }),
|
|
368
|
+
commit: "stop",
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
|
|
228
372
|
// ===========================================================================
|
|
229
373
|
// Exports
|
|
230
374
|
// ===========================================================================
|
|
231
375
|
|
|
232
|
-
export const builtInWorkflows: readonly Workflow[] = [
|
|
376
|
+
export const builtInWorkflows: readonly Workflow[] = [
|
|
377
|
+
shipWorkflow,
|
|
378
|
+
buildWorkflow,
|
|
379
|
+
archWorkflow,
|
|
380
|
+
vetWorkflow,
|
|
381
|
+
polishWorkflow,
|
|
382
|
+
];
|
|
@@ -48,7 +48,7 @@ export const SIBLINGS: readonly SiblingPlugin[] = [
|
|
|
48
48
|
{
|
|
49
49
|
pkg: "npm:@juicesharp/rpiv-web-tools",
|
|
50
50
|
matches: /rpiv-web-tools/i,
|
|
51
|
-
provides: "web_search + web_fetch tools + /web-
|
|
51
|
+
provides: "web_search + web_fetch tools + /web-tools",
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
pkg: "npm:@juicesharp/rpiv-args",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-pi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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",
|