@mgiles/perk 3.2.0 → 3.3.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.
Files changed (202) hide show
  1. package/README.md +5 -0
  2. package/extension/authoring/gist/draft.ts +198 -0
  3. package/extension/authoring/gist/prose.ts +46 -0
  4. package/extension/authoring/gist/review.ts +133 -0
  5. package/extension/authoring/gist/save.ts +118 -0
  6. package/extension/authoring/objective/draft.ts +345 -0
  7. package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
  8. package/extension/authoring/objective/planning.ts +124 -0
  9. package/extension/authoring/objective/prose.ts +103 -0
  10. package/extension/authoring/objective/review.ts +128 -0
  11. package/extension/authoring/objective/save.ts +224 -0
  12. package/extension/authoring/plan/draft.ts +84 -0
  13. package/extension/authoring/plan/prose.ts +41 -0
  14. package/extension/authoring/plan/review.ts +269 -0
  15. package/extension/authoring/plan/save.ts +256 -0
  16. package/extension/authoring/plan/source.ts +82 -0
  17. package/extension/authoring/refinement/context.ts +468 -0
  18. package/extension/authoring/refinement/draft.ts +261 -0
  19. package/extension/authoring/refinement/prose.ts +79 -0
  20. package/extension/authoring/refinement/review.ts +111 -0
  21. package/extension/authoring/refinement/save.ts +119 -0
  22. package/extension/authoring/review/approvalGate.ts +34 -0
  23. package/extension/authoring/review/draftContext.ts +68 -0
  24. package/extension/codeReview/automated.ts +352 -0
  25. package/extension/codeReview/submission.ts +229 -0
  26. package/extension/delivery/address.ts +295 -0
  27. package/extension/delivery/ci.ts +355 -0
  28. package/extension/delivery/commitCompact.ts +93 -0
  29. package/extension/delivery/conflictResolution.ts +247 -0
  30. package/extension/delivery/ready.ts +193 -0
  31. package/extension/delivery/stackConflict.ts +361 -0
  32. package/extension/delivery/stackObjective.ts +16 -0
  33. package/extension/delivery/stackReconcile.ts +165 -0
  34. package/extension/delivery/submit.ts +171 -0
  35. package/extension/index.ts +365 -380
  36. package/extension/learning/analystWave.ts +324 -0
  37. package/extension/learning/audit.ts +667 -0
  38. package/extension/learning/capture.ts +92 -0
  39. package/extension/learning/containment.ts +104 -0
  40. package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
  41. package/extension/learning/dreamAnalysis.ts +435 -0
  42. package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
  43. package/extension/{waves → learning}/dreamReport.ts +35 -31
  44. package/extension/learning/harvest.ts +491 -0
  45. package/extension/learning/prose.ts +66 -0
  46. package/extension/learning/routing.ts +79 -0
  47. package/extension/pi/v1/bashScanTimeout.ts +64 -0
  48. package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
  52. package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
  53. package/extension/pi/v1/codeReview/submit.ts +354 -0
  54. package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
  55. package/extension/pi/v1/contextEvidence.ts +80 -0
  56. package/extension/pi/v1/contextInjection.ts +207 -0
  57. package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
  58. package/extension/pi/v1/delivery/ci.ts +570 -0
  59. package/extension/pi/v1/delivery/commitCompact.ts +201 -0
  60. package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
  61. package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
  62. package/extension/pi/v1/delivery/ready.ts +322 -0
  63. package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
  64. package/extension/pi/v1/delivery/stackDrive.ts +120 -0
  65. package/extension/pi/v1/delivery/stackLand.ts +223 -0
  66. package/extension/pi/v1/delivery/stackRecover.ts +265 -0
  67. package/extension/pi/v1/delivery/stackStatus.ts +237 -0
  68. package/extension/pi/v1/delivery/stackSync.ts +658 -0
  69. package/extension/pi/v1/delivery/submit.ts +389 -0
  70. package/extension/pi/v1/delivery/submitConflict.ts +186 -0
  71. package/extension/pi/v1/draftReview.ts +431 -0
  72. package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
  73. package/extension/pi/v1/gist.ts +794 -0
  74. package/extension/pi/v1/learning/audit.ts +186 -0
  75. package/extension/pi/v1/learning/dream.ts +207 -0
  76. package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
  77. package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
  78. package/extension/pi/v1/learning/learn.ts +585 -0
  79. package/extension/pi/v1/lifecycleGates.ts +127 -0
  80. package/extension/{factories → pi/v1}/objective.ts +53 -33
  81. package/extension/pi/v1/objectiveAuthoring.ts +672 -0
  82. package/extension/pi/v1/objectiveDreamGate.ts +160 -0
  83. package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
  84. package/extension/pi/v1/objectiveRefinement.ts +1320 -0
  85. package/extension/pi/v1/objectiveReview.ts +451 -0
  86. package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
  87. package/extension/pi/v1/plan.ts +812 -0
  88. package/extension/pi/v1/planReview.ts +820 -0
  89. package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
  90. package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
  91. package/extension/pi/v1/providers/plannotator.ts +487 -0
  92. package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
  93. package/extension/pi/v1/providers/selection.ts +43 -0
  94. package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
  95. package/extension/pi/v1/review.ts +538 -0
  96. package/extension/pi/v1/reviewOutcome.ts +9 -0
  97. package/extension/pi/v1/scoutWave.ts +318 -0
  98. package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
  99. package/extension/session/branchWorkflowSession.ts +60 -0
  100. package/extension/session/lifecycle.ts +644 -0
  101. package/extension/session/lifecycleGates.ts +64 -0
  102. package/extension/session/saveDestination.ts +87 -0
  103. package/extension/session/workflowSession.ts +971 -0
  104. package/extension/substrate/agentScratch.ts +27 -54
  105. package/extension/substrate/bashScanTimeout.ts +181 -0
  106. package/extension/substrate/bindingDelivery.ts +38 -30
  107. package/extension/substrate/bindings.ts +4 -5
  108. package/extension/substrate/cache.ts +64 -12
  109. package/extension/substrate/childRestrictions.ts +39 -0
  110. package/extension/substrate/coldDoor.ts +17 -1
  111. package/extension/substrate/config.ts +157 -21
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/prompts.ts +22 -0
  115. package/extension/substrate/registry.ts +2 -0
  116. package/extension/substrate/resolverLease.ts +5 -4
  117. package/extension/substrate/sessionData.ts +85 -152
  118. package/extension/substrate/toolGating.ts +263 -84
  119. package/extension/substrate/unifiedDiff.ts +1 -1
  120. package/extension/substrate/workflowState.ts +178 -163
  121. package/extension/substrate/worktreeResolverLock.ts +261 -0
  122. package/extension/surfaces/surfaces.ts +79 -27
  123. package/extension/waves/adversarialReviewWave.ts +87 -46
  124. package/extension/waves/blockedReports.ts +59 -0
  125. package/extension/waves/draftReviewWave.ts +42 -42
  126. package/extension/waves/laneIdentity.ts +77 -0
  127. package/extension/waves/objectiveExplorerWave.ts +24 -24
  128. package/extension/waves/prReviewWave.ts +89 -77
  129. package/extension/waves/reportWave.ts +438 -578
  130. package/extension/waves/reviewClassifierWave.ts +22 -22
  131. package/extension/waves/rpcAdapter.ts +100 -15
  132. package/extension/waves/scoutWave.ts +192 -0
  133. package/extension/waves/transport.ts +480 -0
  134. package/extension/worker/sdkAdapter.ts +494 -0
  135. package/extension/worker/stageExecution.ts +679 -0
  136. package/extension/workerMain.ts +18 -19
  137. package/package.json +6 -4
  138. package/prompts/_fixtures/live.yaml +43 -18
  139. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  140. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  142. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  143. package/prompts/contexts/objective-refinement.md +17 -0
  144. package/prompts/contexts/read-only.md +1 -1
  145. package/prompts/stages/conflict-resolution-continuation.md +9 -6
  146. package/prompts/stages/conflict-resolution.md +4 -4
  147. package/prompts/stages/objective-plan/guidance.md +2 -2
  148. package/prompts/stages/objective-plan/seed.md +9 -1
  149. package/prompts/stages/objective-reconcile-ready.md +1 -1
  150. package/prompts/stages/objective-reconcile.md +1 -1
  151. package/prompts/stages/objective-refine/seed.md +18 -0
  152. package/prompts/stages/objective-review-browser.md +4 -4
  153. package/prompts/stages/objective-sync.md +1 -1
  154. package/prompts/stages/plan-review-browser.md +4 -4
  155. package/prompts/stages/pr-review-browser/active.md +3 -4
  156. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  157. package/prompts/stages/pr-review-terminal/active.md +3 -3
  158. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  159. package/prompts/stages/pr-review.md +3 -3
  160. package/prompts/stages/stack-review-browser/stack.md +5 -6
  161. package/shared/README.md +8 -0
  162. package/shared/bindings.yaml +3 -3
  163. package/shared/contracts.md +2601 -506
  164. package/shared/fixtures/issues-table.json +130 -0
  165. package/shared/registry.yaml +13 -0
  166. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  167. package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
  168. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  169. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  170. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  171. package/extension/doors/auditWaveTools.ts +0 -352
  172. package/extension/doors/ciExecutor.ts +0 -756
  173. package/extension/doors/commitCompact.ts +0 -251
  174. package/extension/doors/dreamWaveTools.ts +0 -489
  175. package/extension/doors/learn.ts +0 -668
  176. package/extension/doors/lifecycleGates.ts +0 -207
  177. package/extension/doors/objectiveStack.ts +0 -1543
  178. package/extension/doors/prReviewDynamic.ts +0 -276
  179. package/extension/doors/ready.ts +0 -279
  180. package/extension/doors/submit.ts +0 -373
  181. package/extension/doors/submitPrReview.ts +0 -505
  182. package/extension/factories/gistAuthor.ts +0 -94
  183. package/extension/factories/gistDraft.ts +0 -265
  184. package/extension/factories/gistSave.ts +0 -251
  185. package/extension/factories/implementHere.ts +0 -116
  186. package/extension/factories/objectiveAuthor.ts +0 -98
  187. package/extension/factories/objectiveDraft.ts +0 -466
  188. package/extension/factories/objectiveSave.ts +0 -366
  189. package/extension/factories/planDraft.ts +0 -140
  190. package/extension/factories/planMode.ts +0 -205
  191. package/extension/factories/planReview.ts +0 -1237
  192. package/extension/factories/planSave.ts +0 -604
  193. package/extension/factories/planTitle.ts +0 -141
  194. package/extension/substrate/structuredOutput.ts +0 -202
  195. package/extension/waves/auditWave.ts +0 -312
  196. package/extension/waves/harvestWave.ts +0 -399
  197. package/extension/waves/learnWave.ts +0 -155
  198. package/extension/waves/memoryAdapter.ts +0 -139
  199. package/extension/waves/prReviewDynamicWave.ts +0 -777
  200. package/extension/worker/readOnlySession.ts +0 -294
  201. package/extension/worker/worker.ts +0 -899
  202. package/prompts/stages/pr-review-dynamic.md +0 -7
@@ -0,0 +1,480 @@
1
+ // The report-wave TRANSPORT tier: the adapter seam, the receipt primitives, and the script
2
+ // runner — everything that knows a wave is realized as one detached pi-subagents
3
+ // `workflowScript` run. The logical tier (`reportWave.ts`: assignments, normalization,
4
+ // completeness policy) sits strictly above this module; nothing here imports back into it, so
5
+ // the dependency is one-directional by construction (type-only edges count).
6
+ //
7
+ // `startWaveScript` performs the front half (abort pre-check → capability ping →
8
+ // subscribe-before-spawn → async spawn) and returns the run handle plus a NEVER-REJECTING
9
+ // `result` promise carrying the back half (completion wait under the module-owned timeout,
10
+ // best-effort stop on timeout/cancel, aggregate read, receipt assembly,
11
+ // unsubscribe-on-settle); `runWaveScript` is that start + await.
12
+ //
13
+ // The failure vocabulary here is the WAVE-LEVEL subset only (`WaveRunFailureReason`): the six
14
+ // reasons a script run itself can produce, always `key: null`. The logical tier widens it with
15
+ // the assignment-level reasons — `WaveRunFailure` is structurally assignable to the caller-facing
16
+ // `ReportWaveFailure`, so script failures flow upward with zero runtime mapping, and an
17
+ // assignment-level reason on a script-run failure is unrepresentable.
18
+ //
19
+ // Failure posture: LOUD DEGRADE. Every operational arm normalizes into a typed failure — the
20
+ // runner never throws, and there is never a silent fallback to model-authored scripts.
21
+
22
+ // -------------------------------------------------------------------- the attempt receipts
23
+
24
+ /**
25
+ * The terminal disposition of ONE top-level workflow launch, as the runner observed it. Every
26
+ * launch reaches exactly one of these arms (`"running"` is unreachable — the runner always
27
+ * settles); `"unavailable"` preserves even a pre-spawn capability failure as an attempt.
28
+ */
29
+ export type WaveReceiptState =
30
+ | "unavailable" // ping failed/incomplete — nothing launched
31
+ | "spawn-failed" // spawn rejected/threw — no run handle
32
+ | "complete" // completion observed, durable state "complete"
33
+ | "failed" // completion observed, durable/observed failure
34
+ | "timed-out" // module timeout expired (handle preserved)
35
+ | "cancelled"; // AbortSignal honored (handle preserved when spawned)
36
+
37
+ /**
38
+ * One child's identity/artifact trail from the completion payload — OUTPUT-FREE by invariant:
39
+ * reports, summaries, and structured output never enter a receipt. Report authority is separate:
40
+ * durable aggregates, or the explicit native partial carrier (contracts.md §8.35).
41
+ */
42
+ export interface WaveChildReceipt {
43
+ /** The Perk assignment key (native run-correlated childId; legacy overloaded `agent`). */
44
+ key: string;
45
+ /** The child agent name, enriched from the Perk-owned assignment spec where known. */
46
+ agent?: string;
47
+ /** The child's opaque run id — never parsed or synthesized from paths. */
48
+ runId?: string;
49
+ success?: boolean;
50
+ outputState?: "present" | "absent" | "unknown";
51
+ /** String path fields only; output-free. */
52
+ artifactPaths?: Record<string, string>;
53
+ }
54
+
55
+ export interface WaveAggregate {
56
+ state: string;
57
+ error?: string;
58
+ value: unknown;
59
+ }
60
+
61
+ /** One script launch's receipt: the run handle (where known) + the observed children. */
62
+ export interface WaveScriptReceipt {
63
+ /** The top-level async run id (the spawn handle's asyncId). */
64
+ runId?: string;
65
+ asyncDir?: string;
66
+ state: WaveReceiptState;
67
+ children: WaveChildReceipt[];
68
+ }
69
+
70
+ // ------------------------------------------------------------------------- the adapter seam
71
+
72
+ /** The minimal pi event-bus surface an adapter needs (mirrors pi's EventBus, whose `on` returns an unsubscribe function). */
73
+ export interface WaveBus {
74
+ emit(channel: string, data: unknown): void;
75
+ on(channel: string, handler: (data: unknown) => void): () => void;
76
+ }
77
+
78
+ /** A successful capability ping; `asyncCompleteEvent` is the ADVERTISED async-complete channel. */
79
+ export interface WavePing {
80
+ asyncCompleteEvent: string;
81
+ }
82
+
83
+ /** The detached async run a spawn launched. */
84
+ export interface WaveRunHandle {
85
+ asyncId: string;
86
+ asyncDir: string;
87
+ }
88
+
89
+ /**
90
+ * An async-complete notification; at least one identifier is present on real payloads. The
91
+ * observability fields are optional — an identity-only completion stays valid (receipt absence
92
+ * degrades correlation, never behavior). Receipt children stay output-free, with `agent` unset
93
+ * until enrichment against Perk-owned specs. Only explicitly marked native partial settlement
94
+ * carries compact report DATA separately; the runner owns its short retention lifetime.
95
+ */
96
+ export interface WaveCompletion {
97
+ asyncId?: string;
98
+ asyncDir?: string;
99
+ /** The run's raw terminal state string, when the payload carries one. */
100
+ state?: string;
101
+ success?: boolean;
102
+ children?: WaveChildReceipt[];
103
+ terminalOutcome?: { state: "partial"; reason: "timeout" | "budget_exhausted" };
104
+ retainedEntries?: unknown[];
105
+ }
106
+
107
+ /**
108
+ * The explicit acceptance-disable every wave spawn carries. Without it, pi-subagents
109
+ * auto-infers a generic acceptance contract for reviewer/analyst-named or read-only children
110
+ * and injects a fenced `acceptance-report` completion instruction into each child — a COMPETING
111
+ * completion contract observed steering children into invalid `structured_output` attempts.
112
+ * `{level: "none"}` is the sanctioned disable shape (pi-subagents `explicitAcceptanceCanDisable`);
113
+ * `formatAcceptancePrompt` emits nothing at level none, so no contract block reaches a child.
114
+ * Deliberately module-wide with no opt-out: every report-wave child's sole completion contract
115
+ * is the engine-validated `structured_output` report.
116
+ */
117
+ export const WAVE_ACCEPTANCE = {
118
+ level: "none",
119
+ reason: "perk report-wave lanes complete via the engine-validated structured_output report",
120
+ } as const;
121
+
122
+ /** The full spawn params the runner fixes: async-only, ephemeral, fresh-context by definition. */
123
+ export interface WaveSpawnParams {
124
+ workflowScript: string;
125
+ async: true;
126
+ /** Waves are ephemeral by explicit decision — never mission-attached. */
127
+ mission: false;
128
+ /** A report wave is by definition fresh-context. */
129
+ context: "fresh";
130
+ /** The fixed acceptance disable (`WAVE_ACCEPTANCE`) — pi-subagents' workflow-defaults spread
131
+ * delivers it onto every child, suppressing the auto-inferred acceptance contract. */
132
+ acceptance: { level: "none"; reason: string };
133
+ outputSchema: object;
134
+ model?: string;
135
+ /** Orphan insurance: the run enforces the same deadline even if the parent session dies. */
136
+ timeoutMs: number;
137
+ }
138
+
139
+ export interface WaveAdapter {
140
+ /** Capability-checked ping; null ⇒ unavailable (loud degrade upstream). Must be called first. */
141
+ ping(): Promise<WavePing | null>;
142
+ /** Launch the async workflowScript run; throws ⇒ spawn-failed. */
143
+ spawn(params: WaveSpawnParams): Promise<WaveRunHandle>;
144
+ /** Subscribe to run completions (any run — the runner matches the handle); returns unsubscribe. */
145
+ onComplete(handler: (completion: WaveCompletion) => void): () => void;
146
+ /** Best-effort stop of a live run (timeout/cancel path); never throws. */
147
+ stop(handle: WaveRunHandle): Promise<void>;
148
+ /** Read the run's durable aggregate; throws ⇒ aggregate-unreadable. */
149
+ readAggregate(handle: WaveRunHandle): Promise<WaveAggregate>;
150
+ }
151
+
152
+ // -------------------------------------------------------------- the wave-level failure tier
153
+
154
+ /**
155
+ * The wave-level failure vocabulary: exactly the six reasons the script runner can produce.
156
+ * The logical tier's `ReportWaveFailureReason` is the superset union over this plus the
157
+ * assignment-level reasons — the reason literals are shared bytes.
158
+ */
159
+ export type WaveRunFailureReason =
160
+ | "unavailable" // ping failed / capabilities missing
161
+ | "spawn-failed" // RPC spawn rejected or no run handle
162
+ | "timeout" // module-owned timeout expired (best-effort stop issued)
163
+ | "cancelled" // AbortSignal fired (best-effort stop issued)
164
+ | "run-failed" // terminal status.json state ≠ "complete"
165
+ | "aggregate-unreadable"; // status.json missing/corrupt/no workflow.value array
166
+
167
+ /**
168
+ * A wave-level failure: always `key: null` (there is no assignment to blame — the whole run
169
+ * failed). Structurally assignable to the logical tier's `ReportWaveFailure`, so script failures are
170
+ * absorbed upward with zero runtime mapping.
171
+ */
172
+ export interface WaveRunFailure {
173
+ key: null;
174
+ reason: WaveRunFailureReason;
175
+ /** Human-readable diagnosis (error strings routed here, never re-thrown). */
176
+ detail: string;
177
+ }
178
+
179
+ // ------------------------------------------------------------------------- the script runner
180
+
181
+ /**
182
+ * The module-owned wave timeout default: a deliberate tightening vs the 30-minute foreground
183
+ * default the prompt-mechanics wave rode. Per-flow `spec.timeoutMs` overrides; the default is
184
+ * overridable for tests via PERK_WAVE_TIMEOUT_MS.
185
+ */
186
+ export const WAVE_TIMEOUT_MS = 15 * 60_000;
187
+
188
+ function waveTimeoutMs(): number {
189
+ const raw = Number(process.env.PERK_WAVE_TIMEOUT_MS ?? "");
190
+ return Number.isFinite(raw) && raw > 0 ? raw : WAVE_TIMEOUT_MS;
191
+ }
192
+
193
+ /** The judgment-bearing pieces a script run needs (the assignment-free request slice). */
194
+ export interface WaveScriptSpec {
195
+ /** Flow name for error detail/trace (e.g. "pr-review"). */
196
+ flow: string;
197
+ /** The complete, module-rendered workflowScript (never model-authored). */
198
+ workflowScript: string;
199
+ /** Workflow-level default → the engine injects a `structured_output` tool into each child. */
200
+ outputSchema: object;
201
+ /** Workflow-level model default (per-item `model` fields in the script override it). */
202
+ model?: string;
203
+ /** Module default (`WAVE_TIMEOUT_MS`) when omitted. */
204
+ timeoutMs?: number;
205
+ }
206
+
207
+ /** A script outcome: durable value on success; explicit partial evidence never erases failure. */
208
+ export type WaveScriptResult =
209
+ | { ok: true; value: unknown; receipt: WaveScriptReceipt }
210
+ | { ok: false; failure: WaveRunFailure; receipt: WaveScriptReceipt; value?: unknown[] };
211
+
212
+ /**
213
+ * A launched (or launch-refused) script run. On `ok: true` the run is LIVE: `handle` is the
214
+ * detached async run, and `result` settles when the back half finishes (completion wait under
215
+ * the module-owned timeout, AbortSignal honor, best-effort stop on timeout/cancel, the durable
216
+ * aggregate read, receipt assembly, unsubscribe-on-settle). `result` NEVER rejects — every arm
217
+ * normalizes into `WaveScriptResult`, so an uncollected wave can never become an unhandled
218
+ * rejection. Pre-spawn failures (aborted-before-launch, ping fail/null, spawn throw) take the
219
+ * `ok: false` arm with the same failure/receipt values the blocking runner reports, and the
220
+ * completion subscription is released immediately.
221
+ */
222
+ export type WaveScriptStart =
223
+ | { ok: true; handle: WaveRunHandle; result: Promise<WaveScriptResult> }
224
+ | { ok: false; failure: WaveRunFailure; receipt: WaveScriptReceipt };
225
+
226
+ function errorDetail(error: unknown): string {
227
+ return error instanceof Error ? error.message : String(error);
228
+ }
229
+
230
+ /**
231
+ * Start one module-rendered workflowScript through the adapter — the non-blocking front half:
232
+ * capability ping → subscribe-before-spawn (the completion-before-reply buffer) → async spawn.
233
+ * On success the back half (block on the async-complete event under the module-owned timeout,
234
+ * abortable → best-effort stop on timeout/cancel → read the durable aggregate → the
235
+ * `state !== "complete"` / unreadable arms) runs behind the returned `result` promise, which
236
+ * never rejects. The shared operational core under every runner; per-flow value normalization
237
+ * stays with the caller.
238
+ */
239
+ export async function startWaveScript(
240
+ adapter: WaveAdapter,
241
+ spec: WaveScriptSpec,
242
+ signal?: AbortSignal,
243
+ ): Promise<WaveScriptStart> {
244
+ // The receipt is assembled in EVERY terminal arm — write-only telemetry: nothing below reads
245
+ // it back into the ok/failure decision.
246
+ const receiptOf = (
247
+ state: WaveReceiptState,
248
+ spawned: WaveRunHandle | null,
249
+ completion?: WaveCompletion,
250
+ ): WaveScriptReceipt => ({
251
+ ...(spawned !== null ? { runId: spawned.asyncId, asyncDir: spawned.asyncDir } : {}),
252
+ state,
253
+ children: completion?.children ?? [],
254
+ });
255
+ const startFailure = (
256
+ reason: WaveRunFailureReason,
257
+ detail: string,
258
+ receipt: WaveScriptReceipt,
259
+ ): WaveScriptStart => ({
260
+ ok: false,
261
+ failure: { key: null, reason, detail },
262
+ receipt,
263
+ });
264
+
265
+ // Read through a closure so TS's readonly-property narrowing never staples the first
266
+ // check's `false` onto the post-await re-check (the signal CAN flip during an await).
267
+ const aborted = (): boolean => signal?.aborted === true;
268
+ const cancelledBeforeLaunch = (): WaveScriptStart =>
269
+ startFailure(
270
+ "cancelled",
271
+ `wave '${spec.flow}' was cancelled before launch`,
272
+ receiptOf("cancelled", null),
273
+ );
274
+
275
+ if (aborted()) return cancelledBeforeLaunch();
276
+
277
+ // 1. Capability check — the loud-degrade arm: the result explicitly names the wave
278
+ // unavailable; callers surface it, never silently fall back to model-authored scripts.
279
+ let ping: WavePing | null;
280
+ try {
281
+ ping = await adapter.ping();
282
+ } catch (error) {
283
+ return startFailure(
284
+ "unavailable",
285
+ `subagent RPC ping failed: ${errorDetail(error)}`,
286
+ receiptOf("unavailable", null),
287
+ );
288
+ }
289
+ if (ping === null) {
290
+ return startFailure(
291
+ "unavailable",
292
+ "pi-subagents did not advertise the report-wave capabilities (ping failed or incomplete)",
293
+ receiptOf("unavailable", null),
294
+ );
295
+ }
296
+
297
+ // An abort can arrive WHILE the ping await is pending — re-check before subscribe/spawn so a
298
+ // cancelled wave never launches (the pre-launch check alone leaves this window open).
299
+ if (aborted()) return cancelledBeforeLaunch();
300
+
301
+ // 2. Subscribe BEFORE spawn: a completion can arrive before the spawn reply resolves (the
302
+ // completion-before-reply race). Buffer only while identity is unknown; afterward retain
303
+ // the first match only, never foreign reports or duplicate completions.
304
+ let handle: WaveRunHandle | null = null;
305
+ let notifyMatch: (() => void) | null = null;
306
+ let matched: WaveCompletion | undefined;
307
+ let accepting = true;
308
+ const buffered: WaveCompletion[] = [];
309
+ const matchesHandle = (completion: WaveCompletion): boolean =>
310
+ handle !== null &&
311
+ ((completion.asyncDir !== undefined && completion.asyncDir === handle.asyncDir) ||
312
+ (completion.asyncId !== undefined && completion.asyncId === handle.asyncId));
313
+ const unsubscribe = adapter.onComplete((completion) => {
314
+ if (!accepting) return;
315
+ if (handle === null) {
316
+ buffered.push(completion);
317
+ return;
318
+ }
319
+ if (!matchesHandle(completion) || matched !== undefined) return;
320
+ matched = completion;
321
+ notifyMatch?.();
322
+ });
323
+
324
+ // 3. Spawn: async-only, ephemeral, fresh-context — the module fixes those; the flow's spec
325
+ // supplies the judgment-bearing pieces (script, schema, model, policy).
326
+ const timeoutMs = spec.timeoutMs ?? waveTimeoutMs();
327
+ try {
328
+ handle = await adapter.spawn({
329
+ workflowScript: spec.workflowScript,
330
+ async: true,
331
+ mission: false,
332
+ context: "fresh",
333
+ acceptance: WAVE_ACCEPTANCE,
334
+ outputSchema: spec.outputSchema,
335
+ ...(spec.model !== undefined ? { model: spec.model } : {}),
336
+ timeoutMs,
337
+ });
338
+ matched = buffered.find(matchesHandle);
339
+ buffered.length = 0;
340
+ } catch (error) {
341
+ accepting = false;
342
+ unsubscribe();
343
+ buffered.length = 0;
344
+ matched = undefined;
345
+ notifyMatch = null;
346
+ return startFailure(
347
+ "spawn-failed",
348
+ `wave spawn failed: ${errorDetail(error)}`,
349
+ receiptOf("spawn-failed", null),
350
+ );
351
+ }
352
+ const spawned = handle;
353
+
354
+ const scriptFailure = (
355
+ reason: WaveRunFailureReason,
356
+ detail: string,
357
+ receipt: WaveScriptReceipt,
358
+ ): WaveScriptResult => ({
359
+ ok: false,
360
+ failure: { key: null, reason, detail },
361
+ receipt,
362
+ });
363
+
364
+ // The back half: every arm below RETURNS a normalized `WaveScriptResult` (never throws), so
365
+ // `result` never rejects; the subscription is released exactly when it settles.
366
+ const settle = async (): Promise<WaveScriptResult> => {
367
+ try {
368
+ // 4. Block on completion with the module-owned timeout; honor the caller's AbortSignal.
369
+ const outcome = await new Promise<"complete" | "timeout" | "cancelled">((resolve) => {
370
+ if (matched !== undefined) {
371
+ accepting = false;
372
+ resolve("complete");
373
+ return;
374
+ }
375
+ const settleOutcome = (value: "complete" | "timeout" | "cancelled"): void => {
376
+ accepting = false;
377
+ clearTimeout(timer);
378
+ signal?.removeEventListener("abort", onAbort);
379
+ notifyMatch = null;
380
+ resolve(value);
381
+ };
382
+ const timer = setTimeout(() => settleOutcome("timeout"), timeoutMs);
383
+ const onAbort = (): void => settleOutcome("cancelled");
384
+ notifyMatch = () => settleOutcome("complete");
385
+ signal?.addEventListener("abort", onAbort, { once: true });
386
+ if (signal?.aborted === true) settleOutcome("cancelled");
387
+ });
388
+ if (outcome !== "complete") {
389
+ // Best-effort stop — adapters never throw here by contract, but a broken adapter's error
390
+ // is still swallowed into the detail rather than re-thrown.
391
+ let stopNote = "";
392
+ try {
393
+ await adapter.stop(spawned);
394
+ } catch (error) {
395
+ stopNote = ` (stop failed: ${errorDetail(error)})`;
396
+ }
397
+ return outcome === "timeout"
398
+ ? scriptFailure(
399
+ "timeout",
400
+ `wave '${spec.flow}' timed out after ${timeoutMs}ms${stopNote}`,
401
+ receiptOf("timed-out", spawned),
402
+ )
403
+ : scriptFailure(
404
+ "cancelled",
405
+ `wave '${spec.flow}' was cancelled${stopNote}`,
406
+ receiptOf("cancelled", spawned),
407
+ );
408
+ }
409
+
410
+ // 5. Read the durable aggregate; surface the terminal-state arms.
411
+ let aggregate: WaveAggregate;
412
+ try {
413
+ aggregate = await adapter.readAggregate(spawned);
414
+ } catch (error) {
415
+ // Aggregate-unreadable: the completion identity is retained — the receipt state derives
416
+ // from the OBSERVED completion (a correlation label, not a verdict; the authoritative
417
+ // failure reason stays in the wave failure).
418
+ return scriptFailure(
419
+ "aggregate-unreadable",
420
+ `wave aggregate unreadable: ${errorDetail(error)}`,
421
+ receiptOf(matched?.success === false ? "failed" : "complete", spawned, matched),
422
+ );
423
+ }
424
+ if (aggregate.state !== "complete") {
425
+ const detail = aggregate.error !== undefined ? `: ${aggregate.error}` : "";
426
+ if (
427
+ (aggregate.state === "failed" || aggregate.state === "partial") &&
428
+ matched?.terminalOutcome !== undefined
429
+ ) {
430
+ // A durable array is authoritative in full, including failed rows and holes. Only
431
+ // its absence admits the public child projection; never merge or promote evidence.
432
+ return {
433
+ ok: false,
434
+ failure: {
435
+ key: null,
436
+ reason: "run-failed",
437
+ detail: `wave run ended '${aggregate.state}' (native partial: ${matched.terminalOutcome.reason})${detail}`,
438
+ },
439
+ value: Array.isArray(aggregate.value)
440
+ ? aggregate.value
441
+ : (matched.retainedEntries ?? []),
442
+ receipt: receiptOf("failed", spawned, matched),
443
+ };
444
+ }
445
+ return scriptFailure(
446
+ "run-failed",
447
+ `wave run ended '${aggregate.state}'${detail}`,
448
+ receiptOf("failed", spawned, matched),
449
+ );
450
+ }
451
+ return {
452
+ ok: true,
453
+ value: aggregate.value,
454
+ receipt: receiptOf("complete", spawned, matched),
455
+ };
456
+ } finally {
457
+ accepting = false;
458
+ unsubscribe();
459
+ buffered.length = 0;
460
+ matched = undefined;
461
+ notifyMatch = null;
462
+ }
463
+ };
464
+ return { ok: true, handle: spawned, result: settle() };
465
+ }
466
+
467
+ /**
468
+ * Run one module-rendered workflowScript to completion — the blocking form: `startWaveScript` +
469
+ * await its `result` (one operational core, behavior identical to the historical blocking
470
+ * runner).
471
+ */
472
+ export async function runWaveScript(
473
+ adapter: WaveAdapter,
474
+ spec: WaveScriptSpec,
475
+ signal?: AbortSignal,
476
+ ): Promise<WaveScriptResult> {
477
+ const start = await startWaveScript(adapter, spec, signal);
478
+ if (!start.ok) return { ok: false, failure: start.failure, receipt: start.receipt };
479
+ return await start.result;
480
+ }