@mgiles/perk 2.3.0 → 3.1.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/README.md +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +135 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- 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 +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +6 -6
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
|
@@ -2,22 +2,41 @@
|
|
|
2
2
|
// typed outcomes under stable lane keys. Report waves were previously model-authored prompt
|
|
3
3
|
// mechanics (a script skeleton the parent model had to transcribe faithfully — the known
|
|
4
4
|
// prompt-drift risk); this module makes the mechanics CODE. It renders the complete, tested
|
|
5
|
-
// `workflowScript`, launches it through a `WaveAdapter` (async-only, `mission: false`),
|
|
5
|
+
// `workflowScript`, launches it through a `WaveAdapter` (async-only, `mission: false`), waits on
|
|
6
6
|
// the run's async-complete event with a module-owned timeout, reads the durable `status.json`
|
|
7
7
|
// `workflow.value` aggregate, and normalizes `{complete, reports[], failures[]}` under a
|
|
8
|
-
// flow-specific completeness policy.
|
|
8
|
+
// flow-specific completeness policy. Each launch additionally records an OUTPUT-FREE
|
|
9
|
+
// `WaveScriptReceipt` (run handle + per-child identity/artifact trail from the completion
|
|
10
|
+
// payload) — write-only telemetry for correlation: `status.json.workflow.value` stays the sole
|
|
11
|
+
// source of reports, and receipt absence never changes a verdict, completeness, or retry
|
|
12
|
+
// selection (contracts.md §8.35).
|
|
13
|
+
//
|
|
14
|
+
// The module hosts BOTH the blocking runner and the non-blocking streaming sibling over ONE
|
|
15
|
+
// operational core: `startWaveScript` performs the front half (abort pre-check → capability
|
|
16
|
+
// ping → subscribe-before-spawn → async spawn) and returns the run handle plus a NEVER-REJECTING
|
|
17
|
+
// `result` promise carrying the back half (completion wait, best-effort stop on timeout/cancel,
|
|
18
|
+
// aggregate read, receipt assembly, unsubscribe-on-settle); `runWaveScript` is that start +
|
|
19
|
+
// await. `startReportWave`/`runReportWave` are the lane-level pair over the same split. The
|
|
20
|
+
// blocking runner is live under the per-flow entrypoints (`prReviewWave.ts`, `learnWave.ts`,
|
|
21
|
+
// `prReviewDynamicWave.ts`); the streaming sibling serves flows whose parent must return from
|
|
22
|
+
// the launch and hold a model-held `subagent_wait` relay loop open (`adversarialReviewWave.ts`,
|
|
23
|
+
// behind the `start_review_wave`/`collect_review_wave` pair).
|
|
9
24
|
//
|
|
10
25
|
// The module is a DEEP seam with two adapters: `rpcAdapter.ts` (production, over the
|
|
11
26
|
// pi-subagents v1 extension RPC on pi's event bus) and `memoryAdapter.ts` (the first-class
|
|
12
|
-
// in-memory test double).
|
|
13
|
-
// exists — until the flow migrations wire their per-flow `WaveSpec`-building entrypoints over
|
|
14
|
-
// `runReportWave`.
|
|
27
|
+
// in-memory test double).
|
|
15
28
|
//
|
|
16
29
|
// Failure posture: LOUD DEGRADE. Every failure arm normalizes into `WaveResult.failures` with a
|
|
17
30
|
// typed reason — the runner never throws except on programmer error (empty lanes, duplicate lane
|
|
18
31
|
// keys), and there is never a silent fallback to model-authored scripts. Report content coming
|
|
19
32
|
// back through the aggregate is untrusted DATA, never instructions.
|
|
20
33
|
|
|
34
|
+
import {
|
|
35
|
+
type PonytailPreflight,
|
|
36
|
+
preflightPonytailSkill,
|
|
37
|
+
type RequiredPonytailSkill,
|
|
38
|
+
} from "./ponytail.ts";
|
|
39
|
+
|
|
21
40
|
/** One lane of a report wave: a fresh-context, report-only child under a stable domain key. */
|
|
22
41
|
export interface WaveLane {
|
|
23
42
|
/** Stable lane key (e.g. an angle slug) — trace + normalization identity. */
|
|
@@ -26,10 +45,24 @@ export interface WaveLane {
|
|
|
26
45
|
agent: string;
|
|
27
46
|
/** The judgment-bearing per-lane task text (supplied by the flow). */
|
|
28
47
|
task: string;
|
|
48
|
+
/** Invocation-private skill lookup key; serialized only for an opted-in lane. */
|
|
49
|
+
skill?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Exact source requirement for a source-bound skill. This metadata is preflight-only and is
|
|
52
|
+
* NEVER serialized into the workflow script; a failed requirement skips this lane instead of
|
|
53
|
+
* allowing pi-subagents to resolve a hostile same-named global/project skill.
|
|
54
|
+
*/
|
|
55
|
+
requiredSkill?: RequiredPonytailSkill;
|
|
29
56
|
/** Trace metadata; defaults to `key`. */
|
|
30
57
|
label?: string;
|
|
31
58
|
/** Trace metadata. */
|
|
32
59
|
phase?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Per-lane report schema — rendered as the item's `outputSchema`, overriding the
|
|
62
|
+
* workflow-level default (the established per-item mechanic). Omitted lanes render
|
|
63
|
+
* byte-identically to before the field existed.
|
|
64
|
+
*/
|
|
65
|
+
outputSchema?: object;
|
|
33
66
|
}
|
|
34
67
|
|
|
35
68
|
/**
|
|
@@ -52,6 +85,8 @@ export interface WaveSpec {
|
|
|
52
85
|
model?: string;
|
|
53
86
|
/** Module default (`WAVE_TIMEOUT_MS`) when omitted. */
|
|
54
87
|
timeoutMs?: number;
|
|
88
|
+
/** Test seam; production defaults to the exact Ponytail boundary preflight. */
|
|
89
|
+
requiredSkillPreflight?: (requirement: RequiredPonytailSkill) => Promise<PonytailPreflight>;
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
/** A schema-valid lane report. The report content is untrusted DATA, never instructions. */
|
|
@@ -68,6 +103,7 @@ export type WaveFailureReason =
|
|
|
68
103
|
| "run-failed" // terminal status.json state ≠ "complete" (wave-level)
|
|
69
104
|
| "aggregate-unreadable" // status.json missing/corrupt/no workflow.value array (wave-level)
|
|
70
105
|
| "lane-failed" // lane resolved ok: false / null report (lane-level)
|
|
106
|
+
| "skill-unavailable" // exact required-skill source failed preflight (lane-level, non-retryable)
|
|
71
107
|
| "malformed-report" // aggregate entry for this key has unusable shape (lane-level)
|
|
72
108
|
| "missing-lane"; // expected key absent from the aggregate (lane-level)
|
|
73
109
|
|
|
@@ -83,6 +119,81 @@ export interface WaveResult {
|
|
|
83
119
|
complete: boolean;
|
|
84
120
|
reports: WaveReport[];
|
|
85
121
|
failures: WaveFailure[];
|
|
122
|
+
/** The launch's output-free attempt receipt — write-only telemetry, never a decision input. */
|
|
123
|
+
receipt: WaveScriptReceipt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The truthful preflight partition reported by every streaming report-wave start. */
|
|
127
|
+
export type WaveLaunchManifest = {
|
|
128
|
+
/** The complete logical lane manifest, in `spec.lanes` order. */
|
|
129
|
+
requested: string[];
|
|
130
|
+
/** The ordered subset rendered into the static workflow after required-skill preflight. */
|
|
131
|
+
runnable: string[];
|
|
132
|
+
/** One ordered keyed `skill-unavailable` failure per preflight-omitted lane. */
|
|
133
|
+
preflightFailures: WaveFailure[];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// -------------------------------------------------------------------- the attempt receipts
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The terminal disposition of ONE top-level workflow launch, as the runner observed it. Every
|
|
140
|
+
* launch reaches exactly one of these arms (`"running"` is unreachable — the runner always
|
|
141
|
+
* settles); `"unavailable"` preserves even a pre-spawn capability failure as an attempt.
|
|
142
|
+
*/
|
|
143
|
+
export type WaveReceiptState =
|
|
144
|
+
| "unavailable" // ping failed/incomplete — nothing launched
|
|
145
|
+
| "spawn-failed" // spawn rejected/threw — no run handle
|
|
146
|
+
| "complete" // completion observed, durable state "complete"
|
|
147
|
+
| "failed" // completion observed, durable/observed failure
|
|
148
|
+
| "timed-out" // module timeout expired (handle preserved)
|
|
149
|
+
| "cancelled"; // AbortSignal honored (handle preserved when spawned)
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* One child lane's identity/artifact trail from the completion payload — OUTPUT-FREE by
|
|
153
|
+
* invariant: reports, summaries, and structured output never enter a receipt (they stay in the
|
|
154
|
+
* durable `status.json.workflow.value`, the sole report authority).
|
|
155
|
+
*/
|
|
156
|
+
export interface WaveChildReceipt {
|
|
157
|
+
/** The Perk lane key (mapped FROM the upstream row's overloaded `agent` field). */
|
|
158
|
+
key: string;
|
|
159
|
+
/** The child agent name, enriched from the Perk-owned lane spec where known. */
|
|
160
|
+
agent?: string;
|
|
161
|
+
/** The child's opaque run id — never parsed or synthesized from paths. */
|
|
162
|
+
runId?: string;
|
|
163
|
+
success?: boolean;
|
|
164
|
+
outputState?: "present" | "absent" | "unknown";
|
|
165
|
+
/** String path fields only; output-free. */
|
|
166
|
+
artifactPaths?: Record<string, string>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** One script launch's receipt: the run handle (where known) + the observed children. */
|
|
170
|
+
export interface WaveScriptReceipt {
|
|
171
|
+
/** The top-level async run id (the spawn handle's asyncId). */
|
|
172
|
+
runId?: string;
|
|
173
|
+
asyncDir?: string;
|
|
174
|
+
state: WaveReceiptState;
|
|
175
|
+
children: WaveChildReceipt[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A flow-attributed attempt: one receipt per top-level workflow launch, ordered by `attempt`
|
|
180
|
+
* (one-based, assigned by the flow entrypoint that owns retry policy). `requestedKeys` is the
|
|
181
|
+
* lane manifest BEFORE launch — never reconstructed from the observed children.
|
|
182
|
+
*/
|
|
183
|
+
export interface WaveAttemptReceipt extends WaveScriptReceipt {
|
|
184
|
+
flow: string;
|
|
185
|
+
attempt: number;
|
|
186
|
+
requestedKeys: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Assemble one flow attempt from a script receipt (the uniform builder the flows share). */
|
|
190
|
+
export function toAttemptReceipt(
|
|
191
|
+
flow: string,
|
|
192
|
+
attempt: number,
|
|
193
|
+
requestedKeys: string[],
|
|
194
|
+
receipt: WaveScriptReceipt,
|
|
195
|
+
): WaveAttemptReceipt {
|
|
196
|
+
return { flow, attempt, requestedKeys: [...requestedKeys], ...receipt };
|
|
86
197
|
}
|
|
87
198
|
|
|
88
199
|
// ------------------------------------------------------------------------- the adapter seam
|
|
@@ -104,12 +215,36 @@ export interface WaveRunHandle {
|
|
|
104
215
|
asyncDir: string;
|
|
105
216
|
}
|
|
106
217
|
|
|
107
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* An async-complete notification; at least one identifier is present on real payloads. The
|
|
220
|
+
* observability fields are optional — an identity-only completion stays valid (receipt absence
|
|
221
|
+
* degrades correlation, never behavior). The adapter normalizes them output-free and leaves
|
|
222
|
+
* each child's `agent` unset (enrichment happens against Perk-owned lane specs).
|
|
223
|
+
*/
|
|
108
224
|
export interface WaveCompletion {
|
|
109
225
|
asyncId?: string;
|
|
110
226
|
asyncDir?: string;
|
|
227
|
+
/** The run's raw terminal state string, when the payload carries one. */
|
|
228
|
+
state?: string;
|
|
229
|
+
success?: boolean;
|
|
230
|
+
children?: WaveChildReceipt[];
|
|
111
231
|
}
|
|
112
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The explicit acceptance-disable every wave spawn carries. Without it, pi-subagents
|
|
235
|
+
* auto-infers a generic acceptance contract for reviewer/analyst-named or read-only children
|
|
236
|
+
* and injects a fenced `acceptance-report` completion instruction into each lane — a COMPETING
|
|
237
|
+
* completion contract observed steering children into invalid `structured_output` attempts.
|
|
238
|
+
* `{level: "none"}` is the sanctioned disable shape (pi-subagents `explicitAcceptanceCanDisable`);
|
|
239
|
+
* `formatAcceptancePrompt` emits nothing at level none, so no contract block reaches a lane.
|
|
240
|
+
* Deliberately module-wide with no opt-out: every report-wave child's sole completion contract
|
|
241
|
+
* is the engine-validated `structured_output` report.
|
|
242
|
+
*/
|
|
243
|
+
export const WAVE_ACCEPTANCE = {
|
|
244
|
+
level: "none",
|
|
245
|
+
reason: "perk report-wave lanes complete via the engine-validated structured_output report",
|
|
246
|
+
} as const;
|
|
247
|
+
|
|
113
248
|
/** The full spawn params the runner fixes: async-only, ephemeral, fresh-context by definition. */
|
|
114
249
|
export interface WaveSpawnParams {
|
|
115
250
|
workflowScript: string;
|
|
@@ -118,6 +253,9 @@ export interface WaveSpawnParams {
|
|
|
118
253
|
mission: false;
|
|
119
254
|
/** A report wave is by definition fresh-context. */
|
|
120
255
|
context: "fresh";
|
|
256
|
+
/** The fixed acceptance disable (`WAVE_ACCEPTANCE`) — pi-subagents' workflow-defaults spread
|
|
257
|
+
* delivers it onto every lane child, suppressing the auto-inferred acceptance contract. */
|
|
258
|
+
acceptance: { level: "none"; reason: string };
|
|
121
259
|
outputSchema: object;
|
|
122
260
|
model?: string;
|
|
123
261
|
/** Orphan insurance: the run enforces the same deadline even if the parent session dies. */
|
|
@@ -139,15 +277,24 @@ export interface WaveAdapter {
|
|
|
139
277
|
|
|
140
278
|
// ---------------------------------------------------------------------------- the renderer
|
|
141
279
|
|
|
280
|
+
/**
|
|
281
|
+
* pi-subagents' scripted-workflow run-key contract for `runs.all` item keys: start
|
|
282
|
+
* alphanumeric, then letters/digits/`.`/`_`/`-`, ≤128 chars total. Mirrored here because the
|
|
283
|
+
* upstream pattern is enforced only inside the live workflow worker — an invalid key fails the
|
|
284
|
+
* WHOLE wave at dispatch (`run-failed`), a path no offline adapter exercises — so the renderer
|
|
285
|
+
* rejects it up front as a programmer error.
|
|
286
|
+
*/
|
|
287
|
+
export const RUN_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
288
|
+
|
|
142
289
|
/**
|
|
143
290
|
* Render the wave `workflowScript`: an explicit-return, all-settled `runs.all` over the lane
|
|
144
291
|
* items, projected to the compact typed aggregate only (lane key, outcome, error, and the
|
|
145
292
|
* schema-validated report — children's prose never enters the aggregate beyond `error`/`output`
|
|
146
293
|
* on failure). Lane items are embedded via `JSON.stringify`, so hostile task text (quotes,
|
|
147
294
|
* newlines, backticks, `${}`) cannot escape the array literal. Throws on programmer error:
|
|
148
|
-
* empty lanes or
|
|
295
|
+
* empty lanes, duplicate lane keys, or a lane key outside the run-key contract.
|
|
149
296
|
*/
|
|
150
|
-
|
|
297
|
+
function validateWaveLanes(lanes: WaveLane[]): void {
|
|
151
298
|
if (lanes.length === 0) {
|
|
152
299
|
throw new Error("renderWaveScript: a report wave needs at least one lane");
|
|
153
300
|
}
|
|
@@ -156,14 +303,25 @@ export function renderWaveScript(lanes: WaveLane[]): string {
|
|
|
156
303
|
if (seen.has(lane.key)) {
|
|
157
304
|
throw new Error(`renderWaveScript: duplicate lane key '${lane.key}'`);
|
|
158
305
|
}
|
|
306
|
+
if (!RUN_KEY_PATTERN.test(lane.key)) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`renderWaveScript: lane key '${lane.key}' violates the pi-subagents run-key contract`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
159
311
|
seen.add(lane.key);
|
|
160
312
|
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function renderWaveScript(lanes: WaveLane[]): string {
|
|
316
|
+
validateWaveLanes(lanes);
|
|
161
317
|
const items = lanes.map((lane) => ({
|
|
162
318
|
key: lane.key,
|
|
163
319
|
agent: lane.agent,
|
|
164
320
|
task: lane.task,
|
|
321
|
+
...(lane.skill !== undefined ? { skill: lane.skill } : {}),
|
|
165
322
|
label: lane.label ?? lane.key,
|
|
166
323
|
...(lane.phase !== undefined ? { phase: lane.phase } : {}),
|
|
324
|
+
...(lane.outputSchema !== undefined ? { outputSchema: lane.outputSchema } : {}),
|
|
167
325
|
}));
|
|
168
326
|
return (
|
|
169
327
|
`const reports = await runs.all(${JSON.stringify(items, null, 2)});\n` +
|
|
@@ -186,8 +344,12 @@ function waveTimeoutMs(): number {
|
|
|
186
344
|
return Number.isFinite(raw) && raw > 0 ? raw : WAVE_TIMEOUT_MS;
|
|
187
345
|
}
|
|
188
346
|
|
|
189
|
-
function waveFailure(
|
|
190
|
-
|
|
347
|
+
function waveFailure(
|
|
348
|
+
reason: WaveFailureReason,
|
|
349
|
+
detail: string,
|
|
350
|
+
receipt: WaveScriptReceipt,
|
|
351
|
+
): WaveResult {
|
|
352
|
+
return { complete: false, reports: [], failures: [{ key: null, reason, detail }], receipt };
|
|
191
353
|
}
|
|
192
354
|
|
|
193
355
|
/** The judgment-bearing pieces a script run needs (the lane-free slice of `WaveSpec`). */
|
|
@@ -205,7 +367,23 @@ export interface WaveScriptSpec {
|
|
|
205
367
|
}
|
|
206
368
|
|
|
207
369
|
/** A script run's outcome: the raw `workflow.value` on success, one wave-level failure otherwise. */
|
|
208
|
-
export type WaveScriptResult =
|
|
370
|
+
export type WaveScriptResult =
|
|
371
|
+
| { ok: true; value: unknown; receipt: WaveScriptReceipt }
|
|
372
|
+
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* A launched (or launch-refused) script run. On `ok: true` the run is LIVE: `handle` is the
|
|
376
|
+
* detached async run, and `result` settles when the back half finishes (completion wait under
|
|
377
|
+
* the module-owned timeout, AbortSignal honor, best-effort stop on timeout/cancel, the durable
|
|
378
|
+
* aggregate read, receipt assembly, unsubscribe-on-settle). `result` NEVER rejects — every arm
|
|
379
|
+
* normalizes into `WaveScriptResult`, so an uncollected wave can never become an unhandled
|
|
380
|
+
* rejection. Pre-spawn failures (aborted-before-launch, ping fail/null, spawn throw) take the
|
|
381
|
+
* `ok: false` arm with the same failure/receipt values the blocking runner reports, and the
|
|
382
|
+
* completion subscription is released immediately.
|
|
383
|
+
*/
|
|
384
|
+
export type WaveScriptStart =
|
|
385
|
+
| { ok: true; handle: WaveRunHandle; result: Promise<WaveScriptResult> }
|
|
386
|
+
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
209
387
|
|
|
210
388
|
function errorDetail(error: unknown): string {
|
|
211
389
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -281,26 +459,51 @@ export function normalizeLanes(
|
|
|
281
459
|
}
|
|
282
460
|
|
|
283
461
|
/**
|
|
284
|
-
*
|
|
285
|
-
* subscribe-before-spawn (the completion-before-reply buffer) → async spawn
|
|
286
|
-
* async-complete event
|
|
287
|
-
*
|
|
288
|
-
* `
|
|
289
|
-
*
|
|
462
|
+
* Start one module-rendered workflowScript through the adapter — the non-blocking front half:
|
|
463
|
+
* capability ping → subscribe-before-spawn (the completion-before-reply buffer) → async spawn.
|
|
464
|
+
* On success the back half (block on the async-complete event under the module-owned timeout,
|
|
465
|
+
* abortable → best-effort stop on timeout/cancel → read the durable aggregate → the
|
|
466
|
+
* `state !== "complete"` / unreadable arms) runs behind the returned `result` promise, which
|
|
467
|
+
* never rejects. The shared operational core under every runner; per-flow value normalization
|
|
468
|
+
* stays with the caller.
|
|
290
469
|
*/
|
|
291
|
-
export async function
|
|
470
|
+
export async function startWaveScript(
|
|
292
471
|
adapter: WaveAdapter,
|
|
293
472
|
spec: WaveScriptSpec,
|
|
294
473
|
signal?: AbortSignal,
|
|
295
|
-
): Promise<
|
|
296
|
-
|
|
474
|
+
): Promise<WaveScriptStart> {
|
|
475
|
+
// The receipt is assembled in EVERY terminal arm — write-only telemetry: nothing below reads
|
|
476
|
+
// it back into the ok/failure decision.
|
|
477
|
+
const receiptOf = (
|
|
478
|
+
state: WaveReceiptState,
|
|
479
|
+
spawned: WaveRunHandle | null,
|
|
480
|
+
completion?: WaveCompletion,
|
|
481
|
+
): WaveScriptReceipt => ({
|
|
482
|
+
...(spawned !== null ? { runId: spawned.asyncId, asyncDir: spawned.asyncDir } : {}),
|
|
483
|
+
state,
|
|
484
|
+
children: completion?.children ?? [],
|
|
485
|
+
});
|
|
486
|
+
const startFailure = (
|
|
487
|
+
reason: WaveFailureReason,
|
|
488
|
+
detail: string,
|
|
489
|
+
receipt: WaveScriptReceipt,
|
|
490
|
+
): WaveScriptStart => ({
|
|
297
491
|
ok: false,
|
|
298
492
|
failure: { key: null, reason, detail },
|
|
493
|
+
receipt,
|
|
299
494
|
});
|
|
300
495
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
496
|
+
// Read through a closure so TS's readonly-property narrowing never staples the first
|
|
497
|
+
// check's `false` onto the post-await re-check (the signal CAN flip during an await).
|
|
498
|
+
const aborted = (): boolean => signal?.aborted === true;
|
|
499
|
+
const cancelledBeforeLaunch = (): WaveScriptStart =>
|
|
500
|
+
startFailure(
|
|
501
|
+
"cancelled",
|
|
502
|
+
`wave '${spec.flow}' was cancelled before launch`,
|
|
503
|
+
receiptOf("cancelled", null),
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (aborted()) return cancelledBeforeLaunch();
|
|
304
507
|
|
|
305
508
|
// 1. Capability check — the loud-degrade arm: the result explicitly names the wave
|
|
306
509
|
// unavailable; callers surface it, never silently fall back to model-authored scripts.
|
|
@@ -308,15 +511,24 @@ export async function runWaveScript(
|
|
|
308
511
|
try {
|
|
309
512
|
ping = await adapter.ping();
|
|
310
513
|
} catch (error) {
|
|
311
|
-
return
|
|
514
|
+
return startFailure(
|
|
515
|
+
"unavailable",
|
|
516
|
+
`subagent RPC ping failed: ${errorDetail(error)}`,
|
|
517
|
+
receiptOf("unavailable", null),
|
|
518
|
+
);
|
|
312
519
|
}
|
|
313
520
|
if (ping === null) {
|
|
314
|
-
return
|
|
521
|
+
return startFailure(
|
|
315
522
|
"unavailable",
|
|
316
523
|
"pi-subagents did not advertise the report-wave capabilities (ping failed or incomplete)",
|
|
524
|
+
receiptOf("unavailable", null),
|
|
317
525
|
);
|
|
318
526
|
}
|
|
319
527
|
|
|
528
|
+
// An abort can arrive WHILE the ping await is pending — re-check before subscribe/spawn so a
|
|
529
|
+
// cancelled wave never launches (the pre-launch check alone leaves this window open).
|
|
530
|
+
if (aborted()) return cancelledBeforeLaunch();
|
|
531
|
+
|
|
320
532
|
// 2. Subscribe BEFORE spawn: a completion can arrive before the spawn reply resolves (the
|
|
321
533
|
// completion-before-reply race) — every completion is buffered and re-checked once the
|
|
322
534
|
// handle is known.
|
|
@@ -332,108 +544,180 @@ export async function runWaveScript(
|
|
|
332
544
|
if (matchesHandle(completion) && notifyMatch !== null) notifyMatch();
|
|
333
545
|
});
|
|
334
546
|
|
|
547
|
+
// 3. Spawn: async-only, ephemeral, fresh-context — the module fixes those; the flow's spec
|
|
548
|
+
// supplies the judgment-bearing pieces (lanes, schema, model, policy).
|
|
549
|
+
const timeoutMs = spec.timeoutMs ?? waveTimeoutMs();
|
|
335
550
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
551
|
+
handle = await adapter.spawn({
|
|
552
|
+
workflowScript: spec.workflowScript,
|
|
553
|
+
async: true,
|
|
554
|
+
mission: false,
|
|
555
|
+
context: "fresh",
|
|
556
|
+
acceptance: WAVE_ACCEPTANCE,
|
|
557
|
+
outputSchema: spec.outputSchema,
|
|
558
|
+
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
559
|
+
timeoutMs,
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
unsubscribe();
|
|
563
|
+
return startFailure(
|
|
564
|
+
"spawn-failed",
|
|
565
|
+
`wave spawn failed: ${errorDetail(error)}`,
|
|
566
|
+
receiptOf("spawn-failed", null),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const spawned = handle;
|
|
570
|
+
|
|
571
|
+
const scriptFailure = (
|
|
572
|
+
reason: WaveFailureReason,
|
|
573
|
+
detail: string,
|
|
574
|
+
receipt: WaveScriptReceipt,
|
|
575
|
+
): WaveScriptResult => ({
|
|
576
|
+
ok: false,
|
|
577
|
+
failure: { key: null, reason, detail },
|
|
578
|
+
receipt,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// The back half: every arm below RETURNS a normalized `WaveScriptResult` (never throws), so
|
|
582
|
+
// `result` never rejects; the subscription is released exactly when it settles.
|
|
583
|
+
const settle = async (): Promise<WaveScriptResult> => {
|
|
339
584
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
585
|
+
// 4. Block on completion with the module-owned timeout; honor the caller's AbortSignal.
|
|
586
|
+
const outcome = await new Promise<"complete" | "timeout" | "cancelled">((resolve) => {
|
|
587
|
+
if (buffered.some(matchesHandle)) {
|
|
588
|
+
resolve("complete");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const settleOutcome = (value: "complete" | "timeout" | "cancelled"): void => {
|
|
592
|
+
clearTimeout(timer);
|
|
593
|
+
signal?.removeEventListener("abort", onAbort);
|
|
594
|
+
notifyMatch = null;
|
|
595
|
+
resolve(value);
|
|
596
|
+
};
|
|
597
|
+
const timer = setTimeout(() => settleOutcome("timeout"), timeoutMs);
|
|
598
|
+
const onAbort = (): void => settleOutcome("cancelled");
|
|
599
|
+
notifyMatch = () => settleOutcome("complete");
|
|
600
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
601
|
+
if (signal?.aborted === true) settleOutcome("cancelled");
|
|
348
602
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
603
|
+
if (outcome !== "complete") {
|
|
604
|
+
// Best-effort stop — adapters never throw here by contract, but a broken adapter's error
|
|
605
|
+
// is still swallowed into the detail rather than re-thrown.
|
|
606
|
+
let stopNote = "";
|
|
607
|
+
try {
|
|
608
|
+
await adapter.stop(spawned);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
stopNote = ` (stop failed: ${errorDetail(error)})`;
|
|
611
|
+
}
|
|
612
|
+
return outcome === "timeout"
|
|
613
|
+
? scriptFailure(
|
|
614
|
+
"timeout",
|
|
615
|
+
`wave '${spec.flow}' timed out after ${timeoutMs}ms${stopNote}`,
|
|
616
|
+
receiptOf("timed-out", spawned),
|
|
617
|
+
)
|
|
618
|
+
: scriptFailure(
|
|
619
|
+
"cancelled",
|
|
620
|
+
`wave '${spec.flow}' was cancelled${stopNote}`,
|
|
621
|
+
receiptOf("cancelled", spawned),
|
|
622
|
+
);
|
|
358
623
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const onAbort = (): void => settle("cancelled");
|
|
367
|
-
notifyMatch = () => settle("complete");
|
|
368
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
369
|
-
if (signal?.aborted === true) settle("cancelled");
|
|
370
|
-
});
|
|
371
|
-
if (outcome !== "complete") {
|
|
372
|
-
// Best-effort stop — adapters never throw here by contract, but a broken adapter's error
|
|
373
|
-
// is still swallowed into the detail rather than re-thrown.
|
|
374
|
-
let stopNote = "";
|
|
624
|
+
|
|
625
|
+
// The MATCHED completion (retained for the receipt — its normalized children are the
|
|
626
|
+
// child-lane identity/artifact trail; an identity-only completion yields empty children).
|
|
627
|
+
const matched = buffered.find(matchesHandle);
|
|
628
|
+
|
|
629
|
+
// 5. Read the durable aggregate; surface the terminal-state arms.
|
|
630
|
+
let aggregate: { state: string; error?: string; value: unknown };
|
|
375
631
|
try {
|
|
376
|
-
await adapter.
|
|
632
|
+
aggregate = await adapter.readAggregate(spawned);
|
|
377
633
|
} catch (error) {
|
|
378
|
-
|
|
634
|
+
// Aggregate-unreadable: the completion identity is retained — the receipt state derives
|
|
635
|
+
// from the OBSERVED completion (a correlation label, not a verdict; the authoritative
|
|
636
|
+
// failure reason stays in the wave failure).
|
|
637
|
+
return scriptFailure(
|
|
638
|
+
"aggregate-unreadable",
|
|
639
|
+
`wave aggregate unreadable: ${errorDetail(error)}`,
|
|
640
|
+
receiptOf(matched?.success === false ? "failed" : "complete", spawned, matched),
|
|
641
|
+
);
|
|
379
642
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
"aggregate-unreadable",
|
|
392
|
-
`wave aggregate unreadable: ${errorDetail(error)}`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
if (aggregate.state !== "complete") {
|
|
396
|
-
const detail = aggregate.error !== undefined ? `: ${aggregate.error}` : "";
|
|
397
|
-
return scriptFailure("run-failed", `wave run ended '${aggregate.state}'${detail}`);
|
|
643
|
+
if (aggregate.state !== "complete") {
|
|
644
|
+
const detail = aggregate.error !== undefined ? `: ${aggregate.error}` : "";
|
|
645
|
+
return scriptFailure(
|
|
646
|
+
"run-failed",
|
|
647
|
+
`wave run ended '${aggregate.state}'${detail}`,
|
|
648
|
+
receiptOf("failed", spawned, matched),
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return { ok: true, value: aggregate.value, receipt: receiptOf("complete", spawned, matched) };
|
|
652
|
+
} finally {
|
|
653
|
+
unsubscribe();
|
|
398
654
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
unsubscribe();
|
|
402
|
-
}
|
|
655
|
+
};
|
|
656
|
+
return { ok: true, handle: spawned, result: settle() };
|
|
403
657
|
}
|
|
404
658
|
|
|
405
659
|
/**
|
|
406
|
-
* Run
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* `renderWaveScript`).
|
|
660
|
+
* Run one module-rendered workflowScript to completion — the blocking form: `startWaveScript` +
|
|
661
|
+
* await its `result` (one operational core, behavior identical to the historical blocking
|
|
662
|
+
* runner).
|
|
410
663
|
*/
|
|
411
|
-
export async function
|
|
664
|
+
export async function runWaveScript(
|
|
412
665
|
adapter: WaveAdapter,
|
|
413
|
-
spec:
|
|
666
|
+
spec: WaveScriptSpec,
|
|
414
667
|
signal?: AbortSignal,
|
|
415
|
-
): Promise<
|
|
416
|
-
|
|
417
|
-
|
|
668
|
+
): Promise<WaveScriptResult> {
|
|
669
|
+
const start = await startWaveScript(adapter, spec, signal);
|
|
670
|
+
if (!start.ok) return { ok: false, failure: start.failure, receipt: start.receipt };
|
|
671
|
+
return await start.result;
|
|
672
|
+
}
|
|
418
673
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Enrich receipt children's `agent` from the Perk-owned lane specs by key. Children are never
|
|
676
|
+
* synthesized from lanes — an identity-only completion keeps its empty children (receipt absence
|
|
677
|
+
* degrades correlation, never behavior).
|
|
678
|
+
*/
|
|
679
|
+
function enrichReceipt(receipt: WaveScriptReceipt, lanes: WaveLane[]): WaveScriptReceipt {
|
|
680
|
+
return {
|
|
681
|
+
...receipt,
|
|
682
|
+
children: receipt.children.map((child) => {
|
|
683
|
+
if (child.agent !== undefined) return child;
|
|
684
|
+
const agent = lanes.find((lane) => lane.key === child.key)?.agent;
|
|
685
|
+
return agent === undefined ? child : { ...child, agent };
|
|
686
|
+
}),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* A launched (or launch-failed) report wave — the lane-level sibling of `WaveScriptStart`. On
|
|
692
|
+
* `ok: true` the wave is LIVE: `result` settles into the normalized `WaveResult` (lane
|
|
693
|
+
* normalization + completeness policy + receipt enrichment) and never rejects. On `ok: false`
|
|
694
|
+
* the launch failure is already normalized into a `WaveResult` (receipt included) — no promise
|
|
695
|
+
* to await, nothing left running.
|
|
696
|
+
*/
|
|
697
|
+
export type ReportWaveStart =
|
|
698
|
+
| {
|
|
699
|
+
ok: true;
|
|
700
|
+
handle: WaveRunHandle;
|
|
701
|
+
result: Promise<WaveResult>;
|
|
702
|
+
launch: WaveLaunchManifest;
|
|
703
|
+
}
|
|
704
|
+
| { ok: false; result: WaveResult; launch: WaveLaunchManifest };
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Settle one script outcome into the lane-level `WaveResult`: receipt enrichment, the
|
|
708
|
+
* workflow.value array check, per-lane-key normalization, and the completeness policy — the
|
|
709
|
+
* single back half both the blocking runner and the streaming sibling apply.
|
|
710
|
+
*/
|
|
711
|
+
function settleReportWave(run: WaveScriptResult, spec: WaveSpec): WaveResult {
|
|
712
|
+
const receipt = enrichReceipt(run.receipt, spec.lanes);
|
|
430
713
|
if (!run.ok) {
|
|
431
|
-
return { complete: false, reports: [], failures: [run.failure] };
|
|
714
|
+
return { complete: false, reports: [], failures: [run.failure], receipt };
|
|
432
715
|
}
|
|
433
716
|
if (!Array.isArray(run.value)) {
|
|
434
717
|
return waveFailure(
|
|
435
718
|
"aggregate-unreadable",
|
|
436
719
|
"wave aggregate carries no workflow.value array (the script's explicit return is missing)",
|
|
720
|
+
receipt,
|
|
437
721
|
);
|
|
438
722
|
}
|
|
439
723
|
|
|
@@ -445,5 +729,117 @@ export async function runReportWave(
|
|
|
445
729
|
spec.completeness === "strict"
|
|
446
730
|
? failures.length === 0
|
|
447
731
|
: failures.every((failure) => failure.key !== null);
|
|
448
|
-
return { complete, reports, failures };
|
|
732
|
+
return { complete, reports, failures, receipt };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Start a report wave without blocking on completion: render the all-settled lane script (the
|
|
737
|
+
* programmer-error throws — empty lanes / duplicate keys — are preserved), launch it via
|
|
738
|
+
* `startWaveScript`, and on success return the run handle plus a `result` promise that applies
|
|
739
|
+
* the shared settle (normalization + completeness + receipt enrichment) when the run finishes.
|
|
740
|
+
* A launch failure comes back as an already-settled, normalized `WaveResult`.
|
|
741
|
+
*/
|
|
742
|
+
export async function startReportWave(
|
|
743
|
+
adapter: WaveAdapter,
|
|
744
|
+
spec: WaveSpec,
|
|
745
|
+
signal?: AbortSignal,
|
|
746
|
+
): Promise<ReportWaveStart> {
|
|
747
|
+
// Validate the COMPLETE requested manifest before source preflight partitions any lane out.
|
|
748
|
+
// This preserves the programmer-error contract even for an unavailable required skill.
|
|
749
|
+
validateWaveLanes(spec.lanes);
|
|
750
|
+
const preflight = spec.requiredSkillPreflight ?? preflightPonytailSkill;
|
|
751
|
+
const checked = new Map<string, PonytailPreflight>();
|
|
752
|
+
const runnable: WaveLane[] = [];
|
|
753
|
+
const skillFailures: WaveFailure[] = [];
|
|
754
|
+
for (const lane of spec.lanes) {
|
|
755
|
+
if (lane.requiredSkill === undefined) {
|
|
756
|
+
runnable.push(lane);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
let result = checked.get(lane.requiredSkill.skillFile);
|
|
760
|
+
if (result === undefined) {
|
|
761
|
+
result = await preflight(lane.requiredSkill);
|
|
762
|
+
checked.set(lane.requiredSkill.skillFile, result);
|
|
763
|
+
}
|
|
764
|
+
if (result.ok) {
|
|
765
|
+
runnable.push(lane);
|
|
766
|
+
} else {
|
|
767
|
+
skillFailures.push({ key: lane.key, reason: "skill-unavailable", detail: result.detail });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const launch: WaveLaunchManifest = {
|
|
772
|
+
requested: spec.lanes.map((lane) => lane.key),
|
|
773
|
+
runnable: runnable.map((lane) => lane.key),
|
|
774
|
+
preflightFailures: [...skillFailures],
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const settleWithSkillFailures = (result: WaveResult): WaveResult => {
|
|
778
|
+
if (skillFailures.length === 0) return result;
|
|
779
|
+
const failures = [...result.failures, ...skillFailures];
|
|
780
|
+
const complete =
|
|
781
|
+
spec.completeness === "strict"
|
|
782
|
+
? failures.length === 0
|
|
783
|
+
: failures.every((failure) => failure.key !== null);
|
|
784
|
+
return { ...result, complete, failures };
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
if (runnable.length === 0) {
|
|
788
|
+
const receipt: WaveScriptReceipt = { state: "unavailable", children: [] };
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
result: settleWithSkillFailures({ complete: false, reports: [], failures: [], receipt }),
|
|
792
|
+
launch,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Required-skill metadata never reaches the renderer; only runnable lanes spawn.
|
|
797
|
+
const runnableSpec: WaveSpec = { ...spec, lanes: runnable };
|
|
798
|
+
const workflowScript = renderWaveScript(runnable);
|
|
799
|
+
|
|
800
|
+
const start = await startWaveScript(
|
|
801
|
+
adapter,
|
|
802
|
+
{
|
|
803
|
+
flow: spec.flow,
|
|
804
|
+
workflowScript,
|
|
805
|
+
outputSchema: spec.outputSchema,
|
|
806
|
+
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
807
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
808
|
+
},
|
|
809
|
+
signal,
|
|
810
|
+
);
|
|
811
|
+
if (!start.ok) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
result: settleWithSkillFailures(
|
|
815
|
+
settleReportWave(
|
|
816
|
+
{ ok: false, failure: start.failure, receipt: start.receipt },
|
|
817
|
+
runnableSpec,
|
|
818
|
+
),
|
|
819
|
+
),
|
|
820
|
+
launch,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
ok: true,
|
|
825
|
+
handle: start.handle,
|
|
826
|
+
result: start.result.then((run) =>
|
|
827
|
+
settleWithSkillFailures(settleReportWave(run, runnableSpec)),
|
|
828
|
+
),
|
|
829
|
+
launch,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Run a report wave to completion — the blocking form: `startReportWave` + await its `result`
|
|
835
|
+
* (one operational core). Every operational failure normalizes into `WaveResult` — the only
|
|
836
|
+
* throws are programmer errors (empty lanes / duplicate keys, via `renderWaveScript`).
|
|
837
|
+
*/
|
|
838
|
+
export async function runReportWave(
|
|
839
|
+
adapter: WaveAdapter,
|
|
840
|
+
spec: WaveSpec,
|
|
841
|
+
signal?: AbortSignal,
|
|
842
|
+
): Promise<WaveResult> {
|
|
843
|
+
const start = await startReportWave(adapter, spec, signal);
|
|
844
|
+
return start.ok ? await start.result : start.result;
|
|
449
845
|
}
|