@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,324 @@
1
+ // The `/learn` flow's analyst-wave policy over the shared report-wave module: the analyst
2
+ // fan-out as CODE. It owns the four learn angles, the analyst report schema, the tool-enforced
3
+ // angle policy (2–4 angles, `session-deviations` mandatory — parse-don't-validate), the
4
+ // assignment/task composition, the whitelist report decoder (`decodeLearnAnalystReport` — the
5
+ // trust boundary keyed by the validated assignment), and the typed outcome mapping — delegating
6
+ // spawn/timeout/aggregate mechanics to `wave.run` under the `best-effort` completeness policy
7
+ // (a failed analyst is an explicitly-reported skipped angle, never a failed pass). Analyst reports come
8
+ // back as engine-validated structured output (the workflow-level `outputSchema` → the injected
9
+ // `structured_output` tool).
10
+ //
11
+ // Pi-free by construction: the `ReportWave` seam is the only mechanism edge; the adapter
12
+ // constructs the wave at the composition root and threads it.
13
+
14
+ import { join } from "node:path";
15
+ import {
16
+ type ReportAssignment,
17
+ type ReportWave,
18
+ type ReportWaveAttemptReceipt,
19
+ type ReportWaveFailureReason,
20
+ toAttemptReceipt,
21
+ } from "../waves/reportWave.ts";
22
+ import { CAPTURED_DECISIONS, type CapturedDecision, isCapturedDecision } from "./capture.ts";
23
+
24
+ /** The four learn angles; `session-deviations` is the mandatory member of every selection. */
25
+ export const LEARN_ANGLES = [
26
+ "session-deviations",
27
+ "plan-vs-implementation",
28
+ "existing-docs",
29
+ "validation-risk",
30
+ ] as const;
31
+
32
+ export type LearnAngle = (typeof LEARN_ANGLES)[number];
33
+
34
+ const MANDATORY_ANGLE: LearnAngle = "session-deviations";
35
+
36
+ function isLearnAngle(value: string): value is LearnAngle {
37
+ return (LEARN_ANGLES as readonly string[]).includes(value);
38
+ }
39
+
40
+ /**
41
+ * The per-lane analyst report schema (the workflow-level `outputSchema`): closed shape,
42
+ * all-required, enums, `target` required-nullable ({angle, verdict, candidates, fyi} — the same
43
+ * field semantics as the agent def's report contract). Enums are DERIVED from the vocabulary
44
+ * constants (`LEARN_ANGLES`; `CAPTURED_DECISIONS` + schema-only `SKIP` — a skip creates no
45
+ * issue), never hand-mirrored. DELIBERATE DIVERGENCE from `PR_REVIEW_REPORT_SCHEMA`: no if/then
46
+ * verdict↔candidates conditional. Under `best-effort` completeness, salvaging an internally
47
+ * inconsistent report beats failing its lane — the parent derives the real verdict from
48
+ * `candidates[]` (`verdict` is derived data), so an inconsistent verdict costs nothing while a
49
+ * failed lane loses the whole angle.
50
+ */
51
+ export const LEARN_ANALYST_REPORT_SCHEMA = {
52
+ type: "object",
53
+ additionalProperties: false,
54
+ required: ["angle", "verdict", "candidates", "fyi"],
55
+ properties: {
56
+ angle: {
57
+ type: "string",
58
+ enum: [...LEARN_ANGLES],
59
+ },
60
+ verdict: {
61
+ type: "string",
62
+ enum: ["clean", "actionable"],
63
+ },
64
+ candidates: {
65
+ type: "array",
66
+ items: {
67
+ type: "object",
68
+ additionalProperties: false,
69
+ required: ["decision", "summary", "target", "evidence"],
70
+ properties: {
71
+ decision: {
72
+ type: "string",
73
+ enum: [...CAPTURED_DECISIONS, "SKIP"],
74
+ },
75
+ summary: { type: "string" },
76
+ target: { type: ["string", "null"] },
77
+ evidence: { type: "string" },
78
+ },
79
+ },
80
+ },
81
+ fyi: {
82
+ type: "array",
83
+ items: { type: "string" },
84
+ },
85
+ },
86
+ };
87
+
88
+ /** One chosen angle + the parent's optional plan-specific emphasis for its task text. */
89
+ export interface LearnAngleSelection {
90
+ angle: LearnAngle;
91
+ emphasis?: string;
92
+ }
93
+
94
+ /**
95
+ * The angle policy as one parse-don't-validate entry (tested implementation, not guidance):
96
+ * 2–4 angles, no duplicates, only the four known slugs, and `session-deviations` always
97
+ * included. Narrows the shape-decoded rows into `LearnAngleSelection[]` (angle: `LearnAngle`,
98
+ * never a bare string) or returns the human-readable rule violation.
99
+ */
100
+ export function parseAngleSelections(
101
+ raw: readonly { angle: string; emphasis?: string }[],
102
+ ): { ok: true; selections: LearnAngleSelection[] } | { ok: false; message: string } {
103
+ if (raw.length < 2 || raw.length > 4) {
104
+ return { ok: false, message: `choose 2–4 angles (got ${raw.length})` };
105
+ }
106
+ const seen = new Set<LearnAngle>();
107
+ const selections: LearnAngleSelection[] = [];
108
+ for (const { angle, emphasis } of raw) {
109
+ if (!isLearnAngle(angle)) {
110
+ return {
111
+ ok: false,
112
+ message: `unknown angle '${angle}' — the valid angles are ${LEARN_ANGLES.join(", ")}`,
113
+ };
114
+ }
115
+ if (seen.has(angle)) {
116
+ return { ok: false, message: `duplicate angle '${angle}' — each angle at most once` };
117
+ }
118
+ seen.add(angle);
119
+ selections.push({ angle, ...(emphasis !== undefined ? { emphasis } : {}) });
120
+ }
121
+ if (!seen.has(MANDATORY_ANGLE)) {
122
+ return {
123
+ ok: false,
124
+ message: `the '${MANDATORY_ANGLE}' angle is mandatory — always include it`,
125
+ };
126
+ }
127
+ return { ok: true, selections };
128
+ }
129
+
130
+ /**
131
+ * The one derivation point for the bundle's manifest path (shared by the launch routing and the
132
+ * adapter's existence trust check) — `manifestPath` is derived, never passed.
133
+ */
134
+ export function learnManifestPath(bundleDir: string): string {
135
+ return join(bundleDir, "manifest.json");
136
+ }
137
+
138
+ /**
139
+ * Compose one assignment's task text IN CODE (prompt-drift-proof: task text composed in code,
140
+ * never a template): the assigned angle, the absolute manifest path (read first), the bundle dir, and the parent's
141
+ * optional emphasis appended verbatim. Deliberately short — the angle rubric lives in the agent
142
+ * def, not the task.
143
+ */
144
+ function assignmentTask(
145
+ selection: LearnAngleSelection,
146
+ manifestPath: string,
147
+ bundleDir: string,
148
+ ): string {
149
+ const base =
150
+ `angle: ${selection.angle} — analyze ONLY this angle. ` +
151
+ `Read the evidence-bundle manifest FIRST: ${manifestPath} (bundle dir: ${bundleDir}). ` +
152
+ "Do not re-gather the bundle.";
153
+ const emphasis = selection.emphasis?.trim();
154
+ return emphasis !== undefined && emphasis !== "" ? `${base} Emphasis: ${emphasis}` : base;
155
+ }
156
+
157
+ /** One decoded analyst candidate (the typed twin of the schema's `candidates` items). */
158
+ export interface LearnAnalystCandidate {
159
+ decision: CapturedDecision | "SKIP";
160
+ summary: string;
161
+ target: string | null;
162
+ evidence: string;
163
+ }
164
+
165
+ /** The decoded analyst report (the typed twin of `LEARN_ANALYST_REPORT_SCHEMA`). */
166
+ export interface LearnAnalystReport {
167
+ angle: LearnAngle;
168
+ verdict: "clean" | "actionable";
169
+ candidates: LearnAnalystCandidate[];
170
+ fyi: string[];
171
+ }
172
+
173
+ /**
174
+ * Decode one lane's engine-validated structured report at the trust boundary (the
175
+ * `stampHarvestReport` pattern): whitelist construction field-by-field — never a spread — with
176
+ * `angle` set from the validated assignment KEY, so a report can never re-attribute itself.
177
+ * The observable detail taxonomy is exactly two byte-shapes: the angle-contradiction arm (a
178
+ * schema-valid report whose echoed angle names a DIFFERENT lane — unattributable content,
179
+ * never salvaged) and ONE stable generic vocabulary detail for everything else (no per-field
180
+ * diagnostics — schema enforcement is upstream, engine-validated; this decoder is a boundary,
181
+ * not a linter). The defensive narrowings (a non-angle key, a non-record report) are
182
+ * unreachable on the production path — `ReportWave.normalizeAssignments` filters non-object
183
+ * reports and unknown aggregate keys first — so they fold into the generic detail.
184
+ */
185
+ export function decodeLearnAnalystReport(
186
+ key: string,
187
+ report: unknown,
188
+ ): { ok: true; report: LearnAnalystReport } | { ok: false; detail: string } {
189
+ const generic = {
190
+ ok: false as const,
191
+ detail: `analyst report for lane '${key}' is outside the report schema vocabulary`,
192
+ };
193
+ if (!isLearnAngle(key)) return generic;
194
+ if (typeof report !== "object" || report === null || Array.isArray(report)) return generic;
195
+ const raw = report as Record<string, unknown>;
196
+ const angle = raw.angle;
197
+ if (typeof angle !== "string" || !isLearnAngle(angle)) return generic;
198
+ if (angle !== key) {
199
+ return {
200
+ ok: false,
201
+ detail: `analyst report angle '${angle}' contradicts the assigned lane '${key}'`,
202
+ };
203
+ }
204
+ const verdict = raw.verdict;
205
+ if (verdict !== "clean" && verdict !== "actionable") return generic;
206
+ if (!Array.isArray(raw.candidates)) return generic;
207
+ const candidates: LearnAnalystCandidate[] = [];
208
+ for (const item of raw.candidates) {
209
+ if (typeof item !== "object" || item === null || Array.isArray(item)) return generic;
210
+ const c = item as Record<string, unknown>;
211
+ const decision = c.decision;
212
+ if (typeof decision !== "string") return generic;
213
+ if (!isCapturedDecision(decision) && decision !== "SKIP") return generic;
214
+ const summary = c.summary;
215
+ const target = c.target;
216
+ const evidence = c.evidence;
217
+ if (typeof summary !== "string" || typeof evidence !== "string") return generic;
218
+ if (target !== null && typeof target !== "string") return generic;
219
+ candidates.push({ decision, summary, target, evidence });
220
+ }
221
+ const fyiRaw = raw.fyi;
222
+ if (!Array.isArray(fyiRaw)) return generic;
223
+ const fyi: string[] = [];
224
+ for (const entry of fyiRaw) {
225
+ if (typeof entry !== "string") return generic;
226
+ fyi.push(entry);
227
+ }
228
+ return { ok: true, report: { angle: key, verdict, candidates, fyi } };
229
+ }
230
+
231
+ /** The typed analyst-wave outcome: a wave-level failure, or the per-angle reports + skips. */
232
+ export type LearnWaveOutcome =
233
+ | {
234
+ kind: "wave_failed";
235
+ reason: ReportWaveFailureReason;
236
+ detail: string;
237
+ attempts: ReportWaveAttemptReceipt[];
238
+ }
239
+ | {
240
+ kind: "complete";
241
+ reports: { angle: LearnAngle; report: LearnAnalystReport }[];
242
+ skipped: { angle: string; reason: ReportWaveFailureReason; detail: string }[];
243
+ attempts: ReportWaveAttemptReceipt[];
244
+ };
245
+
246
+ /**
247
+ * Run the learn analyst wave and map its result into the typed outcome: one
248
+ * `perk.learn-analyst` child per selected angle over the shared evidence bundle, `best-effort`
249
+ * completeness (assignment failure = a skipped angle; only a wave-level failure — found via
250
+ * `key === null` — makes the outcome `wave_failed`). ONE attempt, no retry — the receipt rides
251
+ * the outcome for observability only. Assumes a validated selection (`parseAngleSelections`
252
+ * runs at the tool boundary); the wave's programmer-error throws (empty/duplicate keys)
253
+ * remain the backstop. Cancellation: the caller's `AbortSignal` threads into `wave.run`;
254
+ * an abort settles the wave as `cancelled` with a best-effort stop of an already-launched run,
255
+ * normalized into the outcome — never a throw; the wave does not outlive the abort.
256
+ */
257
+ export async function runLearnAnalystWave(
258
+ wave: ReportWave,
259
+ opts: {
260
+ bundleDir: string;
261
+ selections: LearnAngleSelection[];
262
+ model?: string;
263
+ signal?: AbortSignal;
264
+ },
265
+ ): Promise<LearnWaveOutcome> {
266
+ const manifestPath = learnManifestPath(opts.bundleDir);
267
+ const assignments: ReportAssignment[] = opts.selections.map((selection) => ({
268
+ key: selection.angle,
269
+ label: selection.angle,
270
+ agent: "perk.learn-analyst",
271
+ phase: "learn",
272
+ task: assignmentTask(selection, manifestPath, opts.bundleDir),
273
+ }));
274
+ const result = await wave.run(
275
+ {
276
+ flow: "learn",
277
+ assignments,
278
+ outputSchema: LEARN_ANALYST_REPORT_SCHEMA,
279
+ completeness: "best-effort",
280
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
281
+ },
282
+ { signal: opts.signal },
283
+ );
284
+ // The learn flow has no retry — ONE attempt over the validated selection.
285
+ const attempts = [
286
+ toAttemptReceipt(
287
+ "learn",
288
+ 1,
289
+ opts.selections.map((s) => s.angle),
290
+ result.receipt,
291
+ ),
292
+ ];
293
+
294
+ if (!result.complete) {
295
+ const waveFailure = result.failures.find((f) => f.key === null);
296
+ return {
297
+ kind: "wave_failed",
298
+ reason: waveFailure?.reason ?? "run-failed",
299
+ detail: waveFailure?.detail ?? "the analyst wave failed without detail",
300
+ attempts,
301
+ };
302
+ }
303
+
304
+ // Decode every lane report at the trust boundary: an undecodable/contradictory report moves
305
+ // its lane to `skipped` (`malformed-report`) with the decoder's detail. Skip ordering:
306
+ // decoder skips first (report order), then the wave's own lane failures.
307
+ const reports: { angle: LearnAngle; report: LearnAnalystReport }[] = [];
308
+ const decoderSkips: { angle: string; reason: ReportWaveFailureReason; detail: string }[] = [];
309
+ for (const r of result.reports) {
310
+ const decoded = decodeLearnAnalystReport(r.key, r.report);
311
+ if (decoded.ok) {
312
+ reports.push({ angle: decoded.report.angle, report: decoded.report });
313
+ } else {
314
+ decoderSkips.push({ angle: r.key, reason: "malformed-report", detail: decoded.detail });
315
+ }
316
+ }
317
+ const skipped = [
318
+ ...decoderSkips,
319
+ ...result.failures
320
+ .filter((f) => f.key !== null)
321
+ .map((f) => ({ angle: f.key as string, reason: f.reason, detail: f.detail })),
322
+ ];
323
+ return { kind: "complete", reports, skipped, attempts };
324
+ }