@juicesharp/rpiv-pi 1.19.0 → 1.19.1
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 +37 -2
- package/extensions/rpiv-core/built-in-workflows.ts +73 -31
- package/extensions/rpiv-core/index.ts +1 -1
- package/extensions/rpiv-core/outcome-derivation.test.ts +9 -5
- package/extensions/rpiv-core/outcome-derivation.ts +1 -0
- package/extensions/rpiv-core/register-built-in-workflows.test.ts +4 -4
- package/extensions/rpiv-core/register-built-in-workflows.ts +2 -2
- package/extensions/rpiv-core/skill-contracts-source.test.ts +5 -3
- package/extensions/rpiv-core/skill-contracts-source.ts +9 -1
- package/extensions/rpiv-core/utils.test.ts +17 -1
- package/extensions/rpiv-core/utils.ts +21 -6
- package/package.json +62 -60
- package/skills/pr-triage/SKILL.md +375 -0
- package/skills/pr-triage/_helpers/pr-fetch.mjs +357 -0
- package/skills/pr-triage/templates/triage.md +159 -0
|
@@ -1050,6 +1050,39 @@ describe("ship workflow", () => {
|
|
|
1050
1050
|
});
|
|
1051
1051
|
});
|
|
1052
1052
|
|
|
1053
|
+
// ---------------------------------------------------------------------------
|
|
1054
|
+
// pr-triage security-gate — the script-stage guard must fail closed on
|
|
1055
|
+
// missing / malformed security_flag (NaN), not silently pass as SAFE.
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
|
|
1058
|
+
describe("pr-triage security-gate", () => {
|
|
1059
|
+
const gateRun = () => {
|
|
1060
|
+
const stage = findWorkflow("pr-triage").stages["security-gate"];
|
|
1061
|
+
if (!stage?.run) throw new Error("pr-triage security-gate stage has no run function");
|
|
1062
|
+
return stage.run as (ctx: { input?: { data?: unknown } }) => void;
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
it("throws on missing input (undefined)", () => {
|
|
1066
|
+
expect(() => gateRun()({ input: undefined })).toThrow(/BLOCK/);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it("throws on non-numeric security_flag (NaN)", () => {
|
|
1070
|
+
expect(() => gateRun()({ input: { data: { security_flag: "not-a-number" } } })).toThrow(/BLOCK/);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it("throws on BLOCK (security_flag = 2)", () => {
|
|
1074
|
+
expect(() => gateRun()({ input: { data: { security_flag: 2 } } })).toThrow(/BLOCK/);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("does not throw on SAFE (security_flag = 0)", () => {
|
|
1078
|
+
expect(() => gateRun()({ input: { data: { security_flag: 0 } } })).not.toThrow();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it("does not throw on REVIEW (security_flag = 1)", () => {
|
|
1082
|
+
expect(() => gateRun()({ input: { data: { security_flag: 1 } } })).not.toThrow();
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1053
1086
|
// ---------------------------------------------------------------------------
|
|
1054
1087
|
// implement reads wiring — every implement stage declares reads: ["plans"]
|
|
1055
1088
|
// and validates clean with contracts threaded in.
|
|
@@ -1058,11 +1091,13 @@ describe("ship workflow", () => {
|
|
|
1058
1091
|
describe("implement reads wiring", () => {
|
|
1059
1092
|
it('every implement stage declares reads: ["plans"]', () => {
|
|
1060
1093
|
for (const wf of builtInWorkflows) {
|
|
1061
|
-
|
|
1094
|
+
// Not every workflow has an implement stage (pr-triage is read-only triage).
|
|
1095
|
+
if (!wf.stages.implement) continue;
|
|
1096
|
+
expect(wf.stages.implement.reads, `${wf.name}.implement`).toEqual(["plans"]);
|
|
1062
1097
|
}
|
|
1063
1098
|
});
|
|
1064
1099
|
|
|
1065
|
-
it(
|
|
1100
|
+
it("every built-in workflow with an implement stage validates clean (contracts threaded in)", () => {
|
|
1066
1101
|
for (const name of ["ship", "build", "arch", "vet", "polish"]) {
|
|
1067
1102
|
const issues = deriveAndValidate(findWorkflow(name), { skillContracts: DECLARED_CONTRACTS });
|
|
1068
1103
|
expect(
|
|
@@ -79,6 +79,14 @@ interface PhaseRecord {
|
|
|
79
79
|
total: number;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Read an artifact file, resolving a workflow-relative path against `cwd`. */
|
|
83
|
+
const readArtifactFile = (path: string, cwd: string): string =>
|
|
84
|
+
readFileSync(isAbsolute(path) ? path : join(cwd, path), "utf-8");
|
|
85
|
+
|
|
86
|
+
/** Build the halting `StagePreflightError` shape every phase fanout/iterate guard `throw`s. */
|
|
87
|
+
const haltPreflight = (who: string, summary: string, detail: string): StagePreflightError =>
|
|
88
|
+
new StagePreflightError("halt", who, summary, detail, true);
|
|
89
|
+
|
|
82
90
|
/**
|
|
83
91
|
* Parse a plan's `phases:` frontmatter into records, derive-checked against the
|
|
84
92
|
* body's `## Phase N:` headings — the source of truth both the single-plan
|
|
@@ -94,12 +102,10 @@ const planPhaseRecords = (content: string, who: string, path: string): readonly
|
|
|
94
102
|
const phases = Array.isArray(raw) ? raw : [];
|
|
95
103
|
const headingCount = [...content.matchAll(PLAN_PHASE_RE)].length;
|
|
96
104
|
if (phases.length !== headingCount) {
|
|
97
|
-
throw
|
|
98
|
-
"halt",
|
|
105
|
+
throw haltPreflight(
|
|
99
106
|
who,
|
|
100
107
|
`${who}: plan ${path} has mismatched phases`,
|
|
101
108
|
`${who}: plan ${path} frontmatter phases (${phases.length}) ≠ '## Phase N:' headings (${headingCount}) — the derived array is stale against the body`,
|
|
102
|
-
true,
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
111
|
// The REQUIRED scalar `phase_count` must equal the derived phase count — it
|
|
@@ -108,12 +114,10 @@ const planPhaseRecords = (content: string, who: string, path: string): readonly
|
|
|
108
114
|
// still degrades to [] (the existing "neither phases nor headings" path); a plan
|
|
109
115
|
// that declares phases but omits phase_count THROWS (the field is contract-required).
|
|
110
116
|
if ((phases.length > 0 || fm.phase_count !== undefined) && fm.phase_count !== phases.length) {
|
|
111
|
-
throw
|
|
112
|
-
"halt",
|
|
117
|
+
throw haltPreflight(
|
|
113
118
|
who,
|
|
114
119
|
`${who}: plan ${path} has invalid phase_count`,
|
|
115
120
|
`${who}: plan ${path} frontmatter phase_count (${String(fm.phase_count)}) ≠ phases length (${phases.length}) — rebuild phase_count from the '## Phase N:' headings`,
|
|
116
|
-
true,
|
|
117
121
|
);
|
|
118
122
|
}
|
|
119
123
|
return phases.map((entry, index) => {
|
|
@@ -132,9 +136,6 @@ const planPhaseRecords = (content: string, who: string, path: string): readonly
|
|
|
132
136
|
const latestFsArtifact = (state: Readonly<RunState>, name: string): Artifact | undefined =>
|
|
133
137
|
state.named[name]?.at(-1)?.artifacts.find((a) => a.handle.kind === "fs");
|
|
134
138
|
|
|
135
|
-
/** Resolve a workflow-relative path against `cwd`. */
|
|
136
|
-
const resolveCwd = (path: string, cwd: string): string => (isAbsolute(path) ? path : join(cwd, path));
|
|
137
|
-
|
|
138
139
|
/**
|
|
139
140
|
* Fan `implement` out over the structured `phases:` frontmatter array of the
|
|
140
141
|
* latest plan published to the named `"plans"` channel. Sourcing from the named
|
|
@@ -153,24 +154,20 @@ const FRONTMATTER_PHASE_FANOUT = fanoutOver({
|
|
|
153
154
|
const path = plan.handle.path;
|
|
154
155
|
let content: string;
|
|
155
156
|
try {
|
|
156
|
-
content =
|
|
157
|
+
content = readArtifactFile(path, cwd);
|
|
157
158
|
} catch (err) {
|
|
158
|
-
throw
|
|
159
|
-
"halt",
|
|
159
|
+
throw haltPreflight(
|
|
160
160
|
"FRONTMATTER_PHASE_FANOUT",
|
|
161
161
|
`FRONTMATTER_PHASE_FANOUT: plan file not found`,
|
|
162
162
|
`FRONTMATTER_PHASE_FANOUT: could not read ${path} — ${err instanceof Error ? err.message : String(err)}`,
|
|
163
|
-
true,
|
|
164
163
|
);
|
|
165
164
|
}
|
|
166
165
|
const records = planPhaseRecords(content, "FRONTMATTER_PHASE_FANOUT", path);
|
|
167
166
|
if (records.length > MAX_PHASES) {
|
|
168
|
-
throw
|
|
169
|
-
"halt",
|
|
167
|
+
throw haltPreflight(
|
|
170
168
|
"FRONTMATTER_PHASE_FANOUT",
|
|
171
169
|
`FRONTMATTER_PHASE_FANOUT: plan ${path} exceeds phase limit`,
|
|
172
170
|
`FRONTMATTER_PHASE_FANOUT: plan ${path} declares ${records.length} phases — exceeds MAX_PHASES (${MAX_PHASES}); split into smaller plans`,
|
|
173
|
-
true,
|
|
174
171
|
);
|
|
175
172
|
}
|
|
176
173
|
const promptPath = handleToString(plan.handle);
|
|
@@ -330,7 +327,7 @@ const REVIEW_PHASE_RE = /^### Phase (\d+) — (.+)$/gm;
|
|
|
330
327
|
const reviewPhaseCount = (state: Readonly<RunState>, cwd: string): number => {
|
|
331
328
|
const review = latestFsArtifact(state, "architecture-reviews");
|
|
332
329
|
if (review?.handle.kind !== "fs") return 0;
|
|
333
|
-
const { frontmatter } = parseFrontmatter(
|
|
330
|
+
const { frontmatter } = parseFrontmatter(readArtifactFile(review.handle.path, cwd));
|
|
334
331
|
const raw = (frontmatter as Record<string, unknown>).phases;
|
|
335
332
|
return Array.isArray(raw) ? raw.length : 0;
|
|
336
333
|
};
|
|
@@ -386,7 +383,7 @@ const REVIEW_PHASE_ITERATE = iterateOver({
|
|
|
386
383
|
const review = latestFsArtifact(state, "architecture-reviews") ?? artifact;
|
|
387
384
|
if (review?.handle.kind !== "fs") return null;
|
|
388
385
|
const reviewPath = review.handle.path; // captured: narrowing is lost inside nested closures below
|
|
389
|
-
const content =
|
|
386
|
+
const content = readArtifactFile(reviewPath, cwd);
|
|
390
387
|
const { frontmatter } = parseFrontmatter(content);
|
|
391
388
|
const raw = (frontmatter as Record<string, unknown>).phases;
|
|
392
389
|
const phases = Array.isArray(raw) ? raw : [];
|
|
@@ -394,12 +391,10 @@ const REVIEW_PHASE_ITERATE = iterateOver({
|
|
|
394
391
|
if (i === 0) {
|
|
395
392
|
const headingCount = [...content.matchAll(REVIEW_PHASE_RE)].length;
|
|
396
393
|
if (phases.length !== headingCount) {
|
|
397
|
-
throw
|
|
398
|
-
"halt",
|
|
394
|
+
throw haltPreflight(
|
|
399
395
|
"REVIEW_PHASE_ITERATE",
|
|
400
396
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} has mismatched phases`,
|
|
401
397
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} frontmatter phases (${phases.length}) ≠ '### Phase N —' headings (${headingCount}) — the derived array is stale against the body`,
|
|
402
|
-
true,
|
|
403
398
|
);
|
|
404
399
|
}
|
|
405
400
|
const indexByN = new Map(phases.map((e, idx) => [phaseNum(e, idx), idx]));
|
|
@@ -407,20 +402,16 @@ const REVIEW_PHASE_ITERATE = iterateOver({
|
|
|
407
402
|
for (const d of phaseDeps(e)) {
|
|
408
403
|
const di = indexByN.get(d);
|
|
409
404
|
if (di === undefined)
|
|
410
|
-
throw
|
|
411
|
-
"halt",
|
|
405
|
+
throw haltPreflight(
|
|
412
406
|
"REVIEW_PHASE_ITERATE",
|
|
413
407
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} has invalid depends_on`,
|
|
414
408
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} phase ${phaseNum(e, idx)} depends_on ${d}, which is not a declared phase`,
|
|
415
|
-
true,
|
|
416
409
|
);
|
|
417
410
|
if (di >= idx)
|
|
418
|
-
throw
|
|
419
|
-
"halt",
|
|
411
|
+
throw haltPreflight(
|
|
420
412
|
"REVIEW_PHASE_ITERATE",
|
|
421
413
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} has cyclic dependency`,
|
|
422
414
|
`REVIEW_PHASE_ITERATE: review ${reviewPath} phase ${phaseNum(e, idx)} depends_on ${d}, which is not an earlier phase (self/forward/cyclic dependency)`,
|
|
423
|
-
true,
|
|
424
415
|
);
|
|
425
416
|
}
|
|
426
417
|
});
|
|
@@ -471,7 +462,7 @@ const PLANS_PHASE_FANOUT = fanoutOver({
|
|
|
471
462
|
for (const a of out.artifacts) {
|
|
472
463
|
if (a.handle.kind !== "fs") continue;
|
|
473
464
|
const path = a.handle.path;
|
|
474
|
-
const content =
|
|
465
|
+
const content = readArtifactFile(path, cwd);
|
|
475
466
|
const promptPath = handleToString(a.handle);
|
|
476
467
|
for (const r of planPhaseRecords(content, "PLANS_PHASE_FANOUT", path)) {
|
|
477
468
|
units.push({
|
|
@@ -482,12 +473,10 @@ const PLANS_PHASE_FANOUT = fanoutOver({
|
|
|
482
473
|
}
|
|
483
474
|
}
|
|
484
475
|
if (units.length > MAX_PHASES) {
|
|
485
|
-
throw
|
|
486
|
-
"halt",
|
|
476
|
+
throw haltPreflight(
|
|
487
477
|
"PLANS_PHASE_FANOUT",
|
|
488
478
|
`PLANS_PHASE_FANOUT: phase limit exceeded`,
|
|
489
479
|
`PLANS_PHASE_FANOUT: ${units.length} phases exceeds MAX_PHASES (${MAX_PHASES})`,
|
|
490
|
-
true,
|
|
491
480
|
);
|
|
492
481
|
}
|
|
493
482
|
return units;
|
|
@@ -535,6 +524,58 @@ const polishWorkflow = defineWorkflow({
|
|
|
535
524
|
},
|
|
536
525
|
});
|
|
537
526
|
|
|
527
|
+
// ===========================================================================
|
|
528
|
+
// pr-triage — pr-triage → security-gate → stop
|
|
529
|
+
// Read-only front door for an incoming GitHub PR. The `pr-triage` skill
|
|
530
|
+
// fetches the PR thread, assesses the diff against whatever standard the repo
|
|
531
|
+
// actually carries, writes a triage artifact, and recommends a triage
|
|
532
|
+
// disposition (Review / Request changes / Hold / Decline) as a plain next
|
|
533
|
+
// action. The `security-gate` script stage reads the skill's
|
|
534
|
+
// `security_flag` and HALTS the run on a BLOCK (≥ 2) via `haltPreflight` — the
|
|
535
|
+
// "security gate first, before any checkout" posture — while SAFE/REVIEW (< 2)
|
|
536
|
+
// fall through to `stop` carrying the verdict.
|
|
537
|
+
//
|
|
538
|
+
// It terminates rather than dispatching a follow-up workflow: triage gates entry
|
|
539
|
+
// to review, it does not merge. The disposition is a plain action; `vet` (the
|
|
540
|
+
// review stage) is offered as optional sugar under Review. Only the security gate
|
|
541
|
+
// is enforced by the graph. A linear guard stage (not a `gate(...)`
|
|
542
|
+
// edge) keeps the halt off the data-routing path — the throw lives inside the
|
|
543
|
+
// script, read from the prior stage's output.
|
|
544
|
+
// ===========================================================================
|
|
545
|
+
|
|
546
|
+
/** BLOCK tier the pr-triage `security_flag` contract field emits (0 SAFE · 1 REVIEW · 2 BLOCK). */
|
|
547
|
+
const PR_TRIAGE_BLOCK = 2;
|
|
548
|
+
|
|
549
|
+
const prTriageWorkflow = defineWorkflow({
|
|
550
|
+
name: "pr-triage",
|
|
551
|
+
description:
|
|
552
|
+
"Read-only triage of a GitHub PR before any review effort. Fetches the PR thread, assesses the diff against the repo's own standards, writes a triage artifact, and recommends a triage disposition (Review / Request changes / Hold / Decline). A security BLOCK halts the run before any checkout. Best as the entry point for an incoming PR. Chain: pr-triage → security-gate → stop.",
|
|
553
|
+
start: "pr-triage",
|
|
554
|
+
stages: {
|
|
555
|
+
"pr-triage": produces(),
|
|
556
|
+
// Skillless guard: read the triage skill's `security_flag` from the prior
|
|
557
|
+
// stage's output and halt on BLOCK. A script stage (not a skill) — no LLM,
|
|
558
|
+
// no session — so the gate is free. On SAFE/REVIEW it is a no-op side effect
|
|
559
|
+
// and the chain advances to `stop`.
|
|
560
|
+
"security-gate": acts.script({
|
|
561
|
+
run: ({ input }) => {
|
|
562
|
+
const flag = Number((input?.data as { security_flag?: unknown } | undefined)?.security_flag);
|
|
563
|
+
if (Number.isNaN(flag) || flag >= PR_TRIAGE_BLOCK) {
|
|
564
|
+
throw haltPreflight(
|
|
565
|
+
"pr-triage",
|
|
566
|
+
"pr-triage: security BLOCK — do not proceed",
|
|
567
|
+
`pr-triage: security_flag=${flag} (BLOCK) — the PR diff carries a high-confidence security risk. Resolve it before any checkout or review; see the triage artifact for the traced finding.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
}),
|
|
572
|
+
},
|
|
573
|
+
edges: {
|
|
574
|
+
"pr-triage": "security-gate",
|
|
575
|
+
"security-gate": "stop",
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
538
579
|
// ===========================================================================
|
|
539
580
|
// Exports
|
|
540
581
|
// ===========================================================================
|
|
@@ -545,4 +586,5 @@ export const builtInWorkflows: readonly Workflow[] = [
|
|
|
545
586
|
archWorkflow,
|
|
546
587
|
vetWorkflow,
|
|
547
588
|
polishWorkflow,
|
|
589
|
+
prTriageWorkflow,
|
|
548
590
|
];
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Tool-owning plugins are siblings (see siblings.ts); install via /rpiv-setup.
|
|
8
8
|
*
|
|
9
9
|
* Workflow runtime + `/wf` command live in `@juicesharp/rpiv-workflow`. We
|
|
10
|
-
* contribute
|
|
10
|
+
* contribute six built-in workflows (ship / build / arch / vet / polish / pr-triage) via the
|
|
11
11
|
* sibling's `registerBuiltIns` programmatic API so they're available to
|
|
12
12
|
* users running `/wf` without authoring their own.
|
|
13
13
|
*/
|
|
@@ -46,8 +46,8 @@ function stripOutcomes(w: Workflow): Workflow {
|
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
|
|
48
48
|
describe("BUCKET_BY_KIND", () => {
|
|
49
|
-
it("contains exactly
|
|
50
|
-
expect(Object.keys(BUCKET_BY_KIND)).toHaveLength(
|
|
49
|
+
it("contains exactly 10 entries", () => {
|
|
50
|
+
expect(Object.keys(BUCKET_BY_KIND)).toHaveLength(10);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it("covers all artifactKinds used by produces skills", () => {
|
|
@@ -61,6 +61,7 @@ describe("BUCKET_BY_KIND", () => {
|
|
|
61
61
|
"architecture-review",
|
|
62
62
|
"frd",
|
|
63
63
|
"handoff",
|
|
64
|
+
"triage",
|
|
64
65
|
];
|
|
65
66
|
for (const kind of expectedKinds) {
|
|
66
67
|
expect(BUCKET_BY_KIND[kind]).toBeDefined();
|
|
@@ -242,10 +243,11 @@ describe("equivalence — built-in workflows", () => {
|
|
|
242
243
|
["validate", "validation"],
|
|
243
244
|
["code-review", "review"],
|
|
244
245
|
["revise", "plan"],
|
|
246
|
+
["pr-triage", "triage"],
|
|
245
247
|
];
|
|
246
248
|
|
|
247
249
|
/**
|
|
248
|
-
* Expected bucket name for each produces stage across all
|
|
250
|
+
* Expected bucket name for each produces stage across all 6 workflows.
|
|
249
251
|
* Key: "workflowName::stageName". Value: expected outcome.name.
|
|
250
252
|
*/
|
|
251
253
|
const EXPECTED: Record<string, string> = {
|
|
@@ -273,6 +275,8 @@ describe("equivalence — built-in workflows", () => {
|
|
|
273
275
|
"polish::blueprint": "plans",
|
|
274
276
|
"polish::validate": "validation",
|
|
275
277
|
"polish::code-review": "reviews",
|
|
278
|
+
// pr-triage
|
|
279
|
+
"pr-triage::pr-triage": "triage",
|
|
276
280
|
};
|
|
277
281
|
|
|
278
282
|
/**
|
|
@@ -343,14 +347,14 @@ describe("equivalence — built-in workflows", () => {
|
|
|
343
347
|
});
|
|
344
348
|
}
|
|
345
349
|
|
|
346
|
-
it("total produces stages across all workflows =
|
|
350
|
+
it("total produces stages across all workflows = 20", () => {
|
|
347
351
|
let count = 0;
|
|
348
352
|
for (const w of builtInWorkflows) {
|
|
349
353
|
for (const stage of Object.values(w.stages)) {
|
|
350
354
|
if (stage.kind === "produces") count++;
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
|
-
expect(count).toBe(
|
|
357
|
+
expect(count).toBe(20);
|
|
354
358
|
});
|
|
355
359
|
});
|
|
356
360
|
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
12
|
import { registerBuiltInWorkflows } from "./register-built-in-workflows.js";
|
|
13
13
|
|
|
14
|
-
// Alphabetical: the
|
|
15
|
-
const BUILT_IN_NAMES = ["arch", "build", "polish", "ship", "vet"];
|
|
14
|
+
// Alphabetical: the six shipped presets.
|
|
15
|
+
const BUILT_IN_NAMES = ["arch", "build", "polish", "pr-triage", "ship", "vet"];
|
|
16
16
|
|
|
17
17
|
describe("registerBuiltInWorkflows", () => {
|
|
18
|
-
it("registers all built-in workflows (
|
|
18
|
+
it("registers all built-in workflows (six presets) when rpiv-workflow is present", async () => {
|
|
19
19
|
const { getBuiltIns, flushBuiltInProviders } = await import("@juicesharp/rpiv-workflow/internal");
|
|
20
20
|
expect(getBuiltIns()).toEqual([]); // setup.ts beforeEach resets the registry
|
|
21
21
|
|
|
22
22
|
await registerBuiltInWorkflows();
|
|
23
23
|
// registerBuiltInWorkflows now registers a LAZY provider — the registry
|
|
24
24
|
// stays empty until the first loadWorkflows() flushes it. Flush directly
|
|
25
|
-
// to assert the provider contributes the
|
|
25
|
+
// to assert the provider contributes the six definitions.
|
|
26
26
|
expect(getBuiltIns()).toEqual([]);
|
|
27
27
|
await flushBuiltInProviders();
|
|
28
28
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { isModuleNotFound } from "./utils.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Register the
|
|
24
|
+
* Register the six built-in workflows (ship / build / arch / vet / polish / pr-triage)
|
|
25
25
|
* with the rpiv-workflow runtime, if that sibling is installed. A missing
|
|
26
26
|
* sibling resolves to a no-op; any other failure is re-thrown so genuine bugs
|
|
27
27
|
* surface rather than hiding behind the absent-sibling path.
|
|
@@ -29,7 +29,7 @@ import { isModuleNotFound } from "./utils.js";
|
|
|
29
29
|
export async function registerBuiltInWorkflows(): Promise<void> {
|
|
30
30
|
try {
|
|
31
31
|
// Thin `/startup` entry (~9ms, no DSL/runner). Register a LAZY provider so
|
|
32
|
-
// `built-in-workflows.js` (the ~180ms authoring-DSL graph) builds the
|
|
32
|
+
// `built-in-workflows.js` (the ~180ms authoring-DSL graph) builds the six
|
|
33
33
|
// definitions on first `/wf`, not at startup. Missing sibling →
|
|
34
34
|
// ERR_MODULE_NOT_FOUND → no-op (no `/wf` without it).
|
|
35
35
|
const { registerBuiltInsProvider, registerBuiltIns } = await import("@juicesharp/rpiv-workflow/startup");
|
|
@@ -206,8 +206,8 @@ describe("bundled skill contracts", () => {
|
|
|
206
206
|
// dropped, or fails to parse (a malformed block is silently skipped).
|
|
207
207
|
const declared = new Map(buildSkillContractsFromFrontmatter(BUNDLED_SKILLS_DIR));
|
|
208
208
|
|
|
209
|
-
it("declares a contract for the
|
|
210
|
-
expect(declared.size).toBe(
|
|
209
|
+
it("declares a contract for the 20 pipeline + orthogonal skills", () => {
|
|
210
|
+
expect(declared.size).toBe(20);
|
|
211
211
|
for (const name of [
|
|
212
212
|
"discover",
|
|
213
213
|
"research",
|
|
@@ -228,6 +228,7 @@ describe("bundled skill contracts", () => {
|
|
|
228
228
|
"resume-handoff",
|
|
229
229
|
"frontend-design",
|
|
230
230
|
"migrate-to-guidance",
|
|
231
|
+
"pr-triage",
|
|
231
232
|
]) {
|
|
232
233
|
expect(declared.has(name)).toBe(true);
|
|
233
234
|
}
|
|
@@ -252,6 +253,7 @@ describe("bundled skill contracts", () => {
|
|
|
252
253
|
it("documents the declared-but-not-harvested orthogonal set", () => {
|
|
253
254
|
// These skills declare a contract but don't appear in any built-in workflow.
|
|
254
255
|
// The orthogonal set: 7 new + discover + explore + commit = 10 skills.
|
|
256
|
+
// (pr-triage IS harvested — it's dispatched by the pr-triage workflow.)
|
|
255
257
|
const harvested = harvestStageContracts(builtInWorkflows);
|
|
256
258
|
const notHarvested: string[] = [];
|
|
257
259
|
for (const [name] of declared) {
|
|
@@ -273,7 +275,7 @@ describe("bundled skill contracts", () => {
|
|
|
273
275
|
);
|
|
274
276
|
});
|
|
275
277
|
|
|
276
|
-
it("every declared kind matches the harvested kind for the
|
|
278
|
+
it("every declared kind matches the harvested kind for the six built-in workflows", () => {
|
|
277
279
|
// Harvest derives each dispatched skill's kind from how the built-ins use
|
|
278
280
|
// it (produces() → "produces", acts() → "side-effect"). A declared kind
|
|
279
281
|
// that disagrees would make the rendered graph lie — catch it here.
|
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
SchemaCompatResult,
|
|
20
20
|
SkillContract,
|
|
21
21
|
} from "@juicesharp/rpiv-workflow/registration";
|
|
22
|
-
import { registerOutcomeDerivation } from "./outcome-derivation.js";
|
|
23
22
|
import { BUNDLED_SKILLS_DIR } from "./paths.js";
|
|
24
23
|
import { isModuleNotFound } from "./utils.js";
|
|
25
24
|
|
|
@@ -169,6 +168,15 @@ export async function registerSkillContractsSource(): Promise<void> {
|
|
|
169
168
|
});
|
|
170
169
|
// Register the contract-derived outcome resolver so `produces` stages
|
|
171
170
|
// auto-wire `rpivBucketOutcome(bucket)` from `artifactKind` at load time.
|
|
171
|
+
// Dynamic import (not static): `outcome-derivation.js` pulls in
|
|
172
|
+
// `artifact-collector.js`, which value-imports `@juicesharp/rpiv-workflow/registration`
|
|
173
|
+
// at module-eval. A static edge would make THAT a load-time requirement of the
|
|
174
|
+
// whole extension — so when the sibling is absent or not co-located, rpiv-core
|
|
175
|
+
// fails to load entirely (the bug in #66). Deferring it here keeps the sibling
|
|
176
|
+
// edge off the entry path: it only resolves after the `/startup` import above
|
|
177
|
+
// already succeeded, and a missing/non-resolvable sibling degrades to the
|
|
178
|
+
// isModuleNotFound no-op below instead of crashing the extension.
|
|
179
|
+
const { registerOutcomeDerivation } = await import("./outcome-derivation.js");
|
|
172
180
|
await registerOutcomeDerivation();
|
|
173
181
|
} catch (err) {
|
|
174
182
|
if (isModuleNotFound(err)) return; // sibling absent — /rpiv-setup prompts the user
|
|
@@ -74,11 +74,27 @@ describe("toErrorMessage", () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
describe("isModuleNotFound", () => {
|
|
77
|
-
it("is true for an ERR_MODULE_NOT_FOUND error", () => {
|
|
77
|
+
it("is true for an ERR_MODULE_NOT_FOUND error (Node native ESM resolver)", () => {
|
|
78
78
|
const err = Object.assign(new Error("Cannot find package"), { code: "ERR_MODULE_NOT_FOUND" });
|
|
79
79
|
expect(isModuleNotFound(err)).toBe(true);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it("is true for a MODULE_NOT_FOUND error (jiti resolver — what Pi actually uses)", () => {
|
|
83
|
+
// jiti 2.7.0 rejects a missing `import("@juicesharp/rpiv-…")` with this
|
|
84
|
+
// CJS-style code; the absent-sibling guards must treat it as a no-op.
|
|
85
|
+
const err = Object.assign(new Error("Cannot find module '@juicesharp/rpiv-workflow/startup'"), {
|
|
86
|
+
code: "MODULE_NOT_FOUND",
|
|
87
|
+
});
|
|
88
|
+
expect(isModuleNotFound(err)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("is true when the resolution code is nested under cause", () => {
|
|
92
|
+
const wrapped = Object.assign(new Error("wrapped"), {
|
|
93
|
+
cause: Object.assign(new Error("Cannot find module"), { code: "MODULE_NOT_FOUND" }),
|
|
94
|
+
});
|
|
95
|
+
expect(isModuleNotFound(wrapped)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
82
98
|
it("is false for other error codes and codeless errors", () => {
|
|
83
99
|
expect(isModuleNotFound(Object.assign(new Error("boom"), { code: "ERR_OTHER" }))).toBe(false);
|
|
84
100
|
expect(isModuleNotFound(new Error("no code"))).toBe(false);
|
|
@@ -52,13 +52,28 @@ export function toErrorMessage(e: unknown, fallback?: string): string {
|
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* Error codes that mean "module-resolution failed" (the sibling isn't installed
|
|
56
|
+
* / isn't resolvable). Two distinct codes because two distinct loaders are in
|
|
57
|
+
* play:
|
|
58
|
+
* - `ERR_MODULE_NOT_FOUND` — Node's native ESM resolver (a plain
|
|
59
|
+
* `await import(...)` under stock Node).
|
|
60
|
+
* - `MODULE_NOT_FOUND` — jiti's resolver, which is what Pi ACTUALLY uses to
|
|
61
|
+
* load `.ts` extensions. A missing nested `import("@juicesharp/rpiv-…")`
|
|
62
|
+
* inside a jiti-loaded module rejects with this CJS-style code (verified on
|
|
63
|
+
* jiti 2.7.0, both `tryNative:false` and the native-fallback path). Matching
|
|
64
|
+
* only the ESM code let these fall through to a logged `[rpiv-core] failed
|
|
65
|
+
* to register …` instead of the intended silent absent-sibling no-op.
|
|
66
|
+
*/
|
|
67
|
+
const MODULE_NOT_FOUND_CODES = new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True for a module-resolution failure (the sibling isn't installed / not
|
|
71
|
+
* resolvable from rpiv-pi's location).
|
|
56
72
|
*
|
|
57
73
|
* Walks the `cause` chain: a clean `await import(...)` of a missing package
|
|
58
|
-
* rejects with
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* chains.
|
|
74
|
+
* rejects with the resolution code directly, but ESM loaders and tooling
|
|
75
|
+
* (vitest's mock layer, some bundlers) wrap that error, nesting the real code
|
|
76
|
+
* under `.cause`. Bounded against pathological self-referential chains.
|
|
62
77
|
*/
|
|
63
78
|
export function isModuleNotFound(err: unknown): boolean {
|
|
64
79
|
for (
|
|
@@ -66,7 +81,7 @@ export function isModuleNotFound(err: unknown): boolean {
|
|
|
66
81
|
cur != null && depth < 16;
|
|
67
82
|
cur = (cur as { cause?: unknown }).cause, depth++
|
|
68
83
|
) {
|
|
69
|
-
if (typeof cur === "object" && (cur as { code?: unknown }).code
|
|
84
|
+
if (typeof cur === "object" && MODULE_NOT_FOUND_CODES.has((cur as { code?: unknown }).code as string)) {
|
|
70
85
|
return true;
|
|
71
86
|
}
|
|
72
87
|
}
|
package/package.json
CHANGED
|
@@ -1,62 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
2
|
+
"name": "@juicesharp/rpiv-pi",
|
|
3
|
+
"version": "1.19.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"rpiv",
|
|
9
|
+
"skills",
|
|
10
|
+
"workflow"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "juicesharp",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/juicesharp/rpiv-mono.git",
|
|
18
|
+
"directory": "packages/rpiv-pi"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-pi#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/juicesharp/rpiv-mono/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"extensions/",
|
|
32
|
+
"skills/",
|
|
33
|
+
"agents/",
|
|
34
|
+
"scripts/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./extensions"
|
|
41
|
+
],
|
|
42
|
+
"skills": [
|
|
43
|
+
"./skills"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@juicesharp/rpiv-config": "^1.19.1"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
51
|
+
"@tintinweb/pi-subagents": "*",
|
|
52
|
+
"@juicesharp/rpiv-ask-user-question": "*",
|
|
53
|
+
"@juicesharp/rpiv-todo": "*",
|
|
54
|
+
"@juicesharp/rpiv-advisor": "*",
|
|
55
|
+
"@juicesharp/rpiv-i18n": "*",
|
|
56
|
+
"@juicesharp/rpiv-web-tools": "*",
|
|
57
|
+
"@juicesharp/rpiv-args": "*",
|
|
58
|
+
"@juicesharp/rpiv-workflow": "*",
|
|
59
|
+
"yaml": "*",
|
|
60
|
+
"typebox": "*",
|
|
61
|
+
"jiti": "*",
|
|
62
|
+
"@standard-schema/spec": "*"
|
|
63
|
+
}
|
|
62
64
|
}
|