@mgiles/perk 2.3.0 → 3.0.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 +132 -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/draftReviewWaveTools.ts +384 -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 +514 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +453 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +26 -16
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +25 -18
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +374 -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 +61 -6
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +16 -1
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +131 -36
- 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 +109 -44
- package/extension/substrate/cache.ts +75 -22
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/config.ts +36 -12
- package/extension/substrate/git.ts +10 -0
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +13 -5
- package/extension/substrate/sessionData.ts +3 -3
- package/extension/substrate/sessionPointers.ts +3 -3
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +170 -23
- package/extension/substrate/workflowState.ts +6 -0
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/surfaces.ts +98 -282
- package/extension/vendor/btw/btw.ts +57 -12
- package/extension/waves/adversarialReviewWave.ts +164 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +189 -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/prReviewDynamicWave.ts +291 -61
- package/extension/waves/prReviewWave.ts +58 -13
- package/extension/waves/reportWave.ts +406 -106
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +3 -3
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +15 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +66 -65
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +1 -2
- package/prompts/contexts/adapters/plannotator-plan.md +1 -2
- 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/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-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 +3 -3
- 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.md +2 -2
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +12 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +3652 -236
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +32 -8
- package/shared/schemas/outputs/objective-doctor.schema.json +449 -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-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,16 +2,29 @@
|
|
|
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
|
|
@@ -30,6 +43,12 @@ export interface WaveLane {
|
|
|
30
43
|
label?: string;
|
|
31
44
|
/** Trace metadata. */
|
|
32
45
|
phase?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Per-lane report schema — rendered as the item's `outputSchema`, overriding the
|
|
48
|
+
* workflow-level default (the established per-item mechanic). Omitted lanes render
|
|
49
|
+
* byte-identically to before the field existed.
|
|
50
|
+
*/
|
|
51
|
+
outputSchema?: object;
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
/**
|
|
@@ -83,6 +102,71 @@ export interface WaveResult {
|
|
|
83
102
|
complete: boolean;
|
|
84
103
|
reports: WaveReport[];
|
|
85
104
|
failures: WaveFailure[];
|
|
105
|
+
/** The launch's output-free attempt receipt — write-only telemetry, never a decision input. */
|
|
106
|
+
receipt: WaveScriptReceipt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -------------------------------------------------------------------- the attempt receipts
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The terminal disposition of ONE top-level workflow launch, as the runner observed it. Every
|
|
113
|
+
* launch reaches exactly one of these arms (`"running"` is unreachable — the runner always
|
|
114
|
+
* settles); `"unavailable"` preserves even a pre-spawn capability failure as an attempt.
|
|
115
|
+
*/
|
|
116
|
+
export type WaveReceiptState =
|
|
117
|
+
| "unavailable" // ping failed/incomplete — nothing launched
|
|
118
|
+
| "spawn-failed" // spawn rejected/threw — no run handle
|
|
119
|
+
| "complete" // completion observed, durable state "complete"
|
|
120
|
+
| "failed" // completion observed, durable/observed failure
|
|
121
|
+
| "timed-out" // module timeout expired (handle preserved)
|
|
122
|
+
| "cancelled"; // AbortSignal honored (handle preserved when spawned)
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One child lane's identity/artifact trail from the completion payload — OUTPUT-FREE by
|
|
126
|
+
* invariant: reports, summaries, and structured output never enter a receipt (they stay in the
|
|
127
|
+
* durable `status.json.workflow.value`, the sole report authority).
|
|
128
|
+
*/
|
|
129
|
+
export interface WaveChildReceipt {
|
|
130
|
+
/** The Perk lane key (mapped FROM the upstream row's overloaded `agent` field). */
|
|
131
|
+
key: string;
|
|
132
|
+
/** The child agent name, enriched from the Perk-owned lane spec where known. */
|
|
133
|
+
agent?: string;
|
|
134
|
+
/** The child's opaque run id — never parsed or synthesized from paths. */
|
|
135
|
+
runId?: string;
|
|
136
|
+
success?: boolean;
|
|
137
|
+
outputState?: "present" | "absent" | "unknown";
|
|
138
|
+
/** String path fields only; output-free. */
|
|
139
|
+
artifactPaths?: Record<string, string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One script launch's receipt: the run handle (where known) + the observed children. */
|
|
143
|
+
export interface WaveScriptReceipt {
|
|
144
|
+
/** The top-level async run id (the spawn handle's asyncId). */
|
|
145
|
+
runId?: string;
|
|
146
|
+
asyncDir?: string;
|
|
147
|
+
state: WaveReceiptState;
|
|
148
|
+
children: WaveChildReceipt[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A flow-attributed attempt: one receipt per top-level workflow launch, ordered by `attempt`
|
|
153
|
+
* (one-based, assigned by the flow entrypoint that owns retry policy). `requestedKeys` is the
|
|
154
|
+
* lane manifest BEFORE launch — never reconstructed from the observed children.
|
|
155
|
+
*/
|
|
156
|
+
export interface WaveAttemptReceipt extends WaveScriptReceipt {
|
|
157
|
+
flow: string;
|
|
158
|
+
attempt: number;
|
|
159
|
+
requestedKeys: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Assemble one flow attempt from a script receipt (the uniform builder the flows share). */
|
|
163
|
+
export function toAttemptReceipt(
|
|
164
|
+
flow: string,
|
|
165
|
+
attempt: number,
|
|
166
|
+
requestedKeys: string[],
|
|
167
|
+
receipt: WaveScriptReceipt,
|
|
168
|
+
): WaveAttemptReceipt {
|
|
169
|
+
return { flow, attempt, requestedKeys: [...requestedKeys], ...receipt };
|
|
86
170
|
}
|
|
87
171
|
|
|
88
172
|
// ------------------------------------------------------------------------- the adapter seam
|
|
@@ -104,12 +188,36 @@ export interface WaveRunHandle {
|
|
|
104
188
|
asyncDir: string;
|
|
105
189
|
}
|
|
106
190
|
|
|
107
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* An async-complete notification; at least one identifier is present on real payloads. The
|
|
193
|
+
* observability fields are optional — an identity-only completion stays valid (receipt absence
|
|
194
|
+
* degrades correlation, never behavior). The adapter normalizes them output-free and leaves
|
|
195
|
+
* each child's `agent` unset (enrichment happens against Perk-owned lane specs).
|
|
196
|
+
*/
|
|
108
197
|
export interface WaveCompletion {
|
|
109
198
|
asyncId?: string;
|
|
110
199
|
asyncDir?: string;
|
|
200
|
+
/** The run's raw terminal state string, when the payload carries one. */
|
|
201
|
+
state?: string;
|
|
202
|
+
success?: boolean;
|
|
203
|
+
children?: WaveChildReceipt[];
|
|
111
204
|
}
|
|
112
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The explicit acceptance-disable every wave spawn carries. Without it, pi-subagents
|
|
208
|
+
* auto-infers a generic acceptance contract for reviewer/analyst-named or read-only children
|
|
209
|
+
* and injects a fenced `acceptance-report` completion instruction into each lane — a COMPETING
|
|
210
|
+
* completion contract observed steering children into invalid `structured_output` attempts.
|
|
211
|
+
* `{level: "none"}` is the sanctioned disable shape (pi-subagents `explicitAcceptanceCanDisable`);
|
|
212
|
+
* `formatAcceptancePrompt` emits nothing at level none, so no contract block reaches a lane.
|
|
213
|
+
* Deliberately module-wide with no opt-out: every report-wave child's sole completion contract
|
|
214
|
+
* is the engine-validated `structured_output` report.
|
|
215
|
+
*/
|
|
216
|
+
export const WAVE_ACCEPTANCE = {
|
|
217
|
+
level: "none",
|
|
218
|
+
reason: "perk report-wave lanes complete via the engine-validated structured_output report",
|
|
219
|
+
} as const;
|
|
220
|
+
|
|
113
221
|
/** The full spawn params the runner fixes: async-only, ephemeral, fresh-context by definition. */
|
|
114
222
|
export interface WaveSpawnParams {
|
|
115
223
|
workflowScript: string;
|
|
@@ -118,6 +226,9 @@ export interface WaveSpawnParams {
|
|
|
118
226
|
mission: false;
|
|
119
227
|
/** A report wave is by definition fresh-context. */
|
|
120
228
|
context: "fresh";
|
|
229
|
+
/** The fixed acceptance disable (`WAVE_ACCEPTANCE`) — pi-subagents' workflow-defaults spread
|
|
230
|
+
* delivers it onto every lane child, suppressing the auto-inferred acceptance contract. */
|
|
231
|
+
acceptance: { level: "none"; reason: string };
|
|
121
232
|
outputSchema: object;
|
|
122
233
|
model?: string;
|
|
123
234
|
/** Orphan insurance: the run enforces the same deadline even if the parent session dies. */
|
|
@@ -139,13 +250,22 @@ export interface WaveAdapter {
|
|
|
139
250
|
|
|
140
251
|
// ---------------------------------------------------------------------------- the renderer
|
|
141
252
|
|
|
253
|
+
/**
|
|
254
|
+
* pi-subagents' scripted-workflow run-key contract for `runs.all` item keys: start
|
|
255
|
+
* alphanumeric, then letters/digits/`.`/`_`/`-`, ≤128 chars total. Mirrored here because the
|
|
256
|
+
* upstream pattern is enforced only inside the live workflow worker — an invalid key fails the
|
|
257
|
+
* WHOLE wave at dispatch (`run-failed`), a path no offline adapter exercises — so the renderer
|
|
258
|
+
* rejects it up front as a programmer error.
|
|
259
|
+
*/
|
|
260
|
+
export const RUN_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
261
|
+
|
|
142
262
|
/**
|
|
143
263
|
* Render the wave `workflowScript`: an explicit-return, all-settled `runs.all` over the lane
|
|
144
264
|
* items, projected to the compact typed aggregate only (lane key, outcome, error, and the
|
|
145
265
|
* schema-validated report — children's prose never enters the aggregate beyond `error`/`output`
|
|
146
266
|
* on failure). Lane items are embedded via `JSON.stringify`, so hostile task text (quotes,
|
|
147
267
|
* newlines, backticks, `${}`) cannot escape the array literal. Throws on programmer error:
|
|
148
|
-
* empty lanes or
|
|
268
|
+
* empty lanes, duplicate lane keys, or a lane key outside the run-key contract.
|
|
149
269
|
*/
|
|
150
270
|
export function renderWaveScript(lanes: WaveLane[]): string {
|
|
151
271
|
if (lanes.length === 0) {
|
|
@@ -156,6 +276,11 @@ export function renderWaveScript(lanes: WaveLane[]): string {
|
|
|
156
276
|
if (seen.has(lane.key)) {
|
|
157
277
|
throw new Error(`renderWaveScript: duplicate lane key '${lane.key}'`);
|
|
158
278
|
}
|
|
279
|
+
if (!RUN_KEY_PATTERN.test(lane.key)) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`renderWaveScript: lane key '${lane.key}' violates the pi-subagents run-key contract`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
159
284
|
seen.add(lane.key);
|
|
160
285
|
}
|
|
161
286
|
const items = lanes.map((lane) => ({
|
|
@@ -164,6 +289,7 @@ export function renderWaveScript(lanes: WaveLane[]): string {
|
|
|
164
289
|
task: lane.task,
|
|
165
290
|
label: lane.label ?? lane.key,
|
|
166
291
|
...(lane.phase !== undefined ? { phase: lane.phase } : {}),
|
|
292
|
+
...(lane.outputSchema !== undefined ? { outputSchema: lane.outputSchema } : {}),
|
|
167
293
|
}));
|
|
168
294
|
return (
|
|
169
295
|
`const reports = await runs.all(${JSON.stringify(items, null, 2)});\n` +
|
|
@@ -186,8 +312,12 @@ function waveTimeoutMs(): number {
|
|
|
186
312
|
return Number.isFinite(raw) && raw > 0 ? raw : WAVE_TIMEOUT_MS;
|
|
187
313
|
}
|
|
188
314
|
|
|
189
|
-
function waveFailure(
|
|
190
|
-
|
|
315
|
+
function waveFailure(
|
|
316
|
+
reason: WaveFailureReason,
|
|
317
|
+
detail: string,
|
|
318
|
+
receipt: WaveScriptReceipt,
|
|
319
|
+
): WaveResult {
|
|
320
|
+
return { complete: false, reports: [], failures: [{ key: null, reason, detail }], receipt };
|
|
191
321
|
}
|
|
192
322
|
|
|
193
323
|
/** The judgment-bearing pieces a script run needs (the lane-free slice of `WaveSpec`). */
|
|
@@ -205,7 +335,23 @@ export interface WaveScriptSpec {
|
|
|
205
335
|
}
|
|
206
336
|
|
|
207
337
|
/** A script run's outcome: the raw `workflow.value` on success, one wave-level failure otherwise. */
|
|
208
|
-
export type WaveScriptResult =
|
|
338
|
+
export type WaveScriptResult =
|
|
339
|
+
| { ok: true; value: unknown; receipt: WaveScriptReceipt }
|
|
340
|
+
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A launched (or launch-refused) script run. On `ok: true` the run is LIVE: `handle` is the
|
|
344
|
+
* detached async run, and `result` settles when the back half finishes (completion wait under
|
|
345
|
+
* the module-owned timeout, AbortSignal honor, best-effort stop on timeout/cancel, the durable
|
|
346
|
+
* aggregate read, receipt assembly, unsubscribe-on-settle). `result` NEVER rejects — every arm
|
|
347
|
+
* normalizes into `WaveScriptResult`, so an uncollected wave can never become an unhandled
|
|
348
|
+
* rejection. Pre-spawn failures (aborted-before-launch, ping fail/null, spawn throw) take the
|
|
349
|
+
* `ok: false` arm with the same failure/receipt values the blocking runner reports, and the
|
|
350
|
+
* completion subscription is released immediately.
|
|
351
|
+
*/
|
|
352
|
+
export type WaveScriptStart =
|
|
353
|
+
| { ok: true; handle: WaveRunHandle; result: Promise<WaveScriptResult> }
|
|
354
|
+
| { ok: false; failure: WaveFailure; receipt: WaveScriptReceipt };
|
|
209
355
|
|
|
210
356
|
function errorDetail(error: unknown): string {
|
|
211
357
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -281,26 +427,51 @@ export function normalizeLanes(
|
|
|
281
427
|
}
|
|
282
428
|
|
|
283
429
|
/**
|
|
284
|
-
*
|
|
285
|
-
* subscribe-before-spawn (the completion-before-reply buffer) → async spawn
|
|
286
|
-
* async-complete event
|
|
287
|
-
*
|
|
288
|
-
* `
|
|
289
|
-
*
|
|
430
|
+
* Start one module-rendered workflowScript through the adapter — the non-blocking front half:
|
|
431
|
+
* capability ping → subscribe-before-spawn (the completion-before-reply buffer) → async spawn.
|
|
432
|
+
* On success the back half (block on the async-complete event under the module-owned timeout,
|
|
433
|
+
* abortable → best-effort stop on timeout/cancel → read the durable aggregate → the
|
|
434
|
+
* `state !== "complete"` / unreadable arms) runs behind the returned `result` promise, which
|
|
435
|
+
* never rejects. The shared operational core under every runner; per-flow value normalization
|
|
436
|
+
* stays with the caller.
|
|
290
437
|
*/
|
|
291
|
-
export async function
|
|
438
|
+
export async function startWaveScript(
|
|
292
439
|
adapter: WaveAdapter,
|
|
293
440
|
spec: WaveScriptSpec,
|
|
294
441
|
signal?: AbortSignal,
|
|
295
|
-
): Promise<
|
|
296
|
-
|
|
442
|
+
): Promise<WaveScriptStart> {
|
|
443
|
+
// The receipt is assembled in EVERY terminal arm — write-only telemetry: nothing below reads
|
|
444
|
+
// it back into the ok/failure decision.
|
|
445
|
+
const receiptOf = (
|
|
446
|
+
state: WaveReceiptState,
|
|
447
|
+
spawned: WaveRunHandle | null,
|
|
448
|
+
completion?: WaveCompletion,
|
|
449
|
+
): WaveScriptReceipt => ({
|
|
450
|
+
...(spawned !== null ? { runId: spawned.asyncId, asyncDir: spawned.asyncDir } : {}),
|
|
451
|
+
state,
|
|
452
|
+
children: completion?.children ?? [],
|
|
453
|
+
});
|
|
454
|
+
const startFailure = (
|
|
455
|
+
reason: WaveFailureReason,
|
|
456
|
+
detail: string,
|
|
457
|
+
receipt: WaveScriptReceipt,
|
|
458
|
+
): WaveScriptStart => ({
|
|
297
459
|
ok: false,
|
|
298
460
|
failure: { key: null, reason, detail },
|
|
461
|
+
receipt,
|
|
299
462
|
});
|
|
300
463
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
464
|
+
// Read through a closure so TS's readonly-property narrowing never staples the first
|
|
465
|
+
// check's `false` onto the post-await re-check (the signal CAN flip during an await).
|
|
466
|
+
const aborted = (): boolean => signal?.aborted === true;
|
|
467
|
+
const cancelledBeforeLaunch = (): WaveScriptStart =>
|
|
468
|
+
startFailure(
|
|
469
|
+
"cancelled",
|
|
470
|
+
`wave '${spec.flow}' was cancelled before launch`,
|
|
471
|
+
receiptOf("cancelled", null),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (aborted()) return cancelledBeforeLaunch();
|
|
304
475
|
|
|
305
476
|
// 1. Capability check — the loud-degrade arm: the result explicitly names the wave
|
|
306
477
|
// unavailable; callers surface it, never silently fall back to model-authored scripts.
|
|
@@ -308,15 +479,24 @@ export async function runWaveScript(
|
|
|
308
479
|
try {
|
|
309
480
|
ping = await adapter.ping();
|
|
310
481
|
} catch (error) {
|
|
311
|
-
return
|
|
482
|
+
return startFailure(
|
|
483
|
+
"unavailable",
|
|
484
|
+
`subagent RPC ping failed: ${errorDetail(error)}`,
|
|
485
|
+
receiptOf("unavailable", null),
|
|
486
|
+
);
|
|
312
487
|
}
|
|
313
488
|
if (ping === null) {
|
|
314
|
-
return
|
|
489
|
+
return startFailure(
|
|
315
490
|
"unavailable",
|
|
316
491
|
"pi-subagents did not advertise the report-wave capabilities (ping failed or incomplete)",
|
|
492
|
+
receiptOf("unavailable", null),
|
|
317
493
|
);
|
|
318
494
|
}
|
|
319
495
|
|
|
496
|
+
// An abort can arrive WHILE the ping await is pending — re-check before subscribe/spawn so a
|
|
497
|
+
// cancelled wave never launches (the pre-launch check alone leaves this window open).
|
|
498
|
+
if (aborted()) return cancelledBeforeLaunch();
|
|
499
|
+
|
|
320
500
|
// 2. Subscribe BEFORE spawn: a completion can arrive before the spawn reply resolves (the
|
|
321
501
|
// completion-before-reply race) — every completion is buffered and re-checked once the
|
|
322
502
|
// handle is known.
|
|
@@ -332,108 +512,175 @@ export async function runWaveScript(
|
|
|
332
512
|
if (matchesHandle(completion) && notifyMatch !== null) notifyMatch();
|
|
333
513
|
});
|
|
334
514
|
|
|
515
|
+
// 3. Spawn: async-only, ephemeral, fresh-context — the module fixes those; the flow's spec
|
|
516
|
+
// supplies the judgment-bearing pieces (lanes, schema, model, policy).
|
|
517
|
+
const timeoutMs = spec.timeoutMs ?? waveTimeoutMs();
|
|
335
518
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
519
|
+
handle = await adapter.spawn({
|
|
520
|
+
workflowScript: spec.workflowScript,
|
|
521
|
+
async: true,
|
|
522
|
+
mission: false,
|
|
523
|
+
context: "fresh",
|
|
524
|
+
acceptance: WAVE_ACCEPTANCE,
|
|
525
|
+
outputSchema: spec.outputSchema,
|
|
526
|
+
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
527
|
+
timeoutMs,
|
|
528
|
+
});
|
|
529
|
+
} catch (error) {
|
|
530
|
+
unsubscribe();
|
|
531
|
+
return startFailure(
|
|
532
|
+
"spawn-failed",
|
|
533
|
+
`wave spawn failed: ${errorDetail(error)}`,
|
|
534
|
+
receiptOf("spawn-failed", null),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const spawned = handle;
|
|
538
|
+
|
|
539
|
+
const scriptFailure = (
|
|
540
|
+
reason: WaveFailureReason,
|
|
541
|
+
detail: string,
|
|
542
|
+
receipt: WaveScriptReceipt,
|
|
543
|
+
): WaveScriptResult => ({
|
|
544
|
+
ok: false,
|
|
545
|
+
failure: { key: null, reason, detail },
|
|
546
|
+
receipt,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// The back half: every arm below RETURNS a normalized `WaveScriptResult` (never throws), so
|
|
550
|
+
// `result` never rejects; the subscription is released exactly when it settles.
|
|
551
|
+
const settle = async (): Promise<WaveScriptResult> => {
|
|
339
552
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
553
|
+
// 4. Block on completion with the module-owned timeout; honor the caller's AbortSignal.
|
|
554
|
+
const outcome = await new Promise<"complete" | "timeout" | "cancelled">((resolve) => {
|
|
555
|
+
if (buffered.some(matchesHandle)) {
|
|
556
|
+
resolve("complete");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const settleOutcome = (value: "complete" | "timeout" | "cancelled"): void => {
|
|
560
|
+
clearTimeout(timer);
|
|
561
|
+
signal?.removeEventListener("abort", onAbort);
|
|
562
|
+
notifyMatch = null;
|
|
563
|
+
resolve(value);
|
|
564
|
+
};
|
|
565
|
+
const timer = setTimeout(() => settleOutcome("timeout"), timeoutMs);
|
|
566
|
+
const onAbort = (): void => settleOutcome("cancelled");
|
|
567
|
+
notifyMatch = () => settleOutcome("complete");
|
|
568
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
569
|
+
if (signal?.aborted === true) settleOutcome("cancelled");
|
|
348
570
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
571
|
+
if (outcome !== "complete") {
|
|
572
|
+
// Best-effort stop — adapters never throw here by contract, but a broken adapter's error
|
|
573
|
+
// is still swallowed into the detail rather than re-thrown.
|
|
574
|
+
let stopNote = "";
|
|
575
|
+
try {
|
|
576
|
+
await adapter.stop(spawned);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
stopNote = ` (stop failed: ${errorDetail(error)})`;
|
|
579
|
+
}
|
|
580
|
+
return outcome === "timeout"
|
|
581
|
+
? scriptFailure(
|
|
582
|
+
"timeout",
|
|
583
|
+
`wave '${spec.flow}' timed out after ${timeoutMs}ms${stopNote}`,
|
|
584
|
+
receiptOf("timed-out", spawned),
|
|
585
|
+
)
|
|
586
|
+
: scriptFailure(
|
|
587
|
+
"cancelled",
|
|
588
|
+
`wave '${spec.flow}' was cancelled${stopNote}`,
|
|
589
|
+
receiptOf("cancelled", spawned),
|
|
590
|
+
);
|
|
358
591
|
}
|
|
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 = "";
|
|
592
|
+
|
|
593
|
+
// The MATCHED completion (retained for the receipt — its normalized children are the
|
|
594
|
+
// child-lane identity/artifact trail; an identity-only completion yields empty children).
|
|
595
|
+
const matched = buffered.find(matchesHandle);
|
|
596
|
+
|
|
597
|
+
// 5. Read the durable aggregate; surface the terminal-state arms.
|
|
598
|
+
let aggregate: { state: string; error?: string; value: unknown };
|
|
375
599
|
try {
|
|
376
|
-
await adapter.
|
|
600
|
+
aggregate = await adapter.readAggregate(spawned);
|
|
377
601
|
} catch (error) {
|
|
378
|
-
|
|
602
|
+
// Aggregate-unreadable: the completion identity is retained — the receipt state derives
|
|
603
|
+
// from the OBSERVED completion (a correlation label, not a verdict; the authoritative
|
|
604
|
+
// failure reason stays in the wave failure).
|
|
605
|
+
return scriptFailure(
|
|
606
|
+
"aggregate-unreadable",
|
|
607
|
+
`wave aggregate unreadable: ${errorDetail(error)}`,
|
|
608
|
+
receiptOf(matched?.success === false ? "failed" : "complete", spawned, matched),
|
|
609
|
+
);
|
|
379
610
|
}
|
|
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}`);
|
|
611
|
+
if (aggregate.state !== "complete") {
|
|
612
|
+
const detail = aggregate.error !== undefined ? `: ${aggregate.error}` : "";
|
|
613
|
+
return scriptFailure(
|
|
614
|
+
"run-failed",
|
|
615
|
+
`wave run ended '${aggregate.state}'${detail}`,
|
|
616
|
+
receiptOf("failed", spawned, matched),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return { ok: true, value: aggregate.value, receipt: receiptOf("complete", spawned, matched) };
|
|
620
|
+
} finally {
|
|
621
|
+
unsubscribe();
|
|
398
622
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
unsubscribe();
|
|
402
|
-
}
|
|
623
|
+
};
|
|
624
|
+
return { ok: true, handle: spawned, result: settle() };
|
|
403
625
|
}
|
|
404
626
|
|
|
405
627
|
/**
|
|
406
|
-
* Run
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* `renderWaveScript`).
|
|
628
|
+
* Run one module-rendered workflowScript to completion — the blocking form: `startWaveScript` +
|
|
629
|
+
* await its `result` (one operational core, behavior identical to the historical blocking
|
|
630
|
+
* runner).
|
|
410
631
|
*/
|
|
411
|
-
export async function
|
|
632
|
+
export async function runWaveScript(
|
|
412
633
|
adapter: WaveAdapter,
|
|
413
|
-
spec:
|
|
634
|
+
spec: WaveScriptSpec,
|
|
414
635
|
signal?: AbortSignal,
|
|
415
|
-
): Promise<
|
|
416
|
-
|
|
417
|
-
|
|
636
|
+
): Promise<WaveScriptResult> {
|
|
637
|
+
const start = await startWaveScript(adapter, spec, signal);
|
|
638
|
+
if (!start.ok) return { ok: false, failure: start.failure, receipt: start.receipt };
|
|
639
|
+
return await start.result;
|
|
640
|
+
}
|
|
418
641
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Enrich receipt children's `agent` from the Perk-owned lane specs by key. Children are never
|
|
644
|
+
* synthesized from lanes — an identity-only completion keeps its empty children (receipt absence
|
|
645
|
+
* degrades correlation, never behavior).
|
|
646
|
+
*/
|
|
647
|
+
function enrichReceipt(receipt: WaveScriptReceipt, lanes: WaveLane[]): WaveScriptReceipt {
|
|
648
|
+
return {
|
|
649
|
+
...receipt,
|
|
650
|
+
children: receipt.children.map((child) => {
|
|
651
|
+
if (child.agent !== undefined) return child;
|
|
652
|
+
const agent = lanes.find((lane) => lane.key === child.key)?.agent;
|
|
653
|
+
return agent === undefined ? child : { ...child, agent };
|
|
654
|
+
}),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* A launched (or launch-failed) report wave — the lane-level sibling of `WaveScriptStart`. On
|
|
660
|
+
* `ok: true` the wave is LIVE: `result` settles into the normalized `WaveResult` (lane
|
|
661
|
+
* normalization + completeness policy + receipt enrichment) and never rejects. On `ok: false`
|
|
662
|
+
* the launch failure is already normalized into a `WaveResult` (receipt included) — no promise
|
|
663
|
+
* to await, nothing left running.
|
|
664
|
+
*/
|
|
665
|
+
export type ReportWaveStart =
|
|
666
|
+
| { ok: true; handle: WaveRunHandle; result: Promise<WaveResult> }
|
|
667
|
+
| { ok: false; result: WaveResult };
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Settle one script outcome into the lane-level `WaveResult`: receipt enrichment, the
|
|
671
|
+
* workflow.value array check, per-lane-key normalization, and the completeness policy — the
|
|
672
|
+
* single back half both the blocking runner and the streaming sibling apply.
|
|
673
|
+
*/
|
|
674
|
+
function settleReportWave(run: WaveScriptResult, spec: WaveSpec): WaveResult {
|
|
675
|
+
const receipt = enrichReceipt(run.receipt, spec.lanes);
|
|
430
676
|
if (!run.ok) {
|
|
431
|
-
return { complete: false, reports: [], failures: [run.failure] };
|
|
677
|
+
return { complete: false, reports: [], failures: [run.failure], receipt };
|
|
432
678
|
}
|
|
433
679
|
if (!Array.isArray(run.value)) {
|
|
434
680
|
return waveFailure(
|
|
435
681
|
"aggregate-unreadable",
|
|
436
682
|
"wave aggregate carries no workflow.value array (the script's explicit return is missing)",
|
|
683
|
+
receipt,
|
|
437
684
|
);
|
|
438
685
|
}
|
|
439
686
|
|
|
@@ -445,5 +692,58 @@ export async function runReportWave(
|
|
|
445
692
|
spec.completeness === "strict"
|
|
446
693
|
? failures.length === 0
|
|
447
694
|
: failures.every((failure) => failure.key !== null);
|
|
448
|
-
return { complete, reports, failures };
|
|
695
|
+
return { complete, reports, failures, receipt };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Start a report wave without blocking on completion: render the all-settled lane script (the
|
|
700
|
+
* programmer-error throws — empty lanes / duplicate keys — are preserved), launch it via
|
|
701
|
+
* `startWaveScript`, and on success return the run handle plus a `result` promise that applies
|
|
702
|
+
* the shared settle (normalization + completeness + receipt enrichment) when the run finishes.
|
|
703
|
+
* A launch failure comes back as an already-settled, normalized `WaveResult`.
|
|
704
|
+
*/
|
|
705
|
+
export async function startReportWave(
|
|
706
|
+
adapter: WaveAdapter,
|
|
707
|
+
spec: WaveSpec,
|
|
708
|
+
signal?: AbortSignal,
|
|
709
|
+
): Promise<ReportWaveStart> {
|
|
710
|
+
// Programmer-error validation first (throws): the script render is spec-only.
|
|
711
|
+
const workflowScript = renderWaveScript(spec.lanes);
|
|
712
|
+
|
|
713
|
+
const start = await startWaveScript(
|
|
714
|
+
adapter,
|
|
715
|
+
{
|
|
716
|
+
flow: spec.flow,
|
|
717
|
+
workflowScript,
|
|
718
|
+
outputSchema: spec.outputSchema,
|
|
719
|
+
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
720
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
721
|
+
},
|
|
722
|
+
signal,
|
|
723
|
+
);
|
|
724
|
+
if (!start.ok) {
|
|
725
|
+
return {
|
|
726
|
+
ok: false,
|
|
727
|
+
result: settleReportWave({ ok: false, failure: start.failure, receipt: start.receipt }, spec),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
ok: true,
|
|
732
|
+
handle: start.handle,
|
|
733
|
+
result: start.result.then((run) => settleReportWave(run, spec)),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Run a report wave to completion — the blocking form: `startReportWave` + await its `result`
|
|
739
|
+
* (one operational core). Every operational failure normalizes into `WaveResult` — the only
|
|
740
|
+
* throws are programmer errors (empty lanes / duplicate keys, via `renderWaveScript`).
|
|
741
|
+
*/
|
|
742
|
+
export async function runReportWave(
|
|
743
|
+
adapter: WaveAdapter,
|
|
744
|
+
spec: WaveSpec,
|
|
745
|
+
signal?: AbortSignal,
|
|
746
|
+
): Promise<WaveResult> {
|
|
747
|
+
const start = await startReportWave(adapter, spec, signal);
|
|
748
|
+
return start.ok ? await start.result : start.result;
|
|
449
749
|
}
|