@agwab/pi-workflow 0.1.1 → 0.1.2

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 (51) hide show
  1. package/README.md +14 -3
  2. package/agents/researcher.md +17 -7
  3. package/dist/artifact-graph-runtime.js +1 -0
  4. package/dist/compiler.js +2 -2
  5. package/dist/dynamic-generated-task-runtime.js +4 -3
  6. package/dist/dynamic-runtime-bundle.js +3 -2
  7. package/dist/extension.js +40 -1
  8. package/dist/subagent-backend.js +82 -27
  9. package/dist/tool-metadata.d.ts +1 -0
  10. package/dist/tool-metadata.js +13 -1
  11. package/dist/workflow-artifact-extension.js +3 -2
  12. package/dist/workflow-artifact-tool.js +84 -4
  13. package/dist/workflow-web-source-extension.d.ts +43 -0
  14. package/dist/workflow-web-source-extension.js +1194 -0
  15. package/dist/workflow-web-source.d.ts +171 -0
  16. package/dist/workflow-web-source.js +897 -0
  17. package/docs/usage.md +32 -18
  18. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  19. package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
  20. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
  21. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
  22. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
  23. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
  24. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
  25. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
  26. package/package.json +2 -2
  27. package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
  28. package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
  29. package/src/artifact-graph-runtime.ts +1 -0
  30. package/src/compiler.ts +2 -1
  31. package/src/dynamic-generated-task-runtime.ts +4 -2
  32. package/src/dynamic-runtime-bundle.ts +3 -2
  33. package/src/extension.ts +46 -1
  34. package/src/subagent-backend.ts +121 -37
  35. package/src/tool-metadata.ts +22 -1
  36. package/src/workflow-artifact-extension.ts +3 -2
  37. package/src/workflow-artifact-tool.ts +96 -4
  38. package/src/workflow-web-source-extension.ts +1411 -0
  39. package/src/workflow-web-source.ts +1171 -0
  40. package/workflows/README.md +1 -1
  41. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
  42. package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
  43. package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
  44. package/workflows/deep-research/helpers/render-executive.mjs +571 -198
  45. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
  46. package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
  47. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
  48. package/workflows/deep-research/spec.json +36 -21
  49. package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
  50. package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
  51. package/workflows/deep-review/spec.json +22 -1
@@ -0,0 +1,502 @@
1
+ // Deterministic evidence-backed renderer for deep-review.
2
+ //
3
+ // Finding cards are rendered from partition-verdicts.control.json, the
4
+ // deterministic post-processing ledger. The model-authored report stage is used
5
+ // only for narrative summary/verdict/risk fields.
6
+
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+
10
+ const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info", "unknown"];
11
+
12
+ function findSource(sources, stageId) {
13
+ for (const [specId, source] of Object.entries(sources ?? {})) {
14
+ if (specId === stageId || specId.startsWith(`${stageId}.`)) return source;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function asArray(value) {
20
+ return Array.isArray(value) ? value : [];
21
+ }
22
+
23
+ function cleanText(value) {
24
+ return String(value ?? "")
25
+ .replace(/\s+/g, " ")
26
+ .replace(/\s+([,.;:!?])/g, "$1")
27
+ .trim();
28
+ }
29
+
30
+ function evidenceText(value) {
31
+ return String(value ?? "")
32
+ .replace(/\r\n/g, "\n")
33
+ .replace(/\r/g, "\n")
34
+ .trim();
35
+ }
36
+
37
+ function escapeTableCell(value) {
38
+ return cleanText(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
39
+ }
40
+
41
+ function inlineCode(value) {
42
+ return `\`${escapeTableCell(value).replace(/`/g, "\\`")}\``;
43
+ }
44
+
45
+ function severityOf(finding) {
46
+ const raw = cleanText(finding?.severity).toLowerCase();
47
+ if (SEVERITY_ORDER.includes(raw)) return raw;
48
+ return raw || "unknown";
49
+ }
50
+
51
+ function severityRank(severity) {
52
+ const index = SEVERITY_ORDER.indexOf(severityOf({ severity }));
53
+ return index === -1 ? SEVERITY_ORDER.length : index;
54
+ }
55
+
56
+ function titleOf(finding) {
57
+ return cleanText(
58
+ finding?.title ??
59
+ finding?.finding ??
60
+ finding?.summary ??
61
+ "Untitled finding",
62
+ );
63
+ }
64
+
65
+ function findingIdOf(finding, index) {
66
+ return cleanText(
67
+ finding?.findingId ??
68
+ finding?.id ??
69
+ `finding-${String(index + 1).padStart(3, "0")}`,
70
+ );
71
+ }
72
+
73
+ function rootCauseIdOf(finding) {
74
+ return cleanText(finding?.rootCauseId ?? "");
75
+ }
76
+
77
+ function locationKey(location) {
78
+ return `${location.file ?? ""}|${location.line ?? ""}|${location.lineEnd ?? ""}|${location.symbol ?? ""}`;
79
+ }
80
+
81
+ function normalizeLocation(location) {
82
+ if (!location || typeof location !== "object") return null;
83
+ const file = cleanText(location.file);
84
+ if (!file) return null;
85
+ const line = Number.isFinite(Number(location.line))
86
+ ? Number(location.line)
87
+ : undefined;
88
+ const lineEnd = Number.isFinite(Number(location.lineEnd))
89
+ ? Number(location.lineEnd)
90
+ : undefined;
91
+ const symbol = cleanText(location.symbol);
92
+ return {
93
+ file,
94
+ ...(line !== undefined ? { line } : {}),
95
+ ...(lineEnd !== undefined ? { lineEnd } : {}),
96
+ ...(symbol ? { symbol } : {}),
97
+ };
98
+ }
99
+
100
+ function locationsOf(finding) {
101
+ const seen = new Set();
102
+ const out = [];
103
+ for (const raw of asArray(finding?.locations)) {
104
+ const location = normalizeLocation(raw);
105
+ if (!location) continue;
106
+ const key = locationKey(location);
107
+ if (seen.has(key)) continue;
108
+ seen.add(key);
109
+ out.push(location);
110
+ }
111
+ return out;
112
+ }
113
+
114
+ function evidenceQuotesOf(finding) {
115
+ const seen = new Set();
116
+ const out = [];
117
+ for (const quote of asArray(finding?.evidenceQuotes)) {
118
+ const text = evidenceText(quote);
119
+ if (!text || seen.has(text)) continue;
120
+ seen.add(text);
121
+ out.push(text);
122
+ }
123
+ return out;
124
+ }
125
+
126
+ function markdownFenceInfo(quote) {
127
+ if (
128
+ /^\s*(const|let|var|function|export|import|await|return|if|for|while)\b/.test(
129
+ quote,
130
+ )
131
+ )
132
+ return "ts";
133
+ if (/^\s*(FROM|ENV|RUN|CMD|COPY|WORKDIR|EXPOSE)\b/i.test(quote))
134
+ return "dockerfile";
135
+ if (/^\s*[{}[]/.test(quote)) return "json";
136
+ return "text";
137
+ }
138
+
139
+ function renderLocationsTable(locations) {
140
+ if (locations.length === 0) return ["Locations: _not provided_", ""];
141
+ return [
142
+ "Locations:",
143
+ "",
144
+ "| File | Line | Symbol |",
145
+ "|---|---:|---|",
146
+ ...locations.map((location) => {
147
+ const line =
148
+ location.line === undefined
149
+ ? "—"
150
+ : location.lineEnd !== undefined && location.lineEnd !== location.line
151
+ ? `${location.line}-${location.lineEnd}`
152
+ : `${location.line}`;
153
+ return `| ${inlineCode(location.file)} | ${escapeTableCell(line)} | ${location.symbol ? inlineCode(location.symbol) : "—"} |`;
154
+ }),
155
+ "",
156
+ ];
157
+ }
158
+
159
+ function renderEvidenceQuotes(quotes) {
160
+ if (quotes.length === 0) return [];
161
+ const out = ["Evidence:", ""];
162
+ for (const quote of quotes) {
163
+ const info = markdownFenceInfo(quote);
164
+ out.push(`\`\`\`${info}`);
165
+ out.push(quote);
166
+ out.push("```", "");
167
+ }
168
+ return out;
169
+ }
170
+
171
+ function renderCounterEvidence(finding) {
172
+ const counter = asArray(finding?.counterEvidence)
173
+ .map((item) =>
174
+ typeof item === "string"
175
+ ? item
176
+ : (item?.evidence ??
177
+ item?.reason ??
178
+ item?.note ??
179
+ JSON.stringify(item)),
180
+ )
181
+ .map((item) => cleanText(item))
182
+ .filter(Boolean);
183
+ if (counter.length === 0 && !finding?.note) return [];
184
+ return [
185
+ "Caveat / counter-evidence:",
186
+ "",
187
+ ...(finding?.note ? [`- ${cleanText(finding.note)}`] : []),
188
+ ...counter.map((item) => `- ${item}`),
189
+ "",
190
+ ];
191
+ }
192
+
193
+ function normalizeFinding(finding, index, verdict) {
194
+ return {
195
+ ...finding,
196
+ findingId: findingIdOf(finding, index),
197
+ rootCauseId: rootCauseIdOf(finding),
198
+ title: titleOf(finding),
199
+ severity: severityOf(finding),
200
+ verdict,
201
+ };
202
+ }
203
+
204
+ function partitionFindings(partition) {
205
+ const keep = asArray(partition?.reportContext?.keep).map((finding, index) =>
206
+ normalizeFinding(finding, index, "KEEP"),
207
+ );
208
+ const weaken = asArray(partition?.reportContext?.weaken).map(
209
+ (finding, index) =>
210
+ normalizeFinding(finding, keep.length + index, "WEAKEN"),
211
+ );
212
+ return { keep, weaken, all: [...keep, ...weaken] };
213
+ }
214
+
215
+ function expectedFindingCount(partition, allFindings) {
216
+ const summary = partition?.partitionSummary;
217
+ const keep = Number(summary?.keep);
218
+ const weaken = Number(summary?.weaken);
219
+ if (Number.isFinite(keep) || Number.isFinite(weaken)) {
220
+ return (
221
+ (Number.isFinite(keep) ? keep : 0) +
222
+ (Number.isFinite(weaken) ? weaken : 0)
223
+ );
224
+ }
225
+ return allFindings.length;
226
+ }
227
+
228
+ function groupBySeverity(findings) {
229
+ const grouped = new Map();
230
+ for (const finding of findings) {
231
+ const severity = severityOf(finding);
232
+ if (!grouped.has(severity)) grouped.set(severity, []);
233
+ grouped.get(severity).push(finding);
234
+ }
235
+ return [...grouped.entries()].sort(
236
+ ([a], [b]) => severityRank(a) - severityRank(b) || a.localeCompare(b),
237
+ );
238
+ }
239
+
240
+ function severityCounts(findings) {
241
+ const counts = {};
242
+ for (const finding of findings) {
243
+ const severity = severityOf(finding);
244
+ counts[severity] = (counts[severity] ?? 0) + 1;
245
+ }
246
+ return counts;
247
+ }
248
+
249
+ function renderSeveritySummary(findings) {
250
+ const counts = severityCounts(findings);
251
+ if (Object.keys(counts).length === 0) return [];
252
+ return [
253
+ "## Finding summary",
254
+ "",
255
+ "| Severity | Count |",
256
+ "|---|---:|",
257
+ ...Object.entries(counts)
258
+ .sort(
259
+ ([a], [b]) => severityRank(a) - severityRank(b) || a.localeCompare(b),
260
+ )
261
+ .map(([severity, count]) => `| ${severity} | ${count} |`),
262
+ "",
263
+ ];
264
+ }
265
+
266
+ function renderFindingCard(finding) {
267
+ const locations = locationsOf(finding);
268
+ const quotes = evidenceQuotesOf(finding);
269
+ const out = [
270
+ `### ${finding.findingId} — ${finding.title}`,
271
+ "",
272
+ `Severity: **${finding.severity}** `,
273
+ ...(finding.rootCauseId
274
+ ? [`Root cause: \`${finding.rootCauseId}\` `]
275
+ : []),
276
+ ...(finding.verdict && finding.verdict !== "KEEP"
277
+ ? [`Verifier verdict: **${finding.verdict}** `]
278
+ : []),
279
+ "",
280
+ ...renderLocationsTable(locations),
281
+ ...renderEvidenceQuotes(quotes),
282
+ ];
283
+ const action = cleanText(
284
+ finding.recommendedAction ?? finding.concreteFix ?? "",
285
+ );
286
+ if (action) {
287
+ out.push("Recommended action:", "", action, "");
288
+ }
289
+ out.push(...renderCounterEvidence(finding));
290
+ return out;
291
+ }
292
+
293
+ function renderFindings(findings) {
294
+ if (findings.length === 0)
295
+ return [
296
+ "## Findings",
297
+ "",
298
+ "No kept or weakened findings were present in the partition ledger.",
299
+ "",
300
+ ];
301
+ const representedIds = findings.map((finding) => finding.findingId);
302
+ const out = [];
303
+ for (const [severity, group] of groupBySeverity(findings)) {
304
+ out.push(
305
+ `## ${severity[0].toUpperCase()}${severity.slice(1)} findings`,
306
+ "",
307
+ );
308
+ for (const finding of group) out.push(...renderFindingCard(finding));
309
+ }
310
+ return { lines: out, representedIds };
311
+ }
312
+
313
+ function renderNeedsHuman(partition) {
314
+ const items = asArray(partition?.reportContext?.needsHuman);
315
+ if (items.length === 0) return [];
316
+ const out = ["## Needs human review", ""];
317
+ for (const raw of items) {
318
+ const finding = normalizeFinding(raw, 0, "NEEDS_HUMAN");
319
+ out.push(
320
+ `- **${finding.severity}** ${finding.findingId} — ${finding.title}`,
321
+ );
322
+ }
323
+ out.push("");
324
+ return out;
325
+ }
326
+
327
+ function renderRisks(report, partition) {
328
+ const risks = asArray(report?.risks).map((risk) =>
329
+ typeof risk === "string"
330
+ ? risk
331
+ : (risk?.risk ?? risk?.note ?? risk?.summary ?? JSON.stringify(risk)),
332
+ );
333
+ const partialFailures = [
334
+ ...asArray(partition?.sourceStatusSummary?.partialFailures),
335
+ ...asArray(partition?.reportContext?.partialFailures),
336
+ ];
337
+ const notes = asArray(partition?.normalizationNotes).map((note) =>
338
+ typeof note === "string" ? note : JSON.stringify(note),
339
+ );
340
+ if (risks.length === 0 && partialFailures.length === 0 && notes.length === 0)
341
+ return [];
342
+ const out = ["## Risks and partial-review limitations", ""];
343
+ for (const risk of risks) out.push(`- ${cleanText(risk)}`);
344
+ for (const failure of partialFailures) {
345
+ out.push(
346
+ `- Partial source: ${cleanText(failure.displayName ?? failure.specId ?? failure.source ?? JSON.stringify(failure))} (${failure.status ?? "unknown"})`,
347
+ );
348
+ }
349
+ for (const note of notes)
350
+ out.push(`- Normalization note: ${cleanText(note)}`);
351
+ out.push("");
352
+ return out;
353
+ }
354
+
355
+ function stringifySummary(report) {
356
+ const summary = report?.summary;
357
+ if (typeof summary === "string" && cleanText(summary))
358
+ return cleanText(summary);
359
+ if (summary && typeof summary === "object") {
360
+ return cleanText(
361
+ summary.summary ??
362
+ report?.digest ??
363
+ summary.verdict ??
364
+ JSON.stringify(summary),
365
+ );
366
+ }
367
+ if (typeof report?.digest === "string" && report.digest.trim()) {
368
+ return cleanText(report.digest);
369
+ }
370
+ return "Deep review completed.";
371
+ }
372
+
373
+ function renderMarkdown({ report, partition, findingCountMismatch }) {
374
+ const { all } = partitionFindings(partition);
375
+ const sortedFindings = all.sort(
376
+ (a, b) =>
377
+ severityRank(a.severity) - severityRank(b.severity) ||
378
+ a.findingId.localeCompare(b.findingId),
379
+ );
380
+ const rendered = renderFindings(sortedFindings);
381
+ const representedIds = rendered.representedIds ?? [];
382
+ const lines = [
383
+ "# Deep review report",
384
+ "",
385
+ `Verdict: **${cleanText(report?.verdict ?? "review_complete") || "review_complete"}**`,
386
+ "",
387
+ "## Summary",
388
+ "",
389
+ stringifySummary(report),
390
+ "",
391
+ ...renderSeveritySummary(sortedFindings),
392
+ ];
393
+ if (findingCountMismatch) {
394
+ lines.push(
395
+ "## Renderer warning",
396
+ "",
397
+ "The deterministic renderer found a mismatch between expected findings from `partition-verdicts` and represented finding IDs. Inspect `partition-verdicts.control.json` before acting on this report.",
398
+ "",
399
+ );
400
+ }
401
+ lines.push(...(rendered.lines ?? rendered));
402
+ lines.push(...renderNeedsHuman(partition));
403
+ lines.push(...renderRisks(report, partition));
404
+ const nextAction = cleanText(report?.recommendedNextAction ?? "");
405
+ if (nextAction) {
406
+ lines.push("## Recommended next action", "", nextAction, "");
407
+ }
408
+ lines.push(
409
+ "## Evidence source",
410
+ "",
411
+ "Finding cards are rendered from deterministic `partition-verdicts.control.json`; summary/verdict/risk prose comes from `report.control.json` when available.",
412
+ "",
413
+ );
414
+ return {
415
+ markdown: lines
416
+ .join("\n")
417
+ .replace(/\n{3,}/g, "\n\n")
418
+ .trim(),
419
+ representedIds,
420
+ };
421
+ }
422
+
423
+ export default async function renderReviewReport({ sources, context = {} }) {
424
+ const partition = findSource(sources, "partition-verdicts");
425
+ const report = findSource(sources, "report") ?? {};
426
+ if (!partition || typeof partition !== "object") {
427
+ return {
428
+ schema: "deep-review-render-v1",
429
+ digest:
430
+ "Deep review rendering failed: missing partition-verdicts control source.",
431
+ status: "blocked",
432
+ blockers: ["missing partition-verdicts control source"],
433
+ markdown: "",
434
+ findingSummary: { total: 0, bySeverity: {} },
435
+ renderedFindingIds: [],
436
+ sourceArtifacts: [],
437
+ gates: {
438
+ renderedAllFindings: false,
439
+ findingCountMismatch: true,
440
+ passed: false,
441
+ },
442
+ };
443
+ }
444
+
445
+ const { all } = partitionFindings(partition);
446
+ const expected = expectedFindingCount(partition, all);
447
+ const findingCountMismatch = expected !== all.length;
448
+ const rendered = renderMarkdown({
449
+ report,
450
+ partition,
451
+ findingCountMismatch,
452
+ });
453
+ const bySeverity = severityCounts(all);
454
+ const renderedAllFindings = rendered.representedIds.length === all.length;
455
+ const passed = !findingCountMismatch && renderedAllFindings;
456
+
457
+ let sidecarPath;
458
+ try {
459
+ if (context.cwd && context.runId && context.taskId) {
460
+ const taskDir = join(
461
+ context.cwd,
462
+ ".pi",
463
+ "workflows",
464
+ context.runId,
465
+ "tasks",
466
+ context.taskId,
467
+ );
468
+ await mkdir(taskDir, { recursive: true });
469
+ sidecarPath = join(taskDir, "review.md");
470
+ await writeFile(sidecarPath, `${rendered.markdown}\n`, "utf8");
471
+ }
472
+ } catch {
473
+ // Sidecar is non-authoritative; keep control output deterministic.
474
+ }
475
+
476
+ return {
477
+ schema: "deep-review-render-v1",
478
+ digest: `Rendered ${all.length} findings: ${
479
+ Object.entries(bySeverity)
480
+ .sort(
481
+ ([a], [b]) => severityRank(a) - severityRank(b) || a.localeCompare(b),
482
+ )
483
+ .map(([severity, count]) => `${severity}=${count}`)
484
+ .join(", ") || "none"
485
+ }.`,
486
+ status: passed ? "passed" : "failed",
487
+ markdown: rendered.markdown,
488
+ findingSummary: { total: all.length, bySeverity },
489
+ renderedFindingIds: rendered.representedIds,
490
+ expectedFindingCount: expected,
491
+ sourceArtifacts: [
492
+ "partition-verdicts.control.json",
493
+ ...(report ? ["report.control.json"] : []),
494
+ ],
495
+ gates: {
496
+ renderedAllFindings,
497
+ findingCountMismatch,
498
+ passed,
499
+ },
500
+ ...(sidecarPath ? { sidecarPath } : {}),
501
+ };
502
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "type": "object",
3
+ "required": [
4
+ "schema",
5
+ "digest",
6
+ "status",
7
+ "markdown",
8
+ "findingSummary",
9
+ "renderedFindingIds",
10
+ "sourceArtifacts",
11
+ "gates"
12
+ ],
13
+ "properties": {
14
+ "schema": { "type": "string", "const": "deep-review-render-v1" },
15
+ "digest": { "type": "string", "minLength": 1 },
16
+ "status": { "type": "string", "enum": ["passed", "failed", "blocked"] },
17
+ "blockers": { "type": "array", "items": { "type": "string" } },
18
+ "markdown": { "type": "string" },
19
+ "findingSummary": {
20
+ "type": "object",
21
+ "required": ["total", "bySeverity"],
22
+ "properties": {
23
+ "total": { "type": "number" },
24
+ "bySeverity": { "type": "object", "additionalProperties": { "type": "number" } }
25
+ },
26
+ "additionalProperties": true
27
+ },
28
+ "renderedFindingIds": {
29
+ "type": "array",
30
+ "items": { "type": "string" }
31
+ },
32
+ "expectedFindingCount": { "type": "number" },
33
+ "sourceArtifacts": {
34
+ "type": "array",
35
+ "items": { "type": "string" }
36
+ },
37
+ "gates": {
38
+ "type": "object",
39
+ "required": ["renderedAllFindings", "findingCountMismatch", "passed"],
40
+ "properties": {
41
+ "renderedAllFindings": { "type": "boolean" },
42
+ "findingCountMismatch": { "type": "boolean" },
43
+ "passed": { "type": "boolean" }
44
+ },
45
+ "additionalProperties": true
46
+ },
47
+ "sidecarPath": { "type": "string" }
48
+ },
49
+ "additionalProperties": true
50
+ }
@@ -152,7 +152,28 @@
152
152
  "maxDigestChars": 1200,
153
153
  "controlSchema": "./schemas/deep-review-report-control.schema.json"
154
154
  },
155
- "prompt": "Use Source Stage Context and the runtime task to produce an evidence-backed deep review synthesis. Prefer the partition-verdicts controlProjection.reportContext as the authoritative finding ledger when it is present; use workflow_artifact reads only when required fields are missing or debug detail is needed. The partition-verdicts source already applied devil-advocate verdicts deterministically: reportContext.keep, reportContext.weaken, reportContext.needsHuman, and reportContext.supportNoteSummaries summarize the final ledger. Include every reportContext.keep item as a finding with its findingId, rootCauseId, and severity fields copied verbatim; the severity join is code-enforced upstream, so never change a keep item's severity. If a keep/weaken item has mergedFindingIds or merged provenance, treat them as provenance for the same root defect, not as separate findings. Each keep and weaken item carries code-preserved findingId, rootCauseId, locations, and evidenceQuotes arrays; copy each finding's findingId, rootCauseId, locations, and evidenceQuotes into your finding verbatim and never drop the line numbers, symbols, or exact quote strings they contain. Include reportContext.weaken items with reduced severity, citing the counterEvidence that justifies each reduction. Exclude drop items from findings. List reportContext.needsHuman items under needsHuman. The support helper may also provide supportNoteSummaries for test gaps, stale comments/docs, or dead-code symptoms related to a root finding; do not promote support notes to findings, but summarize them in risks or evidenceIndex as supporting context. Report partitionSummary counts and any normalizationNotes in risks so silent verdict drift is visible. If sourceStatusSummary/reportContext.partialFailures or any source manifest entry shows non-completed upstream work, mention the partial-review limitation in risks and be conservative about unreviewed scopes. Put machine-readable JSON in <control> with summary, verdict, findings, risks, needsHuman, evidenceIndex, and recommendedNextAction. Each finding must include findingId, rootCauseId, title, severity, locations, and evidenceQuotes copied from its keep/weaken item. Put detailed prose and evidence discussion in <analysis>."
155
+ "prompt": "Use Source Stage Context and the runtime task to produce an evidence-backed deep review synthesis. Prefer the partition-verdicts controlProjection.reportContext as the authoritative finding ledger when it is present; use workflow_artifact reads only when required fields are missing or debug detail is needed. The partition-verdicts source already applied devil-advocate verdicts deterministically: reportContext.keep, reportContext.weaken, reportContext.needsHuman, and reportContext.supportNoteSummaries summarize the final ledger. Include every reportContext.keep item as a finding with its findingId, rootCauseId, and severity fields copied verbatim; the severity join is code-enforced upstream, so never change a keep item's severity. If a keep/weaken item has mergedFindingIds or merged provenance, treat them as provenance for the same root defect, not as separate findings. Each keep and weaken item carries code-preserved findingId, rootCauseId, locations, and evidenceQuotes arrays; copy each finding's findingId, rootCauseId, locations, and evidenceQuotes into your finding verbatim and never drop the line numbers, symbols, or exact quote strings they contain. Include reportContext.weaken items with their ledger severity copied verbatim and cite the counterEvidence that justifies the WEAKEN verdict; do not invent a reduced severity unless the partition-verdicts ledger already persisted one. Exclude drop items from findings. List reportContext.needsHuman items under needsHuman. The support helper may also provide supportNoteSummaries for test gaps, stale comments/docs, or dead-code symptoms related to a root finding; do not promote support notes to findings, but summarize them in risks or evidenceIndex as supporting context. Report partitionSummary counts and any normalizationNotes in risks so silent verdict drift is visible. If sourceStatusSummary/reportContext.partialFailures or any source manifest entry shows non-completed upstream work, mention the partial-review limitation in risks and be conservative about unreviewed scopes. Put machine-readable JSON in <control> with summary, verdict, findings, risks, needsHuman, evidenceIndex, and recommendedNextAction. Each finding must include findingId, rootCauseId, title, severity, locations, and evidenceQuotes copied from its keep/weaken item. Put detailed prose and evidence discussion in <analysis>."
156
+ },
157
+ {
158
+ "id": "final",
159
+ "from": [
160
+ "report",
161
+ "partition-verdicts"
162
+ ],
163
+ "sourcePolicy": "partial",
164
+ "output": {
165
+ "analysis": {
166
+ "required": true
167
+ },
168
+ "refs": {
169
+ "required": true
170
+ },
171
+ "maxDigestChars": 1200,
172
+ "controlSchema": "./schemas/deep-review-render-control.schema.json"
173
+ },
174
+ "support": {
175
+ "uses": "./helpers/render-review-report.mjs"
176
+ }
156
177
  }
157
178
  ]
158
179
  }