@mmerterden/multi-agent-pipeline 20.15.1 → 20.17.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 +45 -0
- package/README.md +6 -5
- package/README.tr.md +6 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +14 -12
- package/docs/features.md +9 -1
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +40 -1
- package/install/codex.mjs +4 -3
- package/manifest.json +70 -51
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/bug-bash/SKILL.md +154 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +46 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +8 -6
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -3
- package/pipeline/commands/multi-agent/update/SKILL.md +5 -1
- package/pipeline/commands/sim-test.md +71 -6
- package/pipeline/lib/plugin-skill-roots.mjs +80 -0
- package/pipeline/lib/usage-lifecycle.mjs +83 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +20 -4
- package/pipeline/multi-agent-refs/features/assertion-judge.md +76 -0
- package/pipeline/multi-agent-refs/features/exploratory-testing.md +140 -0
- package/pipeline/multi-agent-refs/features/review-decision.md +2 -1
- package/pipeline/multi-agent-refs/features/skill-conformance.md +1 -1
- package/pipeline/multi-agent-refs/features/usage-reporting.md +59 -5
- package/pipeline/multi-agent-refs/features/valven.md +110 -0
- package/pipeline/multi-agent-refs/help/en.md +2 -0
- package/pipeline/multi-agent-refs/help/tr.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -1
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -4
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +3 -2
- package/pipeline/schemas/agent-state.schema.json +16 -0
- package/pipeline/schemas/bug-bash-verification.schema.json +41 -0
- package/pipeline/schemas/explore-run.schema.json +108 -0
- package/pipeline/schemas/prefs.schema.json +21 -0
- package/pipeline/scripts/_stack-routing.mjs +2 -0
- package/pipeline/scripts/bug-bash.mjs +379 -0
- package/pipeline/scripts/explore-findings.mjs +410 -0
- package/pipeline/scripts/pr-timeline.mjs +135 -0
- package/pipeline/scripts/pre-commit-check.sh +4 -2
- package/pipeline/scripts/review-decision-gate.mjs +1 -1
- package/pipeline/scripts/skill-conformance.mjs +17 -71
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/uninstall.mjs +28 -3
- package/pipeline/scripts/usage-identity.mjs +36 -10
- package/pipeline/scripts/usage-register.mjs +95 -8
- package/pipeline/scripts/usage-report.mjs +52 -2
- package/pipeline/scripts/valven-gate.mjs +1069 -0
- package/pipeline/scripts/valven-port.mjs +169 -0
- package/pipeline/scripts/valven-split.mjs +341 -0
- package/pipeline/scripts/verify-citations.mjs +14 -1
- package/pipeline/skills/.skill-manifest.json +18 -6
- package/pipeline/skills/.skills-index.json +126 -71
- package/pipeline/skills/shared/README.md +8 -5
- package/pipeline/skills/shared/core/multi-agent-bug-bash/SKILL.md +150 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +2 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +5 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +4 -1
- package/pipeline/skills/shared/external/e2e-testing/SKILL.md +124 -0
- package/pipeline/skills/shared/external/e2e-testing/references/document-fixtures.md +65 -0
- package/pipeline/skills/shared/external/valven-rules/SKILL.md +93 -0
- package/pipeline/skills/shared/external/valven-rules/references/rules.yml +186 -0
- package/pipeline/skills/skills-index.md +76 -71
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/explore-run.schema.json",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"title": "Multi-Agent Pipeline - exploratory test run",
|
|
6
|
+
"description": "One exploratory session of /multi-agent:test or one /multi-agent:bug-bash charter, written as the run goes so explore-findings.mjs can decide when it must stop (check) and what its verdict is (report). Findings are claims with evidence; a bug bash proves them with a reproducing test before reporting them.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["goal", "budget", "steps", "findings"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"goal": { "type": "string", "minLength": 1 },
|
|
11
|
+
"platform": { "enum": ["ios", "android", "web"] },
|
|
12
|
+
"persona": { "type": "string" },
|
|
13
|
+
"budget": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"description": "Per-run budget; explore-findings.mjs fills defaults (8 steps, 10 minutes, 150000 tokens) and clamps to steps 1-12, minutes 3-15, tokens 20000-400000.",
|
|
16
|
+
"properties": {
|
|
17
|
+
"steps": { "type": "integer", "minimum": 1 },
|
|
18
|
+
"minutes": { "type": "number", "minimum": 0 },
|
|
19
|
+
"tokens": { "type": "integer", "minimum": 0 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"usage": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"steps": { "type": "integer", "minimum": 0 },
|
|
26
|
+
"minutes": { "type": "number", "minimum": 0 },
|
|
27
|
+
"tokens": { "type": "integer", "minimum": 0 }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"ended": { "enum": ["finished", "step-limit", "time", "tokens", "stuck", "aborted"] },
|
|
31
|
+
"steps": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["index", "status"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"index": { "type": "integer", "minimum": 1 },
|
|
38
|
+
"title": { "type": "string" },
|
|
39
|
+
"status": { "enum": ["passed", "failed", "blocked", "exhausted"] },
|
|
40
|
+
"code": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "The toolkit's verdict or locator code when the step did not pass: ASSERTION_FAILED and ASSERTION_INCONCLUSIVE are product failures; CREDENTIAL_MISSING, ENVIRONMENT_UNAVAILABLE and SEED_DATA_MISSING block by cause; LOCATOR_NOT_FOUND, LOCATOR_AMBIGUOUS and REPLAY_DIVERGED block as automation; BUDGET_EXCEEDED is exhausted."
|
|
43
|
+
},
|
|
44
|
+
"detail": { "type": "string" },
|
|
45
|
+
"trace": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Path of the toolkit's failure trace.md, when it wrote one."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"findings": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"required": ["kind", "severity", "title", "expected", "observed", "repro", "evidence"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"id": { "type": "string" },
|
|
59
|
+
"step": { "type": "integer", "minimum": 1 },
|
|
60
|
+
"kind": {
|
|
61
|
+
"enum": ["issue", "warning"],
|
|
62
|
+
"description": "issue: a functional defect, fails the run. warning: cosmetic, does not."
|
|
63
|
+
},
|
|
64
|
+
"severity": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"minimum": 1,
|
|
67
|
+
"maximum": 5,
|
|
68
|
+
"description": "1 trivial, 2 low, 3 medium, 4 high, 5 critical."
|
|
69
|
+
},
|
|
70
|
+
"title": { "type": "string", "minLength": 1 },
|
|
71
|
+
"screen": { "type": "string" },
|
|
72
|
+
"expected": { "type": "string", "minLength": 1 },
|
|
73
|
+
"observed": { "type": "string", "minLength": 1 },
|
|
74
|
+
"repro": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"minItems": 1,
|
|
77
|
+
"items": { "type": "string", "minLength": 1 }
|
|
78
|
+
},
|
|
79
|
+
"evidence": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"minItems": 1,
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"required": ["kind", "path"],
|
|
85
|
+
"properties": {
|
|
86
|
+
"kind": { "enum": ["screenshot", "ui-tree", "video", "trace", "log", "text"] },
|
|
87
|
+
"path": { "type": "string", "minLength": 1 }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"spec": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "The acceptance criterion the expectation comes from, quoted, or 'no spec, model expectation'."
|
|
94
|
+
},
|
|
95
|
+
"judge": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"description": "Present when a visual or semantic assertion decided the finding: the fresh judge's checked verdict.",
|
|
98
|
+
"properties": {
|
|
99
|
+
"verdict": { "enum": ["passed", "failed"] },
|
|
100
|
+
"code": { "type": ["string", "null"] },
|
|
101
|
+
"reason": { "type": "string" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1500,6 +1500,27 @@
|
|
|
1500
1500
|
"default": true,
|
|
1501
1501
|
"description": "v8.3+ - Phase 3 Step 1.75 advisory diff risk scoring. When enabled, `pipeline/scripts/diff-risk-score.mjs` runs before reviewer dispatch and the top-N risk-ranked files are injected into each reviewer's prompt as a priority hint (security paths, public API surfaces, untested source changes, schema migrations). Heuristic, deterministic, no LLM cost. Default ON - the run is sub-second and never gates the pipeline. Flip to false to skip the script and the prompt injection entirely."
|
|
1502
1502
|
},
|
|
1503
|
+
"valven": {
|
|
1504
|
+
"type": "object",
|
|
1505
|
+
"additionalProperties": false,
|
|
1506
|
+
"description": "valven-rules delivery gate (the valven-rules skill, shipped in ai-common-toolkit). Runs at Phase 2 exit (Gate 6), Phase 4 Step 2.95 before push, Phase 3 Step 1.762 and every /multi-agent:review. Findings carry tag `valven`; an accepted important or blocking one sets NEEDS WORK, and an oversized branch is split into stacked PRs by valven-split.mjs.",
|
|
1507
|
+
"properties": {
|
|
1508
|
+
"enabled": {
|
|
1509
|
+
"type": "boolean",
|
|
1510
|
+
"default": true,
|
|
1511
|
+
"description": "false turns the gate off everywhere; each run then logs `valven: disabled by prefs` so a skipped check is never read as a pass."
|
|
1512
|
+
},
|
|
1513
|
+
"thresholds": {
|
|
1514
|
+
"type": "object",
|
|
1515
|
+
"description": "Per-rule overrides of the `gate_rules` defaults in rules.yml, keyed by rule ID, e.g. {\"VLV-SIZE-01\": {\"important_over\": 300}}. Only keys the rule already declares are applied.",
|
|
1516
|
+
"propertyNames": { "pattern": "^VLV-[A-Z]+-[0-9]{2}$" },
|
|
1517
|
+
"additionalProperties": {
|
|
1518
|
+
"type": "object",
|
|
1519
|
+
"additionalProperties": { "type": ["number", "string", "array"] }
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1503
1524
|
"reviewScopeGate": {
|
|
1504
1525
|
"type": "boolean",
|
|
1505
1526
|
"default": true,
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bug-bash.mjs - charters, merge and the proof rule of /multi-agent:bug-bash.
|
|
4
|
+
*
|
|
5
|
+
* Exploratory sessions produce claims. A claim becomes a bug in the report only
|
|
6
|
+
* when a reproducing test fails with ASSERTION_FAILED on the assertion that
|
|
7
|
+
* encodes it; every other claim is listed as rejected, with the reason. The
|
|
8
|
+
* model plans charters, drives the sessions and writes the repro tests; this
|
|
9
|
+
* script holds the parts that must not be judgement:
|
|
10
|
+
*
|
|
11
|
+
* charters --file FILE
|
|
12
|
+
* Validates `[{slug, persona, area, start, goal, budget?, stance?}]`:
|
|
13
|
+
* at most MAX_CHARTERS, unique kebab-case slugs, a known persona (or
|
|
14
|
+
* `custom` with its stance written out), a one-sentence goal, a start
|
|
15
|
+
* screen or route. Prints the charters with each budget resolved.
|
|
16
|
+
* Exit 1 when any charter is invalid.
|
|
17
|
+
*
|
|
18
|
+
* merge --sessions DIR
|
|
19
|
+
* Reads DIR/<slug>.json, one exploratory run each (explore-run schema),
|
|
20
|
+
* and merges issues that describe one defect (same screen, same
|
|
21
|
+
* expectation) into candidates BB-1..n, keeping the clearest repro and
|
|
22
|
+
* every charter that hit it. Warnings and contract failures are kept
|
|
23
|
+
* apart; each charter's verdict and blocked causes are summarised.
|
|
24
|
+
*
|
|
25
|
+
* report --merged FILE --verifications FILE [--json]
|
|
26
|
+
* Applies `[{id, status, repro?, reason?, note?, rootCause?, productionRisk?}]`
|
|
27
|
+
* to the candidates and renders the report: confirmed bugs, production
|
|
28
|
+
* risks, rejected by reason, warnings, charters.
|
|
29
|
+
*
|
|
30
|
+
* Exit: 0 printed, 1 invalid charters, 2 usage or unreadable input.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
34
|
+
import { join, basename } from "node:path";
|
|
35
|
+
import { pathToFileURL } from "node:url";
|
|
36
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
37
|
+
import {
|
|
38
|
+
resolveBudget,
|
|
39
|
+
validateFinding,
|
|
40
|
+
evaluateRun,
|
|
41
|
+
SEVERITY_WORDS,
|
|
42
|
+
} from "./explore-findings.mjs";
|
|
43
|
+
|
|
44
|
+
export const MAX_CHARTERS = 10;
|
|
45
|
+
export const MAX_PARALLEL = 4;
|
|
46
|
+
|
|
47
|
+
export const PERSONAS = Object.freeze({
|
|
48
|
+
"first-time-user":
|
|
49
|
+
"A first-time user: follows the screen's own cues, reads every label, and reports anything confusing, broken or inconsistent.",
|
|
50
|
+
skeptic:
|
|
51
|
+
"Distrusts every number, date, count and claim on screen and cross-checks each against every other place it appears.",
|
|
52
|
+
fuzzer:
|
|
53
|
+
"At every input runs the charter's input matrix (empty, very long, unicode, leading spaces, special characters) before anything else, judging each entry before the next.",
|
|
54
|
+
"state-keeper":
|
|
55
|
+
"Creates, edits and deletes, then backgrounds, relaunches, reloads or goes back after each change, and reports state that is lost or stale.",
|
|
56
|
+
"error-paths":
|
|
57
|
+
"Takes the unhappy path on purpose: wrong credentials, offline, denied permissions, expired sessions; reports errors that are missing, misleading or leak detail.",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const REJECT_REASONS = [
|
|
61
|
+
"artifact",
|
|
62
|
+
"environment",
|
|
63
|
+
"design",
|
|
64
|
+
"fixture",
|
|
65
|
+
"did-not-reproduce",
|
|
66
|
+
"duplicate",
|
|
67
|
+
"unproven",
|
|
68
|
+
"unverified",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
72
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object[]} list
|
|
76
|
+
* @returns {{charters: object[], errors: string[]}}
|
|
77
|
+
*/
|
|
78
|
+
export function validateCharters(list) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
const charters = [];
|
|
81
|
+
if (!Array.isArray(list) || !list.length) {
|
|
82
|
+
return { charters, errors: ["charters must be a non-empty array"] };
|
|
83
|
+
}
|
|
84
|
+
if (list.length > MAX_CHARTERS) {
|
|
85
|
+
errors.push(`at most ${MAX_CHARTERS} charters; got ${list.length}`);
|
|
86
|
+
}
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
list.forEach((c, i) => {
|
|
89
|
+
const at = `charter ${i + 1}${c?.slug ? ` (${c.slug})` : ""}`;
|
|
90
|
+
if (!SLUG.test(c?.slug || "")) errors.push(`${at}: slug must be kebab-case`);
|
|
91
|
+
else if (seen.has(c.slug)) errors.push(`${at}: duplicate slug`);
|
|
92
|
+
seen.add(c?.slug);
|
|
93
|
+
if (c?.persona === "custom") {
|
|
94
|
+
if (!nonEmpty(c.stance)) errors.push(`${at}: a custom persona needs its stance written out`);
|
|
95
|
+
} else if (!PERSONAS[c?.persona]) {
|
|
96
|
+
errors.push(`${at}: persona must be one of ${Object.keys(PERSONAS).join(", ")} or custom`);
|
|
97
|
+
}
|
|
98
|
+
if (!nonEmpty(c?.area)) errors.push(`${at}: area is required`);
|
|
99
|
+
if (!nonEmpty(c?.start))
|
|
100
|
+
errors.push(`${at}: start (the screen or route it begins at) is required`);
|
|
101
|
+
const goal = (c?.goal || "").trim();
|
|
102
|
+
if (!goal) errors.push(`${at}: goal is required`);
|
|
103
|
+
else if (/\n/.test(goal) || /[.!?]\s+\S/.test(goal)) {
|
|
104
|
+
errors.push(`${at}: goal must be one sentence: one area, one posture`);
|
|
105
|
+
}
|
|
106
|
+
charters.push({
|
|
107
|
+
...c,
|
|
108
|
+
stance: c?.persona === "custom" ? c.stance : PERSONAS[c?.persona],
|
|
109
|
+
budget: resolveBudget(c?.budget),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
return { charters, errors };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const norm = (s) =>
|
|
116
|
+
String(s || "")
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
119
|
+
.trim();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {{slug:string, persona?:string, run:object}[]} sessions
|
|
123
|
+
*/
|
|
124
|
+
export function mergeSessions(sessions) {
|
|
125
|
+
const byKey = new Map();
|
|
126
|
+
const warnings = [];
|
|
127
|
+
const invalid = [];
|
|
128
|
+
const charters = [];
|
|
129
|
+
for (const s of sessions) {
|
|
130
|
+
const e = evaluateRun(s.run);
|
|
131
|
+
charters.push({
|
|
132
|
+
slug: s.slug,
|
|
133
|
+
persona: s.persona || s.run?.persona || null,
|
|
134
|
+
goal: s.run?.goal || null,
|
|
135
|
+
verdict: e.verdict,
|
|
136
|
+
ended: e.ended,
|
|
137
|
+
usage: e.usage,
|
|
138
|
+
blocked: Object.fromEntries(
|
|
139
|
+
Object.entries(e.blocked)
|
|
140
|
+
.filter(([, l]) => l.length)
|
|
141
|
+
.map(([k, l]) => [k, l.length]),
|
|
142
|
+
),
|
|
143
|
+
});
|
|
144
|
+
for (const f of s.run?.findings || []) {
|
|
145
|
+
const errors = validateFinding(f);
|
|
146
|
+
if (errors.length) {
|
|
147
|
+
invalid.push({ charter: s.slug, title: f?.title ?? null, errors });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (f.kind === "warning") {
|
|
151
|
+
warnings.push({ ...f, charters: [s.slug] });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const key = `${norm(f.screen)}|${norm(f.expected)}`;
|
|
155
|
+
const prev = byKey.get(key);
|
|
156
|
+
if (!prev) {
|
|
157
|
+
byKey.set(key, { ...f, charters: [s.slug] });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!prev.charters.includes(s.slug)) prev.charters.push(s.slug);
|
|
161
|
+
if (f.repro.length > prev.repro.length) {
|
|
162
|
+
Object.assign(prev, {
|
|
163
|
+
...f,
|
|
164
|
+
charters: prev.charters,
|
|
165
|
+
severity: Math.max(prev.severity, f.severity),
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
prev.severity = Math.max(prev.severity, f.severity);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const candidates = [...byKey.values()]
|
|
173
|
+
.sort((a, b) => b.severity - a.severity)
|
|
174
|
+
.map((f, i) => ({ id: `BB-${i + 1}`, ...f }));
|
|
175
|
+
warnings.sort((a, b) => b.severity - a.severity);
|
|
176
|
+
return { candidates, warnings, invalid, charters };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function proofProblem(repro) {
|
|
180
|
+
if (!repro || typeof repro !== "object") return "no repro test was recorded";
|
|
181
|
+
if (!nonEmpty(repro.path)) return "repro.path (the test file) is required";
|
|
182
|
+
if (!nonEmpty(repro.command)) return "repro.command (how it was run) is required";
|
|
183
|
+
if (!nonEmpty(repro.assertion))
|
|
184
|
+
return "repro.assertion (the check that encodes the finding) is required";
|
|
185
|
+
if (repro.code === "PASSED" || repro.code === null || repro.code === undefined) {
|
|
186
|
+
return "did-not-reproduce";
|
|
187
|
+
}
|
|
188
|
+
if (repro.code !== "ASSERTION_FAILED") {
|
|
189
|
+
return `the repro failed with ${repro.code}, not ASSERTION_FAILED: the test is wrong, fix it and rerun`;
|
|
190
|
+
}
|
|
191
|
+
if (!nonEmpty(repro.failedAt) || repro.failedAt.trim() !== repro.assertion.trim()) {
|
|
192
|
+
return `the repro failed on a different assertion (${repro.failedAt || "none recorded"}) than the one that encodes the finding`;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {ReturnType<typeof mergeSessions>} merged
|
|
199
|
+
* @param {object[]} verifications
|
|
200
|
+
*/
|
|
201
|
+
export function buildReport(merged, verifications) {
|
|
202
|
+
const byId = new Map((verifications || []).map((v) => [v?.id, v]));
|
|
203
|
+
const confirmed = [];
|
|
204
|
+
const rejected = [];
|
|
205
|
+
const productionRisks = [];
|
|
206
|
+
for (const c of merged.candidates) {
|
|
207
|
+
const v = byId.get(c.id);
|
|
208
|
+
if (!v) {
|
|
209
|
+
rejected.push({ ...c, reason: "unverified", note: "no verification was recorded" });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (v.status === "confirmed") {
|
|
213
|
+
const problem = proofProblem(v.repro);
|
|
214
|
+
if (!problem) {
|
|
215
|
+
confirmed.push({
|
|
216
|
+
...c,
|
|
217
|
+
proof: v.repro,
|
|
218
|
+
rootCause: v.rootCause || null,
|
|
219
|
+
note: v.note || null,
|
|
220
|
+
});
|
|
221
|
+
} else if (problem === "did-not-reproduce") {
|
|
222
|
+
rejected.push({
|
|
223
|
+
...c,
|
|
224
|
+
reason: "did-not-reproduce",
|
|
225
|
+
note: v.note || "the repro test passed",
|
|
226
|
+
proof: v.repro,
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
rejected.push({ ...c, reason: "unproven", note: problem, proof: v.repro });
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (v.status !== "rejected" || !REJECT_REASONS.includes(v.reason)) {
|
|
234
|
+
rejected.push({
|
|
235
|
+
...c,
|
|
236
|
+
reason: "unverified",
|
|
237
|
+
note: `verification needs status confirmed, or rejected with a reason from: ${REJECT_REASONS.join(", ")}`,
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const row = { ...c, reason: v.reason, note: v.note || null };
|
|
242
|
+
rejected.push(row);
|
|
243
|
+
if (v.reason === "environment" && v.productionRisk === true) productionRisks.push(row);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
confirmed,
|
|
247
|
+
productionRisks,
|
|
248
|
+
rejected,
|
|
249
|
+
warnings: merged.warnings,
|
|
250
|
+
invalid: merged.invalid,
|
|
251
|
+
charters: merged.charters,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {ReturnType<typeof buildReport>} r */
|
|
256
|
+
export function renderBugBash(r) {
|
|
257
|
+
const out = [
|
|
258
|
+
"# Bug bash report",
|
|
259
|
+
"",
|
|
260
|
+
`Confirmed: ${r.confirmed.length} · production risks (unverified): ${r.productionRisks.length} · rejected: ${r.rejected.length} · warnings: ${r.warnings.length} · charters: ${r.charters.length}`,
|
|
261
|
+
"",
|
|
262
|
+
`## Confirmed (${r.confirmed.length})`,
|
|
263
|
+
"",
|
|
264
|
+
];
|
|
265
|
+
r.confirmed.forEach((b) => {
|
|
266
|
+
out.push(
|
|
267
|
+
`### ${b.id} [${SEVERITY_WORDS[b.severity]}] ${b.title}`,
|
|
268
|
+
"",
|
|
269
|
+
`- **Screen**: ${b.screen || "-"}`,
|
|
270
|
+
`- **Expected / observed**: ${b.expected} / ${b.observed}`,
|
|
271
|
+
`- **Repro steps**: ${b.repro.map((s, i) => `${i + 1}. ${s}`).join(" ")}`,
|
|
272
|
+
`- **Repro test**: \`${b.proof.path}\` (\`${b.proof.command}\`) fails with ASSERTION_FAILED at \`${b.proof.assertion}\``,
|
|
273
|
+
`- **Root cause**: ${b.rootCause || "not located"}`,
|
|
274
|
+
`- **Evidence**: ${b.evidence.map((e) => `${e.kind}=${e.path}`).join(" ")}`,
|
|
275
|
+
`- **Found by**: ${b.charters.join(", ")}`,
|
|
276
|
+
"",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
if (!r.confirmed.length) out.push("None.", "");
|
|
280
|
+
out.push(`## Production risks (${r.productionRisks.length}) - unverified`, "");
|
|
281
|
+
for (const p of r.productionRisks) out.push(`- ${p.id} ${p.title}: ${p.note || "-"}`);
|
|
282
|
+
if (!r.productionRisks.length) out.push("None.");
|
|
283
|
+
out.push("", `## Rejected (${r.rejected.length})`, "");
|
|
284
|
+
for (const reason of REJECT_REASONS) {
|
|
285
|
+
const rows = r.rejected.filter((x) => x.reason === reason);
|
|
286
|
+
if (!rows.length) continue;
|
|
287
|
+
out.push(`### ${reason} (${rows.length})`, "");
|
|
288
|
+
for (const x of rows) out.push(`- ${x.id} ${x.title}: ${x.note || "-"}`);
|
|
289
|
+
out.push("");
|
|
290
|
+
}
|
|
291
|
+
if (!r.rejected.length) out.push("None.", "");
|
|
292
|
+
out.push(`## Warnings (${r.warnings.length})`, "");
|
|
293
|
+
for (const w of r.warnings)
|
|
294
|
+
out.push(`- [${SEVERITY_WORDS[w.severity]}] ${w.title} (${w.charters.join(", ")})`);
|
|
295
|
+
if (!r.warnings.length) out.push("None.");
|
|
296
|
+
if (r.invalid.length) {
|
|
297
|
+
out.push("", `## Findings that failed the contract (${r.invalid.length})`, "");
|
|
298
|
+
for (const v of r.invalid)
|
|
299
|
+
out.push(`- ${v.charter}: ${v.title ?? "(untitled)"}: ${v.errors.join("; ")}`);
|
|
300
|
+
}
|
|
301
|
+
out.push(
|
|
302
|
+
"",
|
|
303
|
+
"## Charters",
|
|
304
|
+
"",
|
|
305
|
+
"| Charter | Persona | Verdict | Ended | Blocked |",
|
|
306
|
+
"|---|---|---|---|---|",
|
|
307
|
+
);
|
|
308
|
+
for (const c of r.charters) {
|
|
309
|
+
const blocked = Object.entries(c.blocked)
|
|
310
|
+
.map(([k, n]) => `${k} ${n}`)
|
|
311
|
+
.join(", ");
|
|
312
|
+
out.push(`| ${c.slug} | ${c.persona || "-"} | ${c.verdict} | ${c.ended} | ${blocked || "-"} |`);
|
|
313
|
+
}
|
|
314
|
+
return `${out.join("\n")}\n`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function arg(argv, flag) {
|
|
318
|
+
const i = argv.indexOf(flag);
|
|
319
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
323
|
+
|
|
324
|
+
function readSessions(dir) {
|
|
325
|
+
return readdirSync(dir)
|
|
326
|
+
.filter((f) => f.endsWith(".json"))
|
|
327
|
+
.sort()
|
|
328
|
+
.map((f) => {
|
|
329
|
+
const run = readJson(join(dir, f));
|
|
330
|
+
return { slug: basename(f, ".json"), persona: run.persona || null, run };
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function main() {
|
|
335
|
+
const argv = process.argv.slice(2);
|
|
336
|
+
const mode = argv[0];
|
|
337
|
+
const usage = () => {
|
|
338
|
+
process.stderr.write(
|
|
339
|
+
"usage: bug-bash.mjs charters --file FILE\n" +
|
|
340
|
+
" bug-bash.mjs merge --sessions DIR\n" +
|
|
341
|
+
" bug-bash.mjs report --merged FILE --verifications FILE [--json]\n",
|
|
342
|
+
);
|
|
343
|
+
process.exitCode = 2;
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
if (mode === "charters" && arg(argv, "--file")) {
|
|
347
|
+
const r = validateCharters(readJson(arg(argv, "--file")));
|
|
348
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
349
|
+
if (r.errors.length) process.exitCode = 1;
|
|
350
|
+
} else if (mode === "merge" && arg(argv, "--sessions")) {
|
|
351
|
+
const merged = mergeSessions(readSessions(arg(argv, "--sessions")));
|
|
352
|
+
process.stdout.write(`${JSON.stringify(merged, null, 2)}\n`);
|
|
353
|
+
} else if (mode === "report" && arg(argv, "--merged") && arg(argv, "--verifications")) {
|
|
354
|
+
const r = buildReport(
|
|
355
|
+
readJson(arg(argv, "--merged")),
|
|
356
|
+
readJson(arg(argv, "--verifications")),
|
|
357
|
+
);
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
argv.includes("--json") ? `${JSON.stringify(r, null, 2)}\n` : renderBugBash(r),
|
|
360
|
+
);
|
|
361
|
+
} else {
|
|
362
|
+
usage();
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
process.stderr.write(`bug-bash: ${err.message}\n`);
|
|
366
|
+
process.exitCode = 2;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function invokedDirectly() {
|
|
371
|
+
if (!process.argv[1]) return false;
|
|
372
|
+
try {
|
|
373
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (invokedDirectly()) runMain("bug-bash", main);
|