@mgiles/perk 3.0.0 → 3.2.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/extension/adapters/planAdapterPlannotator.ts +12 -9
- package/extension/doors/address.ts +11 -0
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +43 -15
- package/extension/doors/dreamWaveTools.ts +489 -0
- package/extension/doors/land.ts +6 -0
- package/extension/doors/learn.ts +16 -3
- package/extension/doors/lifecycleGates.ts +36 -1
- package/extension/doors/objectiveReviewBrowser.ts +36 -13
- package/extension/doors/objectiveStack.ts +424 -24
- package/extension/doors/planReviewBrowser.ts +30 -8
- package/extension/doors/plannotatorHandoff.ts +80 -8
- package/extension/doors/prReview.ts +158 -50
- package/extension/doors/prReviewBrowser.ts +75 -27
- package/extension/doors/prReviewDynamic.ts +33 -13
- package/extension/doors/ready.ts +209 -17
- package/extension/doors/reviewWaveTools.ts +61 -17
- package/extension/doors/stackReviewBrowser.ts +573 -0
- package/extension/doors/submit.ts +36 -10
- package/extension/doors/submitPrReview.ts +116 -19
- package/extension/factories/objectiveDraft.ts +95 -27
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +12 -6
- package/extension/factories/objectiveSave.ts +77 -1
- package/extension/factories/planReview.ts +173 -10
- package/extension/index.ts +88 -16
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +92 -2
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +10 -3
- package/extension/substrate/git.ts +85 -2
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/resolverLease.ts +363 -0
- package/extension/substrate/result.ts +3 -2
- package/extension/substrate/sessionData.ts +6 -4
- package/extension/substrate/sessionPointers.ts +3 -4
- package/extension/substrate/toolGating.ts +25 -0
- package/extension/substrate/workflowState.ts +57 -5
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +129 -7
- package/extension/vendor/btw/btw.ts +38 -6
- package/extension/waves/adversarialReviewWave.ts +34 -3
- package/extension/waves/draftReviewWave.ts +17 -1
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +1 -1
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +115 -34
- package/extension/waves/prReviewWave.ts +122 -17
- package/extension/waves/reportWave.ts +103 -7
- package/extension/worker/readOnlySession.ts +2 -3
- package/package.json +6 -3
- package/prompts/_fixtures/live.yaml +112 -0
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -1
- package/prompts/contexts/adapters/plannotator-plan.md +7 -1
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/stages/conflict-resolution-continuation.md +6 -0
- package/prompts/stages/conflict-resolution.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-review-browser.md +1 -1
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +1 -1
- package/prompts/stages/pr-review-browser/active.md +1 -1
- package/prompts/stages/pr-review-browser/foreign.md +1 -1
- package/prompts/stages/pr-review-dynamic.md +5 -5
- package/prompts/stages/pr-review-terminal/active.md +1 -1
- package/prompts/stages/pr-review-terminal/foreign.md +1 -1
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +23 -0
- package/shared/README.md +0 -3
- package/shared/bindings.yaml +6 -0
- package/shared/contracts.md +3784 -1851
- package/shared/registry.yaml +28 -13
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +39 -1
- package/shared/schemas/outputs/objective-stack-status.schema.json +172 -1
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/contracts-history.md +0 -605
|
@@ -91,7 +91,7 @@ function stringOrNull(value: unknown): value is string | null {
|
|
|
91
91
|
* The lexical doc-containment layer (pure): relative, POSIX-normalizes without escaping, and
|
|
92
92
|
* stays under `docs/learned/`. Returns the human-readable violation, or null when contained.
|
|
93
93
|
*/
|
|
94
|
-
function lexicalContainmentError(path: string): string | null {
|
|
94
|
+
export function lexicalContainmentError(path: string): string | null {
|
|
95
95
|
if (posix.isAbsolute(path) || isAbsolute(path)) {
|
|
96
96
|
return "is absolute";
|
|
97
97
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The source-identity boundary for Perk's automatic Ponytail review lanes. Ponytail is installed
|
|
2
|
+
// as an all-resource-disabled project package, so ordinary Pi sessions never discover any of its
|
|
3
|
+
// resources. A review lane opts into exactly one skill through its agent's invocation-local
|
|
4
|
+
// `skillPath`; this module preflights the exact project package + file before that lane can spawn.
|
|
5
|
+
// A same-named project/user skill is never a fallback source.
|
|
6
|
+
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
import { parse as parseMiniYaml } from "../substrate/miniYaml.ts";
|
|
11
|
+
|
|
12
|
+
export const PONYTAIL_PACKAGE_NAME = "@dietrichgebert/ponytail";
|
|
13
|
+
export const PONYTAIL_PACKAGE_ROOT = ".pi/npm/node_modules/@dietrichgebert/ponytail";
|
|
14
|
+
|
|
15
|
+
export type PonytailSkillName = "ponytail" | "ponytail-review";
|
|
16
|
+
|
|
17
|
+
/** Non-serialized lane metadata: the exact package skill a lane requires. */
|
|
18
|
+
export interface RequiredPonytailSkill {
|
|
19
|
+
skill: PonytailSkillName;
|
|
20
|
+
skillFile: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const PONYTAIL_CORE_SKILL: RequiredPonytailSkill = {
|
|
24
|
+
skill: "ponytail",
|
|
25
|
+
skillFile: `${PONYTAIL_PACKAGE_ROOT}/skills/ponytail/SKILL.md`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const PONYTAIL_REVIEW_SKILL: RequiredPonytailSkill = {
|
|
29
|
+
skill: "ponytail-review",
|
|
30
|
+
skillFile: `${PONYTAIL_PACKAGE_ROOT}/skills/ponytail-review/SKILL.md`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PonytailPreflight = { ok: true } | { ok: false; detail: string };
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function frontmatter(text: string): Record<string, unknown> | null {
|
|
40
|
+
if (!text.startsWith("---\n")) return null;
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
const end = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
43
|
+
if (end < 0) return null;
|
|
44
|
+
|
|
45
|
+
// Preflight needs only the one top-level `name` field. Parse that field through Perk's
|
|
46
|
+
// dependency-free YAML seam without widening the contract-file parser to every metadata shape
|
|
47
|
+
// an external skill may carry (notably folded description prose).
|
|
48
|
+
const nameLines = lines.slice(1, end).filter((line) => line.startsWith("name:"));
|
|
49
|
+
if (nameLines.length !== 1) return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = parseMiniYaml(nameLines[0] ?? "");
|
|
52
|
+
return isRecord(parsed) ? parsed : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate one exact source-bound skill against the project install. The check is deliberately
|
|
60
|
+
* self-contained and fail-closed: package identity, `pi.skills`, exact file readability, and
|
|
61
|
+
* frontmatter name must all agree. Callers invoke it once per review pass and skip only the
|
|
62
|
+
* affected lane on failure.
|
|
63
|
+
*/
|
|
64
|
+
export async function preflightPonytailSkill(
|
|
65
|
+
requirement: RequiredPonytailSkill,
|
|
66
|
+
repoRoot = process.cwd(),
|
|
67
|
+
): Promise<PonytailPreflight> {
|
|
68
|
+
const packageRoot = path.resolve(repoRoot, PONYTAIL_PACKAGE_ROOT);
|
|
69
|
+
let manifest: unknown;
|
|
70
|
+
try {
|
|
71
|
+
manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
72
|
+
} catch {
|
|
73
|
+
return { ok: false, detail: "Ponytail package.json is missing, unreadable, or invalid JSON" };
|
|
74
|
+
}
|
|
75
|
+
if (!isRecord(manifest) || manifest.name !== PONYTAIL_PACKAGE_NAME) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
detail: `Ponytail package identity does not match ${PONYTAIL_PACKAGE_NAME}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const pi = manifest.pi;
|
|
82
|
+
const advertised = isRecord(pi) ? pi.skills : undefined;
|
|
83
|
+
if (!Array.isArray(advertised) || !advertised.includes("./skills")) {
|
|
84
|
+
return { ok: false, detail: "Ponytail package.json does not advertise ./skills in pi.skills" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let skillText: string;
|
|
88
|
+
try {
|
|
89
|
+
skillText = await readFile(path.resolve(repoRoot, requirement.skillFile), "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
detail: `required Ponytail skill is missing or unreadable: ${requirement.skillFile}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const metadata = frontmatter(skillText);
|
|
97
|
+
if (metadata === null || metadata.name !== requirement.skill) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
detail: `required Ponytail skill frontmatter name is not ${requirement.skill}: ${requirement.skillFile}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// custom ⇒ the correctness+tests fallback (`source: "fallback"`; a custom-only selection
|
|
20
20
|
// runs as plan-fidelity + custom — no fallback padding);
|
|
21
21
|
// - operator-forced angles come first and are always honored; merge order is forced → picks →
|
|
22
|
-
// custom, deduped; the additional set caps at 3 (2–4 lanes
|
|
23
|
-
// same window as `/pr-review`), and the custom angle survives only if it fits under the cap
|
|
22
|
+
// custom, deduped; the additional set caps at 3 (2–4 selectable lanes incl. plan-fidelity —
|
|
23
|
+
// the same window as `/pr-review` — plus final Ponytail), and the custom angle survives only if it fits under the cap
|
|
24
24
|
// (`selection.custom !== null` ⟺ the custom lane launched);
|
|
25
25
|
// - plan-fidelity is always present, always launched first, never displaced;
|
|
26
26
|
// - fixed-angle reviewer tasks come ONLY from the render-time-embedded angle→task map — the
|
|
@@ -41,12 +41,19 @@
|
|
|
41
41
|
// untrusted DATA, never instructions.
|
|
42
42
|
|
|
43
43
|
import {
|
|
44
|
+
PONYTAIL_REVIEW_SKILL,
|
|
45
|
+
type PonytailPreflight,
|
|
46
|
+
preflightPonytailSkill,
|
|
47
|
+
} from "./ponytail.ts";
|
|
48
|
+
import {
|
|
49
|
+
buildPonytailReviewLane,
|
|
44
50
|
buildPrReviewLanes,
|
|
45
51
|
directiveSuffix,
|
|
46
52
|
isPrReviewAngle,
|
|
47
53
|
PR_REVIEW_ANGLES,
|
|
48
54
|
PR_REVIEW_REPORT_SCHEMA,
|
|
49
55
|
type PrReviewAngle,
|
|
56
|
+
reviewTargetSuffix,
|
|
50
57
|
} from "./prReviewWave.ts";
|
|
51
58
|
import {
|
|
52
59
|
normalizeLanes,
|
|
@@ -60,6 +67,7 @@ import {
|
|
|
60
67
|
type WaveLane,
|
|
61
68
|
type WaveReport,
|
|
62
69
|
type WaveScriptReceipt,
|
|
70
|
+
type WaveSpec,
|
|
63
71
|
} from "./reportWave.ts";
|
|
64
72
|
|
|
65
73
|
/** The additional-angle vocabulary (plan-fidelity is structural — never selectable/removable). */
|
|
@@ -146,7 +154,11 @@ const ALL_ANGLES: readonly PrReviewAngle[] = [
|
|
|
146
154
|
const CUSTOM_SLUG_PATTERN = /^[a-z][a-z0-9-]{2,31}$/;
|
|
147
155
|
|
|
148
156
|
/** The lane-key namespace a custom slug may not collide with (fixed angles + the selector). */
|
|
149
|
-
const RESERVED_LANE_KEYS: readonly string[] = [
|
|
157
|
+
const RESERVED_LANE_KEYS: readonly string[] = [
|
|
158
|
+
...Object.keys(PR_REVIEW_ANGLES),
|
|
159
|
+
"angle-selector",
|
|
160
|
+
"ponytail",
|
|
161
|
+
];
|
|
150
162
|
|
|
151
163
|
// The fixed custom-task template parts — ONE source for the exported builder and the
|
|
152
164
|
// render-time-embedded parts, so in-script custom tasks are byte-identical by construction.
|
|
@@ -186,6 +198,8 @@ function isValidCustomSlug(slug: string): boolean {
|
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
export interface DynamicReviewScriptOptions {
|
|
201
|
+
/** The resolved active-plan PR number shared by the selector and every reviewer. */
|
|
202
|
+
pr: number;
|
|
189
203
|
/** The operator's free-form focus, threaded as DATA to the selector AND every reviewer lane. */
|
|
190
204
|
directive?: string;
|
|
191
205
|
/** Operator-forced additional angles (embedded as a JSON constant; enforced in normalization). */
|
|
@@ -194,6 +208,8 @@ export interface DynamicReviewScriptOptions {
|
|
|
194
208
|
reviewerModel?: string;
|
|
195
209
|
/** The configured `[models.subagents] review-angle-selector` model — the selector item, when set. */
|
|
196
210
|
selectorModel?: string;
|
|
211
|
+
/** Internal preflight result: false omits only the Ponytail child, never its logical key. */
|
|
212
|
+
includePonytail?: boolean;
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
/**
|
|
@@ -201,7 +217,11 @@ export interface DynamicReviewScriptOptions {
|
|
|
201
217
|
* rubric), plus — as DATA — the forced angles when present and the same uniform operator-focus
|
|
202
218
|
* suffix `buildPrReviewLanes` appends to reviewer lanes.
|
|
203
219
|
*/
|
|
204
|
-
function buildSelectorTask(
|
|
220
|
+
function buildSelectorTask(
|
|
221
|
+
pr: number,
|
|
222
|
+
forceAngles: AdditionalPrReviewAngle[],
|
|
223
|
+
suffix: string,
|
|
224
|
+
): string {
|
|
205
225
|
const forcedNote =
|
|
206
226
|
forceAngles.length === 0
|
|
207
227
|
? ""
|
|
@@ -209,9 +229,10 @@ function buildSelectorTask(forceAngles: AdditionalPrReviewAngle[], suffix: strin
|
|
|
209
229
|
", ",
|
|
210
230
|
)} — they will run regardless of your selection; recommend complementary coverage.`;
|
|
211
231
|
return (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"review angles per your agent instructions (they own the
|
|
232
|
+
`Classify active-plan PR #${pr} for dynamic review-angle coverage: fetch the review context ` +
|
|
233
|
+
`yourself only with \`perk pr review-context --expected-pr ${pr} --json\`, classify the ` +
|
|
234
|
+
"change profile, and select the review angles per your agent instructions (they own the " +
|
|
235
|
+
"rubric). Your final action is ONE " +
|
|
215
236
|
"structured_output call." +
|
|
216
237
|
forcedNote +
|
|
217
238
|
suffix
|
|
@@ -232,12 +253,16 @@ function buildSelectorTask(forceAngles: AdditionalPrReviewAngle[], suffix: strin
|
|
|
232
253
|
export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): string {
|
|
233
254
|
// Byte-identical reviewer tasks to the static flow: the map is built by the SAME lane builder
|
|
234
255
|
// (vocabulary + the uniform directive suffix) over all seven angles.
|
|
235
|
-
const lanes = buildPrReviewLanes([...ALL_ANGLES], opts.directive);
|
|
236
|
-
const
|
|
256
|
+
const lanes = buildPrReviewLanes([...ALL_ANGLES], opts.pr, opts.directive);
|
|
257
|
+
const ponytailLane = buildPonytailReviewLane(opts.pr, opts.directive);
|
|
258
|
+
const tasks = Object.fromEntries([
|
|
259
|
+
...lanes.map((lane) => [lane.key, lane.task]),
|
|
260
|
+
[ponytailLane.key, ponytailLane.task],
|
|
261
|
+
]);
|
|
237
262
|
const suffix = directiveSuffix(opts.directive);
|
|
238
263
|
const selectorItem = {
|
|
239
264
|
agent: "perk.review-angle-selector",
|
|
240
|
-
task: buildSelectorTask(opts.forceAngles, suffix),
|
|
265
|
+
task: buildSelectorTask(opts.pr, opts.forceAngles, suffix),
|
|
241
266
|
outputSchema: REVIEW_ANGLE_SELECTOR_SCHEMA,
|
|
242
267
|
...(opts.selectorModel !== undefined ? { model: opts.selectorModel } : {}),
|
|
243
268
|
label: "angle-selector",
|
|
@@ -247,11 +272,13 @@ export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): str
|
|
|
247
272
|
`const TASKS = ${JSON.stringify(tasks, null, 2)};`,
|
|
248
273
|
`const FORCED = ${JSON.stringify(opts.forceAngles)};`,
|
|
249
274
|
`const REVIEWER_MODEL = ${JSON.stringify(opts.reviewerModel ?? null)};`,
|
|
275
|
+
`const PONYTAIL_ENABLED = ${JSON.stringify(opts.includePonytail !== false)};`,
|
|
250
276
|
`const ALLOWLIST_ADDITIONAL = ${JSON.stringify(DYNAMIC_ADDITIONAL_ANGLES)};`,
|
|
251
277
|
`const FALLBACK_ANGLES = ${JSON.stringify(DYNAMIC_FALLBACK_ANGLES)};`,
|
|
252
278
|
`const RESERVED = ${JSON.stringify(RESERVED_LANE_KEYS)};`,
|
|
253
279
|
`const REPORT_SCHEMA = ${JSON.stringify(PR_REVIEW_REPORT_SCHEMA)};`,
|
|
254
280
|
`const CUSTOM_TASK_PARTS = ${JSON.stringify([CUSTOM_TASK_PREFIX, CUSTOM_TASK_MID])};`,
|
|
281
|
+
`const TARGET_SUFFIX = ${JSON.stringify(reviewTargetSuffix(opts.pr))};`,
|
|
255
282
|
`const DIRECTIVE_SUFFIX = ${JSON.stringify(suffix)};`,
|
|
256
283
|
`const CUSTOM_SLUG_RE = new RegExp(${JSON.stringify(CUSTOM_SLUG_PATTERN.source)});`,
|
|
257
284
|
"const reviewerParams = (angle) => ({",
|
|
@@ -259,13 +286,14 @@ export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): str
|
|
|
259
286
|
" task: TASKS[angle],",
|
|
260
287
|
" label: angle,",
|
|
261
288
|
' phase: "review",',
|
|
289
|
+
' ...(angle === "ponytail" ? { skill: "ponytail-review" } : {}),',
|
|
262
290
|
" ...(REVIEWER_MODEL === null ? {} : { model: REVIEWER_MODEL }),",
|
|
263
291
|
"});",
|
|
264
292
|
"// The ONE sanctioned custom lane: the validated scope enters through the fixed template",
|
|
265
293
|
"// (scope-definition-only framing); the per-item schema locks the report's angle echo.",
|
|
266
294
|
"const customParams = (slug, scope) => ({",
|
|
267
295
|
' agent: "perk.pr-reviewer",',
|
|
268
|
-
" task: CUSTOM_TASK_PARTS[0] + slug + CUSTOM_TASK_PARTS[1] + scope + DIRECTIVE_SUFFIX,",
|
|
296
|
+
" task: CUSTOM_TASK_PARTS[0] + slug + CUSTOM_TASK_PARTS[1] + scope + TARGET_SUFFIX + DIRECTIVE_SUFFIX,",
|
|
269
297
|
" label: slug,",
|
|
270
298
|
' phase: "review",',
|
|
271
299
|
" outputSchema: {",
|
|
@@ -278,8 +306,11 @@ export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): str
|
|
|
278
306
|
" (r) => ({ key, ok: r.ok === true, error: r.error ?? null, report: r.structuredOutput ?? null }),",
|
|
279
307
|
" (error) => ({ key, ok: false, error: error instanceof Error ? error.message : String(error), report: null }),",
|
|
280
308
|
");",
|
|
281
|
-
"//
|
|
309
|
+
"// Deterministic lanes launch before and independently of selector output (held promises).",
|
|
282
310
|
'const planFidelity = laneOf("plan-fidelity", runs.run("plan-fidelity", reviewerParams("plan-fidelity")));',
|
|
311
|
+
"const ponytail = PONYTAIL_ENABLED",
|
|
312
|
+
' ? laneOf("ponytail", runs.run("ponytail", reviewerParams("ponytail")))',
|
|
313
|
+
" : null;",
|
|
283
314
|
"let sel = null;",
|
|
284
315
|
"let selectorError = null;",
|
|
285
316
|
"try {",
|
|
@@ -338,11 +369,12 @@ export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): str
|
|
|
338
369
|
" await planFidelity,",
|
|
339
370
|
" ...reviewers.map(({ key, ok, error, structuredOutput }) =>",
|
|
340
371
|
" ({ key, ok, error: error ?? null, report: structuredOutput ?? null })),",
|
|
372
|
+
" ...(ponytail === null ? [] : [await ponytail]),",
|
|
341
373
|
"];",
|
|
342
374
|
"return {",
|
|
343
375
|
" selection: {",
|
|
344
376
|
" source,",
|
|
345
|
-
' effective: ["plan-fidelity", ...additional],',
|
|
377
|
+
' effective: ["plan-fidelity", ...additional, "ponytail"],',
|
|
346
378
|
" forced: FORCED,",
|
|
347
379
|
" custom,",
|
|
348
380
|
" selector_ok: report !== null,",
|
|
@@ -357,7 +389,7 @@ export function renderDynamicReviewScript(opts: DynamicReviewScriptOptions): str
|
|
|
357
389
|
/** The parent-facing selection metadata (observability for the dogfood — DATA only). */
|
|
358
390
|
export interface DynamicSelection {
|
|
359
391
|
source: "selector" | "fallback";
|
|
360
|
-
/** The effective lanes: plan-fidelity + ≤3 additional angles, launch order. */
|
|
392
|
+
/** The effective lanes: plan-fidelity + ≤3 additional angles + final Ponytail, launch order. */
|
|
361
393
|
effective: string[];
|
|
362
394
|
/** The operator-forced additional angles (echoed from the tool param). */
|
|
363
395
|
forced: string[];
|
|
@@ -375,6 +407,8 @@ export interface DynamicSelection {
|
|
|
375
407
|
}
|
|
376
408
|
|
|
377
409
|
export interface PrReviewDynamicOptions {
|
|
410
|
+
/** The resolved active-plan PR number shared by the selector and every reviewer. */
|
|
411
|
+
pr: number;
|
|
378
412
|
/** The operator's free-form focus, threaded as DATA to the selector and every reviewer lane. */
|
|
379
413
|
directive?: string;
|
|
380
414
|
/** Operator-forced additional angles (≤3; plan-fidelity is structural, never forced). */
|
|
@@ -385,6 +419,8 @@ export interface PrReviewDynamicOptions {
|
|
|
385
419
|
selectorModel?: string;
|
|
386
420
|
timeoutMs?: number;
|
|
387
421
|
signal?: AbortSignal;
|
|
422
|
+
/** Test seam; production validates the exact source-bound Ponytail review skill. */
|
|
423
|
+
requiredSkillPreflight?: WaveSpec["requiredSkillPreflight"];
|
|
388
424
|
}
|
|
389
425
|
|
|
390
426
|
export interface PrReviewDynamicOutcome {
|
|
@@ -401,9 +437,9 @@ export interface PrReviewDynamicOutcome {
|
|
|
401
437
|
selection: DynamicSelection | null;
|
|
402
438
|
/**
|
|
403
439
|
* One output-free receipt per top-level launch, run order (observability only — never a
|
|
404
|
-
* decision input). A dynamic launch's `requestedKeys` is the
|
|
405
|
-
* (plan-fidelity + angle-selector)
|
|
406
|
-
* reconstructed into
|
|
440
|
+
* decision input). A dynamic launch's `requestedKeys` is the pre-launch manifest
|
|
441
|
+
* (plan-fidelity + angle-selector + Ponytail); selector-dependent fan-out lanes appear only as
|
|
442
|
+
* receipt children, never reconstructed into that manifest.
|
|
407
443
|
*/
|
|
408
444
|
attempts: WaveAttemptReceipt[];
|
|
409
445
|
}
|
|
@@ -476,15 +512,18 @@ function parseDynamicValue(
|
|
|
476
512
|
}
|
|
477
513
|
// Re-validate the effective selection against the normalization guarantees.
|
|
478
514
|
const isEffectiveKey = (slug: string): boolean =>
|
|
479
|
-
isPrReviewAngle(slug) || (custom !== null && slug === custom.slug);
|
|
515
|
+
isPrReviewAngle(slug) || slug === "ponytail" || (custom !== null && slug === custom.slug);
|
|
480
516
|
if (!effective.every(isEffectiveKey)) {
|
|
481
517
|
return `dynamic selection carries an out-of-allowlist effective angle (${effective.join(", ")})`;
|
|
482
518
|
}
|
|
483
|
-
if (
|
|
484
|
-
return "dynamic selection dropped the mandatory plan-fidelity lane";
|
|
519
|
+
if (effective[0] !== "plan-fidelity") {
|
|
520
|
+
return "dynamic selection dropped or displaced the mandatory plan-fidelity lane";
|
|
521
|
+
}
|
|
522
|
+
if (effective.at(-1) !== "ponytail") {
|
|
523
|
+
return "dynamic selection dropped or displaced the final Ponytail lane";
|
|
485
524
|
}
|
|
486
|
-
if (effective.length >
|
|
487
|
-
return `dynamic selection exceeds the
|
|
525
|
+
if (effective.length > 5) {
|
|
526
|
+
return `dynamic selection exceeds the 5-lane effective cap (${effective.join(", ")})`;
|
|
488
527
|
}
|
|
489
528
|
if (new Set(effective).size !== effective.length) {
|
|
490
529
|
return `dynamic selection carries duplicate effective angles (${effective.join(", ")})`;
|
|
@@ -507,7 +546,7 @@ function parseDynamicValue(
|
|
|
507
546
|
}
|
|
508
547
|
|
|
509
548
|
/** The pre-launch lane manifest of every dynamic script run (the fan-out is unknowable). */
|
|
510
|
-
const DYNAMIC_REQUESTED_KEYS: readonly string[] = ["plan-fidelity", "angle-selector"];
|
|
549
|
+
const DYNAMIC_REQUESTED_KEYS: readonly string[] = ["plan-fidelity", "angle-selector", "ponytail"];
|
|
511
550
|
|
|
512
551
|
/**
|
|
513
552
|
* Enrich receipt children's `agent` via the module's deterministic mapping (the dynamic
|
|
@@ -541,12 +580,15 @@ type DynamicRun =
|
|
|
541
580
|
async function runDynamicOnce(
|
|
542
581
|
adapter: WaveAdapter,
|
|
543
582
|
opts: PrReviewDynamicOptions,
|
|
583
|
+
ponytailCheck: PonytailPreflight,
|
|
544
584
|
): Promise<DynamicRun> {
|
|
545
585
|
const workflowScript = renderDynamicReviewScript({
|
|
586
|
+
pr: opts.pr,
|
|
546
587
|
forceAngles: opts.forceAngles ?? [],
|
|
547
588
|
...(opts.directive !== undefined ? { directive: opts.directive } : {}),
|
|
548
589
|
...(opts.reviewerModel !== undefined ? { reviewerModel: opts.reviewerModel } : {}),
|
|
549
590
|
...(opts.selectorModel !== undefined ? { selectorModel: opts.selectorModel } : {}),
|
|
591
|
+
includePonytail: ponytailCheck.ok,
|
|
550
592
|
});
|
|
551
593
|
const run = await runWaveScript(
|
|
552
594
|
adapter,
|
|
@@ -571,7 +613,17 @@ async function runDynamicOnce(
|
|
|
571
613
|
receipt,
|
|
572
614
|
};
|
|
573
615
|
}
|
|
574
|
-
const
|
|
616
|
+
const runnableKeys = parsed.selection.effective.filter(
|
|
617
|
+
(key) => key !== "ponytail" || ponytailCheck.ok,
|
|
618
|
+
);
|
|
619
|
+
const { reports, failures } = normalizeLanes(runnableKeys, parsed.lanes);
|
|
620
|
+
if (!ponytailCheck.ok) {
|
|
621
|
+
failures.push({
|
|
622
|
+
key: "ponytail",
|
|
623
|
+
reason: "skill-unavailable",
|
|
624
|
+
detail: ponytailCheck.detail,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
575
627
|
return { kind: "parsed", selection: parsed.selection, reports, failures, receipt };
|
|
576
628
|
}
|
|
577
629
|
|
|
@@ -609,9 +661,21 @@ function outcomeOf(
|
|
|
609
661
|
*/
|
|
610
662
|
export async function runPrReviewDynamicWave(
|
|
611
663
|
adapter: WaveAdapter,
|
|
612
|
-
opts: PrReviewDynamicOptions
|
|
664
|
+
opts: PrReviewDynamicOptions,
|
|
613
665
|
): Promise<PrReviewDynamicOutcome> {
|
|
614
|
-
const
|
|
666
|
+
const preflight = opts.requiredSkillPreflight ?? preflightPonytailSkill;
|
|
667
|
+
const ponytailCheck = await preflight(PONYTAIL_REVIEW_SKILL);
|
|
668
|
+
const skillFailures = (): WaveFailure[] =>
|
|
669
|
+
ponytailCheck.ok
|
|
670
|
+
? []
|
|
671
|
+
: [
|
|
672
|
+
{
|
|
673
|
+
key: "ponytail",
|
|
674
|
+
reason: "skill-unavailable",
|
|
675
|
+
detail: ponytailCheck.detail,
|
|
676
|
+
},
|
|
677
|
+
];
|
|
678
|
+
const first = await runDynamicOnce(adapter, opts, ponytailCheck);
|
|
615
679
|
// Ordered attempts — the first attempt's receipt is preserved verbatim when a retry runs.
|
|
616
680
|
const attempts = [
|
|
617
681
|
toAttemptReceipt("pr-review-dynamic", 1, [...DYNAMIC_REQUESTED_KEYS], first.receipt),
|
|
@@ -619,22 +683,22 @@ export async function runPrReviewDynamicWave(
|
|
|
619
683
|
|
|
620
684
|
if (first.kind === "wave-failure") {
|
|
621
685
|
if (!RETRYABLE_WAVE_REASONS.has(first.failure.reason)) {
|
|
622
|
-
return outcomeOf(null, [], [first.failure], [], attempts);
|
|
686
|
+
return outcomeOf(null, [], [first.failure, ...skillFailures()], [], attempts);
|
|
623
687
|
}
|
|
624
688
|
// Retryable wave-level failure ⇒ ONE full dynamic re-run (fresh selector); its selection
|
|
625
689
|
// supersedes. The re-run's outcome is final — never a second retry.
|
|
626
|
-
const second = await runDynamicOnce(adapter, opts);
|
|
690
|
+
const second = await runDynamicOnce(adapter, opts, ponytailCheck);
|
|
627
691
|
attempts.push(
|
|
628
692
|
toAttemptReceipt("pr-review-dynamic", 2, [...DYNAMIC_REQUESTED_KEYS], second.receipt),
|
|
629
693
|
);
|
|
630
694
|
if (second.kind === "wave-failure") {
|
|
631
|
-
return outcomeOf(null, [], [second.failure], [], attempts);
|
|
695
|
+
return outcomeOf(null, [], [second.failure, ...skillFailures()], [], attempts);
|
|
632
696
|
}
|
|
633
697
|
return outcomeOf(
|
|
634
698
|
second.selection,
|
|
635
699
|
second.reports,
|
|
636
700
|
second.failures,
|
|
637
|
-
second.selection.effective,
|
|
701
|
+
second.selection.effective.filter((key) => key !== "ponytail" || ponytailCheck.ok),
|
|
638
702
|
attempts,
|
|
639
703
|
);
|
|
640
704
|
}
|
|
@@ -647,16 +711,21 @@ export async function runPrReviewDynamicWave(
|
|
|
647
711
|
// the custom lane via the fixed template + its per-lane report schema); the selector is never
|
|
648
712
|
// re-run.
|
|
649
713
|
const failedKeys = first.selection.effective.filter((key) =>
|
|
650
|
-
first.failures.some((failure) => failure.key === key),
|
|
714
|
+
first.failures.some((failure) => failure.key === key && failure.reason !== "skill-unavailable"),
|
|
651
715
|
);
|
|
652
716
|
const custom = first.selection.custom;
|
|
653
717
|
const retryAngles = failedKeys.filter(isPrReviewAngle);
|
|
654
718
|
const customRetry = custom !== null && failedKeys.includes(custom.slug) ? custom : null;
|
|
655
|
-
const
|
|
719
|
+
const ponytailRetry = ponytailCheck.ok && failedKeys.includes("ponytail");
|
|
720
|
+
const retryKeys = [
|
|
721
|
+
...retryAngles,
|
|
722
|
+
...(customRetry !== null ? [customRetry.slug] : []),
|
|
723
|
+
...(ponytailRetry ? ["ponytail"] : []),
|
|
724
|
+
];
|
|
656
725
|
if (retryKeys.length === 0) return firstOutcome;
|
|
657
726
|
|
|
658
727
|
const retryLanes: WaveLane[] = [
|
|
659
|
-
...buildPrReviewLanes(retryAngles, opts.directive),
|
|
728
|
+
...buildPrReviewLanes(retryAngles, opts.pr, opts.directive),
|
|
660
729
|
...(customRetry !== null
|
|
661
730
|
? [
|
|
662
731
|
{
|
|
@@ -666,11 +735,13 @@ export async function runPrReviewDynamicWave(
|
|
|
666
735
|
phase: "review",
|
|
667
736
|
task:
|
|
668
737
|
buildCustomLaneTask(customRetry.slug, customRetry.scope) +
|
|
738
|
+
reviewTargetSuffix(opts.pr) +
|
|
669
739
|
directiveSuffix(opts.directive),
|
|
670
740
|
outputSchema: customReportSchema(customRetry.slug),
|
|
671
741
|
},
|
|
672
742
|
]
|
|
673
743
|
: []),
|
|
744
|
+
...(ponytailRetry ? [buildPonytailReviewLane(opts.pr, opts.directive)] : []),
|
|
674
745
|
];
|
|
675
746
|
const staticRetry = await runReportWave(
|
|
676
747
|
adapter,
|
|
@@ -681,6 +752,7 @@ export async function runPrReviewDynamicWave(
|
|
|
681
752
|
completeness: "strict",
|
|
682
753
|
...(opts.reviewerModel !== undefined ? { model: opts.reviewerModel } : {}),
|
|
683
754
|
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
755
|
+
requiredSkillPreflight: async () => ponytailCheck,
|
|
684
756
|
},
|
|
685
757
|
opts.signal,
|
|
686
758
|
);
|
|
@@ -692,5 +764,14 @@ export async function runPrReviewDynamicWave(
|
|
|
692
764
|
...first.reports.filter((report) => !retriedSet.has(report.key)),
|
|
693
765
|
...staticRetry.reports,
|
|
694
766
|
];
|
|
695
|
-
|
|
767
|
+
const carried = first.failures.filter(
|
|
768
|
+
(failure) => failure.reason === "skill-unavailable" && failure.key === "ponytail",
|
|
769
|
+
);
|
|
770
|
+
return outcomeOf(
|
|
771
|
+
first.selection,
|
|
772
|
+
merged,
|
|
773
|
+
[...staticRetry.failures, ...carried],
|
|
774
|
+
retryKeys,
|
|
775
|
+
attempts,
|
|
776
|
+
);
|
|
696
777
|
}
|