@agwab/pi-workflow 0.1.0 → 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.
- package/README.md +14 -3
- package/agents/researcher.md +17 -7
- package/dist/artifact-graph-runtime.js +1 -0
- package/dist/compiler.js +2 -2
- package/dist/dynamic-generated-task-runtime.js +4 -3
- package/dist/dynamic-runtime-bundle.js +3 -2
- package/dist/extension.js +40 -1
- package/dist/subagent-backend.js +82 -27
- package/dist/tool-metadata.d.ts +1 -0
- package/dist/tool-metadata.js +13 -1
- package/dist/workflow-artifact-extension.js +3 -2
- package/dist/workflow-artifact-tool.js +84 -4
- package/dist/workflow-web-source-extension.d.ts +43 -0
- package/dist/workflow-web-source-extension.js +1194 -0
- package/dist/workflow-web-source.d.ts +171 -0
- package/dist/workflow-web-source.js +897 -0
- package/docs/usage.md +32 -45
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
- package/package.json +3 -4
- package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
- package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
- package/src/artifact-graph-runtime.ts +1 -0
- package/src/compiler.ts +2 -1
- package/src/dynamic-generated-task-runtime.ts +4 -2
- package/src/dynamic-runtime-bundle.ts +3 -2
- package/src/extension.ts +46 -1
- package/src/subagent-backend.ts +121 -37
- package/src/tool-metadata.ts +22 -1
- package/src/workflow-artifact-extension.ts +3 -2
- package/src/workflow-artifact-tool.ts +96 -4
- package/src/workflow-web-source-extension.ts +1411 -0
- package/src/workflow-web-source.ts +1171 -0
- package/workflows/README.md +1 -1
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
- package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
- package/workflows/deep-research/helpers/render-executive.mjs +571 -198
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
- package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
- package/workflows/deep-research/spec.json +36 -21
- package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
- package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
- package/workflows/deep-review/spec.json +22 -1
- package/docs/release.md +0 -89
- package/node_modules/@pondwader/socks5-server/.DS_Store +0 -0
- package/node_modules/commander/.DS_Store +0 -0
- package/node_modules/jiti/.DS_Store +0 -0
- package/node_modules/node-forge/.DS_Store +0 -0
- package/node_modules/shell-quote/.DS_Store +0 -0
- package/node_modules/zod/.DS_Store +0 -0
|
@@ -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
|
|
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
|
}
|
package/docs/release.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# pi-workflow release process
|
|
2
|
-
|
|
3
|
-
This repository releases `@agwab/pi-workflow` through the GitHub Actions **Publish** workflow. Local commands validate and dispatch the approved workflow; they should not replace the CI publish path.
|
|
4
|
-
|
|
5
|
-
## Rules
|
|
6
|
-
|
|
7
|
-
Do not publish, tag, push a release commit, or dispatch release automation until the exact release plan is approved. The plan must include:
|
|
8
|
-
|
|
9
|
-
- target version
|
|
10
|
-
- semver rationale
|
|
11
|
-
- changelog/release notes
|
|
12
|
-
- validation to run
|
|
13
|
-
- publish target (`@agwab/pi-workflow` on npm)
|
|
14
|
-
- downstream dependency/lockfile impact, especially bundled `@agwab/pi-subagent` and `pi-web-access`
|
|
15
|
-
|
|
16
|
-
Version bumps are release-task-only. The Publish workflow runs `npm version <target>` and pushes the release commit/tag after validation.
|
|
17
|
-
|
|
18
|
-
## Local validation
|
|
19
|
-
|
|
20
|
-
Run the full release gate before asking for final approval:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm run release:check
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
`release:check` verifies package metadata, npm login, unpublished version, script syntax, typecheck, unit tests, e2e smoke, build, package contents, local-path/secret-like text, and `npm publish --dry-run`.
|
|
27
|
-
|
|
28
|
-
For normal CI-style validation without npm login or unpublished-version checks, run:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm run validate
|
|
32
|
-
npm run e2e
|
|
33
|
-
npm run pack:dry
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Dispatching a release
|
|
37
|
-
|
|
38
|
-
After approval and after `main` is clean and pushed:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm run release:dispatch -- <version> --watch
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The dispatch script checks that:
|
|
45
|
-
|
|
46
|
-
- the current branch is `main`
|
|
47
|
-
- the working tree is clean
|
|
48
|
-
- `HEAD` matches `origin/main`
|
|
49
|
-
- the target version is semver-like
|
|
50
|
-
|
|
51
|
-
Then it dispatches `.github/workflows/publish.yml` with `version=<version>`.
|
|
52
|
-
|
|
53
|
-
## GitHub Actions
|
|
54
|
-
|
|
55
|
-
### CI
|
|
56
|
-
|
|
57
|
-
`.github/workflows/ci.yml` runs on pull requests and pushes to `main`:
|
|
58
|
-
|
|
59
|
-
1. `npm ci --legacy-peer-deps`
|
|
60
|
-
2. `npm run validate`
|
|
61
|
-
3. `npm run e2e`
|
|
62
|
-
4. `npm run pack:dry`
|
|
63
|
-
|
|
64
|
-
### Publish
|
|
65
|
-
|
|
66
|
-
`.github/workflows/publish.yml` is manual-only (`workflow_dispatch`) and only runs on `main`. It:
|
|
67
|
-
|
|
68
|
-
1. installs dependencies with npm
|
|
69
|
-
2. configures the GitHub Actions bot git author
|
|
70
|
-
3. runs `npm version <target>` if needed and creates `v<target>`
|
|
71
|
-
4. checks whether `@agwab/pi-workflow@<target>` already exists on npm
|
|
72
|
-
5. runs `npm run release:check` for unpublished versions
|
|
73
|
-
6. runs validation-only checks for already-published versions
|
|
74
|
-
7. publishes to npm with provenance when needed
|
|
75
|
-
8. pushes the release commit and tag when needed
|
|
76
|
-
9. creates a GitHub release when missing
|
|
77
|
-
|
|
78
|
-
## Post-release verification
|
|
79
|
-
|
|
80
|
-
After the workflow completes, verify and report:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
npm view @agwab/pi-workflow@<version> version
|
|
84
|
-
gh release view v<version>
|
|
85
|
-
git fetch origin --tags
|
|
86
|
-
git rev-parse v<version>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Also report the GitHub Actions run URL and any skipped steps, such as publish skipped because the npm version already existed.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|