@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[] = [shipWorkflow, buildWorkflow, archWorkflow, vetWorkflow];
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-search-config",
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.14.7",
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",