@mmerterden/multi-agent-pipeline 20.16.0 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +6 -5
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +14 -12
  7. package/docs/features.md +9 -1
  8. package/install/catalog-history.json +1 -1
  9. package/install/claude.mjs +41 -6
  10. package/install/codex.mjs +3 -3
  11. package/manifest.json +62 -49
  12. package/package.json +1 -1
  13. package/pipeline/commands/multi-agent/bug-bash/SKILL.md +154 -0
  14. package/pipeline/commands/multi-agent/review/SKILL.md +13 -9
  15. package/pipeline/commands/multi-agent/setup/SKILL.md +8 -6
  16. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -3
  17. package/pipeline/commands/multi-agent/update/SKILL.md +5 -1
  18. package/pipeline/commands/sim-test.md +71 -6
  19. package/pipeline/lib/plugin-skill-roots.mjs +80 -0
  20. package/pipeline/lib/usage-lifecycle.mjs +83 -0
  21. package/pipeline/multi-agent-refs/cross-cli-contract.md +20 -4
  22. package/pipeline/multi-agent-refs/features/assertion-judge.md +76 -0
  23. package/pipeline/multi-agent-refs/features/exploratory-testing.md +140 -0
  24. package/pipeline/multi-agent-refs/features/skill-conformance.md +1 -1
  25. package/pipeline/multi-agent-refs/features/usage-reporting.md +59 -5
  26. package/pipeline/multi-agent-refs/features/valven.md +30 -3
  27. package/pipeline/multi-agent-refs/help/en.md +2 -0
  28. package/pipeline/multi-agent-refs/help/tr.md +2 -0
  29. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  30. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +2 -2
  31. package/pipeline/schemas/bug-bash-verification.schema.json +41 -0
  32. package/pipeline/schemas/explore-run.schema.json +108 -0
  33. package/pipeline/schemas/prefs.schema.json +1 -1
  34. package/pipeline/scripts/_stack-routing.mjs +2 -0
  35. package/pipeline/scripts/bug-bash.mjs +379 -0
  36. package/pipeline/scripts/explore-findings.mjs +410 -0
  37. package/pipeline/scripts/gen-skills-index.mjs +1 -2
  38. package/pipeline/scripts/pr-timeline.mjs +135 -0
  39. package/pipeline/scripts/pre-commit-check.sh +4 -2
  40. package/pipeline/scripts/skill-conformance.mjs +17 -71
  41. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  42. package/pipeline/scripts/uninstall.mjs +29 -8
  43. package/pipeline/scripts/usage-identity.mjs +36 -10
  44. package/pipeline/scripts/usage-register.mjs +95 -8
  45. package/pipeline/scripts/usage-report.mjs +52 -2
  46. package/pipeline/scripts/valven-gate.mjs +292 -11
  47. package/pipeline/skills/.skill-manifest.json +18 -10
  48. package/pipeline/skills/.skills-index.json +28 -6
  49. package/pipeline/skills/shared/README.md +6 -4
  50. package/pipeline/skills/shared/core/multi-agent-bug-bash/SKILL.md +150 -0
  51. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
  52. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +5 -1
  53. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -3
  54. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +4 -1
  55. package/pipeline/skills/shared/external/e2e-testing/SKILL.md +124 -0
  56. package/pipeline/skills/shared/external/e2e-testing/references/document-fixtures.md +65 -0
  57. package/pipeline/skills/shared/external/valven-rules/SKILL.md +93 -0
  58. package/pipeline/skills/shared/{core → external}/valven-rules/references/rules.yml +52 -6
  59. package/pipeline/skills/skills-index.md +4 -2
  60. package/pipeline/skills/shared/core/valven-rules/SKILL.md +0 -62
@@ -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);