@mcrescenzo/opencode-workflows 0.1.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 (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CODE_OF_CONDUCT.md +134 -0
  3. package/CONTRIBUTING.md +95 -0
  4. package/LICENSE +21 -0
  5. package/README.md +746 -0
  6. package/SECURITY.md +38 -0
  7. package/commands/repo-bughunt.md +94 -0
  8. package/commands/repo-review.md +148 -0
  9. package/commands/workflow-live-gates-release-check.md +143 -0
  10. package/docs/workflow-plugin.md +400 -0
  11. package/opencode-workflows.js +5 -0
  12. package/package.json +86 -0
  13. package/skills/opencode-workflow-authoring/SKILL.md +180 -0
  14. package/skills/repo-review-command-protocol/SKILL.md +56 -0
  15. package/skills/workflow-model-tiering/SKILL.md +57 -0
  16. package/skills/workflow-plan-review/SKILL.md +91 -0
  17. package/workflow-kernel/approval-hashing.js +39 -0
  18. package/workflow-kernel/async-util.js +33 -0
  19. package/workflow-kernel/audited-shell-policy.js +200 -0
  20. package/workflow-kernel/authority-policy.js +670 -0
  21. package/workflow-kernel/budget-accounting.js +142 -0
  22. package/workflow-kernel/capability-adapter.js +753 -0
  23. package/workflow-kernel/child-agent-runner.js +1264 -0
  24. package/workflow-kernel/constants.js +117 -0
  25. package/workflow-kernel/diagnostics.js +152 -0
  26. package/workflow-kernel/drain-runtime.js +421 -0
  27. package/workflow-kernel/errors.js +181 -0
  28. package/workflow-kernel/event-journal.js +487 -0
  29. package/workflow-kernel/extension-registry.js +144 -0
  30. package/workflow-kernel/free-text-redactor.js +91 -0
  31. package/workflow-kernel/gate-shapes.js +82 -0
  32. package/workflow-kernel/git-util.js +45 -0
  33. package/workflow-kernel/index.js +72 -0
  34. package/workflow-kernel/integration-mode.js +155 -0
  35. package/workflow-kernel/lane-effort-policy.js +134 -0
  36. package/workflow-kernel/lifecycle-control.js +608 -0
  37. package/workflow-kernel/live-gate-probes.js +916 -0
  38. package/workflow-kernel/notification-toast-cards.js +393 -0
  39. package/workflow-kernel/notification-toast-policy.js +179 -0
  40. package/workflow-kernel/notification-toast-scope.js +100 -0
  41. package/workflow-kernel/notification-toast.js +287 -0
  42. package/workflow-kernel/path-policy.js +219 -0
  43. package/workflow-kernel/result-readback.js +106 -0
  44. package/workflow-kernel/role-template-loading.js +606 -0
  45. package/workflow-kernel/run-context.js +139 -0
  46. package/workflow-kernel/run-observability.js +43 -0
  47. package/workflow-kernel/run-store-fs.js +231 -0
  48. package/workflow-kernel/run-store-locks.js +180 -0
  49. package/workflow-kernel/run-store-projections.js +421 -0
  50. package/workflow-kernel/run-store-rehydrate.js +68 -0
  51. package/workflow-kernel/run-store-state.js +147 -0
  52. package/workflow-kernel/run-store-status-format.js +1154 -0
  53. package/workflow-kernel/run-store-status.js +107 -0
  54. package/workflow-kernel/sandbox-executor.js +1131 -0
  55. package/workflow-kernel/session-access.js +56 -0
  56. package/workflow-kernel/structured-output.js +143 -0
  57. package/workflow-kernel/test-fix-drain-adapter.js +119 -0
  58. package/workflow-kernel/text-json.js +136 -0
  59. package/workflow-kernel/workflow-plugin.js +3017 -0
  60. package/workflow-kernel/workflow-source.js +444 -0
  61. package/workflow-kernel/worktree-adapter.js +256 -0
  62. package/workflow-kernel/worktree-git.js +147 -0
  63. package/workflows/repo-bughunt.js +362 -0
  64. package/workflows/repo-cleanup.js +383 -0
  65. package/workflows/repo-complexity.js +404 -0
  66. package/workflows/repo-deps.js +457 -0
  67. package/workflows/repo-modernize.js +395 -0
  68. package/workflows/repo-perf.js +394 -0
  69. package/workflows/repo-review.js +831 -0
  70. package/workflows/repo-security-audit.js +466 -0
  71. package/workflows/repo-test-gaps.js +377 -0
@@ -0,0 +1,831 @@
1
+ // META orchestrator workflow — /repo-review. Runs the eight repo-* LEAF engines via
2
+ // static-literal workflow() calls (one nesting level — legal because each engine is a
3
+ // leaf that never nests), computes shared recon ONCE, then normalizes + conservatively
4
+ // merges + ranks their findings into ONE cross-domain report. Report-only.
5
+ //
6
+ // Contract: docs/repo-review-leaf-contract.md §14 (Meta-to-leaf arg contract).
7
+ // Recon is computed ONCE and the SAME recon+paths+exclude+depth object reference is
8
+ // injected into every literal one-level workflow("repo-X", leafArgs) call, so all eight
9
+ // domains analyze one coherent file inventory and cross-domain dedupe stays consistent.
10
+ //
11
+ // Authority: profile "read-only-review" (readOnly, requiredGates:[]). The meta NEVER
12
+ // calls materialize, drain, workflow_apply, git, or Beads mutation, and NEVER writes a
13
+ // file — the QuickJS guest cannot. reportPath is always null; the command wrapper
14
+ // persists the report (engine-vs-wrapper reversal, contract §1).
15
+ //
16
+ // Budget: nested workflow() lanes run INSIDE this run and share its maxAgents/concurrency;
17
+ // a nested leaf's own declared maxAgents/concurrency is IGNORED at runtime. Parent
18
+ // maxAgents=100000 intentionally over-provisions (effectively unlimited) one recon lane, the coverage auditor, plus
19
+ // the cumulative cold-run fan-out of all eight leaves under batched parallel()
20
+ // orchestration (≈50 finder lanes for an empty run, ≈150+ for a thorough-depth exhaustive
21
+ // run with skeptics, and potentially far more in large repos). concurrency=16 is the
22
+ // kernel's hard cap and bounds peak concurrent child sessions across the whole tree. An
23
+ // exhaustive thorough run may still budget-stop gracefully if a caller overrides a smaller
24
+ // parent budget: a stopped lane returns null (onFailure-equivalent try/catch), is dropped
25
+ // by .filter(Boolean), and surfaces as partial coverage + a materialization blocker rather
26
+ // than a crash.
27
+ //
28
+ // EXHAUSTIVE-BY-DEFAULT (rrev.27): empty args default to mode:"exhaustive", which selects
29
+ // depth:"thorough", a higher maxReturnFindings, and a coverage-auditor lane. Pass
30
+ // mode:"bounded" for the legacy normal-depth behavior. The read-only boundary is unchanged:
31
+ // the meta NEVER mutates Beads, applies diffs, or writes files. A separate, explicitly-
32
+ // approved review-materialize flow consumes the report ONLY when materializationReady is true.
33
+ export const meta = {
34
+ name: "repo-review",
35
+ description: "Comprehensive repo review meta: runs all eight repo-* domain engines (bughunt, security, test-gaps, cleanup, modernize, perf, complexity, deps) with ONE shared recon, then normalizes + conservatively merges + ranks findings cross-domain into a single report. Exhaustive by default (thorough depth + coverage auditor); report-only, nothing is applied.",
36
+ profile: "read-only-review",
37
+ maxAgents: 100000,
38
+ concurrency: 16,
39
+ phases: ["recon", "domains", "merge", "audit", "synthesize"],
40
+ category: "repo-review-meta",
41
+ notes: "Read-only whole-repo review. Defaults to EXHAUSTIVE (thorough depth, high maxReturnFindings, coverage auditor); pass mode:'bounded' for the legacy normal-depth pass. Emits materializationReady/materializationBlockers so a separately-approved materialize flow can refuse incomplete reports. Model tier labels (fast/deep) are lane INTENT only — the command maps both to the same deep model so no fast model is ever selected.",
42
+ examples: [
43
+ { label: "exhaustive full review (default)", args: { mode: "exhaustive", paths: ["src"] } },
44
+ { label: "bounded legacy review", args: { mode: "bounded", depth: "normal", paths: ["src"] } },
45
+ { label: "focused security and bugs (exhaustive)", args: { mode: "exhaustive", domains: ["bughunt", "security"], paths: ["src", "tests"], batchSize: 2 } },
46
+ ],
47
+ argsSchema: {
48
+ type: ["object", "string", "null"],
49
+ properties: {
50
+ mode: { type: "string", enum: ["exhaustive", "bounded"] },
51
+ paths: { type: "array", items: { type: "string" } },
52
+ exclude: { type: "array", items: { type: "string" } },
53
+ depth: { type: "string", enum: ["quick", "normal", "thorough"] },
54
+ domains: { type: "array", items: { type: "string", enum: ["bughunt", "security", "test-gaps", "cleanup", "modernize", "perf", "complexity", "deps", "repo-bughunt", "repo-security-audit", "repo-test-gaps", "repo-cleanup", "repo-modernize", "repo-perf", "repo-complexity", "repo-deps"] } },
55
+ batchSize: { type: "integer", minimum: 1 },
56
+ maxReturnFindings: { type: "integer", minimum: 1 },
57
+ maxDirs: { type: "integer", minimum: 1 },
58
+ deepMode: { type: "string", enum: ["static", "audited-shell", "network-advisory"] },
59
+ },
60
+ },
61
+ };
62
+
63
+ // ---- suite identity ----
64
+ const DOMAIN = "repo-review";
65
+ const SCHEMA_VERSION = 1;
66
+
67
+ // args may arrive as an object (workflow_run args) or, defensively, a JSON string.
68
+ let RT = args;
69
+ if (typeof RT === "string") { try { RT = RT.trim() ? JSON.parse(RT) : {}; } catch (error) { throw new Error(`Invalid repo-review runtime args JSON: ${error.message}`); } }
70
+ if (!RT || typeof RT !== "object" || Array.isArray(RT)) RT = {};
71
+
72
+ // ---- args ----
73
+ const paths = Array.isArray(RT.paths) && RT.paths.length ? RT.paths : ["."];
74
+ const exclude = Array.isArray(RT.exclude) ? RT.exclude : ["node_modules", "dist", "build", ".git", "vendor", "target", "*.min.*", "*.map"];
75
+ // EXHAUSTIVE-BY-DEFAULT: empty args select exhaustive mode, which drives thorough depth and a
76
+ // high maxReturnFindings. mode:"bounded" preserves the legacy normal-depth behavior.
77
+ const mode = RT.mode === "bounded" ? "bounded" : "exhaustive";
78
+ const exhaustive = mode === "exhaustive";
79
+ const depth = ["quick", "normal", "thorough"].includes(RT.depth) ? RT.depth : (exhaustive ? "thorough" : "normal");
80
+ // batchSize bounds how many leaves run per parallel() batch (peak-concurrency control),
81
+ // mirroring the Claude meta. Positive integer only (0/neg would infinite-loop the batcher).
82
+ // Default = all active domains in ONE batch (scale: serialize the eight leaves into a single
83
+ // batched parallel() call rather than four batches of two).
84
+ const maxReturnFindings = Number.isInteger(RT.maxReturnFindings) && RT.maxReturnFindings > 0 ? RT.maxReturnFindings : 1000000;
85
+ // maxDirs (the complexity per-domain carve-out) is forwarded to repo-complexity so the meta's
86
+ // scale decision reaches the leaf instead of it silently clamping to a low local default.
87
+ const maxDirs = Number.isInteger(RT.maxDirs) && RT.maxDirs > 0 ? RT.maxDirs : 1000000;
88
+
89
+ // Domain filter: the eight leaf domains. Accept either the bare domain ("bughunt") or the
90
+ // leaf name ("repo-bughunt"); normalize by stripping a leading "repo-". Default = all eight.
91
+ const ALL_DOMAINS = ["bughunt", "security", "test-gaps", "cleanup", "modernize", "perf", "complexity", "deps"];
92
+ function normDomain(d) { return typeof d === "string" ? d.replace(/^repo-/, "").trim() : ""; }
93
+ const suppliedDomains = Array.isArray(RT.domains) && RT.domains.length ? RT.domains.map(normDomain) : [];
94
+ const requestedDomains = suppliedDomains.filter((d) => ALL_DOMAINS.includes(d));
95
+ // Reject unknown-only domains with a clear error instead of silently falling back to ALL_DOMAINS.
96
+ // A mix of known+unknown is tolerated (unknown ones are dropped); only an ALL-unknown request is fatal.
97
+ if (suppliedDomains.length && requestedDomains.length === 0) {
98
+ throw new Error(`repo-review: all requested domains are unknown. Supplied: ${JSON.stringify(RT.domains)}. Known domains: ${JSON.stringify(ALL_DOMAINS)}.`);
99
+ }
100
+ const want = new Set(requestedDomains.length ? requestedDomains : ALL_DOMAINS);
101
+ const activeDomains = ALL_DOMAINS.filter((d) => want.has(d));
102
+ const batchSize = Number.isInteger(RT.batchSize) && RT.batchSize > 0 ? RT.batchSize : activeDomains.length;
103
+
104
+ const scope = `Scope: paths = ${JSON.stringify(paths)}. Exclude (do not scan/report): ${JSON.stringify(exclude)}.`;
105
+
106
+ // DEEP MODE (iui1.7): repo-review is static + read-only by default. audited-shell and
107
+ // network-advisory are OPTIONAL, explicitly opt-in deep modes that require VERIFIED gates
108
+ // (enforced at the kernel/command level when a caller passes profile:"inspect-with-shell" etc.).
109
+ // The QuickJS guest itself NEVER requests shell or network — it stays static. This field reports
110
+ // what was REQUESTED so the envelope + report disclose the (intentionally static) coverage limit.
111
+ const deepModeRequested = ["audited-shell", "network-advisory"].includes(RT.deepMode) ? RT.deepMode : "static";
112
+ const deepMode = {
113
+ requested: deepModeRequested,
114
+ active: "static",
115
+ shellCoverage: "none",
116
+ coverageLimitations: deepModeRequested === "static"
117
+ ? "Static, read-only analysis (no shell/git-churn, no network advisory lookups). Optional audited-shell / network-advisory modes require verified gates."
118
+ : `${deepModeRequested} requested but the review engine runs static in-guest; the deep mode requires verified gates enforced at the kernel/command level.`,
119
+ };
120
+
121
+ // ---- standardized return envelope ----
122
+ function envelope(status, extra) {
123
+ return { domain: DOMAIN, schemaVersion: SCHEMA_VERSION, status, abortReason: null, reportPath: null, ...extra };
124
+ }
125
+ const emptyCounts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
126
+
127
+ // ---- shared recon schema (tolerates a prose string via formatRecon) ----
128
+ const RECON_SCHEMA = {
129
+ type: "object", additionalProperties: false,
130
+ properties: {
131
+ languages: { type: "array", items: { type: "string" } },
132
+ frameworks: { type: "array", items: { type: "string" } },
133
+ packageManagers: { type: "array", items: { type: "string" } },
134
+ entryPoints: { type: "array", items: { type: "string" } },
135
+ testLayout: { type: "string" },
136
+ buildTooling: { type: "string" },
137
+ concurrencyModel: { type: "string" },
138
+ errorHandling: { type: "string" },
139
+ externalResources: { type: "array", items: { type: "string" } },
140
+ notes: { type: "string" },
141
+ },
142
+ required: ["languages", "notes"],
143
+ };
144
+ function formatRecon(r) {
145
+ if (typeof r === "string") return r;
146
+ if (!r || typeof r !== "object") return "No recon available.";
147
+ const L = (k, v) => (v && (Array.isArray(v) ? v.length : true) ? `${k}: ${Array.isArray(v) ? v.join(", ") : v}` : null);
148
+ return [
149
+ L("Languages", r.languages), L("Frameworks", r.frameworks), L("Package managers", r.packageManagers),
150
+ L("Entry points", r.entryPoints), L("Test layout", r.testLayout), L("Build tooling", r.buildTooling),
151
+ L("Concurrency model", r.concurrencyModel), L("Error handling", r.errorHandling),
152
+ L("External resources", r.externalResources), L("Notes", r.notes),
153
+ ].filter(Boolean).join("\n");
154
+ }
155
+
156
+ // ---- 1. Shared recon (computed ONCE for all domains; or accept an injected args.recon) ----
157
+ await phase("recon");
158
+ // Deterministic file inventory + sharding (iui1.5): a bounded, sorted fs walk produces a real
159
+ // file manifest so coverage is NOT agent-discovered ("Explore with your tools"). Large repos are
160
+ // partitioned into shards (by source root, capped at shardSize files each); a shard ledger tracks
161
+ // coverage so a missed/failed shard blocks materialization. The QuickJS guest has no fs, so
162
+ // inventoryFiles is a kernel host op. inventoryFailed or any shardMissed is a materialization blocker.
163
+ let inventory = null;
164
+ try {
165
+ inventory = await inventoryFiles({ paths, exclude, shardSize: 2000 });
166
+ } catch (e) {
167
+ inventory = { ok: false, error: String((e && e.message) || e), manifest: null, shards: [], partial: false };
168
+ }
169
+ const inventoryReady = !!(inventory && inventory.ok);
170
+ const manifest = (inventory && inventory.manifest) || null;
171
+ const inventoryShards = (inventory && inventory.shards) || [];
172
+ // Shard ledger: one entry per shard. Status is finalized after the domain phase (a shard is
173
+ // "completed" when at least one domain leaf ran; "missed" when every domain failed so the shard
174
+ // received zero review coverage). A partial inventory (hit the file cap) leaves a coverage gap.
175
+ const shardLedger = inventoryShards.map((s) => ({ id: s.id, root: s.root, fileCount: s.fileCount, languages: s.languages, status: "expected" }));
176
+ if (inventory && inventory.ok) {
177
+ await log(`Inventory: ${manifest.totalFiles} files, ${inventoryShards.length} shard(s) across ${manifest.sourceRoots.length} source root(s)${inventory.partial ? " (PARTIAL — hit file cap)" : ""}.`);
178
+ } else {
179
+ await log(`WARNING: file inventory failed — ${inventory && inventory.error}. Coverage accounting degraded.`);
180
+ }
181
+ const manifestBrief = manifest
182
+ ? `Deterministic file inventory (grounding, not a suggestion): ${manifest.totalFiles} files across ${manifest.sourceRoots.length} source root(s) (${manifest.sourceRoots.slice(0, 12).join(", ")}${manifest.sourceRoots.length > 12 ? ", ..." : ""}); languages: ${Object.entries(manifest.byLanguage).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}(${v})`).join(", ") || "none"}; file roles: ${Object.entries(manifest.byRole).map(([k, v]) => `${k}(${v})`).join(", ") || "none"}; sharded into ${inventoryShards.length} shard(s)${inventory.partial ? " (PARTIAL inventory — some files beyond the cap were not enumerated)" : ""}.`
183
+ : "No deterministic file inventory available; profile from what you can observe.";
184
+
185
+ let recon;
186
+ if (RT.recon !== undefined && RT.recon !== null) {
187
+ recon = RT.recon;
188
+ await log("Using injected shared recon (args.recon present) — every leaf skips self-profiling.");
189
+ } else {
190
+ recon = await agent(
191
+ ["Profile this repository once for a comprehensive multi-domain review (bugs, security, test gaps, cleanliness, complexity, deps, modernization, performance). Return the structured recon fields.",
192
+ scope,
193
+ manifestBrief,
194
+ "Report: languages/frameworks; package managers; entry points; test layout; build tooling; concurrency model; error-handling conventions; external resources (DB, files, network); and notes on anything relevant to reviewing this repo.",
195
+ "Use the deterministic inventory above as the file-set ground truth; only explore further for semantics the file list cannot reveal."].join("\n\n"),
196
+ { label: "recon", schema: RECON_SCHEMA, tier: "fast", onFailure: "returnNull" },
197
+ );
198
+ if (!recon || typeof recon !== "object") {
199
+ await log("WARNING: shared recon returned null/invalid — each leaf will self-profile (~8x recon cost).");
200
+ }
201
+ }
202
+
203
+ // ONE shared args object: the SAME reference is injected into every literal one-level
204
+ // workflow("repo-X", leafArgs) call (contract §14.2). Leaves skip self-profiling because
205
+ // recon is present. maxReturnFindings (effectively-unlimited ceiling) and maxDirs (the
206
+ // complexity per-domain carve-out) are forwarded so the meta's scale decisions reach every
207
+ // leaf instead of each leaf silently defaulting to a low local cap.
208
+ const leafArgs = { recon, paths, exclude, depth, maxReturnFindings, maxDirs };
209
+
210
+ // ---- 2. Run the eight leaf engines (static literals only, batched to bound peak concurrency) ----
211
+ // Each leaf is a LEAF (never nests), so this is one nesting level. workflow() does not take
212
+ // an onFailure option, so each literal call is wrapped in try/catch: a failed/budget-stopped
213
+ // leaf resolves to null and is dropped by .filter(Boolean) — it never aborts the meta.
214
+ async function runBughunt(a, w) { if (!w.has("bughunt")) return null; try { return await workflow("repo-bughunt", a); } catch (e) { return { domain: "bughunt", __error: String((e && e.message) || e) }; } }
215
+ async function runSecurity(a, w) { if (!w.has("security")) return null; try { return await workflow("repo-security-audit", a); } catch (e) { return { domain: "security", __error: String((e && e.message) || e) }; } }
216
+ async function runTestGaps(a, w) { if (!w.has("test-gaps")) return null; try { return await workflow("repo-test-gaps", a); } catch (e) { return { domain: "test-gaps", __error: String((e && e.message) || e) }; } }
217
+ async function runCleanup(a, w) { if (!w.has("cleanup")) return null; try { return await workflow("repo-cleanup", a); } catch (e) { return { domain: "cleanup", __error: String((e && e.message) || e) }; } }
218
+ async function runModernize(a, w) { if (!w.has("modernize")) return null; try { return await workflow("repo-modernize", a); } catch (e) { return { domain: "modernize", __error: String((e && e.message) || e) }; } }
219
+ async function runPerf(a, w) { if (!w.has("perf")) return null; try { return await workflow("repo-perf", a); } catch (e) { return { domain: "perf", __error: String((e && e.message) || e) }; } }
220
+ async function runComplexity(a, w) { if (!w.has("complexity")) return null; try { return await workflow("repo-complexity", a); } catch (e) { return { domain: "complexity", __error: String((e && e.message) || e) }; } }
221
+ async function runDeps(a, w) { if (!w.has("deps")) return null; try { return await workflow("repo-deps", a); } catch (e) { return { domain: "deps", __error: String((e && e.message) || e) }; } }
222
+
223
+ // Ordered runner list (one entry per static-literal leaf). Slicing this into batchSize
224
+ // groups keeps the workflow() call sites static literals while bounding peak concurrency.
225
+ const LEAF_RUNNERS = [
226
+ runBughunt, runSecurity, runTestGaps, runCleanup, runModernize, runPerf, runComplexity, runDeps,
227
+ ];
228
+
229
+ await phase("domains");
230
+ const rawCoverage = [];
231
+ for (let i = 0; i < LEAF_RUNNERS.length; i += batchSize) {
232
+ const batchRunners = LEAF_RUNNERS.slice(i, i + batchSize);
233
+ await log(`Domains batch ${Math.floor(i / batchSize) + 1}: ${batchRunners.map((fn) => fn.name.replace(/^run/, "").toLowerCase()).join(", ")}`);
234
+ // arity-1 thunks (api) => ... so parallel() runs them CONCURRENTLY (zero-arg thunks fail unless { sequential: true } is explicit).
235
+ const got = await parallel(batchRunners.map((fn) => (api) => fn(leafArgs, want)));
236
+ for (const res of got) if (res) rawCoverage.push(res);
237
+ }
238
+
239
+ // Normalize raw leaf outputs into a DomainOutcome ledger (defensive against malformed leaves).
240
+ const EXTRAS_KEYS = ["staleDocs", "migrationPlan", "upgradePlan", "shellCoverage", "coverageLimitations"];
241
+ const leafOutcomes = [];
242
+ const domainExtras = {};
243
+ for (const env of rawCoverage) {
244
+ if (!env || typeof env !== "object") continue;
245
+ const dom = typeof env.domain === "string" ? env.domain.replace(/^repo-/, "") : "unknown";
246
+ if (env.__error) {
247
+ leafOutcomes.push({ domain: dom, status: "failed", counts: emptyCounts, error: env.__error });
248
+ continue;
249
+ }
250
+ const status = env.status === "ok" || env.status === "empty" || env.status === "aborted" ? env.status : (Array.isArray(env.findings) && env.findings.length ? "ok" : "empty");
251
+ const counts = (env.counts && typeof env.counts === "object") ? env.counts : emptyCounts;
252
+ const outcome = { domain: dom, status, counts };
253
+ // Propagate leaf-level truncation into the ledger so the materialization gate can refuse
254
+ // a report whose per-domain finding set was size-capped (materializing from a truncated
255
+ // leaf would silently drop issues).
256
+ if (env.truncatedFindings === true) outcome.truncatedFindings = true;
257
+ // Propagate per-leaf lane coverage telemetry (iui1.2). A dropped finder/verifier/scorer lane
258
+ // means the leaf's coverage is degraded even if it returned status ok/empty; the meta surfaces
259
+ // it as partialCoverage + a materialization blocker so a silent per-lane drop can never produce
260
+ // a report that looks complete but is missing whole issue classes.
261
+ if (env.laneCoverage && typeof env.laneCoverage === "object") {
262
+ outcome.laneCoverage = env.laneCoverage;
263
+ if (env.laneCoverage.dropped > 0) outcome.laneDropped = env.laneCoverage.dropped;
264
+ }
265
+ leafOutcomes.push(outcome);
266
+ // Preserve per-domain top-level extras (carve-outs) surfaced, not merged.
267
+ const extras = {};
268
+ for (const k of EXTRAS_KEYS) if (env[k] !== undefined) extras[k] = env[k];
269
+ if (Object.keys(extras).length) domainExtras[dom] = extras;
270
+ }
271
+
272
+ const ran = leafOutcomes.filter((o) => o.status === "ok" || o.status === "empty");
273
+ const failed = leafOutcomes.filter((o) => o.status === "failed");
274
+ await log(`Domains complete: ${ran.length} ran (ok/empty), ${failed.length} failed, of ${activeDomains.length} active.`);
275
+
276
+ // Finalize shard coverage (iui1.5): leaves run over the whole repo, so a shard is "completed"
277
+ // when at least one domain leaf produced a usable envelope. A shard is "missed" only when EVERY
278
+ // domain leaf failed (the shard's files received zero review). A partial inventory leaves any
279
+ // un-enumerated files as an inherent coverage gap (recorded on the ledger, not a per-shard miss).
280
+ const shardAnyCoverage = ran.length > 0;
281
+ for (const s of shardLedger) s.status = (s.fileCount > 0 && !shardAnyCoverage) ? "missed" : "completed";
282
+ const shardMissedCount = shardLedger.filter((s) => s.status === "missed").length;
283
+ if (shardMissedCount > 0) await log(`Shard coverage: ${shardMissedCount} shard(s) missed (no domain produced coverage).`);
284
+
285
+ // Map domain -> raw findings (only from leaves that returned an envelope with findings).
286
+ const findingsByDomain = {};
287
+ for (const env of rawCoverage) {
288
+ if (!env || typeof env !== "object" || env.__error) continue;
289
+ const dom = typeof env.domain === "string" ? env.domain.replace(/^repo-/, "") : "unknown";
290
+ findingsByDomain[dom] = Array.isArray(env.findings) ? env.findings : [];
291
+ }
292
+
293
+ // ---- 3. Normalize + conservatively merge cross-domain (pure JS — no tokens) ----
294
+ await phase("merge");
295
+ const SEVW = { critical: 4, high: 3, medium: 2, low: 1 };
296
+ const EFFW = { small: 1, medium: 2, large: 3 };
297
+ const EFFD = { small: 1, medium: 0.8, large: 0.6 };
298
+ const normFile = (s) => (s || "").toString().replace(/^\.\//, "").trim();
299
+ const sevRank = (s) => SEVW[s] || 1;
300
+ const maxEffort = (a, b) => ((EFFW[a] || 2) >= (EFFW[b] || 2) ? a : b);
301
+ const weight = (f) => sevRank(f.severity) * ((f.confidence || 0) / 100);
302
+
303
+ // corroborationKey (iui1.6): a CROSS-DOMAIN "same root cause" key DISTINCT from the domain-
304
+ // prefixed materialization fingerprint. Two findings with the same file+category corroborate
305
+ // each other even across domains (fingerprints never match cross-domain because each leaf's
306
+ // fingerprintOf prefixes with its DOMAIN). Used ONLY to LINK (relatesTo) and to BOOST priority
307
+ // for multi-domain corroboration — never to MERGE (the materialization fingerprint stays
308
+ // domain-stable so a fix closes coverage for exactly the issue it fixes).
309
+ function corroborationKeyOf(file, category) {
310
+ return `${normFile(file)}::${String(category || "").toLowerCase().replace(/\s+/g, " ").trim()}`;
311
+ }
312
+
313
+ // Curated per-domain fields preserved for Beads materialization. The meta keeps these under
314
+ // domainDetails in findings.full.json so /review-materialize can build native Beads design and
315
+ // acceptance fields instead of losing implementation-relevant context during normalization.
316
+ const DOMAIN_DETAIL_KEYS = {
317
+ bughunt: ["reproSketch", "fixSketch", "docImpact"],
318
+ security: ["cwe", "attackVector", "exploitability", "docImpact"],
319
+ "test-gaps": ["targetUnderTest", "suggestedTest", "docImpact"],
320
+ cleanup: ["docImpact"],
321
+ modernize: ["deprecatedSince", "replacement", "targetVersion", "docImpact"],
322
+ perf: ["hotness", "estimatedImpact", "complexityBefore", "complexityAfter", "docImpact"],
323
+ complexity: ["churn", "complexityScore", "hotspotScore", "refactorSuggestion", "docImpact"],
324
+ deps: ["package", "currentVersion", "targetVersion", "breaking", "cve", "advisory", "docImpact"],
325
+ };
326
+ function domainDetailsFor(dom, finding) {
327
+ const details = {};
328
+ for (const key of DOMAIN_DETAIL_KEYS[dom] || []) {
329
+ if (finding && Object.prototype.hasOwnProperty.call(finding, key)) details[key] = finding[key];
330
+ }
331
+ return details;
332
+ }
333
+
334
+ function buildUnified() {
335
+ // Flatten every domain's findings into the UnifiedFinding shape. The lead `domain` and
336
+ // `sourceDomain` start as the originating leaf domain; a fingerprint merge unions tags.
337
+ const all = [];
338
+ const domainsInOrder = ALL_DOMAINS.filter((d) => findingsByDomain[d]);
339
+ for (const dom of domainsInOrder) {
340
+ for (const f of findingsByDomain[dom]) {
341
+ if (!f || typeof f !== "object") continue;
342
+ all.push({
343
+ domain: dom,
344
+ sourceDomain: dom,
345
+ sourceDomains: [dom],
346
+ fingerprints: [f.fingerprint].filter(Boolean),
347
+ category: f.category || dom,
348
+ file: normFile(f.file),
349
+ line: Number.isInteger(f.line) && f.line > 0 ? f.line : 0,
350
+ severity: f.severity || "low",
351
+ description: f.description || "",
352
+ proposedChange: f.proposedChange || "",
353
+ confidence: Number.isInteger(f.confidence) ? f.confidence : 60,
354
+ effort: f.effort || "medium",
355
+ domainDetails: domainDetailsFor(dom, f),
356
+ relatesTo: [],
357
+ __corr: corroborationKeyOf(f.file, f.category || dom),
358
+ });
359
+ }
360
+ }
361
+
362
+ // Corroboration domain sets (iui1.6): corroborationKey -> Set<domain>. Drives the multi-domain
363
+ // priority boost that was previously dead code (sourceDomains.length was always 1 because
364
+ // cross-domain findings never fingerprint-merge). Computed from the pre-merge `all` set so a
365
+ // corroborating finding boosts even when it is NOT merged into the lead.
366
+ const corroborationDomains = new Map();
367
+ for (const f of all) {
368
+ if (!f.file) continue;
369
+ const set = corroborationDomains.get(f.__corr);
370
+ if (set) set.add(f.sourceDomain);
371
+ else corroborationDomains.set(f.__corr, new Set([f.sourceDomain]));
372
+ }
373
+
374
+ // CONSERVATIVE MERGE — only on identical fingerprint (intra-domain, since fingerprints are
375
+ // domain-prefixed). O(1) Map lookup (iui1.6) instead of O(n^2) unified.find(). Adoption is
376
+ // decided on ORIGINAL severities first, then upgraded, so the weight compare is never skewed by
377
+ // an in-place severity upgrade. Proximity/corroboration is NEVER a merge (it is a LINK below) so
378
+ // two distinct nearby issues never collapse into one (which would let a fix close coverage for
379
+ // an unfixed bug).
380
+ const byFingerprint = new Map();
381
+ const unified = [];
382
+ for (const f of all) {
383
+ let hit = null;
384
+ for (const fp of f.fingerprints) { const g = byFingerprint.get(fp); if (g) { hit = g; break; } }
385
+ if (!hit) {
386
+ unified.push(f);
387
+ for (const fp of f.fingerprints) byFingerprint.set(fp, f);
388
+ continue;
389
+ }
390
+ // Adoption picks which contributor's LEAD fields the merged finding carries. Higher weight
391
+ // wins; on an EXACT weight tie, break deterministically by (description, proposedChange) so the
392
+ // result NEVER depends on lane/insertion order (rrev.25). `line` tracks the lead so the
393
+ // reported location (and the relatesTo links derived from it) are stable too.
394
+ const wf = weight(f);
395
+ const wh = weight(hit);
396
+ const adopt = wf > wh
397
+ || (wf === wh && (String(f.description) < String(hit.description)
398
+ || (String(f.description) === String(hit.description) && String(f.proposedChange) < String(hit.proposedChange))));
399
+ if (sevRank(f.severity) > sevRank(hit.severity)) hit.severity = f.severity;
400
+ if (adopt) { hit.description = f.description; hit.proposedChange = f.proposedChange; hit.domain = f.domain; hit.sourceDomain = f.sourceDomain; hit.line = f.line; hit.domainDetails = f.domainDetails; hit.__corr = f.__corr; }
401
+ hit.confidence = Math.max(hit.confidence, f.confidence);
402
+ hit.effort = maxEffort(hit.effort, f.effort);
403
+ for (const d of f.sourceDomains) if (!hit.sourceDomains.includes(d)) hit.sourceDomains.push(d);
404
+ for (const fp of f.fingerprints) { if (!hit.fingerprints.includes(fp)) hit.fingerprints.push(fp); byFingerprint.set(fp, hit); }
405
+ }
406
+
407
+ // Singular lead fingerprint (first contributor) so every unified finding carries one.
408
+ for (const f of unified) f.fingerprint = f.fingerprints[0] || null;
409
+
410
+ // Rank FIRST (stable, deterministic) so relatesTo can reference stable ranks. Multi-domain
411
+ // corroboration gets a small, capped boost keyed on corroborationKey (iui1.6) — this previously
412
+ // used sourceDomains.length, which was dead code (always 1) because cross-domain findings never
413
+ // fingerprint-merge. Deterministic tie-break: severity weight, then domain name, then fingerprint.
414
+ for (const f of unified) {
415
+ const corrCount = f.file ? (corroborationDomains.get(f.__corr)?.size || 1) : 1;
416
+ const boost = Math.min(1.25, 1 + 0.1 * (corrCount - 1));
417
+ f.priorityScore = Math.round(sevRank(f.severity) * (f.confidence / 100) * (EFFD[f.effort] || 0.8) * boost * 100) / 100;
418
+ f.corroborationCount = corrCount;
419
+ }
420
+ unified.sort((a, b) => {
421
+ if (b.priorityScore !== a.priorityScore) return b.priorityScore - a.priorityScore;
422
+ if (sevRank(b.severity) !== sevRank(a.severity)) return sevRank(b.severity) - sevRank(a.severity);
423
+ const da = a.sourceDomains.slice().sort().join(",") || a.domain;
424
+ const db = b.sourceDomains.slice().sort().join(",") || b.domain;
425
+ if (da !== db) return da < db ? -1 : 1;
426
+ const fa = a.fingerprint || "";
427
+ const fb = b.fingerprint || "";
428
+ return fa < fb ? -1 : fa > fb ? 1 : 0;
429
+ });
430
+ unified.forEach((f, i) => { f.rank = i + 1; });
431
+
432
+ // relatesTo AFTER the sort, storing the related finding's stable RANK. Bucketed by
433
+ // corroborationKey (iui1.6) for O(n)-average lookup instead of O(n^2). Two link rules, both
434
+ // confined to the SAME file+category bucket (so the inner loop is over a small bucket, not the
435
+ // whole set): (a) PROXIMITY — same file+category within ±3 lines (both lines non-zero);
436
+ // (b) CROSS-DOMAIN CORROBORATION — same file+category from DIFFERENT source domains (no line
437
+ // requirement; the cross-domain "same root cause" link fingerprint merging could never express).
438
+ // Both are LINKs, never merges.
439
+ const byBucket = new Map();
440
+ for (const f of unified) {
441
+ if (!f.file) continue;
442
+ const bucket = byBucket.get(f.__corr);
443
+ if (bucket) bucket.push(f); else byBucket.set(f.__corr, [f]);
444
+ }
445
+ for (const bucket of byBucket.values()) {
446
+ for (let i = 0; i < bucket.length; i++) {
447
+ const a = bucket[i];
448
+ for (let j = i + 1; j < bucket.length; j++) {
449
+ const b = bucket[j];
450
+ const proximity = a.line !== 0 && b.line !== 0 && Math.abs(a.line - b.line) <= 3;
451
+ const crossDomain = a.sourceDomain !== b.sourceDomain;
452
+ if (proximity || crossDomain) {
453
+ if (!a.relatesTo.includes(b.rank)) a.relatesTo.push(b.rank);
454
+ if (!b.relatesTo.includes(a.rank)) b.relatesTo.push(a.rank);
455
+ }
456
+ }
457
+ }
458
+ }
459
+ for (const f of unified) { f.relatesTo = [...new Set(f.relatesTo)].sort((x, y) => x - y); delete f.__corr; }
460
+ return unified;
461
+ }
462
+
463
+ const unified = buildUnified();
464
+ const counts = {
465
+ total: unified.length,
466
+ critical: unified.filter((f) => f.severity === "critical").length,
467
+ high: unified.filter((f) => f.severity === "high").length,
468
+ medium: unified.filter((f) => f.severity === "medium").length,
469
+ low: unified.filter((f) => f.severity === "low").length,
470
+ };
471
+ await log(`Merged into ${unified.length} unified findings across ${ran.length} domain(s).`);
472
+
473
+ // partialCoverage is true when a whole leaf failed/missing OR any leaf dropped a lane
474
+ // (finder/verifier/scorer). A per-lane drop degrades coverage as seriously as a failed leaf —
475
+ // the leaf can return "ok" while a whole finder lens silently returned null (iui1.2).
476
+ const partialCoverage = failed.length > 0 || ran.length < activeDomains.length || leafOutcomes.some((o) => o.laneDropped > 0);
477
+ const totalLaneDropped = leafOutcomes.reduce((s, o) => s + (o.laneDropped || 0), 0);
478
+ if (totalLaneDropped > 0) await log(`Lane coverage degraded: ${totalLaneDropped} lane(s) dropped across leaf(es).`);
479
+
480
+ // ---- materialization readiness gate (pure JS) ----
481
+ // A report is safe to materialize into Beads ONLY when OBJECTIVE coverage is complete and the
482
+ // finding set was not size-capped or lost. materializationBlockers is the authoritative list of
483
+ // such objective signals; the command refuses to OFFER materialize when materializationReady is
484
+ // false, and the separate review-materialize flow re-checks it. This is the review→materialize
485
+ // boundary gate.
486
+ //
487
+ // The gate is OBJECTIVE ONLY. The coverage auditor is a meta-judgment lane, not a finding producer:
488
+ // its subjective assessment (coverageAssessment / confidence / gaps) can NEVER hard-block
489
+ // materialization. Instead the auditor's concerns are surfaced as coverageAdvisories, which the
490
+ // command reports separately ("mechanically safe to materialize; auditor recommends follow-up
491
+ // review areas"). This prevents a conservative or under-informed auditor from vetoing a complete,
492
+ // artifact-backed review (see the goals-4bd5a7fd incident: all 8 domains ok/empty, artifacts ready,
493
+ // 127 findings, yet auditor partial/medium falsely blocked materialization).
494
+ const AUDITOR_SCHEMA = {
495
+ type: "object", additionalProperties: false,
496
+ required: ["coverageAssessment", "confidence", "gaps", "missedAreas"],
497
+ properties: {
498
+ coverageAssessment: { type: "string", enum: ["complete", "partial", "degraded"] },
499
+ confidence: { type: "string", enum: ["high", "medium", "low"] },
500
+ gaps: { type: "array", items: { type: "string" } },
501
+ missedAreas: { type: "array", items: { type: "string" } },
502
+ },
503
+ };
504
+ // coverageGrade is the OBJECTIVE coverage grade derived only from materializationBlockers (the
505
+ // auditor never contributes). "degraded" is a reserved/legacy enum value kept for compatibility;
506
+ // it is not currently produced because auditor-only concerns are coverageAdvisories, not blockers.
507
+ function gradeFor(blockers) {
508
+ if (blockers.length === 0) return "complete";
509
+ if (blockers.includes("truncatedFindings") || blockers.includes("reportMarkdownDropped") || blockers.some((b) => b.startsWith("leafTruncated:"))) return "truncated";
510
+ return "partial";
511
+ }
512
+ // Build the gate from the observable coverage state. `auditor` is null in bounded mode or
513
+ // when the auditor lane failed (onFailure returnNull); both are handled distinctly.
514
+ //
515
+ // Artifact awareness (iui1.4): when the full ranked set + full markdown were spilled to host-owned
516
+ // artifacts (artifactsReady), truncation of the RETURNED preview array and the dropped in-envelope
517
+ // reportMarkdown are intentional compaction, NOT data loss — so they are no longer blockers. Only
518
+ // when artifactization FAILED (artifactFailed) do we block with artifactPersistenceFailed, because
519
+ // then the full data truly was lost to the size cap.
520
+ function materializationGate({ truncated, reportMarkdown, unifiedCount, auditor, artifactsReady, artifactFailed }) {
521
+ const blockers = [];
522
+ if (partialCoverage) blockers.push("partialCoverage");
523
+ // Inventory + shard coverage (iui1.5): a failed inventory means coverage accounting is unknown,
524
+ // and a missed shard means a whole file group received zero review. Either blocks materialization.
525
+ if (!inventoryReady) blockers.push("inventoryFailed");
526
+ if (inventory && inventory.partial) blockers.push("inventoryPartial");
527
+ for (const s of shardLedger) if (s.status === "missed") blockers.push(`shardMissed:${s.id}`);
528
+ // Truncation/reportMarkdown-drop only block when the full data is NOT preserved in artifacts.
529
+ if (!artifactsReady) {
530
+ if (truncated) blockers.push("truncatedFindings");
531
+ // reportMarkdown is legitimately null when there are zero findings; it is only a blocker
532
+ // when findings EXISTED but the report was dropped to fit the 256 KiB cap AND no artifact
533
+ // holds the full markdown (the full ranked detail would be lost).
534
+ if (unifiedCount > 0 && reportMarkdown === null) blockers.push("reportMarkdownDropped");
535
+ }
536
+ if (artifactFailed && unifiedCount > 0) blockers.push("artifactPersistenceFailed");
537
+ for (const o of leafOutcomes) if (o.truncatedFindings) blockers.push(`leafTruncated:${o.domain}`);
538
+ // Per-lane drops (iui1.2): a leaf that dropped finder/verifier/scorer lanes has degraded
539
+ // coverage. Emit one blocker per domain plus finer-grained per-phase blockers so the operator
540
+ // sees WHICH lens class was lost, not just that something dropped.
541
+ for (const o of leafOutcomes) {
542
+ if (!o.laneCoverage || o.laneCoverage.dropped <= 0) continue;
543
+ blockers.push(`leafLaneDrops:${o.domain}`);
544
+ const bp = o.laneCoverage.byPhase || {};
545
+ if (bp.find && bp.find.dropped > 0) blockers.push(`leafFinderDrops:${o.domain}`);
546
+ if (bp.verify && bp.verify.dropped > 0) blockers.push(`leafVerifierDrops:${o.domain}`);
547
+ if (bp.score && bp.score.dropped > 0) blockers.push(`leafScorerDrops:${o.domain}`);
548
+ }
549
+ // Coverage auditor (ADVISORY ONLY). A meta-judgment lane must not veto a complete, artifact-
550
+ // backed review on subjective grounds. The auditor's assessment is surfaced as coverageAdvisories
551
+ // for the command to report separately from the objective blockers. `auditor` is null in bounded
552
+ // mode (no auditor expected) or when the auditor lane failed in exhaustive mode (onFailure
553
+ // returnNull); a failed auditor lane is an advisory gap, not data loss — exhaustive mode promised
554
+ // an auditor and did not get one, but every finding-producing leaf still completed.
555
+ const advisories = [];
556
+ if (exhaustive) {
557
+ if (auditor) {
558
+ if (auditor.coverageAssessment !== "complete") advisories.push(`auditorCoverage:${auditor.coverageAssessment}`);
559
+ if (auditor.confidence === "low") advisories.push("auditorLowConfidence");
560
+ } else {
561
+ advisories.push("auditorUnavailable");
562
+ }
563
+ }
564
+ return { materializationReady: blockers.length === 0, materializationBlockers: blockers, coverageAdvisories: advisories, coverageGrade: gradeFor(blockers) };
565
+ }
566
+
567
+ // ---- coverage auditor (EXHAUSTIVE only) ----
568
+ // A final deep-tier lane that reviews the merged coverage for blind spots before the report
569
+ // is finalized. It cannot add findings; it can only flag gaps that become coverageAdvisories
570
+ // (ADVISORY ONLY — never a materializationBlocker). It runs BEFORE the empty-check so "we found
571
+ // nothing" is audited too (a clean bill of health is most valuable precisely when findings are
572
+ // sparse). onFailure returnNull keeps a failed auditor from crashing the meta (it becomes an
573
+ // "auditorUnavailable" advisory instead). Bounded mode skips it entirely.
574
+ //
575
+ // The auditor is fed OBJECTIVE telemetry (lane completion, inventory/shard status, per-domain
576
+ // empty-vs-failed distinction) so it cannot mistake a clean empty domain for a coverage gap. An
577
+ // empty domain that ran to completion (all lanes completed, zero dropped) is a clean result, not
578
+ // a missed-area: the domain produced no findings after full analysis.
579
+ let coverageAudit = null;
580
+ if (exhaustive) {
581
+ await phase("audit");
582
+ const auditLedger = leafOutcomes.map((o) => {
583
+ const lc = o.laneCoverage;
584
+ const laneStr = lc ? ` [lanes ${lc.completed}/${lc.expected}${lc.dropped ? `, DROPPED ${lc.dropped}` : ""}]` : "";
585
+ const emptyNote = o.status === "empty" && lc && lc.dropped === 0 ? " (ran to completion; no findings survived)" : "";
586
+ return `- ${o.domain}: ${o.status}${o.error ? ` (${o.error})` : ""}${emptyNote} — ${(o.counts && o.counts.total) || 0} findings${o.truncatedFindings ? " (TRUNCATED)" : ""}${laneStr}`;
587
+ }).join("\n");
588
+ coverageAudit = await agent(
589
+ ["You are a coverage auditor for an exhaustive, read-only repo review. Assess whether the review likely missed important issue classes given the repo profile and the coverage ledger below.",
590
+ scope,
591
+ `Recon:\n${formatRecon(recon)}`,
592
+ `Active domains: ${activeDomains.join(", ")}.`,
593
+ `Objective coverage state: partialCoverage=${partialCoverage}; inventory=${inventoryReady ? "ok" : "FAILED"}${inventory && inventory.partial ? " (PARTIAL)" : ""}; shards=${shardLedger.length} (${shardMissedCount} missed); total lane drops=${totalLaneDropped}.`,
594
+ `Coverage ledger:\n${auditLedger}`,
595
+ `Merged findings: ${unified.length} (critical ${counts.critical}, high ${counts.high}, medium ${counts.medium}, low ${counts.low}).`,
596
+ "Consider: did any domain fail or return partial results? Are there repo areas the scope excludes (config, CI, scripts, generated code, secrets, infra) that might hide issues? Is thorough depth sufficient for this repo's size and language mix? Are there obvious issue classes (e.g. license compliance, accessibility, i18n, error-handling paths) no domain covers?",
597
+ "IMPORTANT: a domain listed as 'empty (ran to completion; no findings survived)' is a CLEAN result — the domain analyzed its scope fully and produced no findings. An empty-but-complete domain is NOT a coverage gap. Distinguish 'the domain ran and found nothing' from 'the domain failed or was not run'.",
598
+ "Return coverageAssessment (complete if you are confident the review found the materially important issues; partial if there are real gaps; degraded if coverage is seriously incomplete), your confidence, specific gaps (short strings), and missedAreas (short strings). Be conservative: if unsure, say partial."].join("\n\n"),
599
+ { label: "coverage-auditor", schema: AUDITOR_SCHEMA, tier: "deep", onFailure: "returnNull" },
600
+ );
601
+ if (coverageAudit && typeof coverageAudit === "object") {
602
+ await log(`Coverage auditor: ${coverageAudit.coverageAssessment} (confidence ${coverageAudit.confidence})${coverageAudit.gaps?.length ? ` — ${coverageAudit.gaps.length} gap(s)` : ""}.`);
603
+ } else {
604
+ await log("WARNING: coverage auditor returned null — recording as auditorUnavailable advisory.");
605
+ }
606
+ }
607
+
608
+ // ---- 4. Synthesize ONE cross-domain report (PURE JS render; no model tokens) ----
609
+ await phase("synthesize");
610
+ function mdCell(s) { return String(s == null ? "" : s).replace(/\|/g, "\\|").replace(/\n+/g, " "); }
611
+ function renderMarkdown(rows, c) {
612
+ const ledger = leafOutcomes.map((o) => `- ${o.domain}: ${o.status}${o.error ? ` (${o.error})` : ""} — ${(o.counts && o.counts.total) || 0} findings`).join("\n");
613
+ const lines = [];
614
+ lines.push("# Comprehensive Repo Review Report", "");
615
+ lines.push("> Report-only. No files were modified and nothing was applied.", "");
616
+ // ---- Scale + coverage telemetry (iui1.8) ----
617
+ lines.push("## Scope summary", `- Paths: ${JSON.stringify(paths)}`, `- Exclude: ${JSON.stringify(exclude)}`, `- Active domains: ${activeDomains.join(", ")}`, `- Depth: ${depth} (mode: ${mode})`, "");
618
+ if (scaleProfile) {
619
+ lines.push("## Inventory summary",
620
+ `- Files enumerated: ${scaleProfile.totalFiles}`,
621
+ `- Source roots: ${inventorySummary.sourceRoots}`,
622
+ `- Shards: ${scaleProfile.shards} (${scaleProfile.shardMissed} missed)${scaleProfile.inventoryPartial ? " — PARTIAL inventory (file cap hit)" : ""}`,
623
+ `- Files reviewed: ${scaleProfile.reviewedFiles}${scaleProfile.skippedFiles ? ` (skipped: ${scaleProfile.skippedFiles})` : ""}`,
624
+ "");
625
+ // Lane coverage per domain.
626
+ const laneLines = leafOutcomes.map((o) => {
627
+ const lc = o.laneCoverage;
628
+ if (!lc) return `- ${o.domain}: ${o.status} (no lane telemetry)`;
629
+ return `- ${o.domain}: ${o.status} — lanes ${lc.completed}/${lc.expected}${lc.dropped ? ` (DROPPED ${lc.dropped}: ${mdCell((lc.droppedLabels || []).join(", "))})` : ""}`;
630
+ });
631
+ lines.push("## Lane coverage", ...laneLines, "");
632
+ // Dropped / failed lanes named explicitly so they cannot be missed.
633
+ if (scaleProfile.droppedLanes > 0 || scaleProfile.failedDomains.length) {
634
+ lines.push("## Dropped / failed lanes");
635
+ for (const lbl of scaleProfile.droppedLaneLabels) lines.push(`- dropped: ${mdCell(lbl)}`);
636
+ for (const dom of scaleProfile.failedDomains) lines.push(`- failed domain: ${mdCell(dom)}`);
637
+ lines.push("");
638
+ }
639
+ lines.push("## Artifact status",
640
+ `- Full ranked findings + this report persisted as host-owned artifacts (see \`artifactPaths\`).`,
641
+ "");
642
+ // What this review did NOT do — prevents misreading incomplete coverage as exhaustive.
643
+ const gaps = [];
644
+ if (scaleProfile.droppedLanes > 0) gaps.push(`${scaleProfile.droppedLanes} finder/verifier lane(s) dropped — some issue classes may be under-counted.`);
645
+ if (scaleProfile.shardMissed > 0) gaps.push(`${scaleProfile.shardMissed} shard(s) received zero domain coverage.`);
646
+ if (scaleProfile.inventoryPartial) gaps.push("File inventory hit its cap — some files were not enumerated.");
647
+ if (scaleProfile.failedDomains.length) gaps.push(`Domain(s) failed: ${scaleProfile.failedDomains.join(", ")}.`);
648
+ gaps.push("Static, read-only analysis (no shell/git-churn, no network advisory lookups) — optional deep modes were not enabled.");
649
+ lines.push("## What this review did not do", ...gaps.map((g) => `- ${g}`), "");
650
+ }
651
+ lines.push("## Coverage ledger", ledger, "");
652
+ lines.push("## Summary", `- Total: ${c.total} (critical: ${c.critical}, high: ${c.high}, medium: ${c.medium}, low: ${c.low})`, "");
653
+ lines.push("## Ranked cross-domain findings", "");
654
+ lines.push("| Rank | Severity | Score | Domain(s) | Location | Category | Description |");
655
+ lines.push("| ---- | -------- | ----- | --------- | -------- | -------- | ----------- |");
656
+ for (const f of rows) {
657
+ lines.push(`| ${f.rank} | ${mdCell(f.severity)} | ${f.priorityScore} | ${mdCell(f.sourceDomains.join("+"))} | ${mdCell(f.file)}:${f.line || 0} | ${mdCell(f.category)} | ${mdCell(f.description).slice(0, 140)} |`);
658
+ }
659
+ lines.push("", "## Detail");
660
+ for (const f of rows) {
661
+ lines.push("", `### ${f.rank}. ${mdCell(f.category)} — ${mdCell(f.file)}:${f.line || 0} (${f.severity}, conf ${f.confidence})`);
662
+ lines.push(`- **Domain(s):** ${f.sourceDomains.join(", ")}`);
663
+ lines.push(`- **What:** ${f.description}`);
664
+ if (f.proposedChange) lines.push(`- **Proposed change:** ${f.proposedChange}`);
665
+ if (f.relatesTo.length) lines.push(`- **Related (ranks):** ${f.relatesTo.join(", ")}`);
666
+ lines.push(`- **Fingerprint:** \`${f.fingerprint}\``);
667
+ }
668
+ return lines.join("\n");
669
+ }
670
+
671
+ // ---- artifactize full output (iui1.4) ----
672
+ // The workflow return is capped at MAX_RESULT_BYTES (256 KiB); a large exhaustive report would
673
+ // otherwise be truncated (reportMarkdown dropped, findings halved) and the materialization gate
674
+ // would block. Instead, spill the FULL ranked findings + coverage ledger + full markdown to
675
+ // host-owned artifacts under the run directory. NO data is lost to size fitting: the return
676
+ // envelope carries compact artifactPaths + counts + a top-N preview, and review-materialize reads
677
+ // the full set from findings.full.json via its findingsPath handoff. The QuickJS guest cannot
678
+ // write files; persistArtifacts is a kernel host op rooted under run.dir/artifacts/ (controller-
679
+ // owned, gitignored), preserving the read-only-review contract (no workspace/source writes).
680
+ // Compact inventory summary for the envelope (the full manifest + per-shard file lists live in the
681
+ // shard-ledger.json artifact). shardLedger carries per-shard coverage status for observability.
682
+ const inventorySummary = {
683
+ ready: inventoryReady,
684
+ partial: !!(inventory && inventory.partial),
685
+ totalFiles: manifest ? manifest.totalFiles : 0,
686
+ sourceRoots: manifest ? manifest.sourceRoots.length : 0,
687
+ shards: shardLedger.length,
688
+ shardMissed: shardMissedCount,
689
+ };
690
+
691
+ // scaleProfile (iui1.8): structured scale + coverage telemetry aggregated from the inventory,
692
+ // shard ledger, and per-leaf lane coverage. Makes dropped lanes, missed shards, and skipped files
693
+ // visible at the envelope + report level so an operator cannot misread incomplete coverage as
694
+ // exhaustive. artifactsReady/truncatedFindings are mutated after persistence / size-fitting.
695
+ const scaleProfile = (() => {
696
+ const totalFiles = manifest ? manifest.totalFiles : 0;
697
+ const missedFiles = shardLedger.filter((s) => s.status === "missed").reduce((s2, sh) => s2 + (sh.fileCount || 0), 0);
698
+ let totalLanes = 0, droppedLanes = 0;
699
+ const droppedLaneLabels = [];
700
+ for (const o of leafOutcomes) {
701
+ if (o.laneCoverage && typeof o.laneCoverage === "object") {
702
+ totalLanes += o.laneCoverage.expected || 0;
703
+ droppedLanes += o.laneCoverage.dropped || 0;
704
+ for (const lbl of (o.laneCoverage.droppedLabels || [])) droppedLaneLabels.push(`${o.domain}:${lbl}`);
705
+ }
706
+ }
707
+ return {
708
+ shards: shardLedger.length,
709
+ shardMissed: shardMissedCount,
710
+ totalFiles,
711
+ reviewedFiles: Math.max(0, totalFiles - missedFiles),
712
+ skippedFiles: missedFiles,
713
+ inventoryPartial: !!(inventory && inventory.partial),
714
+ totalLanes,
715
+ droppedLanes,
716
+ droppedLaneLabels,
717
+ failedDomains: failed.map((o) => o.domain),
718
+ artifactsReady: false, // mutated after artifact persistence completes
719
+ truncatedFindings: false, // mutated in fitWithinBudget for the non-empty path
720
+ };
721
+ })();
722
+
723
+ const fullMarkdown = unified.length ? renderMarkdown(unified, counts) : null;
724
+ const findingsJsonl = unified.map((f) => JSON.stringify(f)).join("\n");
725
+ let artifactResult = null;
726
+ try {
727
+ artifactResult = await persistArtifacts({
728
+ namespace: "repo-review",
729
+ files: [
730
+ { name: "findings.full.json", content: unified },
731
+ { name: "findings.jsonl", content: findingsJsonl || "" },
732
+ { name: "coverage-ledger.json", content: { counts, leafOutcomes, domainExtras, partialCoverage, coverageAudit, shardLedger } },
733
+ { name: "leaf-outcomes.json", content: leafOutcomes },
734
+ { name: "shard-ledger.json", content: { inventory: manifest, shards: inventoryShards, shardLedger } },
735
+ ...(fullMarkdown ? [{ name: "report-markdown.md", content: fullMarkdown }] : []),
736
+ ],
737
+ });
738
+ } catch (e) {
739
+ artifactResult = { ok: false, error: String((e && e.message) || e), dir: null, files: [] };
740
+ }
741
+ const artifactsReady = !!(artifactResult && artifactResult.ok);
742
+ const artifactFailed = !artifactsReady;
743
+ scaleProfile.artifactsReady = artifactsReady;
744
+ if (artifactsReady) {
745
+ await log(`Artifacts persisted: ${unified.length} findings + coverage ledger under ${artifactResult.dir}.`);
746
+ } else {
747
+ await log(`WARNING: artifact persistence failed — ${artifactResult && artifactResult.error}. Full findings will be subject to the size cap.`);
748
+ }
749
+ // artifactPaths maps a stable logical key -> absolute host path. findingsJson (findings.full.json)
750
+ // is the canonical handoff to review-materialize (its findingsPath expects a JSON array).
751
+ const ARTIFACT_KEY = {
752
+ "findings.full.json": "findingsJson",
753
+ "findings.jsonl": "findingsJsonl",
754
+ "coverage-ledger.json": "coverageLedgerJson",
755
+ "leaf-outcomes.json": "leafOutcomesJson",
756
+ "shard-ledger.json": "shardLedgerJson",
757
+ "report-markdown.md": "reportMarkdownPath",
758
+ };
759
+ const artifactPaths = {};
760
+ if (artifactResult && Array.isArray(artifactResult.files)) {
761
+ for (const af of artifactResult.files) {
762
+ const key = ARTIFACT_KEY[af.name] || af.name.replace(/[^A-Za-z0-9]+/g, "_");
763
+ artifactPaths[key] = af.path;
764
+ }
765
+ }
766
+ if (unified.length === 0) {
767
+ const summary = `No findings across ${ran.length} of ${activeDomains.length} active domain(s).${failed.length ? ` ${failed.length} domain(s) failed — partial coverage.` : ""}`;
768
+ const gate = materializationGate({ truncated: false, reportMarkdown: null, unifiedCount: 0, auditor: coverageAudit, artifactsReady, artifactFailed });
769
+ return envelope(ran.length ? "empty" : "aborted", {
770
+ abortReason: ran.length ? null : "All active domains failed; no findings available.",
771
+ summary,
772
+ counts,
773
+ findings: [],
774
+ truncatedFindings: false,
775
+ reportMarkdown: null,
776
+ leafOutcomes,
777
+ domainExtras,
778
+ partialCoverage,
779
+ coverageAudit,
780
+ artifactPaths,
781
+ artifactsReady,
782
+ artifactCounts: { findings: unified.length },
783
+ inventorySummary,
784
+ scaleProfile,
785
+ shardLedger,
786
+ deepMode,
787
+ ...gate,
788
+ });
789
+ }
790
+
791
+ // Size-fit the RETURNED preview to the 256 KiB host cap. Because the FULL ranked set + markdown
792
+ // were spilled to artifacts above, this compaction is lossless: counts.total ALWAYS reflects the
793
+ // full ranked set, and the full data lives in artifactPaths. The envelope keeps a top-N preview +
794
+ // reportMarkdown when they fit (dropped only to fit the cap, never losing the artifact copy).
795
+ function utf8ByteLength(value) {
796
+ const s = String(value ?? "");
797
+ let bytes = 0;
798
+ for (let i = 0; i < s.length; i += 1) {
799
+ const code = s.charCodeAt(i);
800
+ if (code <= 0x7f) bytes += 1;
801
+ else if (code <= 0x7ff) bytes += 2;
802
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
803
+ const next = s.charCodeAt(i + 1);
804
+ if (next >= 0xdc00 && next <= 0xdfff) {
805
+ bytes += 4;
806
+ i += 1;
807
+ } else bytes += 3;
808
+ } else bytes += 3;
809
+ }
810
+ return bytes;
811
+ }
812
+ function jsonUtf8ByteLength(value) {
813
+ return utf8ByteLength(JSON.stringify(value));
814
+ }
815
+ function fitWithinBudget() {
816
+ const LIMIT = 230000; // headroom under MAX_RESULT_BYTES (262144) for the {output:...} wrapper + envelope fields
817
+ let returned = unified.slice(0, maxReturnFindings);
818
+ let truncated = unified.length > returned.length;
819
+ let reportMarkdown = fullMarkdown;
820
+ const summary = `Reviewed ${ran.length} of ${activeDomains.length} domain(s): ${counts.total} findings (${counts.critical} critical, ${counts.high} high). Report-only — nothing applied.${partialCoverage ? " PARTIAL coverage." : ""} Full findings in artifacts.`;
821
+ const gate0 = () => materializationGate({ truncated, reportMarkdown, unifiedCount: unified.length, auditor: coverageAudit, artifactsReady, artifactFailed });
822
+ const sizeOf = () => jsonUtf8ByteLength(envelope("ok", { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, leafOutcomes, domainExtras, partialCoverage, artifactPaths, artifactsReady, artifactCounts: { findings: unified.length }, inventorySummary, scaleProfile: { ...scaleProfile, truncatedFindings: truncated }, shardLedger, deepMode, ...gate0(), coverageAudit }));
823
+ if (sizeOf() > LIMIT) reportMarkdown = null;
824
+ while (sizeOf() > LIMIT && returned.length > 10) {
825
+ returned = returned.slice(0, Math.ceil(returned.length / 2));
826
+ truncated = true;
827
+ }
828
+ return envelope("ok", { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, leafOutcomes, domainExtras, partialCoverage, artifactPaths, artifactsReady, artifactCounts: { findings: unified.length }, inventorySummary, scaleProfile: { ...scaleProfile, truncatedFindings: truncated }, shardLedger, deepMode, ...materializationGate({ truncated, reportMarkdown, unifiedCount: unified.length, auditor: coverageAudit, artifactsReady, artifactFailed }), coverageAudit });
829
+ }
830
+
831
+ return fitWithinBudget();