@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.
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +746 -0
- package/SECURITY.md +38 -0
- package/commands/repo-bughunt.md +94 -0
- package/commands/repo-review.md +148 -0
- package/commands/workflow-live-gates-release-check.md +143 -0
- package/docs/workflow-plugin.md +400 -0
- package/opencode-workflows.js +5 -0
- package/package.json +86 -0
- package/skills/opencode-workflow-authoring/SKILL.md +180 -0
- package/skills/repo-review-command-protocol/SKILL.md +56 -0
- package/skills/workflow-model-tiering/SKILL.md +57 -0
- package/skills/workflow-plan-review/SKILL.md +91 -0
- package/workflow-kernel/approval-hashing.js +39 -0
- package/workflow-kernel/async-util.js +33 -0
- package/workflow-kernel/audited-shell-policy.js +200 -0
- package/workflow-kernel/authority-policy.js +670 -0
- package/workflow-kernel/budget-accounting.js +142 -0
- package/workflow-kernel/capability-adapter.js +753 -0
- package/workflow-kernel/child-agent-runner.js +1264 -0
- package/workflow-kernel/constants.js +117 -0
- package/workflow-kernel/diagnostics.js +152 -0
- package/workflow-kernel/drain-runtime.js +421 -0
- package/workflow-kernel/errors.js +181 -0
- package/workflow-kernel/event-journal.js +487 -0
- package/workflow-kernel/extension-registry.js +144 -0
- package/workflow-kernel/free-text-redactor.js +91 -0
- package/workflow-kernel/gate-shapes.js +82 -0
- package/workflow-kernel/git-util.js +45 -0
- package/workflow-kernel/index.js +72 -0
- package/workflow-kernel/integration-mode.js +155 -0
- package/workflow-kernel/lane-effort-policy.js +134 -0
- package/workflow-kernel/lifecycle-control.js +608 -0
- package/workflow-kernel/live-gate-probes.js +916 -0
- package/workflow-kernel/notification-toast-cards.js +393 -0
- package/workflow-kernel/notification-toast-policy.js +179 -0
- package/workflow-kernel/notification-toast-scope.js +100 -0
- package/workflow-kernel/notification-toast.js +287 -0
- package/workflow-kernel/path-policy.js +219 -0
- package/workflow-kernel/result-readback.js +106 -0
- package/workflow-kernel/role-template-loading.js +606 -0
- package/workflow-kernel/run-context.js +139 -0
- package/workflow-kernel/run-observability.js +43 -0
- package/workflow-kernel/run-store-fs.js +231 -0
- package/workflow-kernel/run-store-locks.js +180 -0
- package/workflow-kernel/run-store-projections.js +421 -0
- package/workflow-kernel/run-store-rehydrate.js +68 -0
- package/workflow-kernel/run-store-state.js +147 -0
- package/workflow-kernel/run-store-status-format.js +1154 -0
- package/workflow-kernel/run-store-status.js +107 -0
- package/workflow-kernel/sandbox-executor.js +1131 -0
- package/workflow-kernel/session-access.js +56 -0
- package/workflow-kernel/structured-output.js +143 -0
- package/workflow-kernel/test-fix-drain-adapter.js +119 -0
- package/workflow-kernel/text-json.js +136 -0
- package/workflow-kernel/workflow-plugin.js +3017 -0
- package/workflow-kernel/workflow-source.js +444 -0
- package/workflow-kernel/worktree-adapter.js +256 -0
- package/workflow-kernel/worktree-git.js +147 -0
- package/workflows/repo-bughunt.js +362 -0
- package/workflows/repo-cleanup.js +383 -0
- package/workflows/repo-complexity.js +404 -0
- package/workflows/repo-deps.js +457 -0
- package/workflows/repo-modernize.js +395 -0
- package/workflows/repo-perf.js +394 -0
- package/workflows/repo-review.js +831 -0
- package/workflows/repo-security-audit.js +466 -0
- package/workflows/repo-test-gaps.js +377 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: "repo-bughunt",
|
|
3
|
+
description: "Find correctness bugs across a repo (concurrency, error handling, boundaries, null/empty, resource leaks, API misuse, bad state) and adversarially verify each candidate before reporting. Report-only: returns ranked structured findings; the workflow writes no files.",
|
|
4
|
+
profile: "read-only-review",
|
|
5
|
+
maxAgents: 4096,
|
|
6
|
+
concurrency: 16,
|
|
7
|
+
phases: ["recon", "find", "verify", "synthesize"],
|
|
8
|
+
category: "repo-review-leaf",
|
|
9
|
+
notes: "Read-only report-only bughunt leaf. Recommended callers pass object args; defensive JSON-string args remain supported for compatibility. Finder lanes use fast tier, verification lanes use deep tier.",
|
|
10
|
+
examples: [
|
|
11
|
+
{ label: "normal source scan", args: { depth: "normal", paths: ["src"] } },
|
|
12
|
+
{ label: "focused concurrency scan", args: { depth: "quick", paths: ["src"], categories: ["concurrency"] } },
|
|
13
|
+
],
|
|
14
|
+
argsSchema: {
|
|
15
|
+
type: ["object", "string", "null"],
|
|
16
|
+
properties: {
|
|
17
|
+
paths: { type: "array", items: { type: "string" } },
|
|
18
|
+
exclude: { type: "array", items: { type: "string" } },
|
|
19
|
+
depth: { type: "string", enum: ["quick", "normal", "thorough"] },
|
|
20
|
+
categories: { type: "array", items: { type: "string", enum: ["concurrency", "error-handling", "boundary", "null-empty", "resource-leak", "api-misuse", "bad-state"] } },
|
|
21
|
+
maxReturnFindings: { type: "integer", minimum: 1 },
|
|
22
|
+
recon: { type: ["object", "string"] },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---- suite identity ----
|
|
28
|
+
const DOMAIN = "bughunt";
|
|
29
|
+
const SCHEMA_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
// args may arrive as an object (workflow_run args) or, defensively, a JSON string.
|
|
32
|
+
let RT = args;
|
|
33
|
+
if (typeof RT === "string") { try { RT = RT.trim() ? JSON.parse(RT) : {}; } catch (error) { throw new Error(`Invalid repo-bughunt runtime args JSON: ${error.message}`); } }
|
|
34
|
+
if (!RT || typeof RT !== "object" || Array.isArray(RT)) RT = {};
|
|
35
|
+
|
|
36
|
+
const paths = Array.isArray(RT.paths) && RT.paths.length ? RT.paths : ["."];
|
|
37
|
+
const exclude = Array.isArray(RT.exclude) ? RT.exclude : ["node_modules", "dist", "build", ".git", "vendor", "target", "*.min.*", "*.map"];
|
|
38
|
+
const depth = ["quick", "normal", "thorough"].includes(RT.depth) ? RT.depth : "thorough";
|
|
39
|
+
|
|
40
|
+
// Model selection is intent-based: lanes declare a tier; the kernel resolves tier -> concrete model from
|
|
41
|
+
// run.modelTiers (set by the planning agent), falling back to the session-inherited default. See
|
|
42
|
+
// docs/superpowers/specs/2026-06-23-session-aware-model-tiering-design.md (Piece 1).
|
|
43
|
+
// recon + finders = bulk work (fast); skeptics = subtle correctness (deep); synth = pure JS (no model).
|
|
44
|
+
const TIER_RECON = "fast";
|
|
45
|
+
const TIER_FINDER = "fast";
|
|
46
|
+
const TIER_VERIFY = "deep";
|
|
47
|
+
|
|
48
|
+
const MAX_RETURN_FINDINGS = Number.isInteger(RT.maxReturnFindings) && RT.maxReturnFindings > 0 ? RT.maxReturnFindings : 1000000;
|
|
49
|
+
|
|
50
|
+
const ALL_CATEGORIES = ["concurrency", "error-handling", "boundary", "null-empty", "resource-leak", "api-misuse", "bad-state"];
|
|
51
|
+
const requestedCategories = Array.isArray(RT.categories) ? RT.categories.filter((c) => typeof c === "string" && c.trim()) : [];
|
|
52
|
+
const categories = requestedCategories.length > 0 ? requestedCategories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
|
|
53
|
+
if (requestedCategories.length > 0 && categories.length === 0) {
|
|
54
|
+
throw new Error(`No valid repo-bughunt categories supplied. Valid categories: ${ALL_CATEGORIES.join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scope = `Scope: paths = ${JSON.stringify(paths)}. Exclude (do not scan/report): ${JSON.stringify(exclude)}.`;
|
|
58
|
+
|
|
59
|
+
// ---- lane coverage telemetry (iui1.2) ----
|
|
60
|
+
// Tracks expected/completed/dropped lane counts per phase so a dropped finder/verifier lane
|
|
61
|
+
// (onFailure returnNull + filter(Boolean)) surfaces as degraded coverage instead of a silent
|
|
62
|
+
// null. The meta reads laneCoverage.dropped to block materialization on partial coverage.
|
|
63
|
+
const laneCoverage = { expected: 0, completed: 0, dropped: 0, byPhase: {}, droppedLabels: [] };
|
|
64
|
+
function tallyPhase(name, results, labelOf) {
|
|
65
|
+
const expected = results.length;
|
|
66
|
+
let completed = 0;
|
|
67
|
+
for (let i = 0; i < results.length; i++) {
|
|
68
|
+
if (results[i] === null || results[i] === undefined) laneCoverage.droppedLabels.push(labelOf ? labelOf(i) : `${name}:${i + 1}`);
|
|
69
|
+
else completed++;
|
|
70
|
+
}
|
|
71
|
+
const dropped = expected - completed;
|
|
72
|
+
laneCoverage.expected += expected;
|
|
73
|
+
laneCoverage.completed += completed;
|
|
74
|
+
laneCoverage.dropped += dropped;
|
|
75
|
+
const prev = laneCoverage.byPhase[name] || { expected: 0, completed: 0, dropped: 0 };
|
|
76
|
+
laneCoverage.byPhase[name] = { expected: prev.expected + expected, completed: prev.completed + completed, dropped: prev.dropped + dropped };
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- standardized return envelope ----
|
|
81
|
+
function envelope(status, extra) {
|
|
82
|
+
return { domain: DOMAIN, schemaVersion: SCHEMA_VERSION, status, abortReason: null, reportPath: null, laneCoverage, ...extra };
|
|
83
|
+
}
|
|
84
|
+
const emptyCounts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
85
|
+
|
|
86
|
+
// ---- shared recon schema (tolerates a prose string via formatRecon) ----
|
|
87
|
+
const RECON_SCHEMA = {
|
|
88
|
+
type: "object", additionalProperties: false,
|
|
89
|
+
properties: {
|
|
90
|
+
languages: { type: "array", items: { type: "string" } },
|
|
91
|
+
frameworks: { type: "array", items: { type: "string" } },
|
|
92
|
+
packageManagers: { type: "array", items: { type: "string" } },
|
|
93
|
+
entryPoints: { type: "array", items: { type: "string" } },
|
|
94
|
+
testLayout: { type: "string" },
|
|
95
|
+
buildTooling: { type: "string" },
|
|
96
|
+
concurrencyModel: { type: "string" },
|
|
97
|
+
errorHandling: { type: "string" },
|
|
98
|
+
externalResources: { type: "array", items: { type: "string" } },
|
|
99
|
+
notes: { type: "string" },
|
|
100
|
+
},
|
|
101
|
+
required: ["languages", "notes"],
|
|
102
|
+
};
|
|
103
|
+
function formatRecon(r) {
|
|
104
|
+
if (typeof r === "string") return r;
|
|
105
|
+
if (!r || typeof r !== "object") return "No recon available.";
|
|
106
|
+
const L = (k, v) => (v && (Array.isArray(v) ? v.length : true) ? `${k}: ${Array.isArray(v) ? v.join(", ") : v}` : null);
|
|
107
|
+
return [
|
|
108
|
+
L("Languages", r.languages), L("Frameworks", r.frameworks), L("Package managers", r.packageManagers),
|
|
109
|
+
L("Entry points", r.entryPoints), L("Test layout", r.testLayout), L("Build tooling", r.buildTooling),
|
|
110
|
+
L("Concurrency model", r.concurrencyModel), L("Error handling", r.errorHandling),
|
|
111
|
+
L("External resources", r.externalResources), L("Notes", r.notes),
|
|
112
|
+
].filter(Boolean).join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- stable content fingerprint (djb2; deterministic, no crypto) ----
|
|
116
|
+
// <suite:fingerprintOf>
|
|
117
|
+
function fingerprintOf(f) {
|
|
118
|
+
const norm = (s) => (s || "").toString().toLowerCase().replace(/\s+/g, " ").trim();
|
|
119
|
+
const basis = `${DOMAIN}|${norm(f.file)}|${norm(f.category)}|${norm(f.description).slice(0, 160)}`;
|
|
120
|
+
let h = 5381;
|
|
121
|
+
for (let i = 0; i < basis.length; i++) h = ((h * 33) ^ basis.charCodeAt(i)) >>> 0;
|
|
122
|
+
return `${DOMAIN}-${h.toString(16)}`;
|
|
123
|
+
}
|
|
124
|
+
// </suite:fingerprintOf>
|
|
125
|
+
|
|
126
|
+
// ---- schemas ----
|
|
127
|
+
const FINDINGS_SCHEMA = {
|
|
128
|
+
type: "object", additionalProperties: false,
|
|
129
|
+
properties: {
|
|
130
|
+
findings: {
|
|
131
|
+
type: "array",
|
|
132
|
+
items: {
|
|
133
|
+
type: "object", additionalProperties: false,
|
|
134
|
+
properties: {
|
|
135
|
+
category: { type: "string" },
|
|
136
|
+
file: { type: "string" },
|
|
137
|
+
line: { type: "integer" },
|
|
138
|
+
severity: { type: "string", enum: ["high", "medium", "low"] },
|
|
139
|
+
description: { type: "string" },
|
|
140
|
+
reproSketch: { type: "string" },
|
|
141
|
+
fixSketch: { type: "string" },
|
|
142
|
+
proposedChange: { type: "string" },
|
|
143
|
+
confidence: { type: "integer" },
|
|
144
|
+
effort: { type: "string", enum: ["small", "medium", "large"] },
|
|
145
|
+
docImpact: { type: "string" },
|
|
146
|
+
},
|
|
147
|
+
required: ["category", "file", "line", "severity", "description", "reproSketch", "fixSketch", "proposedChange", "confidence", "effort", "docImpact"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ["findings"],
|
|
152
|
+
};
|
|
153
|
+
const VERDICT_SCHEMA = {
|
|
154
|
+
type: "object", additionalProperties: false,
|
|
155
|
+
properties: {
|
|
156
|
+
refuted: { type: "boolean" },
|
|
157
|
+
reasoning: { type: "string" },
|
|
158
|
+
adjustedConfidence: { type: "integer" },
|
|
159
|
+
},
|
|
160
|
+
required: ["refuted", "reasoning", "adjustedConfidence"],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// ---- lenses ----
|
|
164
|
+
const LENS = {
|
|
165
|
+
"concurrency": "Concurrency/race bugs: data races, unsynchronized shared state, await/async ordering mistakes, missing locks, check-then-act races, deadlock potential, unhandled promise rejections.",
|
|
166
|
+
"error-handling": "Error-handling bugs: swallowed exceptions, errors logged but not handled, wrong error types, missing rollback/cleanup on failure, catch blocks that hide bugs, unchecked return/error codes.",
|
|
167
|
+
"boundary": "Boundary/off-by-one bugs: loop bounds, slice/substring indices, fencepost errors, inclusive/exclusive range mistakes, pagination/limit math.",
|
|
168
|
+
"null-empty": "Null/undefined/empty bugs: dereferencing possibly-null values, missing empty-collection handling, optional chaining gaps, default-value mistakes, NaN/0/\"\" falsy traps.",
|
|
169
|
+
"resource-leak": "Resource leaks: files/sockets/connections/handles not closed, missing finally/defer/with, event listeners not removed, unbounded caches/growth, leaked timers.",
|
|
170
|
+
"api-misuse": "API/contract misuse: wrong argument order/types, ignored required return values, misused library calls, violated preconditions, incorrect lifecycle/ordering of calls.",
|
|
171
|
+
"bad-state": "Incorrect state/mutation: mutating shared/frozen data, stale state after update, invariant violations, incorrect conditional logic, type coercion errors.",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function finderPrompt(cat, recon, roundNote) {
|
|
175
|
+
return [
|
|
176
|
+
`You are the "${cat}" bug finder. REPORT-ONLY — do NOT modify files.`,
|
|
177
|
+
scope,
|
|
178
|
+
`Repo profile (recon):\n${formatRecon(recon)}`,
|
|
179
|
+
`Your lens: ${LENS[cat]}`,
|
|
180
|
+
roundNote || "",
|
|
181
|
+
`For EVERY finding set category to exactly "${cat}". Fill reproSketch (how to trigger) and fixSketch. Set confidence honestly. A wrong bug is worse than a missed one.`,
|
|
182
|
+
`Return findings via the structured output.`,
|
|
183
|
+
].filter(Boolean).join("\n\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function skepticPrompt(f) {
|
|
187
|
+
return [
|
|
188
|
+
"You are a skeptic. Try to REFUTE the candidate bug below — prove it is NOT a real bug.",
|
|
189
|
+
scope,
|
|
190
|
+
`Candidate (${f.category}) at ${f.file}:${f.line}:`,
|
|
191
|
+
`Description: ${f.description}`,
|
|
192
|
+
`Repro: ${f.reproSketch}`,
|
|
193
|
+
"Investigate with your tools. Consider: is the path reachable? is it already guarded/validated upstream? is the input constrained? is this intended behavior?",
|
|
194
|
+
"Set refuted=true if it is not a real, reachable bug. Default to refuted=true when genuinely uncertain.",
|
|
195
|
+
].join("\n\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- 1. Recon (use injected recon if present, else profile once) ----
|
|
199
|
+
await phase("recon");
|
|
200
|
+
const recon = RT.recon
|
|
201
|
+
? RT.recon
|
|
202
|
+
: await agent(
|
|
203
|
+
["Profile this repository for review. Return the structured recon fields.", scope,
|
|
204
|
+
"Report: languages/frameworks; package managers; entry points; test layout; build tooling; concurrency model (threads/async/event loop); error-handling conventions; external resources (DB, files, network); and notes on anything that makes a code path reachable with untrusted/edge input.",
|
|
205
|
+
"Explore with your tools."].join("\n\n"),
|
|
206
|
+
{ label: "recon", schema: RECON_SCHEMA, tier: TIER_RECON, onFailure: "returnNull" },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ---- 2. Find + dedup ----
|
|
210
|
+
function dedup(findings) {
|
|
211
|
+
const seen = new Set(); const out = [];
|
|
212
|
+
for (const f of findings) {
|
|
213
|
+
if (!f) continue;
|
|
214
|
+
const key = `${f.category}::${(f.file || "").trim()}::${f.line || 0}`;
|
|
215
|
+
if (seen.has(key)) continue;
|
|
216
|
+
seen.add(key); out.push(f);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function findRound(roundNote) {
|
|
222
|
+
await phase("find");
|
|
223
|
+
// arity-1 thunks (api) => ... so parallel() runs them CONCURRENTLY (zero-arg thunks fail unless { sequential: true } is explicit).
|
|
224
|
+
const results = await parallel(categories.map((cat) => (api) =>
|
|
225
|
+
api.agent(finderPrompt(cat, recon, roundNote), { label: `find:${cat}`, schema: FINDINGS_SCHEMA, tier: TIER_FINDER, onFailure: "returnNull" })));
|
|
226
|
+
tallyPhase("find", results, (i) => `find:${categories[i]}`);
|
|
227
|
+
return results.filter(Boolean).flatMap((r) => (r.findings || []).map((f) => ({ ...f, category: f.category || "unknown" })));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let findings = dedup(await findRound(null));
|
|
231
|
+
await log(`Round 1: ${findings.length} candidate bugs across ${categories.length} lenses`);
|
|
232
|
+
|
|
233
|
+
if (depth === "thorough") {
|
|
234
|
+
const known = findings.map((f) => `- ${f.category} ${f.file}:${f.line} — ${f.description}`).join("\n");
|
|
235
|
+
const round2 = await findRound(`SECOND pass. Already found below — find only NEW bugs, do not repeat:\n${known}`);
|
|
236
|
+
findings = dedup(findings.concat(round2));
|
|
237
|
+
await log(`After round 2: ${findings.length} candidates`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// positional id (in-run reference) + stable content fingerprint (cross-run dedupe key)
|
|
241
|
+
findings = findings.map((f, i) => ({ ...f, id: `${f.category}-${i + 1}`, fingerprint: fingerprintOf(f) }));
|
|
242
|
+
|
|
243
|
+
if (findings.length === 0) {
|
|
244
|
+
return envelope("empty", { summary: "No bugs found.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- 3. Verify (high-FP profile) ----
|
|
248
|
+
// quick: high-severity only, 1 skeptic. normal: ALL, 1 skeptic. thorough: ALL, 3-skeptic majority.
|
|
249
|
+
let toVerify, votes;
|
|
250
|
+
if (depth === "quick") { toVerify = findings.filter((f) => f.severity === "high"); votes = 1; }
|
|
251
|
+
else if (depth === "thorough") { toVerify = findings; votes = 3; }
|
|
252
|
+
else { toVerify = findings; votes = 1; }
|
|
253
|
+
const verifyIds = new Set(toVerify.map((f) => f.id));
|
|
254
|
+
const passThrough = findings.filter((f) => !verifyIds.has(f.id));
|
|
255
|
+
|
|
256
|
+
let verified = passThrough;
|
|
257
|
+
if (toVerify.length > 0) {
|
|
258
|
+
await phase("verify");
|
|
259
|
+
const checked = await parallel(toVerify.map((f) => (api) => {
|
|
260
|
+
if (votes === 1) {
|
|
261
|
+
return api.agent(skepticPrompt(f), { label: `verify:${f.id}`, schema: VERDICT_SCHEMA, tier: TIER_VERIFY, onFailure: "returnNull" })
|
|
262
|
+
.then((v) => ({ f, keep: !!(v && !v.refuted), conf: v ? v.adjustedConfidence : undefined }));
|
|
263
|
+
}
|
|
264
|
+
return api.parallel([0, 1, 2].map((n) => (inner) =>
|
|
265
|
+
inner.agent(`${skepticPrompt(f)}\n\n(Independent reviewer #${n + 1}.)`, { label: `verify:${f.id}#${n + 1}`, schema: VERDICT_SCHEMA, tier: TIER_VERIFY, onFailure: "returnNull" })))
|
|
266
|
+
.then((vs) => {
|
|
267
|
+
const ok = vs.filter(Boolean);
|
|
268
|
+
const refutedCount = ok.filter((v) => v.refuted).length;
|
|
269
|
+
const avg = ok.length ? Math.round(ok.reduce((s, v) => s + (v.adjustedConfidence || 0), 0) / ok.length) : f.confidence;
|
|
270
|
+
return { f, keep: ok.length > 0 && refutedCount < 2, conf: avg };
|
|
271
|
+
});
|
|
272
|
+
}));
|
|
273
|
+
tallyPhase("verify", checked, (i) => `verify:${toVerify[i].id}`);
|
|
274
|
+
const survivors = checked.filter(Boolean).filter((c) => c.keep)
|
|
275
|
+
.map((c) => ({ ...c.f, confidence: c.conf != null ? c.conf : c.f.confidence }));
|
|
276
|
+
verified = passThrough.concat(survivors);
|
|
277
|
+
await log(`Verified: ${survivors.length}/${toVerify.length} candidates survived; ${verified.length} total`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (verified.length === 0) {
|
|
281
|
+
return envelope("empty", { summary: "No bugs survived verification.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---- 4. Synthesize (PURE JS — dedup, rank, render; the host persists the returned object) ----
|
|
285
|
+
await phase("synthesize");
|
|
286
|
+
const SEVW = { high: 3, medium: 2, low: 1 };
|
|
287
|
+
const EFFD = { small: 1, medium: 0.8, large: 0.6 };
|
|
288
|
+
function score(f) { return (SEVW[f.severity] || 1) * ((f.confidence || 0) / 100) * (EFFD[f.effort] || 0.8); }
|
|
289
|
+
|
|
290
|
+
const ranked = verified.map((f) => ({ ...f })).sort((a, b) => score(b) - score(a));
|
|
291
|
+
ranked.forEach((f, i) => { f.rank = i + 1; });
|
|
292
|
+
|
|
293
|
+
const counts = {
|
|
294
|
+
total: ranked.length,
|
|
295
|
+
critical: 0,
|
|
296
|
+
high: ranked.filter((f) => f.severity === "high").length,
|
|
297
|
+
medium: ranked.filter((f) => f.severity === "medium").length,
|
|
298
|
+
low: ranked.filter((f) => f.severity === "low").length,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
function mdCell(s) { return String(s == null ? "" : s).replace(/\|/g, "\\|").replace(/\n+/g, " "); }
|
|
302
|
+
function renderMarkdown(rows, c) {
|
|
303
|
+
const lines = [];
|
|
304
|
+
lines.push(`# Bug Hunt Report (${DOMAIN})`, "");
|
|
305
|
+
lines.push("> Report-only. No files were modified and nothing was applied.", "");
|
|
306
|
+
lines.push("## Summary", `- Total: ${c.total} (high: ${c.high}, medium: ${c.medium}, low: ${c.low})`, "");
|
|
307
|
+
lines.push("## Ranked findings", "");
|
|
308
|
+
lines.push("| Rank | Category | Severity | Confidence | Location | Description |");
|
|
309
|
+
lines.push("| ---- | -------- | -------- | ---------- | -------- | ----------- |");
|
|
310
|
+
for (const f of rows) {
|
|
311
|
+
lines.push(`| ${f.rank} | ${mdCell(f.category)} | ${mdCell(f.severity)} | ${f.confidence} | ${mdCell(f.file)}:${f.line || 0} | ${mdCell(f.description).slice(0, 140)} |`);
|
|
312
|
+
}
|
|
313
|
+
lines.push("", "## Detail");
|
|
314
|
+
for (const f of rows) {
|
|
315
|
+
lines.push("", `### ${f.rank}. ${mdCell(f.category)} — ${mdCell(f.file)}:${f.line || 0} (${f.severity}, conf ${f.confidence})`);
|
|
316
|
+
lines.push(`- **What:** ${f.description}`);
|
|
317
|
+
lines.push(`- **Repro:** ${f.reproSketch}`);
|
|
318
|
+
lines.push(`- **Fix sketch:** ${f.fixSketch}`);
|
|
319
|
+
lines.push(`- **Proposed change:** ${f.proposedChange}`);
|
|
320
|
+
if (f.docImpact) lines.push(`- **Doc impact:** ${f.docImpact}`);
|
|
321
|
+
lines.push(`- **Fingerprint:** \`${f.fingerprint}\``);
|
|
322
|
+
}
|
|
323
|
+
return lines.join("\n");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Size-fit to the 256 KB host cap: drop reportMarkdown first, then halve findings, until it fits.
|
|
327
|
+
function utf8ByteLength(value) {
|
|
328
|
+
const s = String(value ?? "");
|
|
329
|
+
let bytes = 0;
|
|
330
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
331
|
+
const code = s.charCodeAt(i);
|
|
332
|
+
if (code <= 0x7f) bytes += 1;
|
|
333
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
334
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
|
|
335
|
+
const next = s.charCodeAt(i + 1);
|
|
336
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
337
|
+
bytes += 4;
|
|
338
|
+
i += 1;
|
|
339
|
+
} else bytes += 3;
|
|
340
|
+
} else bytes += 3;
|
|
341
|
+
}
|
|
342
|
+
return bytes;
|
|
343
|
+
}
|
|
344
|
+
function jsonUtf8ByteLength(value) {
|
|
345
|
+
return utf8ByteLength(JSON.stringify(value));
|
|
346
|
+
}
|
|
347
|
+
function fitWithinBudget(status, summary) {
|
|
348
|
+
const LIMIT = 230000; // headroom under MAX_RESULT_BYTES (262144) for the {output:...} wrapper + envelope fields
|
|
349
|
+
let returned = ranked.slice(0, MAX_RETURN_FINDINGS);
|
|
350
|
+
let truncated = ranked.length > returned.length;
|
|
351
|
+
let reportMarkdown = renderMarkdown(ranked, counts);
|
|
352
|
+
const sizeOf = () => jsonUtf8ByteLength(envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown }));
|
|
353
|
+
if (sizeOf() > LIMIT) reportMarkdown = null;
|
|
354
|
+
while (sizeOf() > LIMIT && returned.length > 10) {
|
|
355
|
+
returned = returned.slice(0, Math.ceil(returned.length / 2));
|
|
356
|
+
truncated = true;
|
|
357
|
+
}
|
|
358
|
+
return envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const summary = `Found ${counts.total} bug(s): ${counts.high} high, ${counts.medium} medium, ${counts.low} low. Report-only — nothing applied.`;
|
|
362
|
+
return fitWithinBudget("ok", summary);
|